Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand All @@ -48,7 +51,7 @@ extension MockInAppPurchases: InAppPurchasesForWPComPlansProtocol {
}

func inAppPurchasesAreSupported() async -> Bool {
true
isIAPSupported
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
}
}
Expand All @@ -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) {
Expand All @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea making this 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)
}
}

Expand All @@ -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
Expand Down Expand Up @@ -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.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down