expo-iap 2.9.0-rc.3 → 2.9.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/CHANGELOG.md +29 -6
- package/CLAUDE.md +40 -0
- package/CONTRIBUTING.md +4 -3
- package/build/helpers/subscription.d.ts +3 -0
- package/build/helpers/subscription.d.ts.map +1 -1
- package/build/helpers/subscription.js +10 -5
- package/build/helpers/subscription.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +29 -10
- package/build/index.js.map +1 -1
- package/build/modules/ios.d.ts +4 -5
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +2 -3
- package/build/modules/ios.js.map +1 -1
- package/build/types/ExpoIapAndroid.types.d.ts +2 -2
- package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
- package/build/types/ExpoIapAndroid.types.js.map +1 -1
- package/build/types/ExpoIapIOS.types.d.ts +3 -3
- package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
- package/build/types/ExpoIapIOS.types.js.map +1 -1
- package/build/useIAP.d.ts +1 -1
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +54 -33
- package/build/useIAP.js.map +1 -1
- package/build/utils/constants.d.ts +4 -0
- package/build/utils/constants.d.ts.map +1 -0
- package/build/utils/constants.js +12 -0
- package/build/utils/constants.js.map +1 -0
- package/ios/ExpoIap.podspec +1 -1
- package/ios/ExpoIapModule.swift +341 -334
- package/jest.config.js +19 -14
- package/package.json +2 -3
- package/plugin/build/withIAP.js +25 -23
- package/plugin/build/withLocalOpenIAP.js +5 -1
- package/plugin/src/withIAP.ts +39 -31
- package/plugin/src/withLocalOpenIAP.ts +8 -2
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/src/helpers/subscription.ts +35 -23
- package/src/index.ts +50 -33
- package/src/modules/ios.ts +4 -5
- package/src/types/ExpoIapAndroid.types.ts +4 -3
- package/src/types/ExpoIapIOS.types.ts +3 -4
- package/src/useIAP.ts +73 -52
- package/src/utils/constants.ts +14 -0
- package/ios/ProductStore.swift +0 -27
- package/ios/Types.swift +0 -96
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
|
+
import StoreKit
|
|
2
3
|
import OpenIAP
|
|
4
|
+
import OSLog
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
private let iapLogger = Logger(subsystem: "dev.hyo.expo-iap", category: "ExpoIapModule")
|
|
7
|
+
private func logDebug(_ message: String) {
|
|
8
|
+
// Use OSLog/Logger so logs are structured and filterable
|
|
9
|
+
// Suppress debug logs in Release builds
|
|
5
10
|
#if DEBUG
|
|
6
|
-
|
|
11
|
+
iapLogger.debug("\(message, privacy: .public)")
|
|
7
12
|
#endif
|
|
8
13
|
}
|
|
9
14
|
|
|
15
|
+
// Event names
|
|
10
16
|
struct OpenIapEvent {
|
|
11
17
|
static let PurchaseUpdated = "purchase-updated"
|
|
12
18
|
static let PurchaseError = "purchase-error"
|
|
@@ -16,183 +22,355 @@ struct OpenIapEvent {
|
|
|
16
22
|
@available(iOS 15.0, tvOS 15.0, *)
|
|
17
23
|
@MainActor
|
|
18
24
|
public class ExpoIapModule: Module {
|
|
19
|
-
|
|
20
|
-
private var
|
|
25
|
+
// Subscriptions for OpenIapModule event listeners
|
|
26
|
+
private var purchaseUpdatedSub: Subscription?
|
|
27
|
+
private var purchaseErrorSub: Subscription?
|
|
28
|
+
private var promotedProductSub: Subscription?
|
|
29
|
+
|
|
30
|
+
// Helper to safely remove a listener and nil out the reference
|
|
31
|
+
private func removeListener(_ sub: inout Subscription?) {
|
|
32
|
+
if let s = sub { OpenIapModule.shared.removeListener(s) }
|
|
33
|
+
sub = nil
|
|
34
|
+
}
|
|
21
35
|
|
|
22
|
-
public func definition() -> ModuleDefinition {
|
|
36
|
+
nonisolated public func definition() -> ModuleDefinition {
|
|
23
37
|
Name("ExpoIap")
|
|
24
38
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
Constants {
|
|
40
|
+
OpenIapSerialization.errorCodes()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Events(
|
|
44
|
+
OpenIapEvent.PurchaseUpdated,
|
|
45
|
+
OpenIapEvent.PurchaseError,
|
|
46
|
+
OpenIapEvent.PromotedProductIOS
|
|
47
|
+
)
|
|
29
48
|
|
|
30
|
-
|
|
49
|
+
OnCreate {
|
|
50
|
+
logDebug("Module created")
|
|
51
|
+
Task { @MainActor in
|
|
52
|
+
self.setupStore()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
OnDestroy {
|
|
57
|
+
logDebug("Module destroyed")
|
|
58
|
+
Task { @MainActor in
|
|
59
|
+
await self.cleanupStore()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// MARK: - Connection Management
|
|
31
64
|
|
|
32
65
|
AsyncFunction("initConnection") { () async throws -> Bool in
|
|
33
66
|
logDebug("initConnection called")
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
self.hasListeners = true
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return try await self.iapModule.initConnection()
|
|
67
|
+
let isConnected = try await OpenIapModule.shared.initConnection()
|
|
68
|
+
logDebug("Connection initialized: \(isConnected)")
|
|
69
|
+
return isConnected
|
|
41
70
|
}
|
|
42
71
|
|
|
43
72
|
AsyncFunction("endConnection") { () async throws -> Bool in
|
|
44
73
|
logDebug("endConnection called")
|
|
74
|
+
let _ = try await OpenIapModule.shared.endConnection()
|
|
45
75
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
await self.iapModule.removeAllListeners()
|
|
49
|
-
self.hasListeners = false
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return try await self.iapModule.endConnection()
|
|
76
|
+
logDebug("Connection ended")
|
|
77
|
+
return true
|
|
53
78
|
}
|
|
54
79
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
let products = try await self.iapModule.fetchProducts(request)
|
|
80
|
+
// MARK: - Product Management
|
|
81
|
+
|
|
82
|
+
AsyncFunction("fetchProducts") { (params: [String: Any]) async throws -> [[String: Any?]] in
|
|
83
|
+
logDebug("fetchProducts raw params: \(params)")
|
|
60
84
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
// Handle both object format {skus: [...], type: "..."} and array format
|
|
86
|
+
var skus: [String] = []
|
|
87
|
+
var typeString = "all"
|
|
88
|
+
|
|
89
|
+
if let skusArray = params["skus"] as? [String] {
|
|
90
|
+
// Object format: {skus: [...], type: "..."}
|
|
91
|
+
skus = skusArray
|
|
92
|
+
typeString = params["type"] as? String ?? "all"
|
|
93
|
+
} else {
|
|
94
|
+
// Array format passed directly - reconstruct from indexed keys
|
|
95
|
+
var tempSkus: [String] = []
|
|
96
|
+
var index = 0
|
|
97
|
+
while let sku = params["\(index)"] as? String {
|
|
98
|
+
tempSkus.append(sku)
|
|
99
|
+
index += 1
|
|
100
|
+
}
|
|
101
|
+
skus = tempSkus
|
|
66
102
|
}
|
|
67
103
|
|
|
68
|
-
|
|
69
|
-
logDebug("
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
104
|
+
logDebug("fetchProducts parsed - skus: \(skus), type: \(typeString)")
|
|
105
|
+
logDebug("SKUs count: \(skus.count)")
|
|
106
|
+
|
|
107
|
+
// Validate SKUs
|
|
108
|
+
guard !skus.isEmpty else {
|
|
109
|
+
logDebug("ERROR: Empty SKUs array!")
|
|
110
|
+
throw OpenIapError.purchaseFailed(reason: "Empty SKU list provided")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Convert string to OpenIapRequestProductType enum
|
|
114
|
+
let productType: OpenIapRequestProductType = {
|
|
115
|
+
switch typeString {
|
|
116
|
+
case "inapp":
|
|
117
|
+
return .inapp
|
|
118
|
+
case "subs":
|
|
119
|
+
return .subs
|
|
120
|
+
default:
|
|
121
|
+
return .all
|
|
122
|
+
}
|
|
123
|
+
}()
|
|
124
|
+
|
|
125
|
+
logDebug("Converted type to OpenIapRequestProductType: \(productType)")
|
|
126
|
+
|
|
127
|
+
// Build OpenIapProductRequest and fetch via OpenIapModule
|
|
128
|
+
let request = OpenIapProductRequest(skus: skus, type: productType)
|
|
129
|
+
let products = try await OpenIapModule.shared.fetchProducts(request)
|
|
130
|
+
logDebug("Fetched \(products.count) products from store")
|
|
131
|
+
if products.isEmpty {
|
|
132
|
+
logDebug("No products found. Possible reasons:")
|
|
133
|
+
logDebug("1. Products not configured in App Store Connect")
|
|
134
|
+
logDebug("2. Bundle ID mismatch")
|
|
135
|
+
logDebug("3. Not signed in to sandbox account")
|
|
136
|
+
logDebug("4. Products pending review")
|
|
137
|
+
}
|
|
138
|
+
for product in products {
|
|
139
|
+
logDebug("Product: \(product.id) - \(product.title) - \(product.displayPrice)")
|
|
140
|
+
}
|
|
141
|
+
return OpenIapSerialization.products(products)
|
|
83
142
|
}
|
|
84
143
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
144
|
+
// MARK: - Purchase Operations
|
|
145
|
+
|
|
146
|
+
AsyncFunction("requestPurchase") { (params: [String: Any]) async throws in
|
|
147
|
+
// Extract and validate required fields
|
|
148
|
+
guard let sku = params["sku"] as? String, !sku.isEmpty else {
|
|
149
|
+
throw OpenIapError.purchaseFailed(reason: "Missing required 'sku'")
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Optional fields
|
|
153
|
+
let andFinish = (params["andDangerouslyFinishTransactionAutomatically"] as? Bool) ?? false
|
|
154
|
+
let appAccountToken = params["appAccountToken"] as? String
|
|
155
|
+
let quantity: Int? = {
|
|
156
|
+
if let q = params["quantity"] as? Int { return q }
|
|
157
|
+
if let qd = params["quantity"] as? Double { return Int(qd) }
|
|
158
|
+
return nil
|
|
159
|
+
}()
|
|
160
|
+
|
|
161
|
+
// Discount offer mapping (strings expected from JS)
|
|
162
|
+
// Use OpenIapDiscountOffer from OpenIAP package (avoid relying on legacy typealiases)
|
|
163
|
+
var discountOffer: OpenIapDiscountOffer? = nil
|
|
164
|
+
if let offer = params["withOffer"] as? [String: Any] {
|
|
165
|
+
let identifier = (offer["identifier"] as? String) ?? (offer["id"] as? String) ?? ""
|
|
166
|
+
let keyIdentifier = (offer["keyIdentifier"] as? String) ?? ""
|
|
167
|
+
let nonce = (offer["nonce"] as? String) ?? ""
|
|
168
|
+
let signature = (offer["signature"] as? String) ?? ""
|
|
169
|
+
let timestamp = (offer["timestamp"] as? String) ?? ""
|
|
170
|
+
if !identifier.isEmpty && !keyIdentifier.isEmpty && !nonce.isEmpty && !signature.isEmpty && !timestamp.isEmpty {
|
|
171
|
+
discountOffer = OpenIapDiscountOffer(
|
|
172
|
+
identifier: identifier,
|
|
173
|
+
keyIdentifier: keyIdentifier,
|
|
174
|
+
nonce: nonce,
|
|
175
|
+
signature: signature,
|
|
176
|
+
timestamp: timestamp
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let tokenForLog = appAccountToken ?? "nil"
|
|
182
|
+
let qtyForLog = quantity ?? -1
|
|
183
|
+
logDebug("requestPurchase parsed - sku: \(sku), andFinish: \(andFinish), appAccountToken: \(tokenForLog), quantity: \(qtyForLog), hasOffer: \(discountOffer != nil)")
|
|
93
184
|
|
|
94
|
-
let finishAutomatically = andDangerouslyFinishTransactionAutomaticallyIOS ?? false
|
|
95
|
-
let qty = quantity ?? 1
|
|
96
185
|
|
|
97
|
-
//
|
|
98
|
-
let
|
|
186
|
+
// Build purchase request props using OpenIapRequestPurchaseProps
|
|
187
|
+
let requestProps = OpenIapRequestPurchaseProps(
|
|
99
188
|
sku: sku,
|
|
100
|
-
andDangerouslyFinishTransactionAutomatically:
|
|
189
|
+
andDangerouslyFinishTransactionAutomatically: andFinish,
|
|
101
190
|
appAccountToken: appAccountToken,
|
|
102
|
-
quantity:
|
|
103
|
-
|
|
191
|
+
quantity: quantity,
|
|
192
|
+
withOffer: discountOffer
|
|
104
193
|
)
|
|
105
|
-
|
|
106
|
-
|
|
194
|
+
|
|
195
|
+
do {
|
|
196
|
+
_ = try await OpenIapModule.shared.requestPurchase(requestProps)
|
|
197
|
+
logDebug("Purchase request completed successfully")
|
|
198
|
+
} catch {
|
|
199
|
+
logDebug("Purchase request failed with error: \(error)")
|
|
200
|
+
throw error
|
|
201
|
+
}
|
|
107
202
|
}
|
|
108
203
|
|
|
109
|
-
AsyncFunction("finishTransaction") { (
|
|
110
|
-
logDebug("finishTransaction called
|
|
111
|
-
|
|
204
|
+
AsyncFunction("finishTransaction") { (transactionId: String) async throws -> Bool in
|
|
205
|
+
logDebug("finishTransaction called with id: \(transactionId)")
|
|
206
|
+
let result = try await OpenIapModule.shared.finishTransaction(transactionIdentifier: transactionId)
|
|
207
|
+
return result
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// MARK: - Purchase History
|
|
211
|
+
|
|
212
|
+
AsyncFunction("getAvailablePurchases") { (options: [String: Any?]?) async throws -> [[String: Any?]] in
|
|
213
|
+
logDebug("getAvailablePurchases called")
|
|
214
|
+
|
|
215
|
+
// Build options and get purchases directly from OpenIapModule
|
|
216
|
+
let purchaseOptions: OpenIapGetAvailablePurchasesProps? = options.map {
|
|
217
|
+
OpenIapGetAvailablePurchasesProps(
|
|
218
|
+
alsoPublishToEventListenerIOS: $0["alsoPublishToEventListenerIOS"] as? Bool,
|
|
219
|
+
onlyIncludeActiveItemsIOS: $0["onlyIncludeActiveItemsIOS"] as? Bool
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
let purchases = try await OpenIapModule.shared.getAvailablePurchases(purchaseOptions)
|
|
223
|
+
return OpenIapSerialization.purchases(purchases)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Legacy function for backward compatibility
|
|
227
|
+
AsyncFunction("getAvailableItems") { (alsoPublishToEventListener: Bool, onlyIncludeActiveItems: Bool) async throws -> [[String: Any?]] in
|
|
228
|
+
logDebug("getAvailableItems called (legacy)")
|
|
229
|
+
|
|
230
|
+
let purchaseOptions = OpenIapGetAvailablePurchasesProps(
|
|
231
|
+
alsoPublishToEventListenerIOS: alsoPublishToEventListener,
|
|
232
|
+
onlyIncludeActiveItemsIOS: onlyIncludeActiveItems
|
|
233
|
+
)
|
|
234
|
+
let purchases = try await OpenIapModule.shared.getAvailablePurchases(purchaseOptions)
|
|
235
|
+
return OpenIapSerialization.purchases(purchases)
|
|
112
236
|
}
|
|
113
237
|
|
|
114
238
|
AsyncFunction("getPendingTransactionsIOS") { () async throws -> [[String: Any?]] in
|
|
115
239
|
logDebug("getPendingTransactionsIOS called")
|
|
116
|
-
|
|
117
|
-
|
|
240
|
+
|
|
241
|
+
let pendingTransactions = try await OpenIapModule.shared.getPendingTransactionsIOS()
|
|
242
|
+
return OpenIapSerialization.purchases(pendingTransactions)
|
|
118
243
|
}
|
|
119
244
|
|
|
120
|
-
AsyncFunction("clearTransactionIOS") { () async throws in
|
|
245
|
+
AsyncFunction("clearTransactionIOS") { () async throws -> Bool in
|
|
121
246
|
logDebug("clearTransactionIOS called")
|
|
122
|
-
try await
|
|
247
|
+
try await OpenIapModule.shared.clearTransactionIOS()
|
|
248
|
+
return true
|
|
123
249
|
}
|
|
124
250
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
251
|
+
// MARK: - Receipt & Validation
|
|
252
|
+
|
|
253
|
+
AsyncFunction("getReceiptIOS") { () async throws -> String in
|
|
254
|
+
logDebug("getReceiptIOS called")
|
|
255
|
+
return try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
|
|
128
256
|
}
|
|
129
257
|
|
|
130
|
-
AsyncFunction("
|
|
131
|
-
logDebug("
|
|
132
|
-
|
|
258
|
+
AsyncFunction("requestReceiptRefreshIOS") { () async throws -> String in
|
|
259
|
+
logDebug("requestReceiptRefreshIOS called")
|
|
260
|
+
// Receipt refresh is handled automatically by StoreKit 2
|
|
261
|
+
return try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
|
|
133
262
|
}
|
|
134
263
|
|
|
135
264
|
AsyncFunction("validateReceiptIOS") { (sku: String) async throws -> [String: Any?] in
|
|
136
265
|
logDebug("validateReceiptIOS called for sku: \(sku)")
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
let
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"
|
|
266
|
+
|
|
267
|
+
// Use OpenIapReceiptValidationProps to keep naming parity with OpenIAP
|
|
268
|
+
let props = OpenIapReceiptValidationProps(sku: sku)
|
|
269
|
+
let result = try await OpenIapModule.shared.validateReceiptIOS(props)
|
|
270
|
+
|
|
271
|
+
return [
|
|
272
|
+
"isValid": result.isValid,
|
|
273
|
+
"receiptData": result.receiptData,
|
|
274
|
+
"jwsRepresentation": result.jwsRepresentation,
|
|
275
|
+
// Populate unified purchaseToken for iOS as alias of JWS
|
|
276
|
+
"purchaseToken": result.jwsRepresentation,
|
|
277
|
+
"latestTransaction": result.latestTransaction.map { OpenIapSerialization.purchase($0) },
|
|
144
278
|
]
|
|
145
|
-
if let latest = validation.latestTransaction {
|
|
146
|
-
result["latestTransaction"] = self.serializePurchase(latest)
|
|
147
|
-
}
|
|
148
|
-
return result
|
|
149
279
|
}
|
|
150
280
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
281
|
+
// MARK: - iOS Specific Features
|
|
282
|
+
|
|
283
|
+
AsyncFunction("presentCodeRedemptionSheetIOS") { () async throws -> Bool in
|
|
284
|
+
logDebug("presentCodeRedemptionSheetIOS called")
|
|
285
|
+
let _ = try await OpenIapModule.shared.presentCodeRedemptionSheetIOS()
|
|
286
|
+
return true
|
|
154
287
|
}
|
|
155
288
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return
|
|
289
|
+
AsyncFunction("showManageSubscriptionsIOS") { () async throws -> Bool in
|
|
290
|
+
logDebug("showManageSubscriptionsIOS called")
|
|
291
|
+
let _ = try await OpenIapModule.shared.showManageSubscriptionsIOS()
|
|
292
|
+
return true
|
|
160
293
|
}
|
|
161
294
|
|
|
162
|
-
AsyncFunction("
|
|
163
|
-
logDebug("
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"originalPurchaseDate": appTransaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
|
|
170
|
-
"deviceVerification": appTransaction.deviceVerification,
|
|
171
|
-
"deviceVerificationNonce": appTransaction.deviceVerificationNonce,
|
|
172
|
-
"preorderDate": appTransaction.preorderDate.map { $0.timeIntervalSince1970 * 1000 },
|
|
173
|
-
"jwsRepresentation": nil // Not available in IapAppTransaction
|
|
174
|
-
]
|
|
295
|
+
AsyncFunction("deepLinkToSubscriptionsIOS") { () async throws in
|
|
296
|
+
logDebug("deepLinkToSubscriptionsIOS called")
|
|
297
|
+
// Open App Store subscriptions page directly
|
|
298
|
+
if let url = URL(string: "https://apps.apple.com/account/subscriptions") {
|
|
299
|
+
#if canImport(UIKit)
|
|
300
|
+
await MainActor.run {
|
|
301
|
+
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
175
302
|
}
|
|
303
|
+
#endif
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
AsyncFunction("beginRefundRequestIOS") { (sku: String) async throws -> String? in
|
|
308
|
+
logDebug("beginRefundRequestIOS called for sku: \(sku)")
|
|
309
|
+
return try await OpenIapModule.shared.beginRefundRequestIOS(sku: sku)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
AsyncFunction("getPromotedProductIOS") { () async throws -> [String: Any?]? in
|
|
313
|
+
logDebug("getPromotedProductIOS called")
|
|
314
|
+
|
|
315
|
+
if let promotedProduct = try await OpenIapModule.shared.getPromotedProductIOS() {
|
|
316
|
+
return [
|
|
317
|
+
"productIdentifier": promotedProduct.productIdentifier,
|
|
318
|
+
"localizedTitle": promotedProduct.localizedTitle,
|
|
319
|
+
"localizedDescription": promotedProduct.localizedDescription,
|
|
320
|
+
"price": promotedProduct.price,
|
|
321
|
+
"priceLocale": [
|
|
322
|
+
"currencyCode": promotedProduct.priceLocale.currencyCode,
|
|
323
|
+
"currencySymbol": promotedProduct.priceLocale.currencySymbol
|
|
324
|
+
]
|
|
325
|
+
]
|
|
176
326
|
}
|
|
177
327
|
return nil
|
|
178
328
|
}
|
|
179
329
|
|
|
180
|
-
AsyncFunction("
|
|
330
|
+
AsyncFunction("buyPromotedProductIOS") { () async throws in
|
|
331
|
+
logDebug("buyPromotedProductIOS called")
|
|
332
|
+
try await OpenIapModule.shared.requestPurchaseOnPromotedProductIOS()
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
AsyncFunction("getStorefrontIOS") { () async throws -> String in
|
|
336
|
+
logDebug("getStorefrontIOS called")
|
|
337
|
+
return try await OpenIapModule.shared.getStorefrontIOS()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
AsyncFunction("syncIOS") { () async throws -> Bool in
|
|
341
|
+
logDebug("syncIOS called")
|
|
342
|
+
return try await OpenIapModule.shared.syncIOS()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// MARK: - Additional iOS Methods
|
|
346
|
+
|
|
347
|
+
AsyncFunction("isTransactionVerifiedIOS") { (sku: String) async throws -> Bool in
|
|
348
|
+
logDebug("isTransactionVerifiedIOS called for sku: \(sku)")
|
|
349
|
+
return await OpenIapModule.shared.isTransactionVerifiedIOS(sku: sku)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
AsyncFunction("getTransactionJwsIOS") { (sku: String) async throws -> String? in
|
|
353
|
+
logDebug("getTransactionJwsIOS called for sku: \(sku)")
|
|
354
|
+
return try await OpenIapModule.shared.getTransactionJwsIOS(sku: sku)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
AsyncFunction("isEligibleForIntroOfferIOS") { (groupID: String) async throws -> Bool in
|
|
181
358
|
logDebug("isEligibleForIntroOfferIOS called for groupID: \(groupID)")
|
|
182
|
-
return await
|
|
359
|
+
return await OpenIapModule.shared.isEligibleForIntroOfferIOS(groupID: groupID)
|
|
183
360
|
}
|
|
184
361
|
|
|
185
362
|
AsyncFunction("subscriptionStatusIOS") { (sku: String) async throws -> [[String: Any?]]? in
|
|
186
363
|
logDebug("subscriptionStatusIOS called for sku: \(sku)")
|
|
187
|
-
|
|
364
|
+
|
|
365
|
+
if let statuses = try await OpenIapModule.shared.subscriptionStatusIOS(sku: sku) {
|
|
188
366
|
return statuses.map { status in
|
|
189
|
-
[
|
|
367
|
+
return [
|
|
190
368
|
"state": status.state,
|
|
191
|
-
"
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
369
|
+
"autoRenewStatus": status.renewalInfo?.autoRenewStatus,
|
|
370
|
+
"autoRenewPreference": status.renewalInfo?.autoRenewPreference,
|
|
371
|
+
"expirationReason": status.renewalInfo?.expirationReason,
|
|
372
|
+
"currentProductID": status.renewalInfo?.currentProductID,
|
|
373
|
+
"gracePeriodExpirationDate": status.renewalInfo?.gracePeriodExpirationDate
|
|
196
374
|
]
|
|
197
375
|
}
|
|
198
376
|
}
|
|
@@ -201,240 +379,69 @@ public class ExpoIapModule: Module {
|
|
|
201
379
|
|
|
202
380
|
AsyncFunction("currentEntitlementIOS") { (sku: String) async throws -> [String: Any?]? in
|
|
203
381
|
logDebug("currentEntitlementIOS called for sku: \(sku)")
|
|
204
|
-
|
|
205
|
-
|
|
382
|
+
|
|
383
|
+
if let entitlement = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku) {
|
|
384
|
+
return OpenIapSerialization.purchase(entitlement)
|
|
206
385
|
}
|
|
207
386
|
return nil
|
|
208
387
|
}
|
|
209
388
|
|
|
210
389
|
AsyncFunction("latestTransactionIOS") { (sku: String) async throws -> [String: Any?]? in
|
|
211
390
|
logDebug("latestTransactionIOS called for sku: \(sku)")
|
|
212
|
-
|
|
213
|
-
|
|
391
|
+
|
|
392
|
+
if let transaction = try await OpenIapModule.shared.latestTransactionIOS(sku: sku) {
|
|
393
|
+
return OpenIapSerialization.purchase(transaction)
|
|
214
394
|
}
|
|
215
395
|
return nil
|
|
216
396
|
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// MARK: - Listeners Setup
|
|
400
|
+
|
|
401
|
+
@MainActor
|
|
402
|
+
private func setupStore() {
|
|
403
|
+
logDebug("Setting up OpenIapModule event listeners")
|
|
217
404
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
405
|
+
purchaseUpdatedSub = OpenIapModule.shared.purchaseUpdatedListener { [weak self] purchase in
|
|
406
|
+
Task { @MainActor in
|
|
407
|
+
guard let self else { return }
|
|
408
|
+
logDebug("✅ Purchase success callback - sending event")
|
|
409
|
+
let purchaseData = OpenIapSerialization.purchase(purchase)
|
|
410
|
+
self.sendEvent(OpenIapEvent.PurchaseUpdated, purchaseData)
|
|
411
|
+
}
|
|
221
412
|
}
|
|
222
413
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
414
|
+
purchaseErrorSub = OpenIapModule.shared.purchaseErrorListener { [weak self] error in
|
|
415
|
+
Task { @MainActor in
|
|
416
|
+
guard let self else { return }
|
|
417
|
+
logDebug("❌ Purchase error callback - sending error event")
|
|
418
|
+
// Use OpenIapPurchaseError alias for clarity/parity
|
|
419
|
+
let err: OpenIapPurchaseError = error
|
|
420
|
+
let errorData: [String: Any?] = [
|
|
421
|
+
"code": err.code,
|
|
422
|
+
"message": err.message,
|
|
423
|
+
"productId": err.productId,
|
|
229
424
|
]
|
|
425
|
+
self.sendEvent(OpenIapEvent.PurchaseError, errorData)
|
|
230
426
|
}
|
|
231
|
-
return nil
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
AsyncFunction("requestPurchaseOnPromotedProductIOS") { () async throws in
|
|
235
|
-
logDebug("requestPurchaseOnPromotedProductIOS called")
|
|
236
|
-
try await self.iapModule.requestPurchaseOnPromotedProductIOS()
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
AsyncFunction("syncIOS") { () async throws -> Bool in
|
|
240
|
-
logDebug("syncIOS called")
|
|
241
|
-
return try await self.iapModule.syncIOS()
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
AsyncFunction("presentCodeRedemptionSheetIOS") { () async throws -> Bool in
|
|
245
|
-
logDebug("presentCodeRedemptionSheetIOS called")
|
|
246
|
-
return try await self.iapModule.presentCodeRedemptionSheetIOS()
|
|
247
427
|
}
|
|
248
428
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
logDebug("isTransactionVerifiedIOS called for sku: \(sku)")
|
|
256
|
-
return await self.iapModule.isTransactionVerifiedIOS(sku: sku)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// MARK: - Purchase Listeners
|
|
261
|
-
|
|
262
|
-
private func setupPurchaseListeners() {
|
|
263
|
-
_ = iapModule.purchaseUpdatedListener { [weak self] purchase in
|
|
264
|
-
self?.handlePurchaseUpdated(purchase)
|
|
265
|
-
}
|
|
266
|
-
_ = iapModule.purchaseErrorListener { [weak self] error in
|
|
267
|
-
self?.handlePurchaseError(error)
|
|
429
|
+
promotedProductSub = OpenIapModule.shared.promotedProductListenerIOS { [weak self] productId in
|
|
430
|
+
Task { @MainActor in
|
|
431
|
+
guard let self else { return }
|
|
432
|
+
logDebug("📱 Promoted product callback - sending event for: \(productId)")
|
|
433
|
+
self.sendEvent(OpenIapEvent.PromotedProductIOS, ["productId": productId])
|
|
434
|
+
}
|
|
268
435
|
}
|
|
269
436
|
}
|
|
270
437
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
logDebug("Purchase error: \(error)")
|
|
279
|
-
let serialized: [String: Any?] = [
|
|
280
|
-
"code": error.code,
|
|
281
|
-
"message": error.message,
|
|
282
|
-
"productId": error.productId as Any
|
|
283
|
-
]
|
|
284
|
-
sendEvent(OpenIapEvent.PurchaseError, serialized)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// MARK: - Serialization Helpers
|
|
288
|
-
|
|
289
|
-
private func serializePurchase(_ purchase: OpenIapPurchase) -> [String: Any?] {
|
|
290
|
-
return [
|
|
291
|
-
// PurchaseCommon required fields
|
|
292
|
-
"id": purchase.id,
|
|
293
|
-
"productId": purchase.productId,
|
|
294
|
-
"transactionDate": purchase.transactionDate,
|
|
295
|
-
"transactionReceipt": purchase.transactionReceipt,
|
|
296
|
-
"purchaseToken": purchase.purchaseToken,
|
|
297
|
-
"platform": purchase.platform,
|
|
298
|
-
|
|
299
|
-
// PurchaseCommon optional fields
|
|
300
|
-
"ids": purchase.ids,
|
|
301
|
-
"transactionId": purchase.id, // deprecated but kept for backward compatibility
|
|
302
|
-
"quantity": purchase.quantity,
|
|
303
|
-
"purchaseState": purchase.purchaseState.rawValue,
|
|
304
|
-
"isAutoRenewing": purchase.isAutoRenewing,
|
|
305
|
-
|
|
306
|
-
// PurchaseIOS specific fields
|
|
307
|
-
"quantityIOS": purchase.quantityIOS,
|
|
308
|
-
"originalTransactionDateIOS": purchase.originalTransactionDateIOS,
|
|
309
|
-
"originalTransactionIdentifierIOS": purchase.originalTransactionIdentifierIOS,
|
|
310
|
-
"appAccountToken": purchase.appAccountToken,
|
|
311
|
-
|
|
312
|
-
// Additional iOS fields from StoreKit 2
|
|
313
|
-
"expirationDateIOS": purchase.expirationDateIOS,
|
|
314
|
-
"webOrderLineItemIdIOS": purchase.webOrderLineItemIdIOS,
|
|
315
|
-
"environmentIOS": purchase.environmentIOS,
|
|
316
|
-
"storefrontCountryCodeIOS": purchase.storefrontCountryCodeIOS,
|
|
317
|
-
"appBundleIdIOS": purchase.appBundleIdIOS,
|
|
318
|
-
"productTypeIOS": purchase.productTypeIOS,
|
|
319
|
-
"subscriptionGroupIdIOS": purchase.subscriptionGroupIdIOS,
|
|
320
|
-
"isUpgradedIOS": purchase.isUpgradedIOS,
|
|
321
|
-
"ownershipTypeIOS": purchase.ownershipTypeIOS,
|
|
322
|
-
"reasonIOS": purchase.reasonIOS,
|
|
323
|
-
"reasonStringRepresentationIOS": purchase.reasonStringRepresentationIOS,
|
|
324
|
-
"transactionReasonIOS": purchase.transactionReasonIOS,
|
|
325
|
-
"revocationDateIOS": purchase.revocationDateIOS,
|
|
326
|
-
"revocationReasonIOS": purchase.revocationReasonIOS,
|
|
327
|
-
|
|
328
|
-
// Offer information
|
|
329
|
-
"offerIOS": purchase.offerIOS != nil ? [
|
|
330
|
-
"id": purchase.offerIOS!.id,
|
|
331
|
-
"type": purchase.offerIOS!.type,
|
|
332
|
-
"paymentMode": purchase.offerIOS!.paymentMode
|
|
333
|
-
] : nil,
|
|
334
|
-
|
|
335
|
-
// Price locale fields
|
|
336
|
-
"currencyCodeIOS": purchase.currencyCodeIOS,
|
|
337
|
-
"currencySymbolIOS": purchase.currencySymbolIOS,
|
|
338
|
-
"countryCodeIOS": purchase.countryCodeIOS
|
|
339
|
-
]
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
private func serializeProduct(_ product: OpenIapProduct) -> [String: Any?] {
|
|
343
|
-
var result: [String: Any?] = [
|
|
344
|
-
// Common fields (required by ProductCommon)
|
|
345
|
-
"id": product.id,
|
|
346
|
-
"title": product.title,
|
|
347
|
-
"description": product.description,
|
|
348
|
-
"type": product.type,
|
|
349
|
-
"displayName": product.displayName,
|
|
350
|
-
"displayPrice": product.displayPrice,
|
|
351
|
-
"currency": product.currency,
|
|
352
|
-
"price": product.price,
|
|
353
|
-
"debugDescription": product.debugDescription,
|
|
354
|
-
"platform": product.platform,
|
|
355
|
-
|
|
356
|
-
// iOS-specific fields (required by ProductIOS)
|
|
357
|
-
"displayNameIOS": product.displayNameIOS,
|
|
358
|
-
"isFamilyShareableIOS": product.isFamilyShareableIOS,
|
|
359
|
-
"jsonRepresentationIOS": product.jsonRepresentationIOS,
|
|
360
|
-
"typeIOS": product.typeIOS.rawValue,
|
|
361
|
-
|
|
362
|
-
// Additional iOS-specific fields
|
|
363
|
-
"descriptionIOS": product.description,
|
|
364
|
-
"displayPriceIOS": product.displayPrice,
|
|
365
|
-
"priceIOS": product.price,
|
|
366
|
-
|
|
367
|
-
// ProductSubscriptionIOS specific fields
|
|
368
|
-
"discountsIOS": product.discountsIOS?.map { discount in
|
|
369
|
-
[
|
|
370
|
-
"identifier": discount.identifier,
|
|
371
|
-
"type": discount.type,
|
|
372
|
-
"numberOfPeriods": discount.numberOfPeriods,
|
|
373
|
-
"price": discount.price,
|
|
374
|
-
"priceAmount": discount.priceAmount,
|
|
375
|
-
"paymentMode": discount.paymentMode,
|
|
376
|
-
"subscriptionPeriod": discount.subscriptionPeriod
|
|
377
|
-
]
|
|
378
|
-
},
|
|
379
|
-
"introductoryPriceIOS": product.introductoryPriceIOS,
|
|
380
|
-
"introductoryPriceAsAmountIOS": product.introductoryPriceAsAmountIOS,
|
|
381
|
-
"introductoryPricePaymentModeIOS": product.introductoryPricePaymentModeIOS,
|
|
382
|
-
"introductoryPriceNumberOfPeriodsIOS": product.introductoryPriceNumberOfPeriodsIOS,
|
|
383
|
-
"introductoryPriceSubscriptionPeriodIOS": product.introductoryPriceSubscriptionPeriodIOS,
|
|
384
|
-
"subscriptionPeriodNumberIOS": product.subscriptionPeriodNumberIOS,
|
|
385
|
-
"subscriptionPeriodUnitIOS": product.subscriptionPeriodUnitIOS
|
|
386
|
-
]
|
|
387
|
-
|
|
388
|
-
// Add subscriptionInfoIOS if available
|
|
389
|
-
if let subInfo = product.subscriptionInfoIOS {
|
|
390
|
-
var subInfoDict: [String: Any?] = [
|
|
391
|
-
"subscriptionGroupId": subInfo.subscriptionGroupId,
|
|
392
|
-
"subscriptionPeriod": [
|
|
393
|
-
"unit": subInfo.subscriptionPeriod.unit.rawValue,
|
|
394
|
-
"value": subInfo.subscriptionPeriod.value
|
|
395
|
-
]
|
|
396
|
-
]
|
|
397
|
-
|
|
398
|
-
if let intro = subInfo.introductoryOffer {
|
|
399
|
-
subInfoDict["introductoryOffer"] = [
|
|
400
|
-
"displayPrice": intro.displayPrice,
|
|
401
|
-
"id": intro.id,
|
|
402
|
-
"paymentMode": intro.paymentMode.rawValue,
|
|
403
|
-
"period": [
|
|
404
|
-
"unit": intro.period.unit.rawValue,
|
|
405
|
-
"value": intro.period.value
|
|
406
|
-
],
|
|
407
|
-
"periodCount": intro.periodCount,
|
|
408
|
-
"price": intro.price,
|
|
409
|
-
"type": intro.type.rawValue
|
|
410
|
-
]
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if let promos = subInfo.promotionalOffers {
|
|
414
|
-
subInfoDict["promotionalOffers"] = promos.map { offer in
|
|
415
|
-
[
|
|
416
|
-
"displayPrice": offer.displayPrice,
|
|
417
|
-
"id": offer.id,
|
|
418
|
-
"paymentMode": offer.paymentMode.rawValue,
|
|
419
|
-
"period": [
|
|
420
|
-
"unit": offer.period.unit.rawValue,
|
|
421
|
-
"value": offer.period.value
|
|
422
|
-
],
|
|
423
|
-
"periodCount": offer.periodCount,
|
|
424
|
-
"price": offer.price,
|
|
425
|
-
"type": offer.type.rawValue
|
|
426
|
-
]
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
result["subscriptionInfoIOS"] = subInfoDict
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Deprecated fields for backward compatibility
|
|
434
|
-
result["isFamilyShareable"] = product.isFamilyShareableIOS
|
|
435
|
-
result["jsonRepresentation"] = product.jsonRepresentationIOS
|
|
436
|
-
|
|
437
|
-
return result
|
|
438
|
+
@MainActor
|
|
439
|
+
private func cleanupStore() async {
|
|
440
|
+
logDebug("Cleaning up listeners and ending connection")
|
|
441
|
+
removeListener(&purchaseUpdatedSub)
|
|
442
|
+
removeListener(&purchaseErrorSub)
|
|
443
|
+
removeListener(&promotedProductSub)
|
|
444
|
+
_ = try? await OpenIapModule.shared.endConnection()
|
|
438
445
|
}
|
|
439
446
|
|
|
440
447
|
}
|