Skip to content

Commit 91905fc

Browse files
authored
Merge pull request #8154 from woocommerce/feat/8108-iap-integration
Store creation M2: integrate with mock IAP to test the entire flow
2 parents db394a5 + 9fdf543 commit 91905fc

File tree

11 files changed

+438
-32
lines changed

11 files changed

+438
-32
lines changed

Experiments/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
3737
return true
3838
case .storeCreationM2:
3939
return buildConfig == .localDeveloper || buildConfig == .alpha
40+
case .storeCreationM2WithInAppPurchasesEnabled:
41+
return false
4042
case .justInTimeMessagesOnDashboard:
4143
return true
4244
case .systemStatusReportInSupportRequest:

Experiments/Experiments/FeatureFlag.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ public enum FeatureFlag: Int {
7878
///
7979
case storeCreationM2
8080

81+
/// Whether in-app purchases are enabled for store creation milestone 2 behind `storeCreationM2` feature flag.
82+
/// If disabled, mock in-app purchases are provided by `MockInAppPurchases`.
83+
///
84+
case storeCreationM2WithInAppPurchasesEnabled
85+
8186
/// Just In Time Messages on Dashboard
8287
///
8388
case justInTimeMessagesOnDashboard

WooCommerce/Classes/Authentication/Epilogue/StorePickerViewModel.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,15 @@ extension StorePickerViewModel {
174174
return possibleURLs.contains(siteURL)
175175
})
176176
}
177+
178+
/// Returns the site that matches the given site ID.
179+
///
180+
func site(thatMatchesSiteID siteID: Int64) -> Site? {
181+
guard resultsController.numberOfObjects > 0 else {
182+
return nil
183+
}
184+
return resultsController.fetchedObjects.first(where: { $0.siteID == siteID })
185+
}
177186
}
178187

