Skip to content

Commit 1e6ee6c

Browse files
authored
Merge pull request #8473 from woocommerce/feat/8453-check-role-eligibility
REST API: Check application password availability after logging in
2 parents 8bb8110 + d157c88 commit 1e6ee6c

File tree

14 files changed

+304
-46
lines changed

14 files changed

+304
-46
lines changed

Networking/Networking/ApplicationPassword/ApplicationPasswordUseCase.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,23 @@ import WordPressShared
33
import WordPressKit
44
import enum Alamofire.AFError
55

6-
enum ApplicationPasswordUseCaseError: Error {
6+
public enum ApplicationPasswordUseCaseError: Error {
77
case duplicateName
88
case applicationPasswordsDisabled
99
case failedToConstructLoginOrAdminURLUsingSiteAddress
1010
}
1111

12-
struct ApplicationPassword {
12+
public struct ApplicationPassword {
1313
/// WordPress org username that the application password belongs to
1414
///
15-
let wpOrgUsername: String
15+
public let wpOrgUsername: String
1616

1717
/// Application password
1818
///
19-
let password: Secret<String>
19+
public let password: Secret<String>
2020
}
2121

22-
protocol ApplicationPasswordUseCase {
22+
public protocol ApplicationPasswordUseCase {
2323
/// Returns the locally saved ApplicationPassword if available
2424
///
2525
var applicationPassword: ApplicationPassword? { get }
@@ -37,7 +37,7 @@ protocol ApplicationPasswordUseCase {
3737
func deletePassword() async throws
3838
}
3939

40-
final class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase {
40+
final public class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase {
4141
/// Site Address
4242
///
4343
private let siteAddress: String
@@ -64,10 +64,10 @@ final class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase {
6464
}
6565
}
6666

67-
init(username: String,
68-
password: String,
69-
siteAddress: String,
70-
network: Network? = nil) throws {
67+
public init(username: String,
68+
password: String,
69+
siteAddress: String,
70+
network: Network? = nil) throws {
7171
self.siteAddress = siteAddress
7272
self.username = username
7373

@@ -92,7 +92,7 @@ final class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase {
9292

9393
/// Returns the locally saved ApplicationPassword if available
9494
///
95-
var applicationPassword: ApplicationPassword? {
95+
public var applicationPassword: ApplicationPassword? {
9696
storage.applicationPassword
9797
}
9898

@@ -102,7 +102,7 @@ final class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase {
102102
///
103103
/// - Returns: Generated `ApplicationPassword` instance
104104
///
105-
func generateNewPassword() async throws -> ApplicationPassword {
105+
public func generateNewPassword() async throws -> ApplicationPassword {
106106
async let password = try {
107107
do {
108108
return try await createApplicationPassword()
@@ -121,7 +121,7 @@ final class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase {
121121
///
122122
/// Deletes locally and also sends an API request to delete it from the site
123123
///
124-
func deletePassword() async throws {
124+
public func deletePassword() async throws {
125125
try await deleteApplicationPassword()
126126
}
127127
}

WooCommerce/Classes/Authentication/AuthenticationManager.swift

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import KeychainAccess
33
import WordPressAuthenticator
44
import WordPressKit
55
import Yosemite
6+
import WordPressUI
67
import class Networking.UserAgent
78
import enum Experiments.ABTest
89
import struct Networking.Settings
910
import protocol Experiments.FeatureFlagService
1011
import protocol Storage.StorageManagerType
11-
12+
import protocol Networking.ApplicationPasswordUseCase
13+
import class Networking.DefaultApplicationPasswordUseCase
14+
import enum Networking.ApplicationPasswordUseCaseError
1215

1316
/// Encapsulates all of the interactions with the WordPress Authenticator
1417
///
@@ -45,6 +48,9 @@ class AuthenticationManager: Authentication {
4548

4649
private let analytics: Analytics
4750

51+
/// Keep strong reference of the use case to check for application password availability if necessary.
52+
private var applicationPasswordUseCase: ApplicationPasswordUseCase?
53+
4854
init(storageManager: StorageManagerType = ServiceLocator.storageManager,
4955
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
5056
analytics: Analytics = ServiceLocator.analytics) {
@@ -796,15 +802,68 @@ private extension AuthenticationManager {
796802
return accountMismatchUI(for: site.url, siteCredentials: nil, with: matcher, in: navigationController)
797803
}
798804

805+
/// The error screen to be displayed when the user tries to log in with site credentials
806+
/// with application password disabled.
807+
///
808+
func applicationPasswordDisabledUI(for siteURL: String) -> UIViewController {
809+
let viewModel = ApplicationPasswordDisabledViewModel(siteURL: siteURL)
810+
return ULErrorViewController(viewModel: viewModel)
811+
}
812+
799813
/// Checks if the authenticated user is eligible to use the app and navigates to the home screen.
800814
///
801815
func didAuthenticateUser(to siteURL: String,
802816
with siteCredentials: WordPressOrgCredentials,
803817
in navigationController: UINavigationController,
804818
source: SignInSource?) {
805-
// TODO: check if application password is enabled & check for role eligibility & check for Woo
806-
// then navigate to home screen immediately with a placeholder store ID
807-
startStorePicker(with: WooConstants.placeholderStoreID, in: navigationController)
819+
// check if application password is enabled
820+
guard let applicationPasswordUseCase = try? DefaultApplicationPasswordUseCase(
821+
username: siteCredentials.username,
822+
password: siteCredentials.password,
823+
siteAddress: siteCredentials.siteURL
824+
) else {
825+
return assertionFailure("⛔️ Error creating application password use case")
826+
}
827+
self.applicationPasswordUseCase = applicationPasswordUseCase
828+
checkApplicationPassword(for: siteURL,
829+
with: applicationPasswordUseCase,
830+
in: navigationController) { [weak self] in
831+
guard let self else { return }
832+
// TODO: check for role eligibility & check for Woo
833+
// navigates to home screen immediately with a placeholder store ID
834+
self.startStorePicker(with: WooConstants.placeholderStoreID, in: navigationController)
835+
}
836+
}
837+
838+
func checkApplicationPassword(for siteURL: String,
839+
with useCase: ApplicationPasswordUseCase,
840+
in navigationController: UINavigationController, onSuccess: @escaping () -> Void) {
841+
Task {
842+
do {
843+
let _ = try await useCase.generateNewPassword()
844+
await MainActor.run {
845+
onSuccess()
846+
}
847+
} catch ApplicationPasswordUseCaseError.applicationPasswordsDisabled {
848+
// show application password disabled error
849+
await MainActor.run {
850+
let errorUI = applicationPasswordDisabledUI(for: siteURL)
851+
navigationController.show(errorUI, sender: nil)
852+
}
853+
} catch {
854+
// show generic error
855+
await MainActor.run {
856+
DDLogError("⛔️ Error generating application password: \(error)")
857+
let alert = FancyAlertViewController.makeApplicationPasswordAlert(retryAction: { [weak self] in
858+
self?.checkApplicationPassword(for: siteURL, with: useCase, in: navigationController, onSuccess: onSuccess)
859+
}, restartLoginAction: {
860+
ServiceLocator.stores.deauthenticate()
861+
navigationController.popToRootViewController(animated: true)
862+
})
863+
navigationController.present(alert, animated: true)
864+
}
865+
}
866+
}
808867
}
809868
}
810869

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import UIKit
2+
3+
/// Configuration and actions for an ULErrorViewController,
4+
/// modeling an error when application password is disabled.
5+
///
6+
struct ApplicationPasswordDisabledViewModel: ULErrorViewModel {
7+
init(siteURL: String) {
8+
self.siteURL = siteURL
9+
}
10+
11+
let siteURL: String
12+
let image: UIImage = .errorImage // TODO: update this if needed
13+
14+
var text: NSAttributedString {
15+
let font: UIFont = .body
16+
let boldFont: UIFont = font.bold
17+
18+
let boldSiteAddress = NSAttributedString(string: siteURL.trimHTTPScheme(),
19+
attributes: [.font: boldFont])
20+
let message = NSMutableAttributedString(string: Localization.errorMessage)
21+
22+
message.replaceFirstOccurrence(of: "%@", with: boldSiteAddress)
23+
24+
return message
25+
}
26+
27+
let isAuxiliaryButtonHidden = false
28+
let auxiliaryButtonTitle = Localization.auxiliaryButtonTitle
29+
30+
let primaryButtonTitle = ""
31+
let isPrimaryButtonHidden = true
32+
33+
let secondaryButtonTitle = Localization.secondaryButtonTitle
34+
35+
func viewDidLoad(_ viewController: UIViewController?) {
36+
// TODO: add tracks if necessary
37+
}
38+
39+
func didTapPrimaryButton(in viewController: UIViewController?) {
40+
// no-op
41+
}
42+
43+
func didTapSecondaryButton(in viewController: UIViewController?) {
44+
ServiceLocator.stores.deauthenticate()
45+
viewController?.navigationController?.popToRootViewController(animated: true)
46+
}
47+
48+
func didTapAuxiliaryButton(in viewController: UIViewController?) {
49+
guard let viewController else {
50+
return
51+
}
52+
WebviewHelper.launch(Constants.applicationPasswordLink, with: viewController)
53+
}
54+
}
55+
56+
private extension ApplicationPasswordDisabledViewModel {
57+
enum Localization {
58+
static let errorMessage = NSLocalizedString(
59+
"It seems that your site %@ has Application Password disabled. Please enable it to use the WooCommerce app.",
60+
comment: "An error message displayed when the user tries to log in to the app with site credentials but has application password disabled. " +
61+
"Reads like: It seems that your site google.com has Application Password disabled. " +
62+
"Please enable it to use the WooCommerce app."
63+
)
64+
static let secondaryButtonTitle = NSLocalizedString(
65+
"Log In With Another Account",
66+
comment: "Action button that will restart the login flow."
67+
+ "Presented when the user tries to log in to the app with site credentials but has application password disabled."
68+
)
69+
static let auxiliaryButtonTitle = NSLocalizedString(
70+
"What is Application Password?",
71+
comment: "Button that will navigate to a web page explaining Application Password"
72+
)
73+
}
74+
enum Constants {
75+
static let applicationPasswordLink = "https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/"
76+
}
77+
}

WooCommerce/Classes/ViewRelated/AppCoordinator.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ private extension AppCoordinator {
216216
return
217217
}
218218

219+
/// If authenticating with site credentials only is incomplete,
220+
/// show the prologue screen to force the user to log in again.
221+
guard stores.isAuthenticatedWithoutWPCom == false else {
222+
return displayAuthenticatorWithOnboardingIfNeeded()
223+
}
219224
configureAuthenticator()
220225

221226
let matcher = ULAccountMatcher(storageManager: storageManager)

WooCommerce/Classes/ViewRelated/Fancy Alerts/FancyAlertViewController+UnifiedLogin.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ extension FancyAlertViewController {
5353
let controller = FancyAlertViewController.controllerWithConfiguration(configuration: config)
5454
return controller
5555
}
56+
57+
static func makeApplicationPasswordAlert(retryAction: @escaping () -> Void,
58+
restartLoginAction: @escaping () -> Void) -> FancyAlertViewController {
59+
let retryButton = makeRetryButtonConfig(retryAction: retryAction)
60+
let loginButton = makeLoginButtonConfig(loginAction: restartLoginAction)
61+
let config = FancyAlertViewController.Config(titleText: Localization.cannotLogin,
62+
bodyText: Localization.applicationPasswordError,
63+
headerImage: nil,
64+
dividerPosition: .top,
65+
defaultButton: loginButton,
66+
cancelButton: retryButton)
67+
let controller = FancyAlertViewController.controllerWithConfiguration(configuration: config)
68+
return controller
69+
}
5670
}
5771

5872

@@ -107,6 +121,16 @@ private extension FancyAlertViewController {
107121
comment: "Description of alert for suggestion on how to connect to a WP.com site" +
108122
"Presented when a user logs in with an email that does not have access to a WP.com site"
109123
)
124+
static let applicationPasswordError = NSLocalizedString(
125+
"Error fetching application password for your site.",
126+
comment: "Error message displayed when application password cannot be fetched after authentication."
127+
)
128+
static let retryButton = NSLocalizedString("Try Again", comment: "Button to refetch application password for the current site")
129+
static let retryLoginButton = NSLocalizedString("Log In With Another Account", comment: "Button to restart the login flow.")
130+
static let cannotLogin = NSLocalizedString(
131+
"Cannot log in",
132+
comment: "Title of the alert displayed when application password cannot be fetched after authentication"
133+
)
110134
}
111135

112136
enum Strings {
@@ -124,6 +148,20 @@ private extension FancyAlertViewController {
124148
}
125149
}
126150

151+
static func makeRetryButtonConfig(retryAction: @escaping () -> Void) -> FancyAlertViewController.Config.ButtonConfig {
152+
return FancyAlertViewController.Config.ButtonConfig(Localization.retryButton) { controller, _ in
153+
controller.dismiss(animated: true)
154+
retryAction()
155+
}
156+
}
157+
158+
static func makeLoginButtonConfig(loginAction: @escaping () -> Void) -> FancyAlertViewController.Config.ButtonConfig {
159+
return FancyAlertViewController.Config.ButtonConfig(Localization.retryLoginButton) { controller, _ in
160+
controller.dismiss(animated: true)
161+
loginAction()
162+
}
163+
}
164+
127165
static func makeLearnMoreAboutJetpackButtonConfig(analytics: Analytics) -> FancyAlertViewController.Config.ButtonConfig {
128166
return FancyAlertViewController.Config.ButtonConfig(Localization.learnMore) { controller, _ in
129167
guard let url = URL(string: Strings.whatsJetpackURLString) else {

WooCommerce/Classes/Yosemite/DefaultStoresManager.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ class DefaultStoresManager: StoresManager {
6161
return state is AuthenticatedState
6262
}
6363

64+
/// Indicates if the StoresManager is currently authenticated with site credentials only.
65+
///
66+
var isAuthenticatedWithoutWPCom: Bool {
67+
if case .wporg = sessionManager.defaultCredentials {
68+
return true
69+
}
70+
return false
71+
}
72+
6473
@Published private var isLoggedIn: Bool = false
6574

6675
var isLoggedInPublisher: AnyPublisher<Bool, Never> {

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,6 +1786,7 @@
17861786
DE279BAF26EA03EA002BA963 /* ShippingLabelSinglePackageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE279BAE26EA03EA002BA963 /* ShippingLabelSinglePackageViewModelTests.swift */; };
17871787
DE279BB126EA184A002BA963 /* ShippingLabelPackageListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE279BB026EA184A002BA963 /* ShippingLabelPackageListViewModel.swift */; };
17881788
DE2BF4FD2846192B00FBE68A /* CouponAllowedEmailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2BF4FC2846192B00FBE68A /* CouponAllowedEmailsViewModelTests.swift */; };
1789+
DE2E8EB729547771002E4B14 /* ApplicationPasswordDisabledViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2E8EB629547771002E4B14 /* ApplicationPasswordDisabledViewModel.swift */; };
17891790
DE2FE5812923729A0018040A /* JetpackSetupRequiredViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FE5802923729A0018040A /* JetpackSetupRequiredViewModel.swift */; };
17901791
DE2FE5832924DA2F0018040A /* JetpackSetupRequiredViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FE5822924DA2F0018040A /* JetpackSetupRequiredViewModelTests.swift */; };
17911792
DE2FE5862925DA050018040A /* SiteCredentialLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FE5852925DA050018040A /* SiteCredentialLoginView.swift */; };
@@ -3847,6 +3848,7 @@
38473848
DE279BAE26EA03EA002BA963 /* ShippingLabelSinglePackageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelSinglePackageViewModelTests.swift; sourceTree = "<group>"; };
38483849
DE279BB026EA184A002BA963 /* ShippingLabelPackageListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPackageListViewModel.swift; sourceTree = "<group>"; };
38493850
DE2BF4FC2846192B00FBE68A /* CouponAllowedEmailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponAllowedEmailsViewModelTests.swift; sourceTree = "<group>"; };
3851+
DE2E8EB629547771002E4B14 /* ApplicationPasswordDisabledViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationPasswordDisabledViewModel.swift; sourceTree = "<group>"; };
38503852
DE2FE5802923729A0018040A /* JetpackSetupRequiredViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackSetupRequiredViewModel.swift; sourceTree = "<group>"; };
38513853
DE2FE5822924DA2F0018040A /* JetpackSetupRequiredViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackSetupRequiredViewModelTests.swift; sourceTree = "<group>"; };
38523854
DE2FE5852925DA050018040A /* SiteCredentialLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteCredentialLoginView.swift; sourceTree = "<group>"; };
@@ -8773,6 +8775,7 @@
87738775
DE3404E728B4B96800CF0D97 /* NonAtomicSiteViewModel.swift */,
87748776
DE50294C28BEF8F100551736 /* JetpackConnectionWebViewModel.swift */,
87758777
DE2FE5802923729A0018040A /* JetpackSetupRequiredViewModel.swift */,
8778+
DE2E8EB629547771002E4B14 /* ApplicationPasswordDisabledViewModel.swift */,
87768779
);
87778780
path = "Navigation Exceptions";
87788781
sourceTree = "<group>";
@@ -10312,6 +10315,7 @@
1031210315
02BAB02724D13A6400F8B06E /* ProductVariationFormActionsFactory.swift in Sources */,
1031310316
45CDAFAE2434CFCA00F83C22 /* ProductCatalogVisibilityViewController.swift in Sources */,
1031410317
D85B8333222FABD1002168F3 /* StatusListTableViewCell.swift in Sources */,
10318+
DE2E8EB729547771002E4B14 /* ApplicationPasswordDisabledViewModel.swift in Sources */,
1031510319
0259D65D2582248D003B1CD6 /* PrintShippingLabelViewController.swift in Sources */,
1031610320
D881A31B256B5CC500FE5605 /* ULErrorViewController.swift in Sources */,
1031710321
CE22E3F72170E23C005A6BEF /* PrivacySettingsViewController.swift in Sources */,

0 commit comments

Comments
 (0)