diff --git a/WooCommerce/Classes/Authentication/Store Creation/Plan/MockInAppPurchases.swift b/WooCommerce/Classes/Authentication/Store Creation/Plan/MockInAppPurchases.swift index 9cb3fcc7174..fd2a986f3b2 100644 --- a/WooCommerce/Classes/Authentication/Store Creation/Plan/MockInAppPurchases.swift +++ b/WooCommerce/Classes/Authentication/Store Creation/Plan/MockInAppPurchases.swift @@ -15,16 +15,19 @@ struct MockInAppPurchases { private let fetchProductsDuration: UInt64 private let products: [WPComPlanProduct] private let userIsEntitledToProduct: Bool + private let isIAPSupported: Bool /// - Parameter fetchProductsDuration: How long to wait until the mock plan is returned, in nanoseconds. /// - Parameter products: WPCOM products to return for purchase. /// - Parameter userIsEntitledToProduct: Whether the user is entitled to the matched IAP product. init(fetchProductsDuration: UInt64 = 1_000_000_000, products: [WPComPlanProduct] = Defaults.products, - userIsEntitledToProduct: Bool = false) { + userIsEntitledToProduct: Bool = false, + isIAPSupported: Bool = true) { self.fetchProductsDuration = fetchProductsDuration self.products = products self.userIsEntitledToProduct = userIsEntitledToProduct + self.isIAPSupported = isIAPSupported } } @@ -48,7 +51,7 @@ extension MockInAppPurchases: InAppPurchasesForWPComPlansProtocol { } func inAppPurchasesAreSupported() async -> Bool { - true + isIAPSupported } } diff --git a/WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift b/WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift index 78d079bb47d..25eebb7c6f3 100644 --- a/WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift +++ b/WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift @@ -78,7 +78,7 @@ final class StoreCreationCoordinator: Coordinator { do { let inProgressView = createIAPEligibilityInProgressView() let storeCreationNavigationController = WooNavigationController(rootViewController: inProgressView) - presentStoreCreation(viewController: storeCreationNavigationController) + await presentStoreCreation(viewController: storeCreationNavigationController) guard await purchasesManager.inAppPurchasesAreSupported() else { throw PlanPurchaseError.iapNotSupported @@ -96,8 +96,14 @@ final class StoreCreationCoordinator: Coordinator { startStoreCreationM2(from: storeCreationNavigationController, planToPurchase: product) } catch { + let isWebviewFallbackAllowed = featureFlagService.isFeatureFlagEnabled(.storeCreationM2WithInAppPurchasesEnabled) == false navigationController.dismiss(animated: true) { [weak self] in - self?.startStoreCreationM1() + guard let self else { return } + if isWebviewFallbackAllowed { + self.startStoreCreationM1() + } else { + self.showIneligibleUI(from: self.navigationController, error: error) + } } } } @@ -118,7 +124,9 @@ private extension StoreCreationCoordinator { // Disables interactive dismissal of the store creation modal. webNavigationController.isModalInPresentation = true - presentStoreCreation(viewController: webNavigationController) + Task { @MainActor in + await presentStoreCreation(viewController: webNavigationController) + } } func startStoreCreationM2(from navigationController: UINavigationController, planToPurchase: WPComPlanProduct) { @@ -135,15 +143,25 @@ private extension StoreCreationCoordinator { analytics.track(event: .StoreCreation.siteCreationStep(step: .storeName)) } - func presentStoreCreation(viewController: UIViewController) { - // If the navigation controller is already presenting another view, the view needs to be dismissed before store - // creation view can be presented. - if navigationController.presentedViewController != nil { - navigationController.dismiss(animated: true) { [weak self] in - self?.navigationController.present(viewController, animated: true) + @MainActor + func presentStoreCreation(viewController: UIViewController) async { + await withCheckedContinuation { continuation in + // If the navigation controller is already presenting another view, the view needs to be dismissed before store + // creation view can be presented. + if navigationController.presentedViewController != nil { + navigationController.dismiss(animated: true) { [weak self] in + guard let self else { + return continuation.resume() + } + self.navigationController.present(viewController, animated: true) { + continuation.resume() + } + } + } else { + navigationController.present(viewController, animated: true) { + continuation.resume() + } } - } else { - navigationController.present(viewController, animated: true) } } @@ -154,6 +172,28 @@ private extension StoreCreationCoordinator { message: Localization.WaitingForIAPEligibility.message), hidesNavigationBar: true) } + + /// Shows UI when the user is not eligible for store creation. + func showIneligibleUI(from navigationController: UINavigationController, error: Error) { + let message: String + switch error { + case PlanPurchaseError.iapNotSupported: + message = Localization.IAPIneligibleAlert.notSupportedMessage + case PlanPurchaseError.productNotEligible: + message = Localization.IAPIneligibleAlert.productNotEligibleMessage + default: + message = Localization.IAPIneligibleAlert.defaultMessage + } + + let alert = UIAlertController(title: nil, + message: message, + preferredStyle: .alert) + alert.view.tintColor = .text + + alert.addCancelActionWithTitle(Localization.IAPIneligibleAlert.dismissActionTitle) { _ in } + + navigationController.present(alert, animated: true) + } } // MARK: - Store creation M1 @@ -486,6 +526,23 @@ private extension StoreCreationCoordinator { ) } + enum IAPIneligibleAlert { + static let notSupportedMessage = NSLocalizedString( + "We're sorry, but store creation is not currently available in your country in the app.", + comment: "Message of the alert when the user cannot create a store because their App Store country is not supported." + ) + static let productNotEligibleMessage = NSLocalizedString( + "Sorry, but you can only create one store. Your account is already associated with an active store.", + comment: "Message of the alert when the user cannot create a store because they already created one before." + ) + static let defaultMessage = NSLocalizedString( + "We're sorry, but store creation is not currently available in the app.", + comment: "Message of the alert when the user cannot create a store for some reason." + ) + static let dismissActionTitle = NSLocalizedString("OK", + comment: "Button title to cancel the alert when the user cannot create a store.") + } + enum DiscardChangesAlert { static let title = NSLocalizedString("Do you want to leave?", comment: "Title of the alert when the user dismisses the store creation flow.") diff --git a/WooCommerce/WooCommerceTests/Authentication/Store Creation/StoreCreationCoordinatorTests.swift b/WooCommerce/WooCommerceTests/Authentication/Store Creation/StoreCreationCoordinatorTests.swift index 749db67db78..93456abf2aa 100644 --- a/WooCommerce/WooCommerceTests/Authentication/Store Creation/StoreCreationCoordinatorTests.swift +++ b/WooCommerce/WooCommerceTests/Authentication/Store Creation/StoreCreationCoordinatorTests.swift @@ -92,10 +92,8 @@ final class StoreCreationCoordinatorTests: XCTestCase { // Then waitUntil { - self.navigationController.presentedViewController is UINavigationController + (self.navigationController.presentedViewController as? UINavigationController)?.topViewController is StoreNameFormHostingController } - let storeCreationNavigationController = try XCTUnwrap(navigationController.presentedViewController as? UINavigationController) - assertThat(storeCreationNavigationController.topViewController, isAnInstanceOf: StoreNameFormHostingController.self) } func test_StoreNameFormHostingController_is_presented_when_navigationController_is_showing_another_view_with_iap_enabled() throws { @@ -115,10 +113,32 @@ final class StoreCreationCoordinatorTests: XCTestCase { // Then waitUntil { - self.navigationController.presentedViewController is UINavigationController + (self.navigationController.presentedViewController as? UINavigationController)?.topViewController is StoreNameFormHostingController + } + } + + func test_UIAlertController_is_presented_when_iap_is_not_supported_with_iap_enabled() throws { + // Given + let featureFlagService = MockFeatureFlagService(isStoreCreationM2Enabled: true, + isStoreCreationM2WithInAppPurchasesEnabled: true) + let coordinator = StoreCreationCoordinator(source: .storePicker, + navigationController: navigationController, + featureFlagService: featureFlagService, + purchasesManager: MockInAppPurchases(fetchProductsDuration: 0, isIAPSupported: false)) + waitFor { promise in + self.navigationController.present(.init(), animated: false) { + promise(()) + } + } + XCTAssertNotNil(navigationController.presentedViewController) + + // When + coordinator.start() + + // Then + waitUntil { + self.navigationController.presentedViewController is UIAlertController } - let storeCreationNavigationController = try XCTUnwrap(navigationController.presentedViewController as? UINavigationController) - assertThat(storeCreationNavigationController.topViewController, isAnInstanceOf: StoreNameFormHostingController.self) } func test_StoreNameFormHostingController_is_presented_when_navigationController_is_presenting_another_view_with_iap_disabled() throws {