expo-iap 3.0.8 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -2
- package/CONTRIBUTING.md +19 -0
- package/README.md +18 -6
- package/android/build.gradle +24 -1
- package/android/src/main/java/expo/modules/iap/ExpoIapLog.kt +69 -0
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +190 -59
- package/build/index.d.ts +20 -47
- package/build/index.d.ts.map +1 -1
- package/build/index.js +94 -137
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +2 -1
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +16 -1
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +29 -16
- package/build/modules/ios.js.map +1 -1
- package/build/types.d.ts +8 -6
- package/build/types.d.ts.map +1 -1
- package/build/types.js.map +1 -1
- package/build/useIAP.d.ts +1 -1
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +12 -15
- package/build/useIAP.js.map +1 -1
- package/build/utils/errorMapping.d.ts +32 -23
- package/build/utils/errorMapping.d.ts.map +1 -1
- package/build/utils/errorMapping.js +117 -22
- package/build/utils/errorMapping.js.map +1 -1
- package/ios/ExpoIap.podspec +3 -2
- package/ios/ExpoIapHelper.swift +96 -0
- package/ios/ExpoIapLog.swift +127 -0
- package/ios/ExpoIapModule.swift +218 -340
- package/openiap-versions.json +5 -0
- package/package.json +2 -2
- package/plugin/build/withIAP.js +6 -4
- package/plugin/src/withIAP.ts +14 -4
- package/scripts/update-types.mjs +20 -1
- package/src/index.ts +122 -165
- package/src/modules/android.ts +2 -1
- package/src/modules/ios.ts +31 -19
- package/src/types.ts +8 -6
- package/src/useIAP.ts +17 -25
- package/src/utils/errorMapping.ts +203 -23
- package/build/purchase-error.d.ts +0 -67
- package/build/purchase-error.d.ts.map +0 -1
- package/build/purchase-error.js +0 -166
- package/build/purchase-error.js.map +0 -1
- package/build/utils/purchase.d.ts +0 -9
- package/build/utils/purchase.d.ts.map +0 -1
- package/build/utils/purchase.js +0 -34
- package/build/utils/purchase.js.map +0 -1
- package/src/purchase-error.ts +0 -265
- package/src/utils/purchase.ts +0 -52
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -1,53 +1,18 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
|
-
import
|
|
2
|
+
import Foundation
|
|
3
3
|
import OpenIAP
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
private func logDebug(_ message: String) {
|
|
8
|
-
// Use OSLog/Logger so logs are structured and filterable
|
|
9
|
-
// Suppress debug logs in Release builds
|
|
10
|
-
#if DEBUG
|
|
11
|
-
iapLogger.debug("\(message, privacy: .public)")
|
|
12
|
-
#endif
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// MARK: - Swift helpers for optional dictionary compaction
|
|
16
|
-
extension Sequence where Element == [String: Any?] {
|
|
17
|
-
fileprivate func compactingValues() -> [[String: Any]] {
|
|
18
|
-
return self.map { $0.compactMapValues { $0 } }
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
extension Dictionary where Key == String, Value == Any? {
|
|
23
|
-
fileprivate func compactingValues() -> [String: Any] {
|
|
24
|
-
return self.compactMapValues { $0 }
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Event names
|
|
29
|
-
struct OpenIapEvent {
|
|
30
|
-
static let PurchaseUpdated = "purchase-updated"
|
|
31
|
-
static let PurchaseError = "purchase-error"
|
|
32
|
-
static let PromotedProductIOS = "promoted-product-ios"
|
|
33
|
-
}
|
|
4
|
+
#if canImport(UIKit)
|
|
5
|
+
import UIKit
|
|
6
|
+
#endif
|
|
34
7
|
|
|
35
8
|
@available(iOS 15.0, tvOS 15.0, *)
|
|
36
9
|
@MainActor
|
|
37
|
-
public class ExpoIapModule: Module {
|
|
38
|
-
|
|
39
|
-
private var isInitialized: Bool = false
|
|
40
|
-
// Subscriptions for OpenIapModule event listeners
|
|
10
|
+
public final class ExpoIapModule: Module {
|
|
11
|
+
private var isInitialized = false
|
|
41
12
|
private var purchaseUpdatedSub: Subscription?
|
|
42
13
|
private var purchaseErrorSub: Subscription?
|
|
43
14
|
private var promotedProductSub: Subscription?
|
|
44
15
|
|
|
45
|
-
// Helper to safely remove a listener and nil out the reference
|
|
46
|
-
private func removeListener(_ sub: inout Subscription?) {
|
|
47
|
-
if let s = sub { OpenIapModule.shared.removeListener(s) }
|
|
48
|
-
sub = nil
|
|
49
|
-
}
|
|
50
|
-
|
|
51
16
|
nonisolated public func definition() -> ModuleDefinition {
|
|
52
17
|
Name("ExpoIap")
|
|
53
18
|
|
|
@@ -56,473 +21,386 @@ public class ExpoIapModule: Module {
|
|
|
56
21
|
}
|
|
57
22
|
|
|
58
23
|
Events(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
24
|
+
IapEvent.purchaseUpdated.rawValue,
|
|
25
|
+
IapEvent.purchaseError.rawValue,
|
|
26
|
+
IapEvent.promotedProductIos.rawValue
|
|
62
27
|
)
|
|
63
28
|
|
|
64
29
|
OnCreate {
|
|
65
|
-
logDebug("Module created")
|
|
66
30
|
Task { @MainActor in
|
|
67
31
|
self.setupStore()
|
|
68
32
|
}
|
|
69
33
|
}
|
|
70
34
|
|
|
71
35
|
OnDestroy {
|
|
72
|
-
logDebug("Module destroyed")
|
|
73
36
|
Task { @MainActor in
|
|
74
37
|
await self.cleanupStore()
|
|
75
38
|
}
|
|
76
39
|
}
|
|
77
40
|
|
|
78
|
-
// MARK: - Connection Management
|
|
79
|
-
|
|
80
41
|
AsyncFunction("initConnection") { () async throws -> Bool in
|
|
81
|
-
logDebug("initConnection called")
|
|
82
42
|
let isConnected = try await OpenIapModule.shared.initConnection()
|
|
83
|
-
// Track initialization locally for ensureConnection()
|
|
84
43
|
await MainActor.run { self.isInitialized = isConnected }
|
|
85
|
-
logDebug("Connection initialized: \(isConnected)")
|
|
86
44
|
return isConnected
|
|
87
45
|
}
|
|
88
46
|
|
|
89
47
|
AsyncFunction("endConnection") { () async throws -> Bool in
|
|
90
|
-
|
|
91
|
-
let _ = try await OpenIapModule.shared.endConnection()
|
|
92
|
-
|
|
93
|
-
logDebug("Connection ended")
|
|
48
|
+
let succeeded = try await OpenIapModule.shared.endConnection()
|
|
94
49
|
await MainActor.run { self.isInitialized = false }
|
|
95
|
-
return
|
|
50
|
+
return succeeded
|
|
96
51
|
}
|
|
97
52
|
|
|
98
|
-
// MARK: - Product Management
|
|
99
|
-
|
|
100
53
|
AsyncFunction("fetchProducts") { (params: [String: Any]) async throws -> [[String: Any]] in
|
|
54
|
+
ExpoIapLog.payload("fetchProducts", payload: params)
|
|
101
55
|
try await ensureConnection()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if let skusArray = params["skus"] as? [String] {
|
|
109
|
-
// Object format: {skus: [...], type: "..."}
|
|
110
|
-
skus = skusArray
|
|
111
|
-
typeString = params["type"] as? String ?? "all"
|
|
112
|
-
} else {
|
|
113
|
-
// Array format passed directly - reconstruct from indexed keys
|
|
114
|
-
var tempSkus: [String] = []
|
|
115
|
-
var index = 0
|
|
116
|
-
while let sku = params["\(index)"] as? String {
|
|
117
|
-
tempSkus.append(sku)
|
|
118
|
-
index += 1
|
|
119
|
-
}
|
|
120
|
-
skus = tempSkus
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
logDebug("fetchProducts parsed - skus: \(skus), type: \(typeString)")
|
|
124
|
-
logDebug("SKUs count: \(skus.count)")
|
|
125
|
-
|
|
126
|
-
// Validate SKUs
|
|
127
|
-
guard !skus.isEmpty else {
|
|
128
|
-
logDebug("ERROR: Empty SKUs array!")
|
|
129
|
-
throw OpenIapError.emptySkuList()
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Convert string to OpenIapRequestProductType enum
|
|
133
|
-
let productType: OpenIapRequestProductType = {
|
|
134
|
-
switch typeString {
|
|
135
|
-
case "inapp":
|
|
136
|
-
return .inapp
|
|
137
|
-
case "subs":
|
|
138
|
-
return .subs
|
|
139
|
-
default:
|
|
140
|
-
return .all
|
|
141
|
-
}
|
|
142
|
-
}()
|
|
143
|
-
|
|
144
|
-
logDebug("Converted type to OpenIapRequestProductType: \(productType)")
|
|
145
|
-
|
|
146
|
-
// Build OpenIapProductRequest and fetch via OpenIapModule
|
|
147
|
-
let request = OpenIapProductRequest(skus: skus, type: productType)
|
|
148
|
-
let products = try await OpenIapModule.shared.fetchProducts(request)
|
|
149
|
-
logDebug("Fetched \(products.count) products from store")
|
|
150
|
-
if products.isEmpty {
|
|
151
|
-
logDebug("No products found. Possible reasons:")
|
|
152
|
-
logDebug("1. Products not configured in App Store Connect")
|
|
153
|
-
logDebug("2. Bundle ID mismatch")
|
|
154
|
-
logDebug("3. Not signed in to sandbox account")
|
|
155
|
-
logDebug("4. Products pending review")
|
|
156
|
-
}
|
|
157
|
-
for product in products {
|
|
158
|
-
logDebug("Product: \(product.id) - \(product.title) - \(product.displayPrice)")
|
|
159
|
-
}
|
|
160
|
-
// Ensure non-optional values for Expo bridge
|
|
161
|
-
return OpenIapSerialization.products(products).compactingValues()
|
|
56
|
+
let request = try ExpoIapHelper.decodeProductRequest(from: params)
|
|
57
|
+
let result = try await OpenIapModule.shared.fetchProducts(request)
|
|
58
|
+
let products = ExpoIapHelper.sanitizeArray(OpenIapSerialization.products(result))
|
|
59
|
+
ExpoIapLog.result("fetchProducts", value: products)
|
|
60
|
+
return products
|
|
162
61
|
}
|
|
163
62
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
AsyncFunction("requestPurchase") { (params: [String: Any]) async throws in
|
|
167
|
-
// Extract and validate required fields
|
|
168
|
-
guard let sku = params["sku"] as? String, !sku.isEmpty else {
|
|
169
|
-
throw OpenIapError.make(
|
|
170
|
-
code: OpenIapError.PurchaseError, message: "Missing required 'sku'")
|
|
171
|
-
}
|
|
63
|
+
AsyncFunction("requestPurchase") { (payload: [String: Any]) async throws -> Any? in
|
|
64
|
+
ExpoIapLog.payload("requestPurchase", payload: payload)
|
|
172
65
|
try await ensureConnection()
|
|
66
|
+
let props = try ExpoIapHelper.decodeRequestPurchaseProps(from: payload)
|
|
173
67
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
let quantity: Int? = {
|
|
179
|
-
if let q = params["quantity"] as? Int { return q }
|
|
180
|
-
if let qd = params["quantity"] as? Double { return Int(qd) }
|
|
181
|
-
return nil
|
|
182
|
-
}()
|
|
183
|
-
|
|
184
|
-
// Discount offer mapping (strings expected from JS)
|
|
185
|
-
// Use OpenIapDiscountOffer from OpenIAP package (avoid relying on legacy typealiases)
|
|
186
|
-
var discountOffer: OpenIapDiscountOffer? = nil
|
|
187
|
-
if let offer = params["withOffer"] as? [String: Any] {
|
|
188
|
-
let identifier = (offer["identifier"] as? String) ?? (offer["id"] as? String) ?? ""
|
|
189
|
-
let keyIdentifier = (offer["keyIdentifier"] as? String) ?? ""
|
|
190
|
-
let nonce = (offer["nonce"] as? String) ?? ""
|
|
191
|
-
let signature = (offer["signature"] as? String) ?? ""
|
|
192
|
-
let timestamp = (offer["timestamp"] as? String) ?? ""
|
|
193
|
-
if !identifier.isEmpty && !keyIdentifier.isEmpty && !nonce.isEmpty
|
|
194
|
-
&& !signature.isEmpty && !timestamp.isEmpty
|
|
195
|
-
{
|
|
196
|
-
discountOffer = OpenIapDiscountOffer(
|
|
197
|
-
identifier: identifier,
|
|
198
|
-
keyIdentifier: keyIdentifier,
|
|
199
|
-
nonce: nonce,
|
|
200
|
-
signature: signature,
|
|
201
|
-
timestamp: timestamp
|
|
202
|
-
)
|
|
68
|
+
do {
|
|
69
|
+
guard let result = try await OpenIapModule.shared.requestPurchase(props) else {
|
|
70
|
+
ExpoIapLog.result("requestPurchase", value: nil)
|
|
71
|
+
return nil
|
|
203
72
|
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
let tokenForLog = appAccountToken ?? "nil"
|
|
207
|
-
let qtyForLog = quantity ?? -1
|
|
208
|
-
logDebug(
|
|
209
|
-
"requestPurchase parsed - sku: \(sku), andFinish: \(andFinish), appAccountToken: \(tokenForLog), quantity: \(qtyForLog), hasOffer: \(discountOffer != nil)"
|
|
210
|
-
)
|
|
211
73
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
logDebug("Purchase request completed successfully")
|
|
224
|
-
} catch {
|
|
225
|
-
logDebug("Purchase request failed with error: \(error)")
|
|
226
|
-
if let openIapError = error as? OpenIapError {
|
|
227
|
-
throw openIapError
|
|
74
|
+
switch result {
|
|
75
|
+
case .purchase(let maybePurchase):
|
|
76
|
+
guard let purchase = maybePurchase else { return nil }
|
|
77
|
+
let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.purchase(purchase))
|
|
78
|
+
ExpoIapLog.result("requestPurchase", value: sanitized)
|
|
79
|
+
return sanitized
|
|
80
|
+
case .purchases(let maybePurchases):
|
|
81
|
+
guard let purchases = maybePurchases else { return nil }
|
|
82
|
+
let sanitized = ExpoIapHelper.sanitizeArray(OpenIapSerialization.purchases(purchases))
|
|
83
|
+
ExpoIapLog.result("requestPurchase", value: sanitized)
|
|
84
|
+
return sanitized
|
|
228
85
|
}
|
|
229
|
-
|
|
230
|
-
|
|
86
|
+
} catch let error as PurchaseError {
|
|
87
|
+
ExpoIapLog.failure("requestPurchase", error: error)
|
|
88
|
+
throw error
|
|
89
|
+
} catch {
|
|
90
|
+
ExpoIapLog.failure("requestPurchase", error: error)
|
|
91
|
+
throw PurchaseError.make(code: .purchaseError, message: error.localizedDescription)
|
|
231
92
|
}
|
|
232
93
|
}
|
|
233
94
|
|
|
234
|
-
AsyncFunction("finishTransaction") {
|
|
95
|
+
AsyncFunction("finishTransaction") {
|
|
96
|
+
(purchasePayload: [String: Any], isConsumable: Bool?) async throws -> Bool in
|
|
97
|
+
ExpoIapLog.payload(
|
|
98
|
+
"finishTransaction",
|
|
99
|
+
payload: [
|
|
100
|
+
"purchase": purchasePayload,
|
|
101
|
+
"isConsumable": isConsumable as Any,
|
|
102
|
+
]
|
|
103
|
+
)
|
|
235
104
|
try await ensureConnection()
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
105
|
+
let purchaseInput = try OpenIapSerialization.purchaseInput(from: purchasePayload)
|
|
106
|
+
try await OpenIapModule.shared.finishTransaction(
|
|
107
|
+
purchase: purchaseInput,
|
|
108
|
+
isConsumable: isConsumable
|
|
109
|
+
)
|
|
110
|
+
ExpoIapLog.result("finishTransaction", value: true)
|
|
111
|
+
return true
|
|
240
112
|
}
|
|
241
113
|
|
|
242
|
-
// MARK: - Purchase History
|
|
243
|
-
|
|
244
114
|
AsyncFunction("getAvailablePurchases") {
|
|
245
|
-
(options: [String: Any
|
|
115
|
+
(options: [String: Any]?) async throws -> [[String: Any]] in
|
|
116
|
+
ExpoIapLog.payload("getAvailablePurchases", payload: options ?? [:])
|
|
246
117
|
try await ensureConnection()
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// Build options and get purchases directly from OpenIapModule
|
|
250
|
-
let purchaseOptions: OpenIapGetAvailablePurchasesProps? = options.map {
|
|
251
|
-
OpenIapGetAvailablePurchasesProps(
|
|
252
|
-
alsoPublishToEventListenerIOS: $0["alsoPublishToEventListenerIOS"] as? Bool,
|
|
253
|
-
onlyIncludeActiveItemsIOS: $0["onlyIncludeActiveItemsIOS"] as? Bool
|
|
254
|
-
)
|
|
255
|
-
}
|
|
118
|
+
let purchaseOptions = try options.map { try OpenIapSerialization.purchaseOptions(from: $0) }
|
|
256
119
|
let purchases = try await OpenIapModule.shared.getAvailablePurchases(purchaseOptions)
|
|
257
|
-
|
|
120
|
+
let sanitized = ExpoIapHelper.sanitizeArray(OpenIapSerialization.purchases(purchases))
|
|
121
|
+
ExpoIapLog.result("getAvailablePurchases", value: sanitized)
|
|
122
|
+
return sanitized
|
|
258
123
|
}
|
|
259
124
|
|
|
260
|
-
// Legacy function for backward compatibility
|
|
261
125
|
AsyncFunction("getAvailableItems") {
|
|
262
|
-
(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
onlyIncludeActiveItemsIOS: onlyIncludeActiveItems
|
|
126
|
+
(alsoPublish: Bool, onlyIncludeActive: Bool) async throws -> [[String: Any]] in
|
|
127
|
+
ExpoIapLog.payload(
|
|
128
|
+
"getAvailableItems",
|
|
129
|
+
payload: [
|
|
130
|
+
"alsoPublishToEventListenerIOS": alsoPublish,
|
|
131
|
+
"onlyIncludeActiveItemsIOS": onlyIncludeActive,
|
|
132
|
+
]
|
|
270
133
|
)
|
|
271
|
-
|
|
272
|
-
|
|
134
|
+
try await ensureConnection()
|
|
135
|
+
let optionsDictionary: [String: Any] = [
|
|
136
|
+
"alsoPublishToEventListenerIOS": alsoPublish,
|
|
137
|
+
"onlyIncludeActiveItemsIOS": onlyIncludeActive
|
|
138
|
+
]
|
|
139
|
+
let options = try OpenIapSerialization.purchaseOptions(from: optionsDictionary)
|
|
140
|
+
let purchases = try await OpenIapModule.shared.getAvailablePurchases(options)
|
|
141
|
+
let sanitized = ExpoIapHelper.sanitizeArray(OpenIapSerialization.purchases(purchases))
|
|
142
|
+
ExpoIapLog.result("getAvailableItems", value: sanitized)
|
|
143
|
+
return sanitized
|
|
273
144
|
}
|
|
274
145
|
|
|
275
146
|
AsyncFunction("getPendingTransactionsIOS") { () async throws -> [[String: Any]] in
|
|
147
|
+
ExpoIapLog.payload("getPendingTransactionsIOS", payload: nil)
|
|
276
148
|
try await ensureConnection()
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return
|
|
149
|
+
let pending = try await OpenIapModule.shared.getPendingTransactionsIOS()
|
|
150
|
+
let sanitized = pending.map { ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode($0)) }
|
|
151
|
+
ExpoIapLog.result("getPendingTransactionsIOS", value: sanitized)
|
|
152
|
+
return sanitized
|
|
281
153
|
}
|
|
282
154
|
|
|
283
155
|
AsyncFunction("clearTransactionIOS") { () async throws -> Bool in
|
|
156
|
+
ExpoIapLog.payload("clearTransactionIOS", payload: nil)
|
|
284
157
|
try await ensureConnection()
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return
|
|
158
|
+
let success = try await OpenIapModule.shared.clearTransactionIOS()
|
|
159
|
+
ExpoIapLog.result("clearTransactionIOS", value: success)
|
|
160
|
+
return success
|
|
288
161
|
}
|
|
289
162
|
|
|
290
|
-
// MARK: - Receipt & Validation
|
|
291
|
-
|
|
292
163
|
AsyncFunction("getReceiptIOS") { () async throws -> String in
|
|
164
|
+
ExpoIapLog.payload("getReceiptIOS", payload: nil)
|
|
293
165
|
try await ensureConnection()
|
|
294
|
-
|
|
295
|
-
|
|
166
|
+
let receipt = try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
|
|
167
|
+
ExpoIapLog.result("getReceiptIOS", value: receipt)
|
|
168
|
+
return receipt
|
|
296
169
|
}
|
|
297
170
|
|
|
298
|
-
// Backward-compatible alias expected by JS layer/tests
|
|
299
171
|
AsyncFunction("getReceiptDataIOS") { () async throws -> String in
|
|
172
|
+
ExpoIapLog.payload("getReceiptDataIOS", payload: nil)
|
|
300
173
|
try await ensureConnection()
|
|
301
|
-
|
|
302
|
-
|
|
174
|
+
let receipt = try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
|
|
175
|
+
ExpoIapLog.result("getReceiptDataIOS", value: receipt)
|
|
176
|
+
return receipt
|
|
303
177
|
}
|
|
304
178
|
|
|
305
179
|
AsyncFunction("requestReceiptRefreshIOS") { () async throws -> String in
|
|
180
|
+
ExpoIapLog.payload("requestReceiptRefreshIOS", payload: nil)
|
|
306
181
|
try await ensureConnection()
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return
|
|
182
|
+
let receipt = try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
|
|
183
|
+
ExpoIapLog.result("requestReceiptRefreshIOS", value: receipt)
|
|
184
|
+
return receipt
|
|
310
185
|
}
|
|
311
186
|
|
|
312
187
|
AsyncFunction("validateReceiptIOS") { (sku: String) async throws -> [String: Any] in
|
|
188
|
+
ExpoIapLog.payload("validateReceiptIOS", payload: ["sku": sku])
|
|
313
189
|
try await ensureConnection()
|
|
314
|
-
logDebug("validateReceiptIOS called for sku: \(sku)")
|
|
315
190
|
do {
|
|
316
|
-
|
|
317
|
-
let props = OpenIapReceiptValidationProps(sku: sku)
|
|
191
|
+
let props = try OpenIapSerialization.receiptValidationProps(from: ["sku": sku])
|
|
318
192
|
let result = try await OpenIapModule.shared.validateReceiptIOS(props)
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
},
|
|
328
|
-
]
|
|
329
|
-
return dict.compactingValues()
|
|
193
|
+
var payload = OpenIapSerialization.encode(result)
|
|
194
|
+
payload["purchaseToken"] = result.jwsRepresentation
|
|
195
|
+
let sanitized = ExpoIapHelper.sanitizeDictionary(payload)
|
|
196
|
+
ExpoIapLog.result("validateReceiptIOS", value: sanitized)
|
|
197
|
+
return sanitized
|
|
198
|
+
} catch let error as PurchaseError {
|
|
199
|
+
ExpoIapLog.failure("validateReceiptIOS", error: error)
|
|
200
|
+
throw error
|
|
330
201
|
} catch {
|
|
331
|
-
|
|
202
|
+
ExpoIapLog.failure("validateReceiptIOS", error: error)
|
|
203
|
+
throw PurchaseError.make(code: .receiptFailed)
|
|
332
204
|
}
|
|
333
205
|
}
|
|
334
206
|
|
|
335
|
-
// MARK: - iOS Specific Features
|
|
336
|
-
|
|
337
207
|
AsyncFunction("presentCodeRedemptionSheetIOS") { () async throws -> Bool in
|
|
208
|
+
ExpoIapLog.payload("presentCodeRedemptionSheetIOS", payload: nil)
|
|
338
209
|
try await ensureConnection()
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
return
|
|
210
|
+
let success = try await OpenIapModule.shared.presentCodeRedemptionSheetIOS()
|
|
211
|
+
ExpoIapLog.result("presentCodeRedemptionSheetIOS", value: success)
|
|
212
|
+
return success
|
|
342
213
|
}
|
|
343
214
|
|
|
344
215
|
AsyncFunction("showManageSubscriptionsIOS") { () async throws -> [[String: Any]] in
|
|
216
|
+
ExpoIapLog.payload("showManageSubscriptionsIOS", payload: nil)
|
|
345
217
|
try await ensureConnection()
|
|
346
|
-
logDebug("showManageSubscriptionsIOS called")
|
|
347
|
-
// OpenIAP returns already-serialized dictionaries here.
|
|
348
218
|
let purchases = try await OpenIapModule.shared.showManageSubscriptionsIOS()
|
|
349
|
-
|
|
219
|
+
let sanitized = purchases.map { ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode($0)) }
|
|
220
|
+
ExpoIapLog.result("showManageSubscriptionsIOS", value: sanitized)
|
|
221
|
+
return sanitized
|
|
350
222
|
}
|
|
351
223
|
|
|
352
|
-
AsyncFunction("deepLinkToSubscriptionsIOS") { () async throws in
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
359
|
-
}
|
|
360
|
-
#endif
|
|
361
|
-
}
|
|
224
|
+
AsyncFunction("deepLinkToSubscriptionsIOS") { () async throws -> Bool in
|
|
225
|
+
ExpoIapLog.payload("deepLinkToSubscriptionsIOS", payload: nil)
|
|
226
|
+
try await ensureConnection()
|
|
227
|
+
try await OpenIapModule.shared.deepLinkToSubscriptions(nil)
|
|
228
|
+
ExpoIapLog.result("deepLinkToSubscriptionsIOS", value: true)
|
|
229
|
+
return true
|
|
362
230
|
}
|
|
363
231
|
|
|
364
232
|
AsyncFunction("beginRefundRequestIOS") { (sku: String) async throws -> String? in
|
|
233
|
+
ExpoIapLog.payload("beginRefundRequestIOS", payload: ["sku": sku])
|
|
365
234
|
try await ensureConnection()
|
|
366
|
-
|
|
367
|
-
|
|
235
|
+
let result = try await OpenIapModule.shared.beginRefundRequestIOS(sku: sku)
|
|
236
|
+
ExpoIapLog.result("beginRefundRequestIOS", value: result)
|
|
237
|
+
return result
|
|
368
238
|
}
|
|
369
239
|
|
|
370
240
|
AsyncFunction("getPromotedProductIOS") { () async throws -> [String: Any]? in
|
|
241
|
+
ExpoIapLog.payload("getPromotedProductIOS", payload: nil)
|
|
371
242
|
try await ensureConnection()
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
let request = OpenIapProductRequest(skus: [promoted.productIdentifier], type: .all)
|
|
377
|
-
let products = try await OpenIapModule.shared.fetchProducts(request)
|
|
378
|
-
let serialized = OpenIapSerialization.products(products).compactingValues()
|
|
379
|
-
return serialized.first
|
|
243
|
+
if let product = try await OpenIapModule.shared.getPromotedProductIOS() {
|
|
244
|
+
let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(product))
|
|
245
|
+
ExpoIapLog.result("getPromotedProductIOS", value: sanitized)
|
|
246
|
+
return sanitized
|
|
380
247
|
}
|
|
248
|
+
ExpoIapLog.result("getPromotedProductIOS", value: nil)
|
|
381
249
|
return nil
|
|
382
250
|
}
|
|
251
|
+
|
|
383
252
|
AsyncFunction("getStorefrontIOS") { () async throws -> String in
|
|
253
|
+
ExpoIapLog.payload("getStorefrontIOS", payload: nil)
|
|
384
254
|
try await ensureConnection()
|
|
385
|
-
|
|
386
|
-
|
|
255
|
+
let storefront = try await OpenIapModule.shared.getStorefrontIOS()
|
|
256
|
+
ExpoIapLog.result("getStorefrontIOS", value: storefront)
|
|
257
|
+
return storefront
|
|
387
258
|
}
|
|
388
259
|
|
|
389
260
|
AsyncFunction("syncIOS") { () async throws -> Bool in
|
|
261
|
+
ExpoIapLog.payload("syncIOS", payload: nil)
|
|
390
262
|
try await ensureConnection()
|
|
391
|
-
|
|
392
|
-
|
|
263
|
+
let success = try await OpenIapModule.shared.syncIOS()
|
|
264
|
+
ExpoIapLog.result("syncIOS", value: success)
|
|
265
|
+
return success
|
|
393
266
|
}
|
|
394
267
|
|
|
395
|
-
// MARK: - Additional iOS Methods
|
|
396
|
-
|
|
397
268
|
AsyncFunction("isTransactionVerifiedIOS") { (sku: String) async throws -> Bool in
|
|
269
|
+
ExpoIapLog.payload("isTransactionVerifiedIOS", payload: ["sku": sku])
|
|
398
270
|
try await ensureConnection()
|
|
399
|
-
|
|
400
|
-
|
|
271
|
+
let verified = try await OpenIapModule.shared.isTransactionVerifiedIOS(sku: sku)
|
|
272
|
+
ExpoIapLog.result("isTransactionVerifiedIOS", value: verified)
|
|
273
|
+
return verified
|
|
401
274
|
}
|
|
402
275
|
|
|
403
276
|
AsyncFunction("getTransactionJwsIOS") { (sku: String) async throws -> String? in
|
|
277
|
+
ExpoIapLog.payload("getTransactionJwsIOS", payload: ["sku": sku])
|
|
404
278
|
try await ensureConnection()
|
|
405
|
-
|
|
406
|
-
|
|
279
|
+
let jws = try await OpenIapModule.shared.getTransactionJwsIOS(sku: sku)
|
|
280
|
+
ExpoIapLog.result("getTransactionJwsIOS", value: jws)
|
|
281
|
+
return jws
|
|
407
282
|
}
|
|
408
283
|
|
|
409
284
|
AsyncFunction("isEligibleForIntroOfferIOS") { (groupID: String) async throws -> Bool in
|
|
285
|
+
ExpoIapLog.payload("isEligibleForIntroOfferIOS", payload: ["groupID": groupID])
|
|
410
286
|
try await ensureConnection()
|
|
411
|
-
|
|
412
|
-
|
|
287
|
+
let eligible = try await OpenIapModule.shared.isEligibleForIntroOfferIOS(groupID: groupID)
|
|
288
|
+
ExpoIapLog.result("isEligibleForIntroOfferIOS", value: eligible)
|
|
289
|
+
return eligible
|
|
413
290
|
}
|
|
414
291
|
|
|
415
292
|
AsyncFunction("subscriptionStatusIOS") { (sku: String) async throws -> [[String: Any]]? in
|
|
293
|
+
ExpoIapLog.payload("subscriptionStatusIOS", payload: ["sku": sku])
|
|
416
294
|
try await ensureConnection()
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
// { state: SubscriptionState; renewalInfo?: { jsonRepresentation?: string; willAutoRenew: boolean; autoRenewPreference?: string } }
|
|
422
|
-
return statuses.map { status in
|
|
423
|
-
var dict: [String: Any?] = [
|
|
424
|
-
"state": status.state
|
|
425
|
-
]
|
|
426
|
-
|
|
427
|
-
if let info = status.renewalInfo {
|
|
428
|
-
// autoRenewStatus is a Bool from OpenIAP types
|
|
429
|
-
let renewalInfo: [String: Any?] = [
|
|
430
|
-
"willAutoRenew": info.autoRenewStatus,
|
|
431
|
-
"autoRenewPreference": info.autoRenewPreference,
|
|
432
|
-
]
|
|
433
|
-
dict["renewalInfo"] = renewalInfo
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return dict.compactingValues()
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
return nil
|
|
295
|
+
let statuses = try await OpenIapModule.shared.subscriptionStatusIOS(sku: sku)
|
|
296
|
+
let sanitized = statuses.map { ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode($0)) }
|
|
297
|
+
ExpoIapLog.result("subscriptionStatusIOS", value: sanitized)
|
|
298
|
+
return sanitized
|
|
440
299
|
}
|
|
441
300
|
|
|
442
301
|
AsyncFunction("currentEntitlementIOS") { (sku: String) async throws -> [String: Any]? in
|
|
302
|
+
ExpoIapLog.payload("currentEntitlementIOS", payload: ["sku": sku])
|
|
443
303
|
try await ensureConnection()
|
|
444
|
-
logDebug("currentEntitlementIOS called for sku: \(sku)")
|
|
445
304
|
do {
|
|
446
|
-
if let entitlement = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku)
|
|
447
|
-
|
|
448
|
-
|
|
305
|
+
if let entitlement = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku) {
|
|
306
|
+
let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(entitlement))
|
|
307
|
+
ExpoIapLog.result("currentEntitlementIOS", value: sanitized)
|
|
308
|
+
return sanitized
|
|
449
309
|
}
|
|
310
|
+
ExpoIapLog.result("currentEntitlementIOS", value: nil)
|
|
450
311
|
return nil
|
|
312
|
+
} catch let error as PurchaseError {
|
|
313
|
+
ExpoIapLog.failure("currentEntitlementIOS", error: error)
|
|
314
|
+
throw error
|
|
451
315
|
} catch {
|
|
452
|
-
|
|
316
|
+
ExpoIapLog.failure("currentEntitlementIOS", error: error)
|
|
317
|
+
throw PurchaseError.make(code: .skuNotFound, productId: sku)
|
|
453
318
|
}
|
|
454
319
|
}
|
|
455
320
|
|
|
456
321
|
AsyncFunction("latestTransactionIOS") { (sku: String) async throws -> [String: Any]? in
|
|
322
|
+
ExpoIapLog.payload("latestTransactionIOS", payload: ["sku": sku])
|
|
457
323
|
try await ensureConnection()
|
|
458
|
-
logDebug("latestTransactionIOS called for sku: \(sku)")
|
|
459
324
|
do {
|
|
460
325
|
if let transaction = try await OpenIapModule.shared.latestTransactionIOS(sku: sku) {
|
|
461
|
-
|
|
326
|
+
let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(transaction))
|
|
327
|
+
ExpoIapLog.result("latestTransactionIOS", value: sanitized)
|
|
328
|
+
return sanitized
|
|
462
329
|
}
|
|
330
|
+
ExpoIapLog.result("latestTransactionIOS", value: nil)
|
|
463
331
|
return nil
|
|
332
|
+
} catch let error as PurchaseError {
|
|
333
|
+
ExpoIapLog.failure("latestTransactionIOS", error: error)
|
|
334
|
+
throw error
|
|
464
335
|
} catch {
|
|
465
|
-
|
|
336
|
+
ExpoIapLog.failure("latestTransactionIOS", error: error)
|
|
337
|
+
throw PurchaseError.make(code: .skuNotFound, productId: sku)
|
|
466
338
|
}
|
|
467
339
|
}
|
|
468
340
|
}
|
|
469
341
|
|
|
470
|
-
// MARK: - Listeners Setup
|
|
471
|
-
|
|
472
342
|
@MainActor
|
|
473
343
|
private func setupStore() {
|
|
474
|
-
logDebug("Setting up OpenIapModule event listeners")
|
|
475
|
-
|
|
476
344
|
purchaseUpdatedSub = OpenIapModule.shared.purchaseUpdatedListener { [weak self] purchase in
|
|
477
345
|
Task { @MainActor in
|
|
478
346
|
guard let self else { return }
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
self.sendEvent(OpenIapEvent.PurchaseUpdated, purchaseData)
|
|
347
|
+
let payload = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.purchase(purchase))
|
|
348
|
+
self.sendEvent(IapEvent.purchaseUpdated.rawValue, payload)
|
|
482
349
|
}
|
|
483
350
|
}
|
|
484
351
|
|
|
485
|
-
purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self]
|
|
352
|
+
purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
|
|
486
353
|
Task { @MainActor in
|
|
487
354
|
guard let self else { return }
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
"code": event.code,
|
|
491
|
-
"message": event.message,
|
|
492
|
-
"productId": event.productId,
|
|
493
|
-
]
|
|
494
|
-
self.sendEvent(OpenIapEvent.PurchaseError, errorData)
|
|
355
|
+
let payload = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(error))
|
|
356
|
+
self.sendEvent(IapEvent.purchaseError.rawValue, payload)
|
|
495
357
|
}
|
|
496
358
|
}
|
|
497
359
|
|
|
498
|
-
promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS {
|
|
499
|
-
[weak self] productId in
|
|
360
|
+
promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS { [weak self] productId in
|
|
500
361
|
Task { @MainActor in
|
|
501
362
|
guard let self else { return }
|
|
502
|
-
|
|
503
|
-
|
|
363
|
+
do {
|
|
364
|
+
if let product = try await OpenIapModule.shared.getPromotedProductIOS() {
|
|
365
|
+
let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(product))
|
|
366
|
+
self.sendEvent(IapEvent.promotedProductIos.rawValue, sanitized)
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
ExpoIapLog.failure("promotedProductListenerIOS", error: error)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
self.sendEvent(
|
|
374
|
+
IapEvent.promotedProductIos.rawValue,
|
|
375
|
+
["productId": productId]
|
|
376
|
+
)
|
|
504
377
|
}
|
|
505
378
|
}
|
|
506
379
|
}
|
|
507
380
|
|
|
508
381
|
@MainActor
|
|
509
382
|
private func cleanupStore() async {
|
|
510
|
-
logDebug("Cleaning up listeners and ending connection")
|
|
511
383
|
removeListener(&purchaseUpdatedSub)
|
|
512
384
|
removeListener(&purchaseErrorSub)
|
|
513
385
|
removeListener(&promotedProductSub)
|
|
514
386
|
_ = try? await OpenIapModule.shared.endConnection()
|
|
515
387
|
}
|
|
516
388
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
guard isInitialized else {
|
|
521
|
-
throw OpenIapError.make(
|
|
522
|
-
code: OpenIapError.InitConnection,
|
|
523
|
-
message: "Connection not initialized. Call initConnection() first."
|
|
524
|
-
)
|
|
389
|
+
private func removeListener(_ subscription: inout Subscription?) {
|
|
390
|
+
if let current = subscription {
|
|
391
|
+
OpenIapModule.shared.removeListener(current)
|
|
525
392
|
}
|
|
393
|
+
subscription = nil
|
|
526
394
|
}
|
|
527
395
|
|
|
396
|
+
private func ensureConnection() async throws {
|
|
397
|
+
try await MainActor.run {
|
|
398
|
+
guard self.isInitialized else {
|
|
399
|
+
throw PurchaseError.make(
|
|
400
|
+
code: .initConnection,
|
|
401
|
+
message: "Connection not initialized. Call initConnection() first."
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
528
406
|
}
|