Skip to content
2 changes: 1 addition & 1 deletion RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

25.5
-----

* [***] Use web-based sign-in flow (hidden under a feature flag) [#23675]

25.4
-----
Expand Down
84 changes: 82 additions & 2 deletions WordPress/Classes/Login/WordPressDotComAuthenticator.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import AuthenticationServices
import Foundation
import UIKit
import WordPressAuthenticator

import Alamofire

Expand All @@ -10,14 +11,84 @@ import Alamofire
struct WordPressDotComAuthenticator {
enum Error: Swift.Error {
case invalidCallbackURL
case loginDenied(message: String)
case obtainAccessToken
case urlError(URLError)
case parsing(DecodingError)
case cancelled
case unknown(Swift.Error)
}

@MainActor
func signIn(from viewController: UINavigationController) async {
let token: String
do {
token = try await authenticate(from: viewController)
Copy link
Contributor

@kean kean Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The animation at the top is distracting.

Untitled.mp4

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why it looks like this for me. It's different on your recording. I think there is a bug with the first welcome page.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we present this login screen as a pushed navigation item instead of a popover?

That'd be more consistent with the current setup

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I thought we had the ability to do it in the SwiftUI setup for self-hosted sites, but it's ok – we can dig into that further once we're not relying on a bunch of WPAuthenticator stuff anymore.

I have a few suggestions about this in #23702

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I thought we had the ability to do it in the SwiftUI setup for self-hosted sites

I just checked. The application password flow for self-hosted site also presents Safari view as a modal.

} catch {
if let error = error as? WordPressDotComAuthenticator.Error {
presentSignInError(error, from: viewController)
} else {
wpAssertionFailure("WP.com web login failed", userInfo: ["error": "\(error)"])
}
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) Extract the first part into a separate method so that there is no return in the middle of this long method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 43409de

}

let delegate = WordPressAuthenticator.shared.delegate!
let credentials = AuthenticatorCredentials(wpcom: WordPressComCredentials(authToken: token, isJetpackLogin: false, multifactor: false))
SVProgressHUD.show()
delegate.sync(credentials: credentials) {
SVProgressHUD.dismiss()

delegate.presentLoginEpilogue(
in: viewController,
for: credentials,
source: .custom(source: "web-login"),
onDismiss: { /* Do nothing */ }
)
}
}

private func presentSignInError(_ error: WordPressDotComAuthenticator.Error, from viewController: UIViewController) {
// Show an alert for non-cancellation errors.
let alertMessage: String
switch error {
case .cancelled:
// `.cancelled` error is thrown when user taps the cancel button in the presented Safari view controller.
// No need to show an alert for this error.
return
case let .loginDenied(message):
alertMessage = message
case let .urlError(error):
alertMessage = error.localizedDescription
case .invalidCallbackURL, .obtainAccessToken, .parsing, .unknown:
// These errors are unexpected.
wpAssertionFailure("WP.com web login failed", userInfo: ["error": "\(error)"])
alertMessage = SharedStrings.Error.generic
}

let alert = UIAlertController(
title: NSLocalizedString("generic.error.title", value: "Error", comment: "A generic title for an error"),
message: alertMessage,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: SharedStrings.Button.close, style: .cancel, handler: nil))
viewController.present(alert, animated: true)
}

func authenticate(from viewController: UIViewController) async throws -> String {
WPAnalytics.track(.wpcomWebSignIn, properties: ["stage": "start"])

do {
let value = try await _authenticate(from: viewController)
WPAnalytics.track(.wpcomWebSignIn, properties: ["stage": "success"])
return value
} catch {
WPAnalytics.track(.wpcomWebSignIn, properties: ["stage": "error", "error": "\(error)"])
throw error
}
}