179188
private extension StorePickerViewModel {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import Foundation
2+
import StoreKit
3+
4+
#if DEBUG
5+
6+
/// Only used during store creation development before IAP server side is ready.
7+
struct MockInAppPurchases {
8+
struct Plan: WPComPlanProduct {
9+
let displayName: String
10+
let description: String
11+
let id: String
12+
let displayPrice: String
13+
}
14+
15+
private let fetchProductsDuration: UInt64
16+
private let products: [WPComPlanProduct]
17+
private let userIsEntitledToProduct: Bool
18+
19+
/// - Parameter fetchProductsDuration: How long to wait until the mock plan is returned, in nanoseconds.
20+
/// - Parameter products: WPCOM products to return for purchase.
21+
/// - Parameter userIsEntitledToProduct: Whether the user is entitled to the matched IAP product.
22+
init(fetchProductsDuration: UInt64 = 1_000_000_000,
23+
products: [WPComPlanProduct] = Defaults.products,
24+
userIsEntitledToProduct: Bool = false) {
25+
self.fetchProductsDuration = fetchProductsDuration
26+
self.products = products
27+
self.userIsEntitledToProduct = userIsEntitledToProduct
28+
}
29+
}
30+
31+
extension MockInAppPurchases: InAppPurchasesForWPComPlansProtocol {
32+
func fetchProducts() async throws -> [WPComPlanProduct] {
33+
try await Task.sleep(nanoseconds: fetchProductsDuration)
34+
return products
35+
}
36+
37+
func userIsEntitledToProduct(with id: String) async throws -> Bool {
38+
userIsEntitledToProduct
39+
}
40+
41+
func purchaseProduct(with id: String, for remoteSiteId: Int64) async throws -> InAppPurchaseResult {
42+
// Returns `.pending` in case of success because `StoreKit.Transaction` cannot be easily mocked.
43+
.pending
44+
}
45+
46+
func retryWPComSyncForPurchasedProduct(with id: String) async throws {
47+
// no-op
48+
}
49+
50+
func inAppPurchasesAreSupported() async -> Bool {
51+
true
52+
}
53+
}
54+
55+
private extension MockInAppPurchases {
56+
enum Defaults {
57+
static let products: [WPComPlanProduct] = [
58+
Plan(displayName: "Debug Monthly",
59+
description: "1 Month of Debug Woo",
60+
id: "debug.woocommerce.ecommerce.monthly",
61+
displayPrice: "$69.99")
62+
]
63+
}
64+
}
65+
66+
#endif

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ import SwiftUI
22

33
/// Hosting controller that wraps the `StoreCreationPlanView`.
44
final class StoreCreationPlanHostingController: UIHostingController<StoreCreationPlanView> {
5-
private let onPurchase: () -> Void
5+
private let onPurchase: () async -> Void
66
private let onClose: () -> Void
77

88
init(viewModel: StoreCreationPlanViewModel,
9-
onPurchase: @escaping () -> Void,
9+
onPurchase: @escaping () async -> Void,
1010
onClose: @escaping () -> Void) {
1111
self.onPurchase = onPurchase
1212
self.onClose = onClose
1313
super.init(rootView: StoreCreationPlanView(viewModel: viewModel))
1414

1515
rootView.onPurchase = { [weak self] in
16-
self?.onPurchase()
16+
await self?.onPurchase()
1717
}
1818
}
1919

@@ -32,6 +32,12 @@ final class StoreCreationPlanHostingController: UIHostingController<StoreCreatio
3232
func configureNavigationBarAppearance() {
3333
addCloseNavigationBarButton(target: self, action: #selector(closeButtonTapped))
3434

35+
// If large title is only disabled with `navigationBarTitleDisplayMode(.inline)` in the SwiftUI view,
36+
// navigating from a screen with large title results in a gap below the navigation bar briefly.
37+
// Also disabling large title in the hosting controller smoothens the transition.
38+
navigationItem.largeTitleDisplayMode = .never
39+
navigationController?.navigationBar.prefersLargeTitles = false
40+
3541
let appearance = UINavigationBarAppearance()
3642
appearance.configureWithTransparentBackground()
3743
appearance.backgroundColor = .withColorStudio(.wooCommercePurple, shade: .shade90)
@@ -49,10 +55,12 @@ final class StoreCreationPlanHostingController: UIHostingController<StoreCreatio
4955
/// Displays the WPCOM eCommerce plan for purchase during the store creation flow.
5056
struct StoreCreationPlanView: View {
5157
/// Set in the hosting controller.
52-
var onPurchase: (() -> Void) = {}
58+
var onPurchase: (() async -> Void) = {}
5359

5460
let viewModel: StoreCreationPlanViewModel
5561

62+
@State private var isPurchaseInProgress: Bool = false
63+
5664
var body: some View {
5765
VStack(alignment: .leading, spacing: 0) {
5866
ScrollView {
@@ -126,9 +134,13 @@ struct StoreCreationPlanView: View {
126134

127135
// Continue button.
128136
Button(String(format: Localization.continueButtonTitleFormat, viewModel.plan.displayPrice)) {
129-
onPurchase()
137+
Task { @MainActor in
138+
isPurchaseInProgress = true
139+
await onPurchase()
140+
isPurchaseInProgress = false
141+
}
130142
}
131-
.buttonStyle(PrimaryButtonStyle())
143+
.buttonStyle(PrimaryLoadingButtonStyle(isLoading: isPurchaseInProgress))
132144
.padding(Layout.defaultButtonPadding)
133145

134146
// Refund information.
@@ -144,6 +156,10 @@ struct StoreCreationPlanView: View {
144156
.background(Color(.withColorStudio(.wooCommercePurple, shade: .shade90)))
145157
// This screen is using the dark theme for both light and dark modes.
146158
.environment(\.colorScheme, .dark)
159+
// Disables large title to avoid a large gap below the navigation bar.
160+
.navigationBarTitleDisplayMode(.inline)
161+
// Hides the back button and shows a close button in the hosting controller instead.
162+
.navigationBarBackButtonHidden(true)
147163
}
148164
}
149165

WooCommerce/Classes/Authentication/Store Creation/Store name/StoreNameForm.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ struct StoreNameForm: View {
104104
.buttonStyle(PrimaryButtonStyle())
105105
.disabled(name.isEmpty)
106106
}
107+
// Disables large title to avoid a large gap below the navigation bar.
108+
.navigationBarTitleDisplayMode(.inline)
107109
}
108110
}
109111

0 commit comments

Comments
 (0)