From ab51d6986b5dda6b95ff5a15479318844a91983a Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 15 Aug 2022 14:43:47 +0800 Subject: [PATCH 1/6] Add a new configuration to show the magic link as a secondary action instead of a table view row in the password screen. --- .../WordPressAuthenticatorConfiguration.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/WordPressAuthenticator/Authenticator/WordPressAuthenticatorConfiguration.swift b/WordPressAuthenticator/Authenticator/WordPressAuthenticatorConfiguration.swift index 414890003..a638ad28a 100644 --- a/WordPressAuthenticator/Authenticator/WordPressAuthenticatorConfiguration.swift +++ b/WordPressAuthenticator/Authenticator/WordPressAuthenticatorConfiguration.swift @@ -108,6 +108,10 @@ public struct WordPressAuthenticatorConfiguration { /// If disabled, password is shown by default with an option to send a magic link. let isWPComMagicLinkPreferredToPassword: Bool + /// If enabled, the alternative magic link action on the password screen is shown as a secondary call-to-action at the bottom. + /// If disabled, the alternative magic link action on the password screen is shown below the reset password action. + let isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen: Bool + /// Designated Initializer /// public init (wpcomClientId: String, @@ -131,7 +135,8 @@ public struct WordPressAuthenticatorConfiguration { continueWithSiteAddressFirst: Bool = false, enableSiteCredentialsLoginForSelfHostedSites: Bool = false, isWPComLoginRequiredForSiteCredentialsLogin: Bool = false, - isWPComMagicLinkPreferredToPassword: Bool = false) { + isWPComMagicLinkPreferredToPassword: Bool = false, + isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen: Bool = false) { self.wpcomClientId = wpcomClientId self.wpcomSecret = wpcomSecret @@ -155,5 +160,6 @@ public struct WordPressAuthenticatorConfiguration { self.enableSiteCredentialsLoginForSelfHostedSites = enableSiteCredentialsLoginForSelfHostedSites self.isWPComLoginRequiredForSiteCredentialsLogin = isWPComLoginRequiredForSiteCredentialsLogin self.isWPComMagicLinkPreferredToPassword = isWPComMagicLinkPreferredToPassword + self.isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen = isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen } } From 86a3f4eb2a08954effe67a7439c9a442138e7e90 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 15 Aug 2022 14:45:38 +0800 Subject: [PATCH 2/6] Move magic link async request to a struct `MagicLinkRequester` for reuse in the password screen. --- .../project.pbxproj | 4 +++ .../Login/MagicLinkRequester.swift | 28 +++++++++++++++++++ .../Password/PasswordCoordinator.swift | 24 ++-------------- 3 files changed, 34 insertions(+), 22 deletions(-) create mode 100644 WordPressAuthenticator/Unified Auth/View Related/Login/MagicLinkRequester.swift diff --git a/WordPressAuthenticator.xcodeproj/project.pbxproj b/WordPressAuthenticator.xcodeproj/project.pbxproj index 4b033589b..a4a29bca8 100644 --- a/WordPressAuthenticator.xcodeproj/project.pbxproj +++ b/WordPressAuthenticator.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 020BE74A23B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020BE74923B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift */; }; + 020DEF6428AA091100C85D51 /* MagicLinkRequester.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020DEF6328AA091100C85D51 /* MagicLinkRequester.swift */; }; 02A526CA28A3499C00FD1812 /* MagicLinkRequestedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A526C828A3499C00FD1812 /* MagicLinkRequestedViewController.swift */; }; 02A526CB28A3499C00FD1812 /* MagicLinkRequestedViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 02A526C928A3499C00FD1812 /* MagicLinkRequestedViewController.xib */; }; 02A526CD28A3A23900FD1812 /* UIButton+Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A526CC28A3A23900FD1812 /* UIButton+Styles.swift */; }; @@ -213,6 +214,7 @@ /* Begin PBXFileReference section */ 020BE74923B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressAuthenticatorDisplayImages.swift; sourceTree = ""; }; + 020DEF6328AA091100C85D51 /* MagicLinkRequester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicLinkRequester.swift; sourceTree = ""; }; 02A526C828A3499C00FD1812 /* MagicLinkRequestedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicLinkRequestedViewController.swift; sourceTree = ""; }; 02A526C928A3499C00FD1812 /* MagicLinkRequestedViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MagicLinkRequestedViewController.xib; sourceTree = ""; }; 02A526CC28A3A23900FD1812 /* UIButton+Styles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Styles.swift"; sourceTree = ""; }; @@ -861,6 +863,7 @@ CE811D6824EDC14A00F4CCD6 /* LoginMagicLink.storyboard */, 02A526C828A3499C00FD1812 /* MagicLinkRequestedViewController.swift */, 02A526C928A3499C00FD1812 /* MagicLinkRequestedViewController.xib */, + 020DEF6328AA091100C85D51 /* MagicLinkRequester.swift */, ); path = Login; sourceTree = ""; @@ -1272,6 +1275,7 @@ 98C9195B2308E3DA00A90E12 /* AppleAuthenticator.swift in Sources */, B56090F9208A533200399AE4 /* WordPressAuthenticator+Events.swift in Sources */, CEDE0D93242011E000CB3345 /* NSObject+Helpers.swift in Sources */, + 020DEF6428AA091100C85D51 /* MagicLinkRequester.swift in Sources */, 020BE74A23B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift in Sources */, B560913A208A563800399AE4 /* LoginLinkRequestViewController.swift in Sources */, B560910C208A54F800399AE4 /* WordPressComOAuthClientFacade.m in Sources */, diff --git a/WordPressAuthenticator/Unified Auth/View Related/Login/MagicLinkRequester.swift b/WordPressAuthenticator/Unified Auth/View Related/Login/MagicLinkRequester.swift new file mode 100644 index 000000000..efb7409a9 --- /dev/null +++ b/WordPressAuthenticator/Unified Auth/View Related/Login/MagicLinkRequester.swift @@ -0,0 +1,28 @@ +import Foundation + +/// Encapsulates the async request for a magic link and email validation for use cases that send a magic link. +struct MagicLinkRequester { + /// Makes the call to request a magic authentication link be emailed to the user if possible. + func requestMagicLink(email: String, jetpackLogin: Bool) async -> Result { + await withCheckedContinuation { continuation in + guard email.isValidEmail() else { + return continuation.resume(returning: .failure(MagicLinkRequestError.invalidEmail)) + } + + let service = WordPressComAccountService() + service.requestAuthenticationLink(for: email, + jetpackLogin: jetpackLogin, + success: { + continuation.resume(returning: .success(())) + }, failure: { error in + continuation.resume(returning: .failure(error)) + }) + } + } +} + +extension MagicLinkRequester { + enum MagicLinkRequestError: Error { + case invalidEmail + } +} diff --git a/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordCoordinator.swift b/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordCoordinator.swift index 000baf75d..495f0b65e 100644 --- a/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordCoordinator.swift +++ b/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordCoordinator.swift @@ -39,30 +39,10 @@ final class PasswordCoordinator { } private extension PasswordCoordinator { - enum MagicLinkRequestError: Error { - case invalidEmail - } - /// Makes the call to request a magic authentication link be emailed to the user. func requestMagicLink() async -> Result { - await withCheckedContinuation { continuation in - loginFields.meta.emailMagicLinkSource = .login - - let email = loginFields.username - guard email.isValidEmail() else { - return continuation.resume(returning: .failure(MagicLinkRequestError.invalidEmail)) - } - - - let service = WordPressComAccountService() - service.requestAuthenticationLink(for: email, - jetpackLogin: loginFields.meta.jetpackLogin, - success: { - continuation.resume(returning: .success(())) - }, failure: { error in - continuation.resume(returning: .failure(error)) - }) - } + loginFields.meta.emailMagicLinkSource = .login + return await MagicLinkRequester().requestMagicLink(email: loginFields.username, jetpackLogin: loginFields.meta.jetpackLogin) } /// After a magic link is successfully sent, navigates the user to the requested screen. From f4144aae8952855bbebaf08c9171317a7cd61378 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 15 Aug 2022 14:48:16 +0800 Subject: [PATCH 3/6] PasswordViewController: add a bottom button stack view with a new secondary button for magic link login. --- .../View Related/Password/Password.storyboard | 72 +++++++----- .../Password/PasswordViewController.swift | 109 +++++++++++------- 2 files changed, 116 insertions(+), 65 deletions(-) diff --git a/WordPressAuthenticator/Unified Auth/View Related/Password/Password.storyboard b/WordPressAuthenticator/Unified Auth/View Related/Password/Password.storyboard index e0def54a5..85cfde6f2 100644 --- a/WordPressAuthenticator/Unified Auth/View Related/Password/Password.storyboard +++ b/WordPressAuthenticator/Unified Auth/View Related/Password/Password.storyboard @@ -1,10 +1,11 @@ - + - + + @@ -20,7 +21,7 @@ - + @@ -28,42 +29,56 @@ - + - + + + + + + + - + + - - + + - - + - - + + - + + @@ -73,10 +88,10 @@ - + @@ -88,4 +103,9 @@ + + + + + diff --git a/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordViewController.swift b/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordViewController.swift index a573fb899..b1666b61c 100644 --- a/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordViewController.swift +++ b/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordViewController.swift @@ -9,6 +9,7 @@ class PasswordViewController: LoginViewController { @IBOutlet private weak var tableView: UITableView! @IBOutlet var bottomContentConstraint: NSLayoutConstraint? + @IBOutlet private weak var secondaryButton: NUXButton! private weak var passwordField: UITextField? private var rows = [Row]() @@ -16,6 +17,8 @@ class PasswordViewController: LoginViewController { private var shouldChangeVoiceOverFocus: Bool = false private var loginLinkCell: TextLinkButtonTableViewCell? + private let isMagicLinkShownAsSecondaryAction: Bool = WordPressAuthenticator.shared.configuration.isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen + /// Depending on where we're coming from, this screen needs to track a password challenge /// (if logging on with a Social account) or not (if logging in through WP.com). /// @@ -51,6 +54,7 @@ class PasswordViewController: LoginViewController { defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 setTableViewMargins(forWidth: view.frame.width) + configureLoginWithMagicLinkButton() localizePrimaryButton() registerTableViewCells() loadRows() @@ -174,7 +178,7 @@ class PasswordViewController: LoginViewController { // Is everything filled out? if !loginFields.validateFieldsPopulatedForSignin() { - let errorMsg = Constants.missingInfoError + let errorMsg = Localization.missingInfoError displayError(message: errorMsg, moveVoiceOverFocus: true) return @@ -248,6 +252,50 @@ extension PasswordViewController: NUXKeyboardResponder { } +// MARK: - Magic Link + +private extension PasswordViewController { + func configureLoginWithMagicLinkButton() { + if isMagicLinkShownAsSecondaryAction { + secondaryButton.setTitle(Localization.loginWithMagicLink, for: .normal) + secondaryButton.accessibilityIdentifier = AccessibilityIdentifier.loginWithMagicLink + secondaryButton.on(.touchUpInside) { [weak self] _ in + Task { @MainActor [weak self] in + guard let self = self else { return } + self.secondaryButton.isEnabled = false + await self.loginWithMagicLink() + self.secondaryButton.isEnabled = true + } + } + } else { + secondaryButton.isHidden = true + } + } + + func loginWithMagicLink() async { + tracker.track(click: .requestMagicLink) + loginFields.meta.emailMagicLinkSource = .login + + configureViewLoading(true) + let result = await MagicLinkRequester().requestMagicLink(email: loginFields.username, jetpackLogin: loginFields.meta.jetpackLogin) + switch result { + case .success: + didRequestAuthenticationLink() + case .failure(let error): + switch error { + case MagicLinkRequester.MagicLinkRequestError.invalidEmail: + DDLogError("Attempted to request authentication link, but the email address did not appear valid.") + let alert = buildInvalidEmailAlert() + present(alert, animated: true, completion: nil) + default: + tracker.track(failure: error.localizedDescription) + displayError(error as NSError, sourceTag: sourceTag) + } + } + configureViewLoading(false) + } +} + // MARK: - Table Management private extension PasswordViewController { @@ -284,7 +332,10 @@ private extension PasswordViewController { } rows.append(.forgotPassword) - rows.append(.sendMagicLink) + + if !isMagicLinkShownAsSecondaryAction { + rows.append(.sendMagicLink) + } } /// Configure cells. @@ -394,7 +445,7 @@ private extension PasswordViewController { cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.getLoginLinkButtonTitle, accessibilityTrait: .link, showBorder: true) - cell.accessibilityIdentifier = "Get Login Link Button" + cell.accessibilityIdentifier = AccessibilityIdentifier.loginWithMagicLink // Save reference to the login link cell so it can be enabled/disabled. loginLinkCell = cell @@ -406,8 +457,9 @@ private extension PasswordViewController { cell.enableButton(false) - self.tracker.track(click: .requestMagicLink) - self.requestAuthenticationLink() + Task { @MainActor [weak self] in + await self?.loginWithMagicLink() + } } } @@ -441,40 +493,11 @@ private extension PasswordViewController { submitButton as Any ] - UIAccessibility.post(notification: .screenChanged, argument: passwordField) - } - - /// Makes the call to request a magic authentication link be emailed to the user. - /// - func requestAuthenticationLink() { - loginFields.meta.emailMagicLinkSource = .login - - let email = loginFields.username - guard email.isValidEmail() else { - DDLogError("Attempted to request authentication link, but the email address did not appear valid.") - let alert = buildInvalidEmailAlert() - present(alert, animated: true, completion: nil) - return + if isMagicLinkShownAsSecondaryAction { + view.accessibilityElements?.append(secondaryButton as Any) } - configureViewLoading(true) - let service = WordPressComAccountService() - service.requestAuthenticationLink(for: email, - jetpackLogin: loginFields.meta.jetpackLogin, - success: { [weak self] in - self?.didRequestAuthenticationLink() - self?.configureViewLoading(false) - - }, failure: { [weak self] (error: Error) in - guard let self = self else { - return - } - - self.tracker.track(failure: error.localizedDescription) - - self.displayError(error as NSError, sourceTag: self.sourceTag) - self.configureViewLoading(false) - }) + UIAccessibility.post(notification: .screenChanged, argument: passwordField) } /// When a magic link successfully sends, navigate the user to the next step. @@ -541,11 +564,19 @@ private extension PasswordViewController { } } } +} - /// Constants +private extension PasswordViewController { + /// Localization constants /// - struct Constants { + enum Localization { static let missingInfoError = NSLocalizedString("Please fill out all the fields", comment: "A short prompt asking the user to properly fill out all login fields.") + static let loginWithMagicLink = NSLocalizedString("Or log in with magic link", + comment: "The button title for a secondary call-to-action button on the password screen. When the user wants to try sending a magic link instead of entering a password.") + } + + enum AccessibilityIdentifier { + static let loginWithMagicLink = "Get Login Link Button" } } From 69fcfb2a17b9d1d6847c94361a9ce35fcd722449 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 15 Aug 2022 14:49:41 +0800 Subject: [PATCH 4/6] Bump pod version. --- WordPressAuthenticator.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressAuthenticator.podspec b/WordPressAuthenticator.podspec index 97f440b9c..50f3fa958 100644 --- a/WordPressAuthenticator.podspec +++ b/WordPressAuthenticator.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |s| s.name = 'WordPressAuthenticator' - s.version = '2.2.0' + s.version = '2.2.1-beta.1' s.summary = 'WordPressAuthenticator implements an easy and elegant way to authenticate your WordPress Apps.' s.description = <<-DESC From 2f8395f1b4994d930e673f81cb36833840539bf2 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 15 Aug 2022 16:00:32 +0800 Subject: [PATCH 5/6] Analytics: track a different flow value when magic link emphasis configuration is enabled. --- .../Analytics/AuthenticatorAnalyticsTracker.swift | 5 +++++ .../View Related/Password/PasswordViewController.swift | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/WordPressAuthenticator/Analytics/AuthenticatorAnalyticsTracker.swift b/WordPressAuthenticator/Analytics/AuthenticatorAnalyticsTracker.swift index 83738bfee..58d689c1f 100644 --- a/WordPressAuthenticator/Analytics/AuthenticatorAnalyticsTracker.swift +++ b/WordPressAuthenticator/Analytics/AuthenticatorAnalyticsTracker.swift @@ -74,6 +74,11 @@ public class AuthenticatorAnalyticsTracker { /// case loginWithPassword = "login_password" + /// This flow starts when the user decides to login with a password instead, with magic link logic emphasis + /// where the CTA is a secondary CTA instead of a table view row + /// + case loginWithPasswordWithMagicLinkEmphasis = "login_password_magic_link_emphasis" + /// This flow starts when the user decides to log in with their site address /// case loginWithSiteAddress = "login_site_address" diff --git a/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordViewController.swift b/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordViewController.swift index b1666b61c..ed69d24fb 100644 --- a/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordViewController.swift +++ b/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordViewController.swift @@ -79,7 +79,7 @@ class PasswordViewController: LoginViewController { tracker.set(step: .passwordChallenge) } } else { - tracker.set(flow: .loginWithPassword) + tracker.set(flow: isMagicLinkShownAsSecondaryAction ? .loginWithPasswordWithMagicLinkEmphasis : .loginWithPassword) if isMovingToParent { tracker.track(step: .start) From 68a874849374f1cfc11c0ee8c4a82b75b4e7afeb Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Tue, 16 Aug 2022 14:24:19 +0800 Subject: [PATCH 6/6] Disable submit button and show a spinner in the magic link button when requesting a magic link. --- .../Password/PasswordViewController.swift | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordViewController.swift b/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordViewController.swift index ed69d24fb..48f7e867a 100644 --- a/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordViewController.swift +++ b/WordPressAuthenticator/Unified Auth/View Related/Password/PasswordViewController.swift @@ -276,7 +276,7 @@ private extension PasswordViewController { tracker.track(click: .requestMagicLink) loginFields.meta.emailMagicLinkSource = .login - configureViewLoading(true) + updateLoadingUI(isRequestingMagicLink: true) let result = await MagicLinkRequester().requestMagicLink(email: loginFields.username, jetpackLogin: loginFields.meta.jetpackLogin) switch result { case .success: @@ -292,7 +292,25 @@ private extension PasswordViewController { displayError(error as NSError, sourceTag: sourceTag) } } - configureViewLoading(false) + updateLoadingUI(isRequestingMagicLink: false) + } + + func updateLoadingUI(isRequestingMagicLink: Bool) { + if isRequestingMagicLink { + if isMagicLinkShownAsSecondaryAction { + submitButton?.isEnabled = false + secondaryButton.showActivityIndicator(true) + } else { + configureViewLoading(true) + } + } else { + if isMagicLinkShownAsSecondaryAction { + submitButton?.isEnabled = true + secondaryButton.showActivityIndicator(false) + } else { + configureViewLoading(false) + } + } } }