Skip to content

Commit b71385b

Browse files
authored
[Login] Improve error messaging displaying a new error screen when application password is disabled (#15031)
2 parents f5ccde0 + 5dd3e83 commit b71385b

File tree

9 files changed

+172
-59
lines changed

9 files changed

+172
-59
lines changed

Networking/Networking/ApplicationPassword/RequestAuthenticator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
22

3-
enum RequestAuthenticatorError: Error {
3+
public enum RequestAuthenticatorError: Error {
44
case applicationPasswordUseCaseNotAvailable
55
case applicationPasswordNotAvailable
66
}

RELEASE-NOTES.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111
- [*] Background image upload: Fix missing error notice in iPhones [https://github.com/woocommerce/woocommerce-ios/pull/15117]
1212
- [*] Background image upload: Show a notice when the user leaves product details while uploads are pending [https://github.com/woocommerce/woocommerce-ios/pull/15134]
1313
- [*] Filters applied in product selector no longer affect the main product list screen. [https://github.com/woocommerce/woocommerce-ios/pull/14764]
14+
- [*] Better error messages if Application Password login is disabled on user's website. [https://github.com/woocommerce/woocommerce-ios/pull/15031]
1415
- [**] Product Images: Update error handling [https://github.com/woocommerce/woocommerce-ios/pull/15105]
1516

1617
21.7
1718
-----
1819
- [**] Fixed an issue with the WordPress Media Library not loading due to a decoding error in media details sizes. [https://github.com/woocommerce/woocommerce-ios/pull/15056]
19-
- [*] Orders: Improved accessibility UI for Tax Rates related views [https://github.com/woocommerce/woocommerce-ios/pull/15043]
20+
- [*] Now, usernames and emails in text fields across multiple login views are no longer capitalized. [https://github.com/woocommerce/woocommerce-ios/pull/15002]
2021
- [*] Product Details: Display cover tag on the first product image [https://github.com/woocommerce/woocommerce-ios/pull/15041]
2122
- [*] Payments: Update learn more links to open Stripe-specific docs when using that gateway [https://github.com/woocommerce/woocommerce-ios/pull/15035]
23+
- [*] Orders: Improved accessibility UI for Tax Rates related views [https://github.com/woocommerce/woocommerce-ios/pull/15043]
24+
- [*] Product Details: Display cover tag on the first product image [https://github.com/woocommerce/woocommerce-ios/pull/15041]
2225
- [*] Now, usernames and emails in text fields across multiple login views are no longer capitalized. [https://github.com/woocommerce/woocommerce-ios/pull/15002]
2326

2427
21.6

WooCommerce/Classes/Authentication/Application Password/ApplicationPasswordAuthorizationViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Yosemite
44
/// View model for `ApplicationPasswordAuthorizationWebViewController`.
55
///
66
final class ApplicationPasswordAuthorizationViewModel {
7-
private let siteURL: String
7+
let siteURL: String
88
private let stores: StoresManager
99

1010
init(siteURL: String,

WooCommerce/Classes/Authentication/Application Password/ApplicationPasswordAuthorizationWebViewController.swift

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import UIKit
33
import WebKit
44
import protocol WooFoundation.Analytics
55
import struct Networking.ApplicationPassword
6+
import enum Networking.RequestAuthenticatorError
67

78
/// View with embedded web view to authorize application password for a site.
89
///
@@ -34,6 +35,9 @@ final class ApplicationPasswordAuthorizationWebViewController: UIViewController
3435
return indicator
3536
}()
3637

38+
/// The view controller that was presenting the application password flow.
39+
private var previousViewController: UIViewController?
40+
3741
/// WP Core requires that the UUID has lowercased letters.
3842
private let appID = UUID().uuidString.lowercased()
3943

@@ -50,9 +54,11 @@ final class ApplicationPasswordAuthorizationWebViewController: UIViewController
5054
private var authorizationURL: String?
5155

5256
init(viewModel: ApplicationPasswordAuthorizationViewModel,
57+
previousViewController: UIViewController?,
5358
analytics: Analytics = ServiceLocator.analytics,
5459
onSuccess: @escaping (ApplicationPassword, UINavigationController?) -> Void) {
5560
self.viewModel = viewModel
61+
self.previousViewController = previousViewController
5662
self.onSuccess = onSuccess
5763
self.analytics = analytics
5864
super.init(nibName: nil, bundle: nil)
@@ -157,20 +163,46 @@ private extension ApplicationPasswordAuthorizationWebViewController {
157163
guard let url = try await viewModel.fetchAuthURL() else {
158164
DDLogError("⛔️ No authorization URL found for application passwords")
159165
analytics.track(.applicationPasswordAuthorizationURLNotAvailable)
160-
return showErrorAlert(message: Localization.applicationPasswordDisabled)
166+
navigateToApplicationPasswordDisabledUI()
167+
return
161168
}
162169
loadAuthorizationPage(url: url)
163170
} catch {
164171
DDLogError("⛔️ Error fetching authorization URL for application passwords \(error)")
165172
analytics.track(.applicationPasswordAuthorizationURLFetchFailed, withError: error)
166-
showErrorAlert(message: Localization.errorFetchingAuthURL, onRetry: { [weak self] in
167-
self?.fetchAuthorizationURL()
168-
})
173+
if let authError = error as? Networking.RequestAuthenticatorError,
174+
authError == .applicationPasswordNotAvailable {
175+
navigateToApplicationPasswordDisabledUI()
176+
} else {
177+
showErrorAlert(message: Localization.errorFetchingAuthURL)
178+
}
169179
}
170180
activityIndicator.stopAnimating()
171181
}
172182
}
173183

184+
/// Pops to the previous view controller (if provided) or pops one level otherwise.
185+
@objc private func navigateToPreviousViewController() {
186+
if let previousViewController, let navigationController {
187+
navigationController.popToViewController(previousViewController, animated: true)
188+
} else {
189+
navigationController?.popViewController(animated: true)
190+
}
191+
}
192+
193+
private func navigateToApplicationPasswordDisabledUI() {
194+
let errorUI = applicationPasswordDisabledUI(for: viewModel.siteURL)
195+
// When the error view controller is popped, navigate to previous VC
196+
errorUI.navigationItem.leftBarButtonItem = UIBarButtonItem(
197+
image: UIImage(systemName: "chevron.backward"),
198+
style: .plain,
199+
target: self,
200+
action: #selector(navigateToPreviousViewController)
201+
)
202+
// Push instead of present
203+
navigationController?.pushViewController(errorUI, animated: true)
204+
}
205+
174206
func loadAuthorizationPage(url: URL) {
175207
let parameters: [URLQueryItem] = [
176208
URLQueryItem(name: Constants.Query.appName, value: appName),
@@ -220,6 +252,14 @@ private extension ApplicationPasswordAuthorizationWebViewController {
220252
}
221253
present(alertController, animated: true)
222254
}
255+
256+
/// The error screen to be displayed when the user tries to log in with site credentials
257+
/// with application password disabled.
258+
///
259+
func applicationPasswordDisabledUI(for siteURL: String) -> UIViewController {
260+
let viewModel = ApplicationPasswordDisabledViewModel(siteURL: siteURL, previousViewController: previousViewController)
261+
return ULErrorViewController(viewModel: viewModel)
262+
}
223263
}
224264

225265
extension ApplicationPasswordAuthorizationWebViewController: WKNavigationDelegate {

WooCommerce/Classes/Authentication/AuthenticationManager.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -685,9 +685,10 @@ private extension AuthenticationManager {
685685

686686
/// Web view to authorize application password for a given site.
687687
///
688-
func applicationPasswordWebView(for siteURL: String) -> UIViewController {
688+
func applicationPasswordWebView(for siteURL: String, previousVC: UIViewController?) -> UIViewController {
689689
let viewModel = ApplicationPasswordAuthorizationViewModel(siteURL: siteURL)
690690
let controller = ApplicationPasswordAuthorizationWebViewController(viewModel: viewModel,
691+
previousViewController: previousVC,
691692
onSuccess: { [weak self] applicationPassword, navigationController in
692693
guard let navigationController else {
693694
DDLogInfo("⚠️ No navigation controller found")
@@ -754,13 +755,15 @@ private extension AuthenticationManager {
754755
) else {
755756
return assertionFailure("⛔️ Error creating application password use case")
756757
}
757-
checkSiteCredentialLogin(to: siteURL, with: useCase, in: navigationController)
758+
checkSiteCredentialLogin(to: siteURL, with: useCase, in: navigationController, previousViewController: nil)
758759
}
759760

760761
func checkSiteCredentialLogin(to siteURL: String,
761762
with useCase: ApplicationPasswordUseCase,
762-
in navigationController: UINavigationController) {
763-
let checker = PostSiteCredentialLoginChecker(applicationPasswordUseCase: useCase)
763+
in navigationController: UINavigationController,
764+
previousViewController: UIViewController? = nil) {
765+
let checker = PostSiteCredentialLoginChecker(applicationPasswordUseCase: useCase,
766+
previousViewController: previousViewController)
764767
checker.checkEligibility(for: siteURL, from: navigationController) { [weak self] in
765768
guard let self else { return }
766769
// Tracking `signedIn` after the user logged in using site creds & application password is created
@@ -806,7 +809,7 @@ private extension AuthenticationManager {
806809
/// Presents app password site login using a web view.
807810
///
808811
private func presentApplicationPasswordWebView(for siteURL: String, in viewController: UIViewController) {
809-
let webViewController = applicationPasswordWebView(for: siteURL)
812+
let webViewController = applicationPasswordWebView(for: siteURL, previousVC: viewController)
810813
viewController.navigationController?.pushViewController(webViewController, animated: true)
811814
}
812815
}

WooCommerce/Classes/Authentication/Navigation Exceptions/ApplicationPasswordDisabledViewModel.swift

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@ import WordPressAuthenticator
55
/// modeling an error when application password is disabled.
66
///
77
struct ApplicationPasswordDisabledViewModel: ULErrorViewModel {
8-
init(siteURL: String) {
8+
init(siteURL: String,
9+
previousViewController: UIViewController?,
10+
authentication: Authentication = ServiceLocator.authenticationManager) {
911
self.siteURL = siteURL
12+
self.previousViewController = previousViewController
13+
self.authentication = authentication
1014
}
1115

1216
let siteURL: String
13-
let image: UIImage = .errorImage // TODO: update this if needed
17+
let authentication: Authentication
18+
let image: UIImage = .errorImage
19+
20+
// The VC that was showing before the application password flow. This is used to navigate back without guesswork.
21+
let previousViewController: UIViewController?
1422

1523
var text: NSAttributedString {
1624
let font: UIFont = .body
@@ -20,7 +28,7 @@ struct ApplicationPasswordDisabledViewModel: ULErrorViewModel {
2028
attributes: [.font: boldFont])
2129
let message = NSMutableAttributedString(string: Localization.errorMessage)
2230

23-
message.replaceFirstOccurrence(of: "%@", with: boldSiteAddress)
31+
message.replaceFirstOccurrence(of: "%1$@", with: boldSiteAddress)
2432

2533
return message
2634
}
@@ -34,14 +42,16 @@ struct ApplicationPasswordDisabledViewModel: ULErrorViewModel {
3442
let secondaryButtonTitle = Localization.secondaryButtonTitle
3543

3644
func viewDidLoad(_ viewController: UIViewController?) {
37-
// TODO: add tracks if necessary
3845
}
3946

47+
// Pop to the previous VC
4048
func didTapPrimaryButton(in viewController: UIViewController?) {
41-
guard let viewController else {
42-
return
49+
guard let navigationController = viewController?.navigationController else { return }
50+
if let previousViewController {
51+
navigationController.popToViewController(previousViewController, animated: true)
52+
} else {
53+
navigationController.popViewController(animated: true)
4354
}
44-
WordPressAuthenticator.showLoginForJustWPCom(from: viewController)
4555
}
4656

4757
func didTapSecondaryButton(in viewController: UIViewController?) {
@@ -55,28 +65,48 @@ struct ApplicationPasswordDisabledViewModel: ULErrorViewModel {
5565
}
5666
WebviewHelper.launch(Constants.applicationPasswordLink, with: viewController)
5767
}
68+
69+
var rightBarButtonItemTitle: String? {
70+
return Localization.helpButtonTitle
71+
}
72+
73+
func didTapRightBarButtonItem(in viewController: UIViewController?) {
74+
guard let viewController else {
75+
return
76+
}
77+
authentication.presentSupport(from: viewController, screen: .noWooError)
78+
}
5879
}
5980

6081
private extension ApplicationPasswordDisabledViewModel {
6182
enum Localization {
6283
static let errorMessage = NSLocalizedString(
63-
"It seems that your site %@ has Application Password disabled. Please enable it to use the WooCommerce app.",
84+
"applicationPasswordDisabled.errorMessage",
85+
value: "It seems that your site %1$@ has Application Password disabled. Please enable it to use the WooCommerce app.",
6486
comment: "An error message displayed when the user tries to log in to the app with site credentials but has application password disabled. " +
6587
"Reads like: It seems that your site google.com has Application Password disabled. " +
6688
"Please enable it to use the WooCommerce app."
6789
)
90+
static let primaryButtonTitle = NSLocalizedString(
91+
"applicationPasswordDisabled.retry.buttonTitle",
92+
value: "Retry",
93+
comment: "Button to retry fetching application password authorization if application password is disabled"
94+
)
6895
static let secondaryButtonTitle = NSLocalizedString(
69-
"Log In With Another Account",
96+
"applicationPasswordDisabled.secondaryButtonTitle",
97+
value: "Log In With Another Account",
7098
comment: "Action button that will restart the login flow."
7199
+ "Presented when the user tries to log in to the app with site credentials but has application password disabled."
72100
)
73101
static let auxiliaryButtonTitle = NSLocalizedString(
74-
"What is Application Password?",
102+
"applicationPasswordDisabled.auxiliaryButtonTitle",
103+
value: "What is Application Password?",
75104
comment: "Button that will navigate to a web page explaining Application Password"
76105
)
77-
static let primaryButtonTitle = NSLocalizedString(
78-
"Log in with WordPress.com",
79-
comment: "Button that will navigate to the authentication flow with WP.com"
106+
static let helpButtonTitle = NSLocalizedString(
107+
"applicationPasswordDisabled.helpButtonTitle",
108+
value: "Help",
109+
comment: "Button that will navigate to the support area"
80110
)
81111
}
82112
enum Constants {

WooCommerce/Classes/Authentication/PostSiteCredentialLoginChecker.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@ final class PostSiteCredentialLoginChecker {
1414
private let applicationPasswordUseCase: ApplicationPasswordUseCase
1515
private let roleEligibilityUseCase: RoleEligibilityUseCaseProtocol
1616
private let analytics: Analytics
17+
private let previousViewController: UIViewController?
1718

1819
init(applicationPasswordUseCase: ApplicationPasswordUseCase,
1920
roleEligibilityUseCase: RoleEligibilityUseCaseProtocol = RoleEligibilityUseCase(stores: ServiceLocator.stores),
2021
stores: StoresManager = ServiceLocator.stores,
21-
analytics: Analytics = ServiceLocator.analytics) {
22+
analytics: Analytics = ServiceLocator.analytics,
23+
previousViewController: UIViewController?) {
2224
self.applicationPasswordUseCase = applicationPasswordUseCase
2325
self.roleEligibilityUseCase = roleEligibilityUseCase
2426
self.stores = stores
2527
self.analytics = analytics
28+
self.previousViewController = previousViewController
2629
}
2730

2831
/// Checks whether the user is eligible to use the app.
@@ -56,8 +59,8 @@ private extension PostSiteCredentialLoginChecker {
5659
analytics.track(event: .ApplicationPassword.applicationPasswordGenerationFailed(scenario: .generation, error: error))
5760
switch error {
5861
case ApplicationPasswordUseCaseError.applicationPasswordsDisabled:
59-
// show application password disabled error
60-
let errorUI = applicationPasswordDisabledUI(for: siteURL)
62+
// show application password disabled error, and use the previous view controller.
63+
let errorUI = applicationPasswordDisabledUI(for: siteURL, previousViewController: previousViewController)
6164
navigationController.show(errorUI, sender: nil)
6265
case ApplicationPasswordUseCaseError.unauthorizedRequest:
6366
showAlert(message: Localization.unauthorizedForAppPassword, in: navigationController)
@@ -179,8 +182,8 @@ private extension PostSiteCredentialLoginChecker {
179182
/// The error screen to be displayed when the user tries to log in with site credentials
180183
/// with application password disabled.
181184
///
182-
func applicationPasswordDisabledUI(for siteURL: String) -> UIViewController {
183-
let viewModel = ApplicationPasswordDisabledViewModel(siteURL: siteURL)
185+
func applicationPasswordDisabledUI(for siteURL: String, previousViewController: UIViewController?) -> UIViewController {
186+
let viewModel = ApplicationPasswordDisabledViewModel(siteURL: siteURL, previousViewController: previousViewController)
184187
return ULErrorViewController(viewModel: viewModel)
185188
}
186189
}

0 commit comments

Comments
 (0)