Skip to content

Commit d6c77da

Browse files
authored
Replace native WP.com sign in with web-based sign in (#23675)
* Consolidate tracking of web-based WP.com login * Add a clousre to overwrite handling of the WP.com login button * Replace native WP.com sign in with web-based sign in * Add a release note * Extract error alert into a function * Handle a scenario where users deny access on web login * Disable web log for UI tests
1 parent ff4ba82 commit d6c77da

File tree

8 files changed

+123
-15
lines changed

8 files changed

+123
-15
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
25.5
66
-----
7-
7+
* [***] Use web-based sign-in flow (hidden under a feature flag) [#23675]
88

99
25.4
1010
-----

WordPress/Classes/Login/WordPressDotComAuthenticator.swift

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import AuthenticationServices
22
import Foundation
33
import UIKit
4+
import WordPressAuthenticator
45

56
import Alamofire
67

@@ -10,14 +11,84 @@ import Alamofire
1011
struct WordPressDotComAuthenticator {
1112
enum Error: Swift.Error {
1213
case invalidCallbackURL
14+
case loginDenied(message: String)
1315
case obtainAccessToken
1416
case urlError(URLError)
1517
case parsing(DecodingError)
1618
case cancelled
1719
case unknown(Swift.Error)
1820
}
1921

22+
@MainActor
23+
func signIn(from viewController: UINavigationController) async {
24+
let token: String
25+
do {
26+
token = try await authenticate(from: viewController)
27+
} catch {
28+
if let error = error as? WordPressDotComAuthenticator.Error {
29+
presentSignInError(error, from: viewController)
30+
} else {
31+
wpAssertionFailure("WP.com web login failed", userInfo: ["error": "\(error)"])
32+
}
33+
return
34+
}
35+
36+
let delegate = WordPressAuthenticator.shared.delegate!
37+
let credentials = AuthenticatorCredentials(wpcom: WordPressComCredentials(authToken: token, isJetpackLogin: false, multifactor: false))
38+
SVProgressHUD.show()
39+
delegate.sync(credentials: credentials) {
40+
SVProgressHUD.dismiss()
41+
42+
delegate.presentLoginEpilogue(
43+
in: viewController,
44+
for: credentials,
45+
source: .custom(source: "web-login"),
46+
onDismiss: { /* Do nothing */ }
47+
)
48+
}
49+
}
50+
51+
private func presentSignInError(_ error: WordPressDotComAuthenticator.Error, from viewController: UIViewController) {
52+
// Show an alert for non-cancellation errors.
53+
let alertMessage: String
54+
switch error {
55+
case .cancelled:
56+
// `.cancelled` error is thrown when user taps the cancel button in the presented Safari view controller.
57+
// No need to show an alert for this error.
58+
return
59+
case let .loginDenied(message):
60+
alertMessage = message
61+
case let .urlError(error):
62+
alertMessage = error.localizedDescription
63+
case .invalidCallbackURL, .obtainAccessToken, .parsing, .unknown:
64+
// These errors are unexpected.
65+
wpAssertionFailure("WP.com web login failed", userInfo: ["error": "\(error)"])
66+
alertMessage = SharedStrings.Error.generic
67+
}
68+
69+
let alert = UIAlertController(
70+
title: NSLocalizedString("generic.error.title", value: "Error", comment: "A generic title for an error"),
71+
message: alertMessage,
72+
preferredStyle: .alert
73+
)
74+
alert.addAction(UIAlertAction(title: SharedStrings.Button.close, style: .cancel, handler: nil))
75+
viewController.present(alert, animated: true)
76+
}
77+
2078
func authenticate(from viewController: UIViewController) async throws -> String {
79+
WPAnalytics.track(.wpcomWebSignIn, properties: ["stage": "start"])
80+
81+
do {
82+
let value = try await _authenticate(from: viewController)
83+
WPAnalytics.track(.wpcomWebSignIn, properties: ["stage": "success"])
84+
return value
85+
} catch {
86+
WPAnalytics.track(.wpcomWebSignIn, properties: ["stage": "error", "error": "\(error)"])
87+
throw error
88+
}
89+
}
90+
91+
private func _authenticate(from viewController: UIViewController) async throws -> String {
2192
let clientId = ApiCredentials.client
2293
let clientSecret = ApiCredentials.secret
2394
let redirectURI = "x-wordpress-app://oauth2-callback"
@@ -55,8 +126,17 @@ struct WordPressDotComAuthenticator {
55126
clientSecret: String,
56127
redirectURI: String
57128
) async throws -> String {
58-
guard let query = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems,
59-
let code = query.first(where: { $0.name == "code" })?.value else {
129+
guard let query = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems else {
130+
throw Error.invalidCallbackURL
131+
}
132+
133+
let queryMap: [String: String] = query.reduce(into: [:]) { $0[$1.name] = $1.value }
134+
135+
guard let code = queryMap["code"] else {
136+
if queryMap["error"] == "access_denied" {
137+
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")
138+
throw Error.loginDenied(message: message)
139+
}
60140
throw Error.invalidCallbackURL
61141
}
62142

WordPress/Classes/System/Root View/SplitViewRootPresenter.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,18 +181,13 @@ final class SplitViewRootPresenter: RootViewPresenter {
181181
}
182182

183183
@MainActor private func signIn() async {
184-
WPAnalytics.track(.wpcomWebSignIn, properties: ["source": "sidebar", "stage": "start"])
185-
186184
let token: String
187185
do {
188186
token = try await WordPressDotComAuthenticator().authenticate(from: splitVC)
189187
} catch {
190-
WPAnalytics.track(.wpcomWebSignIn, properties: ["source": "sidebar", "stage": "error", "error": "\(error)"])
191188
return
192189
}
193190

194-
WPAnalytics.track(.wpcomWebSignIn, properties: ["source": "sidebar", "stage": "success"])
195-
196191
SVProgressHUD.show()
197192
let credentials = WordPressComCredentials(authToken: token, isJetpackLogin: false, multifactor: false)
198193
WordPressAuthenticator.shared.delegate!.sync(credentials: .init(wpcom: credentials)) {

WordPress/Classes/System/Root View/WordPressAuthenticatorProtocol.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,34 @@ protocol WordPressAuthenticatorProtocol {
88

99
extension WordPressAuthenticator: WordPressAuthenticatorProtocol {
1010
static func loginUI() -> UIViewController? {
11-
Self.loginUI(showCancel: false, restrictToWPCom: false, onLoginButtonTapped: nil)
11+
Self.loginUI(showCancel: false, restrictToWPCom: false, onLoginButtonTapped: nil, continueWithDotCom: { viewController in
12+
guard Self.dotComWebLoginEnabled, let navigationController = viewController.navigationController else {
13+
return false
14+
}
15+
16+
Task { @MainActor in
17+
await WordPressDotComAuthenticator().signIn(from: navigationController)
18+
}
19+
20+
return true
21+
})
22+
}
23+
24+
static var dotComWebLoginEnabled: Bool {
25+
// Some UI tests go through the native login flow. They should be updated once the web sign in flow is fully
26+
// rolled out. We'll disable web login for UI tests for now.
27+
if UITestConfigurator.isUITesting() {
28+
return false
29+
}
30+
31+
// TODO: Replce with a remote feature flag.
32+
// Enable web-based login for debug builds until the remote feature flag is available.
33+
#if DEBUG
34+
let webLoginEnabled = true
35+
#else
36+
let webLoginEnabled = false
37+
#endif
38+
39+
return webLoginEnabled
1240
}
1341
}

WordPress/Classes/System/UITesting/UITestConfigurator.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import Foundation
22

33
struct UITestConfigurator {
4+
static func isUITesting() -> Bool {
5+
CommandLine.arguments.contains("-ui-testing")
6+
}
7+
48
static func prepareApplicationForUITests(in app: UIApplication, window: UIWindow) {
59
let arguments = CommandLine.arguments
610
if arguments.contains("-ui-testing") {

WordPress/Classes/ViewRelated/Me/Me Main/MeViewController.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -525,18 +525,13 @@ class MeViewController: UITableViewController {
525525
///
526526
fileprivate func promptForLoginOrSignup() {
527527
Task { @MainActor in
528-
WPAnalytics.track(.wpcomWebSignIn, properties: ["source": "me", "stage": "start"])
529-
530528
let token: String
531529
do {
532530
token = try await WordPressDotComAuthenticator().authenticate(from: self)
533531
} catch {
534-
WPAnalytics.track(.wpcomWebSignIn, properties: ["source": "me", "stage": "error", "error": "\(error)"])
535532
return
536533
}
537534

538-
WPAnalytics.track(.wpcomWebSignIn, properties: ["source": "me", "stage": "success"])
539-
540535
SVProgressHUD.show()
541536
let credentials = WordPressComCredentials(authToken: token, isJetpackLogin: false, multifactor: false)
542537
WordPressAuthenticator.shared.delegate!.sync(credentials: .init(wpcom: credentials)) {

WordPressAuthenticator/Sources/Authenticator/WordPressAuthenticator.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ import WordPressKit
165165
/// - restrictToWPCom: Whether only WordPress.com login is enabled.
166166
/// - onLoginButtonTapped: Called when the login button on the prologue screen is tapped.
167167
/// - Returns: The root view controller for the login flow.
168-
public class func loginUI(showCancel: Bool = false, restrictToWPCom: Bool = false, onLoginButtonTapped: (() -> Void)? = nil) -> UIViewController? {
168+
public class func loginUI(showCancel: Bool = false, restrictToWPCom: Bool = false, onLoginButtonTapped: (() -> Void)? = nil, continueWithDotCom: ((UIViewController) -> Bool)? = nil) -> UIViewController? {
169169
let storyboard = Storyboard.login.instance
170170
guard let controller = storyboard.instantiateInitialViewController() else {
171171
assertionFailure("Cannot instantiate initial login controller from Login.storyboard")
@@ -174,6 +174,7 @@ import WordPressKit
174174

175175
if let loginNavController = controller as? LoginNavigationController, let loginPrologueViewController = loginNavController.viewControllers.first as? LoginPrologueViewController {
176176
loginPrologueViewController.showCancel = showCancel
177+
loginPrologueViewController.continueWithDotComOverwrite = continueWithDotCom
177178
}
178179

179180
controller.modalPresentationStyle = .fullScreen

WordPressAuthenticator/Sources/Signin/LoginPrologueViewController.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class LoginPrologueViewController: LoginViewController {
1111
private var buttonViewController: NUXButtonViewController?
1212
private var stackedButtonsViewController: NUXStackedButtonsViewController?
1313
var showCancel = false
14+
var continueWithDotComOverwrite: ((UIViewController) -> Bool)? = nil
1415

1516
@IBOutlet private weak var buttonContainerView: UIView!
1617
/// Blur effect on button container view
@@ -526,6 +527,10 @@ class LoginPrologueViewController: LoginViewController {
526527
/// Unified "Continue with WordPress.com" prologue button action.
527528
///
528529
private func continueWithDotCom() {
530+
if let continueWithDotComOverwrite, continueWithDotComOverwrite(self) {
531+
return
532+
}
533+
529534
guard let vc = GetStartedViewController.instantiate(from: .getStarted) else {
530535
WPAuthenticatorLogError("Failed to navigate from LoginPrologueViewController to GetStartedViewController")
531536
return

0 commit comments

Comments
 (0)