Skip to content

Commit e81d612

Browse files
authored
Merge pull request #8393 from woocommerce/issue/8392-iap-ineligible
Store creation M3: handle IAP ineligible scenario by showing an alert
2 parents 0dfcac1 + bda4164 commit e81d612

File tree

3 files changed

+99
-19
lines changed

3 files changed

+99
-19
lines changed

WooCommerce/Classes/Authentication/Store Creation/Plan/MockInAppPurchases.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,19 @@ struct MockInAppPurchases {
1515
private let fetchProductsDuration: UInt64
1616
private let products: [WPComPlanProduct]
1717
private let userIsEntitledToProduct: Bool
18+
private let isIAPSupported: Bool
1819

1920
/// - Parameter fetchProductsDuration: How long to wait until the mock plan is returned, in nanoseconds.
2021
/// - Parameter products: WPCOM products to return for purchase.
2122
/// - Parameter userIsEntitledToProduct: Whether the user is entitled to the matched IAP product.
2223
init(fetchProductsDuration: UInt64 = 1_000_000_000,
2324
products: [WPComPlanProduct] = Defaults.products,
24-
userIsEntitledToProduct: Bool = false) {
25+
userIsEntitledToProduct: Bool = false,
26+
isIAPSupported: Bool = true) {
2527
self.fetchProductsDuration = fetchProductsDuration
2628
self.products = products
2729
self.userIsEntitledToProduct = userIsEntitledToProduct
30+
self.isIAPSupported = isIAPSupported
2831
}
2932
}
3033

@@ -48,7 +51,7 @@ extension MockInAppPurchases: InAppPurchasesForWPComPlansProtocol {
4851
}
4952

5053
func inAppPurchasesAreSupported() async -> Bool {
51-
true
54+
isIAPSupported
5255
}
5356
}
5457

WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ final class StoreCreationCoordinator: Coordinator {
7878
do {
7979
let inProgressView = createIAPEligibilityInProgressView()
8080
let storeCreationNavigationController = WooNavigationController(rootViewController: inProgressView)
81-
presentStoreCreation(viewController: storeCreationNavigationController)
81+
await presentStoreCreation(viewController: storeCreationNavigationController)
8282

8383
guard await purchasesManager.inAppPurchasesAreSupported() else {
8484
throw PlanPurchaseError.iapNotSupported
@@ -96,8 +96,14 @@ final class StoreCreationCoordinator: Coordinator {
9696

9797
startStoreCreationM2(from: storeCreationNavigationController, planToPurchase: product)
9898
} catch {
99+
let isWebviewFallbackAllowed = featureFlagService.isFeatureFlagEnabled(.storeCreationM2WithInAppPurchasesEnabled) == false
99100
navigationController.dismiss(animated: true) { [weak self] in
100-
self?.startStoreCreationM1()
101+
guard let self else { return }
102+
if isWebviewFallbackAllowed {
103+
self.startStoreCreationM1()
104+
} else {
105+
self.showIneligibleUI(from: self.navigationController, error: error)
106+
}
101107
}
102108
}
103109
}
@@ -118,7 +124,9 @@ private extension StoreCreationCoordinator {
118124
// Disables interactive dismissal of the store creation modal.
119125
webNavigationController.isModalInPresentation = true
120126

121-
presentStoreCreation(viewController: webNavigationController)
127+
Task { @MainActor in
128+
await presentStoreCreation(viewController: webNavigationController)
129+
}
122130
}
123131

124132
func startStoreCreationM2(from navigationController: UINavigationController, planToPurchase: WPComPlanProduct) {
@@ -135,15 +143,25 @@ private extension StoreCreationCoordinator {
135143
analytics.track(event: .StoreCreation.siteCreationStep(step: .storeName))
136144
}
137145

138-
func presentStoreCreation(viewController: UIViewController) {
139-
// If the navigation controller is already presenting another view, the view needs to be dismissed before store
140-
// creation view can be presented.
141-
if navigationController.presentedViewController != nil {
142-
navigationController.dismiss(animated: true) { [weak self] in
143-
self?.navigationController.present(viewController, animated: true)
146+
@MainActor
147+
func presentStoreCreation(viewController: UIViewController) async {
148+
await withCheckedContinuation { continuation in
149+
// If the navigation controller is already presenting another view, the view needs to be dismissed before store
150+
// creation view can be presented.
151+
if navigationController.presentedViewController != nil {
152+
navigationController.dismiss(animated: true) { [weak self] in
153+
guard let self else {
154+
return continuation.resume()
155+
}
156+
self.navigationController.present(viewController, animated: true) {
157+
continuation.resume()
158+
}
159+
}
160+
} else {
161+
navigationController.present(viewController, animated: true) {
162+
continuation.resume()
163+
}
144164
}
145-
} else {
146-
navigationController.present(viewController, animated: true)
147165
}
148166
}
149167

@@ -154,6 +172,28 @@ private extension StoreCreationCoordinator {
154172
message: Localization.WaitingForIAPEligibility.message),
155173
hidesNavigationBar: true)
156174
}
175+
176+
/// Shows UI when the user is not eligible for store creation.
177+
func showIneligibleUI(from navigationController: UINavigationController, error: Error) {
178+
let message: String
179+
switch error {
180+
case PlanPurchaseError.iapNotSupported:
181+
message = Localization.IAPIneligibleAlert.notSupportedMessage
182+
case PlanPurchaseError.productNotEligible:
183+
message = Localization.IAPIneligibleAlert.productNotEligibleMessage
184+
default:
185+
message = Localization.IAPIneligibleAlert.defaultMessage
186+
}
187+
188+
let alert = UIAlertController(title: nil,
189+
message: message,
190+
preferredStyle: .alert)
191+
alert.view.tintColor = .text
192+
193+
alert.addCancelActionWithTitle(Localization.IAPIneligibleAlert.dismissActionTitle) { _ in }
194+
195+
navigationController.present(alert, animated: true)
196+
}
157197
}
158198

159199
// MARK: - Store creation M1
@@ -486,6 +526,23 @@ private extension StoreCreationCoordinator {
486526
)
487527
}
488528

529+
enum IAPIneligibleAlert {
530+
static let notSupportedMessage = NSLocalizedString(
531+
"We're sorry, but store creation is not currently available in your country in the app.",
532+
comment: "Message of the alert when the user cannot create a store because their App Store country is not supported."
533+
)
534+
static let productNotEligibleMessage = NSLocalizedString(
535+
"Sorry, but you can only create one store. Your account is already associated with an active store.",
536+
comment: "Message of the alert when the user cannot create a store because they already created one before."
537+
)
538+
static let defaultMessage = NSLocalizedString(
539+
"We're sorry, but store creation is not currently available in the app.",
540+
comment: "Message of the alert when the user cannot create a store for some reason."
541+
)
542+
static let dismissActionTitle = NSLocalizedString("OK",
543+
comment: "Button title to cancel the alert when the user cannot create a store.")
544+
}
545+
489546
enum DiscardChangesAlert {
490547
static let title = NSLocalizedString("Do you want to leave?",
491548
comment: "Title of the alert when the user dismisses the store creation flow.")

WooCommerce/WooCommerceTests/Authentication/Store Creation/StoreCreationCoordinatorTests.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,8 @@ final class StoreCreationCoordinatorTests: XCTestCase {
9292

9393
// Then
9494
waitUntil {
95-
self.navigationController.presentedViewController is UINavigationController
95+
(self.navigationController.presentedViewController as? UINavigationController)?.topViewController is StoreNameFormHostingController
9696
}
97-
let storeCreationNavigationController = try XCTUnwrap(navigationController.presentedViewController as? UINavigationController)
98-
assertThat(storeCreationNavigationController.topViewController, isAnInstanceOf: StoreNameFormHostingController.self)
9997
}
10098

10199
func test_StoreNameFormHostingController_is_presented_when_navigationController_is_showing_another_view_with_iap_enabled() throws {
@@ -115,10 +113,32 @@ final class StoreCreationCoordinatorTests: XCTestCase {
115113

116114
// Then
117115
waitUntil {
118-
self.navigationController.presentedViewController is UINavigationController
116+
(self.navigationController.presentedViewController as? UINavigationController)?.topViewController is StoreNameFormHostingController
117+
}
118+
}
119+
120+
func test_UIAlertController_is_presented_when_iap_is_not_supported_with_iap_enabled() throws {
121+
// Given
122+
let featureFlagService = MockFeatureFlagService(isStoreCreationM2Enabled: true,
123+
isStoreCreationM2WithInAppPurchasesEnabled: true)
124+
let coordinator = StoreCreationCoordinator(source: .storePicker,
125+
navigationController: navigationController,
126+
featureFlagService: featureFlagService,
127+
purchasesManager: MockInAppPurchases(fetchProductsDuration: 0, isIAPSupported: false))
128+
waitFor { promise in
129+
self.navigationController.present(.init(), animated: false) {
130+
promise(())
131+
}
132+
}
133+
XCTAssertNotNil(navigationController.presentedViewController)
134+
135+
// When
136+
coordinator.start()
137+
138+
// Then
139+
waitUntil {
140+
self.navigationController.presentedViewController is UIAlertController
119141
}
120-
let storeCreationNavigationController = try XCTUnwrap(navigationController.presentedViewController as? UINavigationController)
121-
assertThat(storeCreationNavigationController.topViewController, isAnInstanceOf: StoreNameFormHostingController.self)
122142
}
123143

124144
func test_StoreNameFormHostingController_is_presented_when_navigationController_is_presenting_another_view_with_iap_disabled() throws {

0 commit comments

Comments
 (0)