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