private func _authenticate(from viewController: UIViewController) async throws -> String {
let clientId = ApiCredentials.client
let clientSecret = ApiCredentials.secret
let redirectURI = "x-wordpress-app://oauth2-callback"
Expand Down Expand Up @@ -55,8 +126,17 @@ struct WordPressDotComAuthenticator {
clientSecret: String,
redirectURI: String
) async throws -> String {
guard let query = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems,
let code = query.first(where: { $0.name == "code" })?.value else {
guard let query = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems else {
throw Error.invalidCallbackURL
}

let queryMap: [String: String] = query.reduce(into: [:]) { $0[$1.name] = $1.value }

guard let code = queryMap["code"] else {
if queryMap["error"] == "access_denied" {
let message = NSLocalizedString("wpComLogin.error.accessDenied", value: "Access denied. You need to approve to log in to WordPress.com", comment: "Error message when user denies access to WordPress.com")
throw Error.loginDenied(message: message)
}
throw Error.invalidCallbackURL
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,18 +181,13 @@ final class SplitViewRootPresenter: RootViewPresenter {
}

@MainActor private func signIn() async {
WPAnalytics.track(.wpcomWebSignIn, properties: ["source": "sidebar", "stage": "start"])

let token: String
do {
token = try await WordPressDotComAuthenticator().authenticate(from: splitVC)
} catch {
WPAnalytics.track(.wpcomWebSignIn, properties: ["source": "sidebar", "stage": "error", "error": "\(error)"])
return
}

WPAnalytics.track(.wpcomWebSignIn, properties: ["source": "sidebar", "stage": "success"])

SVProgressHUD.show()
let credentials = WordPressComCredentials(authToken: token, isJetpackLogin: false, multifactor: false)
WordPressAuthenticator.shared.delegate!.sync(credentials: .init(wpcom: credentials)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,34 @@ protocol WordPressAuthenticatorProtocol {

extension WordPressAuthenticator: WordPressAuthenticatorProtocol {
static func loginUI() -> UIViewController? {
Self.loginUI(showCancel: false, restrictToWPCom: false, onLoginButtonTapped: nil)
Self.loginUI(showCancel: false, restrictToWPCom: false, onLoginButtonTapped: nil, continueWithDotCom: { viewController in
guard Self.dotComWebLoginEnabled, let navigationController = viewController.navigationController else {
return false
}

Task { @MainActor in
await WordPressDotComAuthenticator().signIn(from: navigationController)
}

return true
})
}

static var dotComWebLoginEnabled: Bool {
// Some UI tests go through the native login flow. They should be updated once the web sign in flow is fully
// rolled out. We'll disable web login for UI tests for now.
if UITestConfigurator.isUITesting() {
return false
}

// TODO: Replce with a remote feature flag.
// Enable web-based login for debug builds until the remote feature flag is available.
#if DEBUG
let webLoginEnabled = true
#else
let webLoginEnabled = false
#endif

return webLoginEnabled
}
}
4 changes: 4 additions & 0 deletions WordPress/Classes/System/UITesting/UITestConfigurator.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import Foundation

struct UITestConfigurator {
static func isUITesting() -> Bool {
CommandLine.arguments.contains("-ui-testing")
}

static func prepareApplicationForUITests(in app: UIApplication, window: UIWindow) {
let arguments = CommandLine.arguments
if arguments.contains("-ui-testing") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -525,18 +525,13 @@ class MeViewController: UITableViewController {
///
fileprivate func promptForLoginOrSignup() {
Task { @MainActor in
WPAnalytics.track(.wpcomWebSignIn, properties: ["source": "me", "stage": "start"])

let token: String
do {
token = try await WordPressDotComAuthenticator().authenticate(from: self)
} catch {
WPAnalytics.track(.wpcomWebSignIn, properties: ["source": "me", "stage": "error", "error": "\(error)"])
return
}

WPAnalytics.track(.wpcomWebSignIn, properties: ["source": "me", "stage": "success"])

SVProgressHUD.show()
let credentials = WordPressComCredentials(authToken: token, isJetpackLogin: false, multifactor: false)
WordPressAuthenticator.shared.delegate!.sync(credentials: .init(wpcom: credentials)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ import WordPressKit
/// - restrictToWPCom: Whether only WordPress.com login is enabled.
/// - onLoginButtonTapped: Called when the login button on the prologue screen is tapped.
/// - Returns: The root view controller for the login flow.
public class func loginUI(showCancel: Bool = false, restrictToWPCom: Bool = false, onLoginButtonTapped: (() -> Void)? = nil) -> UIViewController? {
public class func loginUI(showCancel: Bool = false, restrictToWPCom: Bool = false, onLoginButtonTapped: (() -> Void)? = nil, continueWithDotCom: ((UIViewController) -> Bool)? = nil) -> UIViewController? {
let storyboard = Storyboard.login.instance
guard let controller = storyboard.instantiateInitialViewController() else {
assertionFailure("Cannot instantiate initial login controller from Login.storyboard")
Expand All @@ -174,6 +174,7 @@ import WordPressKit

if let loginNavController = controller as? LoginNavigationController, let loginPrologueViewController = loginNavController.viewControllers.first as? LoginPrologueViewController {
loginPrologueViewController.showCancel = showCancel
loginPrologueViewController.continueWithDotComOverwrite = continueWithDotCom
}

controller.modalPresentationStyle = .fullScreen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class LoginPrologueViewController: LoginViewController {
private var buttonViewController: NUXButtonViewController?
private var stackedButtonsViewController: NUXStackedButtonsViewController?
var showCancel = false
var continueWithDotComOverwrite: ((UIViewController) -> Bool)? = nil

@IBOutlet private weak var buttonContainerView: UIView!
/// Blur effect on button container view
Expand Down Expand Up @@ -526,6 +527,10 @@ class LoginPrologueViewController: LoginViewController {
/// Unified "Continue with WordPress.com" prologue button action.
///
private func continueWithDotCom() {
if let continueWithDotComOverwrite, continueWithDotComOverwrite(self) {
return
}

guard let vc = GetStartedViewController.instantiate(from: .getStarted) else {
WPAuthenticatorLogError("Failed to navigate from LoginPrologueViewController to GetStartedViewController")
return
Expand Down