diff --git a/Experiments/Experiments/DefaultFeatureFlagService.swift b/Experiments/Experiments/DefaultFeatureFlagService.swift index 91051722922..35302ad70ce 100644 --- a/Experiments/Experiments/DefaultFeatureFlagService.swift +++ b/Experiments/Experiments/DefaultFeatureFlagService.swift @@ -95,6 +95,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService { return false case .sendReceiptsForPointOfSale: return false + case .jetpackSetupWPComAccountCreation: + return buildConfig == .localDeveloper || buildConfig == .alpha default: return true } diff --git a/Experiments/Experiments/FeatureFlag.swift b/Experiments/Experiments/FeatureFlag.swift index 2d90410e3c0..a5e3f190731 100644 --- a/Experiments/Experiments/FeatureFlag.swift +++ b/Experiments/Experiments/FeatureFlag.swift @@ -203,4 +203,8 @@ public enum FeatureFlag: Int { /// Adds support for sending receipts after the payment for POS /// case sendReceiptsForPointOfSale + + /// Enables WPCom account creation during Jetpack setup + /// + case jetpackSetupWPComAccountCreation } diff --git a/Podfile b/Podfile index c72330c674f..86a31392fd4 100644 --- a/Podfile +++ b/Podfile @@ -93,7 +93,7 @@ target 'WooCommerce' do # To allow pod to pick up beta versions use -beta. E.g., 1.1.7-beta.1 # pod 'WordPressAuthenticator', '~> 9.10.0' # pod 'WordPressAuthenticator', git: 'https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git', branch: '' - pod 'WordPressAuthenticator', git: 'https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git', commit: 'f591b6e0442f9f153ad3842efdb8c81d26235c44' + pod 'WordPressAuthenticator', git: 'https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git', commit: 'cd53ee3d6209619be6f7bb61f1707fbebf77798c' # pod 'WordPressAuthenticator', path: '../WordPressAuthenticator-iOS' wordpress_shared diff --git a/Podfile.lock b/Podfile.lock index 7f0e7a46b7e..550573d1fa7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -28,10 +28,10 @@ PODS: - Gridicons (~> 1.0) - "NSURL+IDN (= 0.4)" - SVProgressHUD (~> 2.2.5) - - WordPressKit (~> 17.0) + - WordPressKit (~> 17.3) - WordPressShared (~> 2.1-beta) - WordPressUI (~> 1.7-beta) - - WordPressKit (17.2.0): + - WordPressKit (17.3.0): - NSObject-SafeExpectations (~> 0.0.4) - UIDeviceIdentifier (~> 2.0) - WordPressShared (~> 2.0-beta) @@ -67,7 +67,7 @@ DEPENDENCIES: - StripeTerminal (~> 3.9.1) - SwiftLint (= 0.54.0) - WordPress-Editor-iOS (~> 1.19) - - WordPressAuthenticator (from `https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git`, commit `f591b6e0442f9f153ad3842efdb8c81d26235c44`) + - WordPressAuthenticator (from `https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git`, commit `cd53ee3d6209619be6f7bb61f1707fbebf77798c`) - WordPressShared (~> 2.1) - WordPressUI (~> 1.15) - Wormholy (~> 1.6.6) @@ -75,8 +75,6 @@ DEPENDENCIES: - ZendeskSupportSDK (~> 9.0.0) SPEC REPOS: - https://github.com/wordpress-mobile/cocoapods-specs.git: - - WordPressKit trunk: - Alamofire - Automattic-Tracks-iOS @@ -95,6 +93,7 @@ SPEC REPOS: - UIDeviceIdentifier - WordPress-Aztec-iOS - WordPress-Editor-iOS + - WordPressKit - WordPressShared - WordPressUI - Wormholy @@ -110,12 +109,12 @@ SPEC REPOS: EXTERNAL SOURCES: WordPressAuthenticator: - :commit: f591b6e0442f9f153ad3842efdb8c81d26235c44 + :commit: cd53ee3d6209619be6f7bb61f1707fbebf77798c :git: https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git CHECKOUT OPTIONS: WordPressAuthenticator: - :commit: f591b6e0442f9f153ad3842efdb8c81d26235c44 + :commit: cd53ee3d6209619be6f7bb61f1707fbebf77798c :git: https://github.com/wordpress-mobile/WordPressAuthenticator-iOS.git SPEC CHECKSUMS: @@ -136,8 +135,8 @@ SPEC CHECKSUMS: UIDeviceIdentifier: 442b65b4ff1832d4ca9c2a157815cb29ad981b17 WordPress-Aztec-iOS: 8eaa928fb3a5694924ed3befac64beaae5656e12 WordPress-Editor-iOS: 98ce1fc542c3a09e48ddc9423405b1d1e48240f1 - WordPressAuthenticator: d151cc7ebc1cfcbf5c28bb2c9afe760a75b49ba6 - WordPressKit: de44094b3be8998504a3a57700bc3e96e3b46f57 + WordPressAuthenticator: 0f4f47d6f71cfa763cb636876d69170a3c710002 + WordPressKit: faf8c6de7c2acfe71cf95b4db896901060967089 WordPressShared: 0aa459e5257a77184db87805a998f447443c9706 WordPressUI: 700e3ec5a9f77b6920c8104c338c85788036ab3c Wormholy: 09da0b876f9276031fd47383627cb75e194fc068 @@ -151,6 +150,6 @@ SPEC CHECKSUMS: ZendeskSupportProvidersSDK: 281acf2bb731d2a67f913cfe653ed0da9f5b2f42 ZendeskSupportSDK: b512cfc74b6bf8490e589f02cf52e27ed4f2bebe -PODFILE CHECKSUM: 51f5cabba416d490c8f90395fa0a86fde13dd931 +PODFILE CHECKSUM: f02fa3bd6b4c798923cccc4fbe03de910e60891b COCOAPODS: 1.16.1 diff --git a/WooCommerce/Classes/Authentication/WPComLogin/WPComEmailLoginView.swift b/WooCommerce/Classes/Authentication/WPComLogin/WPComEmailLoginView.swift index be4d93ca551..288ab2f3c2a 100644 --- a/WooCommerce/Classes/Authentication/WPComLogin/WPComEmailLoginView.swift +++ b/WooCommerce/Classes/Authentication/WPComLogin/WPComEmailLoginView.swift @@ -134,6 +134,7 @@ struct WPComEmailLoginView_Previews: PreviewProvider { static var previews: some View { WPComEmailLoginView(viewModel: .init(siteURL: "https://example.com", requiresConnectionOnly: true, + allowAccountCreation: false, onPasswordUIRequest: { _ in }, onMagicLinkUIRequest: { _ in }, onError: { _ in })) diff --git a/WooCommerce/Classes/Authentication/WPComLogin/WPComEmailLoginViewModel.swift b/WooCommerce/Classes/Authentication/WPComLogin/WPComEmailLoginViewModel.swift index 834055fadf5..d5ef7b62337 100644 --- a/WooCommerce/Classes/Authentication/WPComLogin/WPComEmailLoginViewModel.swift +++ b/WooCommerce/Classes/Authentication/WPComLogin/WPComEmailLoginViewModel.swift @@ -2,11 +2,17 @@ import Combine import UIKit import WordPressAuthenticator import protocol WooFoundation.Analytics +import enum WordPressKit.WordPressAPIError +import struct WordPressKit.WordPressComRestApiEndpointError /// A protocol used to mock `WordPressComAccountService` for unit tests. protocol WordPressComAccountServiceProtocol { func isPasswordlessAccount(username: String, success: @escaping (Bool) -> Void, failure: @escaping (Error) -> Void) - func requestAuthenticationLink(for email: String, jetpackLogin: Bool, success: @escaping () -> Void, failure: @escaping (Error) -> Void) + func requestAuthenticationLink(for email: String, + jetpackLogin: Bool, + createAccountIfNotFound: Bool, + success: @escaping () -> Void, + failure: @escaping (Error) -> Void) } /// Conformance @@ -21,6 +27,7 @@ final class WPComEmailLoginViewModel: ObservableObject { let termsAttributedString: NSAttributedString + private let allowAccountCreation: Bool private let accountService: WordPressComAccountServiceProtocol private let analytics: Analytics private let onPasswordUIRequest: (String) -> Void @@ -31,12 +38,14 @@ final class WPComEmailLoginViewModel: ObservableObject { init(siteURL: String, requiresConnectionOnly: Bool, + allowAccountCreation: Bool, debounceDuration: Double = Constants.fieldDebounceDuration, accountService: WordPressComAccountServiceProtocol = WordPressComAccountService(), analytics: Analytics = ServiceLocator.analytics, onPasswordUIRequest: @escaping (String) -> Void, onMagicLinkUIRequest: @escaping (String) -> Void, onError: @escaping (String) -> Void) { + self.allowAccountCreation = allowAccountCreation self.analytics = analytics self.accountService = accountService self.onPasswordUIRequest = onPasswordUIRequest @@ -78,8 +87,16 @@ final class WPComEmailLoginViewModel: ObservableObject { } await startAuthentication(email: email, isPasswordlessAccount: passwordless) } catch { - analytics.track(event: .JetpackSetup.loginFlow(step: .emailAddress, failure: error)) - onError(error.localizedDescription) + guard allowAccountCreation, + let apiError = error as? WordPressAPIError, + case .endpointError(let endpointError) = apiError, + endpointError.apiErrorCode == Constants.unknownUserErrorCode else { + analytics.track(event: .JetpackSetup.loginFlow(step: .emailAddress, failure: error)) + onError(error.localizedDescription) + return + } + + await requestAuthenticationLink(email: email, forAccountCreation: true) } } @@ -93,10 +110,13 @@ final class WPComEmailLoginViewModel: ObservableObject { } @MainActor - func requestAuthenticationLink(email: String) async { + func requestAuthenticationLink(email: String, forAccountCreation: Bool = false) async { do { try await withCheckedThrowingContinuation { continuation in - accountService.requestAuthenticationLink(for: email, jetpackLogin: false, success: { + accountService.requestAuthenticationLink(for: email, + jetpackLogin: false, + createAccountIfNotFound: forAccountCreation, + success: { continuation.resume() }, failure: { error in continuation.resume(throwing: error) @@ -116,6 +136,7 @@ extension WPComEmailLoginViewModel { static let jetpackTermsURL = "https://jetpack.com/redirect/?source=wpcom-tos&site=" static let jetpackShareDetailsURL = "https://jetpack.com/redirect/?source=jetpack-support-what-data-does-jetpack-sync&site=" static let wpcomErrorCodeKey = "WordPressComRestApiErrorCodeKey" + static let unknownUserErrorCode = "unknown_user" } enum Localization { diff --git a/WooCommerce/Classes/Authentication/WPComLogin/WPComLoginCoordinator.swift b/WooCommerce/Classes/Authentication/WPComLogin/WPComLoginCoordinator.swift index 70f74402166..26b5440d434 100644 --- a/WooCommerce/Classes/Authentication/WPComLogin/WPComLoginCoordinator.swift +++ b/WooCommerce/Classes/Authentication/WPComLogin/WPComLoginCoordinator.swift @@ -151,7 +151,10 @@ private extension WPComLoginCoordinator { @MainActor func requestAuthenticationLink(email: String) async throws { try await withCheckedThrowingContinuation { continuation in - accountService.requestAuthenticationLink(for: email, jetpackLogin: false, success: { + accountService.requestAuthenticationLink(for: email, + jetpackLogin: false, + createAccountIfNotFound: false, + success: { continuation.resume() }, failure: { error in continuation.resume(throwing: error) diff --git a/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift b/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift index 6a39bf71323..eecb4449bcc 100644 --- a/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift +++ b/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift @@ -1,4 +1,5 @@ import UIKit +import Experiments import Yosemite import enum Networking.NetworkError import class Networking.AlamofireNetwork @@ -16,6 +17,7 @@ final class JetpackSetupCoordinator { private var jetpackConnectedEmail: String? private let stores: StoresManager private let analytics: Analytics + private let featureFlagService: FeatureFlagService private let dotcomAuthScheme: String private var loginNavigationController: LoginNavigationController? @@ -24,6 +26,7 @@ final class JetpackSetupCoordinator { private lazy var emailLoginViewModel: WPComEmailLoginViewModel = { .init(siteURL: site.url, requiresConnectionOnly: requiresConnectionOnly, + allowAccountCreation: featureFlagService.isFeatureFlagEnabled(.jetpackSetupWPComAccountCreation), onPasswordUIRequest: showPasswordUI(email:), onMagicLinkUIRequest: showMagicLinkUI(email:), onError: { [weak self] message in @@ -40,13 +43,15 @@ final class JetpackSetupCoordinator { dotcomAuthScheme: String = ApiCredentials.dotcomAuthScheme, rootViewController: UIViewController, stores: StoresManager = ServiceLocator.stores, - analytics: Analytics = ServiceLocator.analytics) { + analytics: Analytics = ServiceLocator.analytics, + featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) { self.site = site self.dotcomAuthScheme = dotcomAuthScheme self.requiresConnectionOnly = false // to be updated later after fetching Jetpack status self.rootViewController = rootViewController self.stores = stores self.analytics = analytics + self.featureFlagService = featureFlagService /// the authenticator needs to be initialized with configs /// to be used for requesting authentication link and handle login later. diff --git a/WooCommerce/WooCommerceTests/Mocks/MockWordPressComAccountService.swift b/WooCommerce/WooCommerceTests/Mocks/MockWordPressComAccountService.swift index a3cb9344f07..f04a4b62e39 100644 --- a/WooCommerce/WooCommerceTests/Mocks/MockWordPressComAccountService.swift +++ b/WooCommerce/WooCommerceTests/Mocks/MockWordPressComAccountService.swift @@ -17,7 +17,11 @@ final class MockWordPressComAccountService: WordPressComAccountServiceProtocol { failure(passwordlessAccountCheckError) } - func requestAuthenticationLink(for email: String, jetpackLogin: Bool, success: @escaping () -> Void, failure: @escaping (Error) -> Void) { + func requestAuthenticationLink(for email: String, + jetpackLogin: Bool, + createAccountIfNotFound: Bool, + success: @escaping () -> Void, + failure: @escaping (Error) -> Void) { triggeredRequestAuthenticationLink = true guard let authenticationLinkRequestError else { return success() diff --git a/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/WPComLogin/WPComEmailLoginViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/WPComLogin/WPComEmailLoginViewModelTests.swift index 866db08a807..198a08f012f 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/WPComLogin/WPComEmailLoginViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/WPComLogin/WPComEmailLoginViewModelTests.swift @@ -1,4 +1,5 @@ import XCTest +@testable import WordPressKit @testable import WooCommerce final class WPComEmailLoginViewModelTests: XCTestCase { @@ -8,6 +9,7 @@ final class WPComEmailLoginViewModelTests: XCTestCase { let siteURL = "https://example.com" let viewModel = WPComEmailLoginViewModel(siteURL: siteURL, requiresConnectionOnly: false, + allowAccountCreation: false, onPasswordUIRequest: { _ in }, onMagicLinkUIRequest: { _ in }, onError: { _ in }) @@ -24,6 +26,7 @@ final class WPComEmailLoginViewModelTests: XCTestCase { let siteURL = "https://example.com" let viewModel = WPComEmailLoginViewModel(siteURL: siteURL, requiresConnectionOnly: true, + allowAccountCreation: false, onPasswordUIRequest: { _ in }, onMagicLinkUIRequest: { _ in }, onError: { _ in }) @@ -40,6 +43,7 @@ final class WPComEmailLoginViewModelTests: XCTestCase { let siteURL = "https://example.com" let viewModel = WPComEmailLoginViewModel(siteURL: siteURL, requiresConnectionOnly: false, + allowAccountCreation: false, onPasswordUIRequest: { _ in }, onMagicLinkUIRequest: { _ in }, onError: { _ in }) @@ -56,6 +60,7 @@ final class WPComEmailLoginViewModelTests: XCTestCase { let siteURL = "https://example.com" let viewModel = WPComEmailLoginViewModel(siteURL: siteURL, requiresConnectionOnly: true, + allowAccountCreation: false, onPasswordUIRequest: { _ in }, onMagicLinkUIRequest: { _ in }, onError: { _ in }) @@ -72,6 +77,7 @@ final class WPComEmailLoginViewModelTests: XCTestCase { let siteURL = "https://example.com" let viewModel = WPComEmailLoginViewModel(siteURL: siteURL, requiresConnectionOnly: true, + allowAccountCreation: false, onPasswordUIRequest: { _ in }, onMagicLinkUIRequest: { _ in }, onError: { _ in }) @@ -92,6 +98,7 @@ final class WPComEmailLoginViewModelTests: XCTestCase { mockAccountService.shouldReturnPasswordlessAccount = true let viewModel = WPComEmailLoginViewModel(siteURL: "https://example.com", requiresConnectionOnly: true, + allowAccountCreation: false, accountService: mockAccountService, onPasswordUIRequest: { _ in }, onMagicLinkUIRequest: { _ in }, @@ -115,6 +122,7 @@ final class WPComEmailLoginViewModelTests: XCTestCase { var triggeredOnError = false let viewModel = WPComEmailLoginViewModel(siteURL: "https://example.com", requiresConnectionOnly: true, + allowAccountCreation: false, accountService: mockAccountService, onPasswordUIRequest: { _ in }, onMagicLinkUIRequest: { _ in }, @@ -133,6 +141,7 @@ final class WPComEmailLoginViewModelTests: XCTestCase { var triggeredPasswordUIRequest = false let viewModel = WPComEmailLoginViewModel(siteURL: "https://example.com", requiresConnectionOnly: true, + allowAccountCreation: false, accountService: mockAccountService, onPasswordUIRequest: { _ in triggeredPasswordUIRequest = true }, onMagicLinkUIRequest: { _ in }, @@ -150,6 +159,7 @@ final class WPComEmailLoginViewModelTests: XCTestCase { var triggeredOnMagicLinkUIRequest = false let viewModel = WPComEmailLoginViewModel(siteURL: "https://example.com", requiresConnectionOnly: true, + allowAccountCreation: false, accountService: mockAccountService, onPasswordUIRequest: { _ in }, onMagicLinkUIRequest: { _ in triggeredOnMagicLinkUIRequest = true }, @@ -168,6 +178,7 @@ final class WPComEmailLoginViewModelTests: XCTestCase { var triggeredOnError = false let viewModel = WPComEmailLoginViewModel(siteURL: "https://example.com", requiresConnectionOnly: true, + allowAccountCreation: false, accountService: mockAccountService, onPasswordUIRequest: { _ in }, onMagicLinkUIRequest: { _ in }, @@ -178,4 +189,54 @@ final class WPComEmailLoginViewModelTests: XCTestCase { // Then XCTAssertTrue(triggeredOnError) } + + func test_given_unknown_email_when_allowAccountCreation_true_then_create_account() async { + // Given + let mockAccountService = MockWordPressComAccountService() + mockAccountService.passwordlessAccountCheckError = WordPressAPIError.endpointError( + WordPressComRestApiEndpointError( + code: WordPressComRestApiErrorCode.unknown, + apiErrorCode: "unknown_user" + ) + ) + var triggeredOnMagicLinkUIRequest = false + let viewModel = WPComEmailLoginViewModel(siteURL: "https://example.com", + requiresConnectionOnly: true, + allowAccountCreation: true, + accountService: mockAccountService, + onPasswordUIRequest: { _ in }, + onMagicLinkUIRequest: { _ in triggeredOnMagicLinkUIRequest = true }, + onError: { _ in }) + + // When + await viewModel.checkWordPressComAccount(email: "mail@example.com") + + // Then + XCTAssertTrue(triggeredOnMagicLinkUIRequest) + } + + func test_given_unknown_email_when_allowAccountCreation_false_then_trigger_onError() async { + // Given + let mockAccountService = MockWordPressComAccountService() + mockAccountService.passwordlessAccountCheckError = WordPressAPIError.endpointError( + WordPressComRestApiEndpointError( + code: WordPressComRestApiErrorCode.unknown, + apiErrorCode: "unknown_user" + ) + ) + var triggeredOnError = false + let viewModel = WPComEmailLoginViewModel(siteURL: "https://example.com", + requiresConnectionOnly: true, + allowAccountCreation: false, + accountService: mockAccountService, + onPasswordUIRequest: { _ in }, + onMagicLinkUIRequest: { _ in }, + onError: { _ in triggeredOnError = true }) + + // When + await viewModel.checkWordPressComAccount(email: "mail@example.com") + + // Then + XCTAssertTrue(triggeredOnError) + } }