Skip to content

Commit 7d8e06b

Browse files
authored
Merge pull request #7959 from woocommerce/issue/7934-iap-site-creation-flow-interface
[In-App Purchases] Site creation flow interface
2 parents 10098bb + d658a7f commit 7d8e06b

File tree

5 files changed

+254
-28
lines changed

5 files changed

+254
-28
lines changed

WooCommerce/Classes/ViewRelated/InAppPurchases/InAppPurchasesDebugView.swift

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,90 @@ import SwiftUI
22
import StoreKit
33
import Yosemite
44

5+
@MainActor
56
struct InAppPurchasesDebugView: View {
67
let siteID: Int64
7-
private let stores = ServiceLocator.stores
8-
@State var products: [StoreKit.Product] = []
8+
private let inAppPurchasesForWPComPlansManager = InAppPurchasesForWPComPlansManager()
9+
@State var products: [WPComPlanProduct] = []
10+
@State var entitledProductIDs: [String] = []
11+
@State var inAppPurchasesAreSupported = true
912

1013
var body: some View {
1114
List {
1215
Section {
1316
Button("Reload products") {
14-
loadProducts()
17+
Task {
18+
await loadProducts()
19+
}
1520
}
1621
}
1722
Section("Products") {
1823
if products.isEmpty {
1924
Text("No products")
2025
} else {
21-
ForEach(products) { product in
22-
Button(product.description) {
23-
stores.dispatch(InAppPurchaseAction.purchaseProduct(siteID: siteID, product: product, completion: { _ in }))
26+
ForEach(products, id: \.id) { product in
27+
Button(entitledProductIDs.contains(product.id) ? "Entitled: \(product.description)" : product.description) {
28+
Task {
29+
try? await inAppPurchasesForWPComPlansManager.purchaseProduct(with: product.id, for: siteID)
30+
}
2431
}
2532
}
2633
}
2734
}
35+
36+
Section {
37+
Button("Retry WPCom Synchronization for entitled products") {
38+
retryWPComSynchronizationForPurchasedProducts()
39+
}.disabled(!inAppPurchasesAreSupported || entitledProductIDs.isEmpty)
40+
}
41+
42+
if !inAppPurchasesAreSupported {
43+
Section {
44+
Text("In-App Purchases are not supported for this user")
45+
.foregroundColor(.red)
46+
}
47+
}
2848
}
2949
.navigationTitle("IAP Debug")
30-
.onAppear {
31-
loadProducts()
50+
.task {
51+
await loadProducts()
52+
}
53+
}
54+
55+
private func loadProducts() async {
56+
do {
57+
inAppPurchasesAreSupported = await inAppPurchasesForWPComPlansManager.inAppPurchasesAreSupported()
58+
59+
guard inAppPurchasesAreSupported else {
60+
return
61+
}
62+
63+
self.products = try await inAppPurchasesForWPComPlansManager.fetchProducts()
64+
await loadUserEntitlements()
65+
} catch {
66+
print("Error loading products: \(error)")
67+
}
68+
}
69+
70+
private func loadUserEntitlements() async {
71+
do {
72+
for product in self.products {
73+
if try await inAppPurchasesForWPComPlansManager.userIsEntitledToProduct(with: product.id) {
74+
self.entitledProductIDs.append(product.id)
75+
}
76+
}
77+
}
78+
catch {
79+
print("Error loading user entitlements: \(error)")
3280
}
3381
}
3482

35-
private func loadProducts() {
36-
stores.dispatch(InAppPurchaseAction.loadProducts(completion: { result in
37-
switch result {
38-
case .success(let products):
39-
self.products = products
40-
case .failure(let error):
41-
print("Error loading products: \(error)")
83+
private func retryWPComSynchronizationForPurchasedProducts() {
84+
Task {
85+
for id in entitledProductIDs {
86+
try await inAppPurchasesForWPComPlansManager.retryWPComSyncForPurchasedProduct(with: id)
4287
}
43-
}))
88+
}
4489
}
4590
}
4691

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import Foundation
2+
import StoreKit
3+
import Yosemite
4+
5+
protocol WPComPlanProduct {
6+
// The localized product name, to be used as title in UI
7+
var displayName: String { get }
8+
// The localized product description
9+
var description: String { get }
10+
// The unique product identifier. To be used in further actions e.g purchasing a product
11+
var id: String { get }
12+
// The localized price, including currency
13+
var displayPrice: String { get }
14+
}
15+
16+
extension StoreKit.Product: WPComPlanProduct {}
17+
18+
protocol InAppPurchasesForWPComPlansProtocol {
19+
/// Retrieves asynchronously all WPCom plans In-App Purchases products.
20+
///
21+
func fetchProducts() async throws -> [WPComPlanProduct]
22+
23+
/// Returns whether the user is entitled the product identified with the passed id.
24+
///
25+
/// - Parameters:
26+
/// - id: the id of the product whose entitlement is to be verified
27+
///
28+
func userIsEntitledToProduct(with id: String) async throws -> Bool
29+
30+
/// Triggers the purchase of WPCom plan specified by the passed product id, linked to the passed site Id.
31+
///
32+
/// - Parameters:
33+
/// id: the id of the product to be purchased
34+
/// remoteSiteId: the id of the site linked to the purchasing plan
35+
///
36+
func purchaseProduct(with id: String, for remoteSiteId: Int64) async throws
37+
38+
/// Retries forwarding the product purchase to our backend, so the plan can be unlocked.
39+
/// This can happen when the purchase was previously successful but unlocking the WPCom plan request
40+
/// failed.
41+
///
42+
/// - Parameters:
43+
/// id: the id of the purchased product whose WPCom plan unlock failed
44+
///
45+
func retryWPComSyncForPurchasedProduct(with id: String) async throws
46+
47+
/// Returns whether In-App Purchases are supported for the current user configuration
48+
///
49+
func inAppPurchasesAreSupported() async -> Bool
50+
}
51+
52+
@MainActor
53+
final class InAppPurchasesForWPComPlansManager: InAppPurchasesForWPComPlansProtocol {
54+
private let stores: StoresManager
55+
56+
init(stores: StoresManager = ServiceLocator.stores) {
57+
self.stores = stores
58+
}
59+
60+
func fetchProducts() async throws -> [WPComPlanProduct] {
61+
try await withCheckedThrowingContinuation { continuation in
62+
stores.dispatch(InAppPurchaseAction.loadProducts(completion: { result in
63+
continuation.resume(with: result)
64+
}))
65+
}
66+
}
67+
68+
func userIsEntitledToProduct(with id: String) async throws -> Bool {
69+
try await withCheckedThrowingContinuation { continuation in
70+
stores.dispatch(InAppPurchaseAction.userIsEntitledToProduct(productID: id, completion: { result in
71+
continuation.resume(with: result)
72+
}))
73+
}
74+
}
75+
76+
func purchaseProduct(with id: String, for remoteSiteId: Int64) async throws {
77+
_ = try await withCheckedThrowingContinuation { continuation in
78+
stores.dispatch(InAppPurchaseAction.purchaseProduct(siteID: remoteSiteId, productID: id, completion: { result in
79+
continuation.resume(with: result)
80+
}))
81+
}
82+
}
83+
84+
func retryWPComSyncForPurchasedProduct(with id: String) async throws {
85+
try await withCheckedThrowingContinuation { continuation in
86+
stores.dispatch(InAppPurchaseAction.retryWPComSyncForPurchasedProduct(productID: id, completion: { result in
87+
continuation.resume(with: result)
88+
}))
89+
}
90+
}
91+
92+
func inAppPurchasesAreSupported() async -> Bool {
93+
await withCheckedContinuation { continuation in
94+
stores.dispatch(InAppPurchaseAction.inAppPurchasesAreSupported(completion: { result in
95+
continuation.resume(returning: result)
96+
}))
97+
}
98+
}
99+
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1328,6 +1328,7 @@
13281328
B958A7D628B5310100823EEF /* URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = B958A7D428B5302500823EEF /* URLOpener.swift */; };
13291329
B958A7D828B5316A00823EEF /* MockURLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = B958A7D728B5316A00823EEF /* MockURLOpener.swift */; };
13301330
B96B536B2816ECFC00F753E6 /* CardPresentPluginsDataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96B536A2816ECFC00F753E6 /* CardPresentPluginsDataProviderTests.swift */; };
1331+
B96D6C0729081AE5003D2DC0 /* InAppPurchasesForWPComPlansManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B96D6C0629081AE5003D2DC0 /* InAppPurchasesForWPComPlansManager.swift */; };
13311332
B9B0391628A6824200DC1C83 /* PermanentNoticePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B0391528A6824200DC1C83 /* PermanentNoticePresenter.swift */; };
13321333
B9B0391828A6838400DC1C83 /* PermanentNoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B0391728A6838400DC1C83 /* PermanentNoticeView.swift */; };
13331334
B9B0391A28A68ADE00DC1C83 /* ConstraintsUpdatingHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B0391928A68ADE00DC1C83 /* ConstraintsUpdatingHostingController.swift */; };
@@ -3258,6 +3259,7 @@
32583259
B958A7D428B5302500823EEF /* URLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLOpener.swift; sourceTree = "<group>"; };
32593260
B958A7D728B5316A00823EEF /* MockURLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLOpener.swift; sourceTree = "<group>"; };
32603261
B96B536A2816ECFC00F753E6 /* CardPresentPluginsDataProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPluginsDataProviderTests.swift; sourceTree = "<group>"; };
3262+
B96D6C0629081AE5003D2DC0 /* InAppPurchasesForWPComPlansManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchasesForWPComPlansManager.swift; sourceTree = "<group>"; };
32613263
B9B0391528A6824200DC1C83 /* PermanentNoticePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermanentNoticePresenter.swift; sourceTree = "<group>"; };
32623264
B9B0391728A6838400DC1C83 /* PermanentNoticeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PermanentNoticeView.swift; sourceTree = "<group>"; };
32633265
B9B0391928A68ADE00DC1C83 /* ConstraintsUpdatingHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstraintsUpdatingHostingController.swift; sourceTree = "<group>"; };
@@ -8554,6 +8556,7 @@
85548556
isa = PBXGroup;
85558557
children = (
85568558
E1325EFA28FD544E00EC9B2A /* InAppPurchasesDebugView.swift */,
8559+
B96D6C0629081AE5003D2DC0 /* InAppPurchasesForWPComPlansManager.swift */,
85578560
);
85588561
path = InAppPurchases;
85598562
sourceTree = "<group>";
@@ -10537,6 +10540,7 @@
1053710540
319A626127ACAE3400BC96C3 /* InPersonPaymentsPluginChoicesView.swift in Sources */,
1053810541
6856D31F941A33BAE66F394D /* KeyboardFrameAdjustmentProvider.swift in Sources */,
1053910542
CCFC50592743E021001E505F /* EditableOrderViewModel.swift in Sources */,
10543+
B96D6C0729081AE5003D2DC0 /* InAppPurchasesForWPComPlansManager.swift in Sources */,
1054010544
6856DB2E741639716E149967 /* KeyboardStateProvider.swift in Sources */,
1054110545
ABC35F18E744C5576B986CB3 /* InPersonPaymentsUnavailableView.swift in Sources */,
1054210546
ABC35528D2D6BE6F516E5CEF /* InPersonPaymentsOnboardingError.swift in Sources */,

Yosemite/Yosemite/Actions/InAppPurchaseAction.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@ import StoreKit
33

44
public enum InAppPurchaseAction: Action {
55
case loadProducts(completion: (Result<[StoreKit.Product], Error>) -> Void)
6-
case purchaseProduct(siteID: Int64, product: StoreKit.Product, completion: (Result<StoreKit.Product.PurchaseResult, Error>) -> Void)
6+
case purchaseProduct(siteID: Int64, productID: String, completion: (Result<StoreKit.Product.PurchaseResult, Error>) -> Void)
7+
case userIsEntitledToProduct(productID: String, completion: (Result<Bool, Error>) -> Void)
8+
case inAppPurchasesAreSupported(completion: (Bool) -> Void)
9+
case retryWPComSyncForPurchasedProduct(productID: String, completion: (Result<(), Error>) -> Void)
710
}

0 commit comments

Comments
 (0)