expo-iap 2.8.7 → 2.9.0-rc.2
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 +28 -0
- package/CLAUDE.md +7 -0
- package/CONTRIBUTING.md +3 -4
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +7 -7
- package/android/src/main/java/expo/modules/iap/Types.kt +1 -1
- package/build/ExpoIap.types.d.ts +4 -4
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js +3 -0
- package/build/ExpoIap.types.js.map +1 -1
- package/build/helpers/subscription.d.ts.map +1 -1
- package/build/helpers/subscription.js +3 -6
- package/build/helpers/subscription.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +14 -12
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts.map +1 -1
- 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.map +1 -1
- package/build/useIAP.js.map +1 -1
- package/ios/ExpoIap.podspec +1 -0
- package/ios/ExpoIapModule.swift +353 -1180
- package/jest.config.js +14 -17
- package/package.json +5 -3
- package/plugin/build/withIAP.d.ts +7 -1
- package/plugin/build/withIAP.js +16 -2
- package/plugin/build/withLocalOpenIAP.d.ts +9 -0
- package/plugin/build/withLocalOpenIAP.js +85 -0
- package/plugin/src/withIAP.ts +21 -2
- package/plugin/src/withLocalOpenIAP.ts +66 -0
- package/plugin/tsconfig.tsbuildinfo +1 -1
- package/src/ExpoIap.types.ts +5 -11
- package/src/helpers/subscription.ts +21 -28
- package/src/index.ts +27 -25
- package/src/modules/android.ts +7 -7
- package/src/modules/ios.ts +11 -5
- package/src/types/ExpoIapAndroid.types.ts +3 -4
- package/src/types/ExpoIapIOS.types.ts +4 -3
- package/src/useIAP.ts +10 -4
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
func serializeDebug(_ s: String) -> String? {
|
|
5
|
-
#if DEBUG
|
|
6
|
-
return s
|
|
7
|
-
#else
|
|
8
|
-
return nil
|
|
9
|
-
#endif
|
|
10
|
-
}
|
|
2
|
+
import OpenIAP
|
|
11
3
|
|
|
12
4
|
func logDebug(_ message: String) {
|
|
13
5
|
#if DEBUG
|
|
@@ -15,1253 +7,434 @@ func logDebug(_ message: String) {
|
|
|
15
7
|
#endif
|
|
16
8
|
}
|
|
17
9
|
|
|
18
|
-
struct
|
|
10
|
+
struct OpenIapEvent {
|
|
19
11
|
static let PurchaseUpdated = "purchase-updated"
|
|
20
12
|
static let PurchaseError = "purchase-error"
|
|
21
13
|
static let PromotedProductIOS = "promoted-product-ios"
|
|
22
14
|
}
|
|
23
15
|
|
|
24
|
-
@available(iOS 15.0, *)
|
|
25
|
-
|
|
26
|
-
let _ =
|
|
27
|
-
transaction.productType.rawValue.lowercased().contains("renewable")
|
|
28
|
-
|| transaction.expirationDate != nil
|
|
29
|
-
|
|
30
|
-
var transactionReasonIOS: String? = nil
|
|
31
|
-
var webOrderLineItemId: Int? = nil
|
|
32
|
-
var jsonData: [String: Any]? = nil
|
|
33
|
-
var jwsReceipt: String = ""
|
|
34
|
-
|
|
35
|
-
let jsonRep = transaction.jsonRepresentation
|
|
36
|
-
jwsReceipt = String(data: jsonRep, encoding: .utf8) ?? ""
|
|
37
|
-
|
|
38
|
-
do {
|
|
39
|
-
if let jsonObj = try JSONSerialization.jsonObject(with: jsonRep) as? [String: Any] {
|
|
40
|
-
jsonData = jsonObj
|
|
41
|
-
transactionReasonIOS = jsonObj["transactionReason"] as? String
|
|
42
|
-
if let webOrderId = jsonObj["webOrderLineItemID"] as? NSNumber {
|
|
43
|
-
webOrderLineItemId = webOrderId.intValue
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
print("Error parsing JSON representation: \(error)")
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
var purchaseMap: [String: Any?] = [
|
|
51
|
-
"id": String(transaction.id),
|
|
52
|
-
"productId": transaction.productID,
|
|
53
|
-
"ids": [transaction.productID],
|
|
54
|
-
"transactionId": String(transaction.id), // @deprecated - use id instead
|
|
55
|
-
"transactionDate": transaction.purchaseDate.timeIntervalSince1970 * 1000,
|
|
56
|
-
"transactionReceipt": jwsReceipt,
|
|
57
|
-
"platform": "ios",
|
|
58
|
-
|
|
59
|
-
"quantityIOS": transaction.purchasedQuantity,
|
|
60
|
-
"originalTransactionDateIOS": transaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
|
|
61
|
-
"originalTransactionIdentifierIOS": String(transaction.originalID),
|
|
62
|
-
"appAccountToken": transaction.appAccountToken?.uuidString,
|
|
63
|
-
|
|
64
|
-
"appBundleIdIOS": transaction.appBundleID,
|
|
65
|
-
"productTypeIOS": transaction.productType.rawValue,
|
|
66
|
-
"subscriptionGroupIdIOS": transaction.subscriptionGroupID,
|
|
67
|
-
|
|
68
|
-
"webOrderLineItemIdIOS": webOrderLineItemId,
|
|
69
|
-
|
|
70
|
-
"expirationDateIOS": transaction.expirationDate.map { $0.timeIntervalSince1970 * 1000 },
|
|
71
|
-
|
|
72
|
-
"isUpgradedIOS": transaction.isUpgraded,
|
|
73
|
-
"ownershipTypeIOS": transaction.ownershipType.rawValue,
|
|
74
|
-
|
|
75
|
-
"revocationDateIOS": transaction.revocationDate.map { $0.timeIntervalSince1970 * 1000 },
|
|
76
|
-
"revocationReasonIOS": transaction.revocationReason?.rawValue,
|
|
77
|
-
"transactionReasonIOS": transactionReasonIOS,
|
|
78
|
-
]
|
|
79
|
-
|
|
80
|
-
if (jwsRepresentationIOS != nil) {
|
|
81
|
-
logDebug("serializeTransaction adding jwsRepresentationIOS with length: \(jwsRepresentationIOS!.count)")
|
|
82
|
-
purchaseMap["jwsRepresentationIOS"] = jwsRepresentationIOS
|
|
83
|
-
purchaseMap["purchaseToken"] = jwsRepresentationIOS
|
|
84
|
-
} else {
|
|
85
|
-
logDebug("serializeTransaction jwsRepresentationIOS is nil")
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if #available(iOS 16.0, *) {
|
|
89
|
-
purchaseMap["environmentIOS"] = transaction.environment.rawValue
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if #available(iOS 17.0, *) {
|
|
93
|
-
purchaseMap["storefrontCountryCodeIOS"] = transaction.storefront.countryCode
|
|
94
|
-
purchaseMap["reasonIOS"] = transaction.reason.rawValue
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if #available(iOS 17.2, *) {
|
|
98
|
-
if let offer = transaction.offer {
|
|
99
|
-
purchaseMap["offerIOS"] = [
|
|
100
|
-
"id": offer.id ?? "",
|
|
101
|
-
"type": offer.type.rawValue,
|
|
102
|
-
"paymentMode": offer.paymentMode?.rawValue ?? "",
|
|
103
|
-
]
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if #available(iOS 15.4, *), let jsonData = jsonData {
|
|
108
|
-
if let price = jsonData["price"] as? NSNumber {
|
|
109
|
-
// START: Deprecated - will be removed in v2.9.0
|
|
110
|
-
// Use currencyCodeIOS, currencySymbolIOS, countryCodeIOS instead
|
|
111
|
-
purchaseMap["priceIOS"] = price.doubleValue
|
|
112
|
-
// END: Deprecated - will be removed in v2.9.0
|
|
113
|
-
}
|
|
114
|
-
if let currency = jsonData["currency"] as? String {
|
|
115
|
-
purchaseMap["currencyCodeIOS"] = currency
|
|
116
|
-
|
|
117
|
-
// Try to get currency symbol from locale
|
|
118
|
-
let locale = Locale(identifier: Locale.identifier(fromComponents: [NSLocale.Key.currencyCode.rawValue: currency]))
|
|
119
|
-
purchaseMap["currencySymbolIOS"] = locale.currencySymbol
|
|
120
|
-
|
|
121
|
-
// START: Deprecated - will be removed in v2.9.0
|
|
122
|
-
// Use currencyCodeIOS instead
|
|
123
|
-
purchaseMap["currencyIOS"] = currency
|
|
124
|
-
// END: Deprecated - will be removed in v2.9.0
|
|
125
|
-
}
|
|
126
|
-
// Extract country code from storefront if available
|
|
127
|
-
if let storefront = jsonData["storefront"] as? String {
|
|
128
|
-
purchaseMap["countryCodeIOS"] = storefront
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return purchaseMap
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private let DEFAULT_SUBSCRIPTION_PERIOD_UNIT = "DAY" // Default fallback unit for subscription periods.
|
|
136
|
-
|
|
137
|
-
func getPeriodIOS(_ unit: Product.SubscriptionPeriod.Unit) -> String {
|
|
138
|
-
return switch (unit) {
|
|
139
|
-
case .day: "DAY"
|
|
140
|
-
case .week: "WEEK"
|
|
141
|
-
case .month: "MONTH"
|
|
142
|
-
case .year: "YEAR"
|
|
143
|
-
@unknown default:
|
|
144
|
-
fatalError("Unknown subscription period unit: \(unit)")
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
func serializeOffer(_ offer: Product.SubscriptionOffer?) -> [String: Any?]? {
|
|
149
|
-
guard let offer = offer else { return nil }
|
|
150
|
-
|
|
151
|
-
return [
|
|
152
|
-
"id": offer.id,
|
|
153
|
-
"period": [
|
|
154
|
-
"unit": getPeriodIOS(offer.period.unit),
|
|
155
|
-
"value": offer.period.value
|
|
156
|
-
],
|
|
157
|
-
"periodCount": offer.periodCount,
|
|
158
|
-
"paymentMode": offer.paymentMode.rawValue,
|
|
159
|
-
"type": offer.type.rawValue,
|
|
160
|
-
"price": offer.price,
|
|
161
|
-
"displayPrice": offer.displayPrice,
|
|
162
|
-
]
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
func serializeSubscription(_ s: Product.SubscriptionInfo?) -> [String: Any?]? {
|
|
166
|
-
guard let s = s else { return nil }
|
|
167
|
-
return [
|
|
168
|
-
"introductoryOffer": serializeOffer(s.introductoryOffer),
|
|
169
|
-
"promotionalOffers": s.promotionalOffers.map(serializeOffer),
|
|
170
|
-
"subscriptionGroupId": s.subscriptionGroupID,
|
|
171
|
-
"subscriptionPeriod": [
|
|
172
|
-
"unit": getPeriodIOS(s.subscriptionPeriod.unit),
|
|
173
|
-
"value": s.subscriptionPeriod.value
|
|
174
|
-
],
|
|
175
|
-
]
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
@available(iOS 15.0, *)
|
|
180
|
-
func serializeProduct(_ p: Product) -> [String: Any?] {
|
|
181
|
-
// Convert Product.ProductType to our expected 'inapp' or 'subs' string
|
|
182
|
-
let productType: String = p.subscription != nil ? "subs" : "inapp"
|
|
183
|
-
|
|
184
|
-
// For subscription products, add discounts and introductory price
|
|
185
|
-
var discounts: [[String: Any?]]? = nil
|
|
186
|
-
var introductoryPrice: String? = nil
|
|
187
|
-
var introductoryPriceAsAmountIOS: String? = nil
|
|
188
|
-
var introductoryPricePaymentModeIOS: String? = nil
|
|
189
|
-
var introductoryPriceNumberOfPeriodsIOS: String? = nil
|
|
190
|
-
var introductoryPriceSubscriptionPeriodIOS: String? = nil
|
|
191
|
-
var subscriptionPeriodNumberIOS: String? = nil
|
|
192
|
-
var subscriptionPeriodUnitIOS: String? = nil
|
|
193
|
-
|
|
194
|
-
if let subscription = p.subscription {
|
|
195
|
-
// Extract discount information from promotional offers
|
|
196
|
-
if !subscription.promotionalOffers.isEmpty {
|
|
197
|
-
discounts = subscription.promotionalOffers.compactMap { offer in
|
|
198
|
-
return [
|
|
199
|
-
"identifier": offer.id ?? "",
|
|
200
|
-
"type": offer.type.rawValue,
|
|
201
|
-
"numberOfPeriods": "\(offer.periodCount)",
|
|
202
|
-
"price": "\(offer.price)",
|
|
203
|
-
"localizedPrice": offer.displayPrice,
|
|
204
|
-
"paymentMode": offer.paymentMode.rawValue,
|
|
205
|
-
"subscriptionPeriod": getPeriodIOS(offer.period.unit)
|
|
206
|
-
]
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Extract introductory price from introductory offer
|
|
211
|
-
if let introOffer = subscription.introductoryOffer {
|
|
212
|
-
introductoryPrice = introOffer.displayPrice
|
|
213
|
-
introductoryPriceAsAmountIOS = "\(introOffer.price)"
|
|
214
|
-
introductoryPricePaymentModeIOS = introOffer.paymentMode.rawValue
|
|
215
|
-
introductoryPriceNumberOfPeriodsIOS = "\(introOffer.periodCount)"
|
|
216
|
-
introductoryPriceSubscriptionPeriodIOS = getPeriodIOS(introOffer.period.unit)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Extract subscription period information
|
|
220
|
-
subscriptionPeriodNumberIOS = "\(subscription.subscriptionPeriod.value)"
|
|
221
|
-
subscriptionPeriodUnitIOS = getPeriodIOS(subscription.subscriptionPeriod.unit)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return [
|
|
225
|
-
"debugDescription": serializeDebug(p.debugDescription),
|
|
226
|
-
"description": p.description,
|
|
227
|
-
// New iOS-suffixed fields
|
|
228
|
-
"displayNameIOS": p.displayName,
|
|
229
|
-
"discountsIOS": discounts,
|
|
230
|
-
"introductoryPriceIOS": introductoryPrice,
|
|
231
|
-
"introductoryPriceAsAmountIOS": introductoryPriceAsAmountIOS,
|
|
232
|
-
"introductoryPricePaymentModeIOS": introductoryPricePaymentModeIOS,
|
|
233
|
-
"introductoryPriceNumberOfPeriodsIOS": introductoryPriceNumberOfPeriodsIOS,
|
|
234
|
-
"introductoryPriceSubscriptionPeriodIOS": introductoryPriceSubscriptionPeriodIOS,
|
|
235
|
-
"subscriptionPeriodNumberIOS": subscriptionPeriodNumberIOS,
|
|
236
|
-
"subscriptionPeriodUnitIOS": subscriptionPeriodUnitIOS,
|
|
237
|
-
"displayPrice": p.displayPrice,
|
|
238
|
-
"id": p.id,
|
|
239
|
-
"title": p.displayName,
|
|
240
|
-
"isFamilyShareableIOS": p.isFamilyShareable,
|
|
241
|
-
"jsonRepresentationIOS": String(data: p.jsonRepresentation, encoding: .utf8),
|
|
242
|
-
"price": p.price,
|
|
243
|
-
"subscriptionInfoIOS": serializeSubscription(p.subscription),
|
|
244
|
-
"type": productType,
|
|
245
|
-
"currency": p.priceFormatStyle.currencyCode,
|
|
246
|
-
"platform": "ios",
|
|
247
|
-
// START: Deprecated - will be removed in v2.9.0
|
|
248
|
-
// Use displayNameIOS instead of displayName
|
|
249
|
-
"displayName": p.displayName,
|
|
250
|
-
// Use discountsIOS instead of discounts
|
|
251
|
-
"discounts": discounts,
|
|
252
|
-
// Use introductoryPriceIOS instead of introductoryPrice
|
|
253
|
-
"introductoryPrice": introductoryPrice,
|
|
254
|
-
// Use isFamilyShareableIOS instead of isFamilyShareable
|
|
255
|
-
"isFamilyShareable": p.isFamilyShareable,
|
|
256
|
-
// Use jsonRepresentationIOS instead of jsonRepresentation
|
|
257
|
-
"jsonRepresentation": String(data: p.jsonRepresentation, encoding: .utf8),
|
|
258
|
-
// Use subscriptionInfoIOS instead of subscription
|
|
259
|
-
"subscription": serializeSubscription(p.subscription),
|
|
260
|
-
// END: Deprecated - will be removed in v2.9.0
|
|
261
|
-
]
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
@available(iOS 15.0, *)
|
|
265
|
-
@Sendable func serialize(_ rs: Transaction.RefundRequestStatus?) -> String? {
|
|
266
|
-
guard let rs = rs else { return nil }
|
|
267
|
-
switch rs {
|
|
268
|
-
case .success: return "success"
|
|
269
|
-
case .userCancelled: return "userCancelled"
|
|
270
|
-
default:
|
|
271
|
-
return nil
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
@available(iOS 15.0, *)
|
|
276
|
-
func serializeSubscriptionStatus(_ status: Product.SubscriptionInfo.Status) -> [String: Any?] {
|
|
277
|
-
return [
|
|
278
|
-
"state": status.state.rawValue,
|
|
279
|
-
"renewalInfo": serializeRenewalInfo(status.renewalInfo),
|
|
280
|
-
"platform": "ios",
|
|
281
|
-
]
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
@available(iOS 15.0, *)
|
|
285
|
-
func serializeRenewalInfo(_ renewalInfo: VerificationResult<Product.SubscriptionInfo.RenewalInfo>)
|
|
286
|
-
-> [String: Any?]?
|
|
287
|
-
{
|
|
288
|
-
switch renewalInfo {
|
|
289
|
-
case .unverified:
|
|
290
|
-
return nil
|
|
291
|
-
case .verified(let info):
|
|
292
|
-
return [
|
|
293
|
-
"autoRenewStatus": info.willAutoRenew,
|
|
294
|
-
"autoRenewPreference": info.autoRenewPreference,
|
|
295
|
-
"expirationReason": info.expirationReason,
|
|
296
|
-
"deviceVerification": info.deviceVerification,
|
|
297
|
-
"currentProductID": info.currentProductID,
|
|
298
|
-
"debugDescription": info.debugDescription,
|
|
299
|
-
"gracePeriodExpirationDate": info.gracePeriodExpirationDate,
|
|
300
|
-
"platform": "ios",
|
|
301
|
-
]
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
@available(iOS 15.0, *)
|
|
16
|
+
@available(iOS 15.0, tvOS 15.0, *)
|
|
17
|
+
@MainActor
|
|
306
18
|
public class ExpoIapModule: Module {
|
|
307
|
-
private
|
|
308
|
-
private var productStore: ProductStore?
|
|
19
|
+
private let iapModule = OpenIapModule.shared
|
|
309
20
|
private var hasListeners = false
|
|
310
|
-
private var updateListenerTask: Task<Void, Error>?
|
|
311
|
-
private var subscriptionPollingTask: Task<Void, Error>?
|
|
312
|
-
private var pollingSkus: Set<String> = []
|
|
313
|
-
private var paymentObserver: PaymentObserver?
|
|
314
|
-
private var promotedPayment: SKPayment?
|
|
315
|
-
private var promotedProduct: SKProduct?
|
|
316
21
|
|
|
317
|
-
// Add a flag to track initialization state
|
|
318
|
-
private var isInitialized = false
|
|
319
|
-
|
|
320
22
|
public func definition() -> ModuleDefinition {
|
|
321
23
|
Name("ExpoIap")
|
|
322
|
-
|
|
24
|
+
|
|
25
|
+
// Export native constants for error code mapping
|
|
323
26
|
Constants([
|
|
324
27
|
"ERROR_CODES": IapErrorCode.toDictionary()
|
|
325
28
|
])
|
|
326
|
-
|
|
327
|
-
Events(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
self.addTransactionObserver()
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
OnStopObserving {
|
|
335
|
-
self.hasListeners = false
|
|
336
|
-
self.removeTransactionObserver()
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
Function("initConnection") { () -> Bool in
|
|
340
|
-
// Clean up any existing state first (important for hot reload)
|
|
341
|
-
self.cleanupExistingState()
|
|
342
|
-
|
|
343
|
-
// Initialize fresh state
|
|
344
|
-
self.productStore = ProductStore()
|
|
29
|
+
|
|
30
|
+
Events(OpenIapEvent.PurchaseUpdated, OpenIapEvent.PurchaseError, OpenIapEvent.PromotedProductIOS)
|
|
31
|
+
|
|
32
|
+
AsyncFunction("initConnection") { () async throws -> Bool in
|
|
33
|
+
logDebug("initConnection called")
|
|
345
34
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
self.
|
|
349
|
-
SKPaymentQueue.default().add(self.paymentObserver!)
|
|
35
|
+
if !self.hasListeners {
|
|
36
|
+
self.setupPurchaseListeners()
|
|
37
|
+
self.hasListeners = true
|
|
350
38
|
}
|
|
351
39
|
|
|
352
|
-
self.
|
|
353
|
-
return AppStore.canMakePayments
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
AsyncFunction("getStorefront") {
|
|
357
|
-
let storefront = await Storefront.current
|
|
358
|
-
return storefront?.countryCode
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
AsyncFunction("getAppTransactionIOS") { () async throws -> [String: Any?]? in
|
|
362
|
-
if #available(iOS 16.0, *) {
|
|
363
|
-
#if compiler(>=5.7)
|
|
364
|
-
let verificationResult = try await AppTransaction.shared
|
|
365
|
-
|
|
366
|
-
let appTransaction: AppTransaction
|
|
367
|
-
switch verificationResult {
|
|
368
|
-
case .verified(let verified):
|
|
369
|
-
appTransaction = verified
|
|
370
|
-
case .unverified(_, _):
|
|
371
|
-
return nil
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
var result: [String: Any?] = [
|
|
375
|
-
"bundleId": appTransaction.bundleID,
|
|
376
|
-
"appVersion": appTransaction.appVersion,
|
|
377
|
-
"originalAppVersion": appTransaction.originalAppVersion,
|
|
378
|
-
"originalPurchaseDate": appTransaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
|
|
379
|
-
"deviceVerification": appTransaction.deviceVerification.base64EncodedString(),
|
|
380
|
-
"deviceVerificationNonce": appTransaction.deviceVerificationNonce.uuidString,
|
|
381
|
-
"environment": appTransaction.environment.rawValue,
|
|
382
|
-
"signedDate": appTransaction.signedDate.timeIntervalSince1970 * 1000,
|
|
383
|
-
"appId": appTransaction.appID,
|
|
384
|
-
"appVersionId": appTransaction.appVersionID,
|
|
385
|
-
"preorderDate": appTransaction.preorderDate.map { $0.timeIntervalSince1970 * 1000 }
|
|
386
|
-
]
|
|
387
|
-
|
|
388
|
-
// iOS 18.4+ properties - only compile with Xcode 16.4+ (Swift 6.1+)
|
|
389
|
-
// This prevents build failures on Xcode 16.3 and below
|
|
390
|
-
#if swift(>=6.1)
|
|
391
|
-
if #available(iOS 18.4, *) {
|
|
392
|
-
result["appTransactionId"] = appTransaction.appTransactionID
|
|
393
|
-
result["originalPlatform"] = appTransaction.originalPlatform.rawValue
|
|
394
|
-
}
|
|
395
|
-
#endif
|
|
396
|
-
|
|
397
|
-
return result
|
|
398
|
-
#else
|
|
399
|
-
throw Exception(
|
|
400
|
-
name: "ExpoIapModule",
|
|
401
|
-
description: "getAppTransaction requires Xcode 15.0+ with iOS 16.0 SDK for compilation",
|
|
402
|
-
code: IapErrorCode.unknown
|
|
403
|
-
)
|
|
404
|
-
#endif
|
|
405
|
-
} else {
|
|
406
|
-
throw Exception(
|
|
407
|
-
name: "ExpoIapModule",
|
|
408
|
-
description: "getAppTransaction requires iOS 16.0 or later",
|
|
409
|
-
code: IapErrorCode.unknown
|
|
410
|
-
)
|
|
411
|
-
}
|
|
40
|
+
return try await self.iapModule.initConnection()
|
|
412
41
|
}
|
|
413
42
|
|
|
414
|
-
AsyncFunction("
|
|
415
|
-
|
|
416
|
-
return nil
|
|
417
|
-
}
|
|
43
|
+
AsyncFunction("endConnection") { () async throws -> Bool in
|
|
44
|
+
logDebug("endConnection called")
|
|
418
45
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
"localizedDescription": product.localizedDescription,
|
|
424
|
-
"price": product.price.doubleValue,
|
|
425
|
-
"priceLocale": [
|
|
426
|
-
"currencyCode": product.priceLocale.currencyCode ?? "",
|
|
427
|
-
"currencySymbol": product.priceLocale.currencySymbol ?? "",
|
|
428
|
-
"countryCode": product.priceLocale.regionCode ?? ""
|
|
429
|
-
]
|
|
430
|
-
]
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
AsyncFunction("requestPurchaseOnPromotedProductIOS") { () -> Void in
|
|
434
|
-
guard let payment = self.promotedPayment else {
|
|
435
|
-
throw Exception(
|
|
436
|
-
name: "ExpoIapModule",
|
|
437
|
-
description: "No promoted product available",
|
|
438
|
-
code: IapErrorCode.itemUnavailable
|
|
439
|
-
)
|
|
46
|
+
if self.hasListeners {
|
|
47
|
+
// OpenIAP now exposes unified listener management
|
|
48
|
+
await self.iapModule.removeAllListeners()
|
|
49
|
+
self.hasListeners = false
|
|
440
50
|
}
|
|
441
51
|
|
|
442
|
-
|
|
443
|
-
SKPaymentQueue.default().add(payment)
|
|
444
|
-
|
|
445
|
-
// Clear the promoted product data
|
|
446
|
-
self.promotedPayment = nil
|
|
447
|
-
self.promotedProduct = nil
|
|
52
|
+
return try await self.iapModule.endConnection()
|
|
448
53
|
}
|
|
449
|
-
|
|
450
|
-
AsyncFunction("fetchProducts") { (skus: [String]) -> [[String: Any?]
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
let
|
|
54
|
+
|
|
55
|
+
AsyncFunction("fetchProducts") { (skus: [String]) async throws -> [[String: Any?]] in
|
|
56
|
+
logDebug("fetchProducts called with skus: \(skus)")
|
|
57
|
+
// Use ProductRequest for OpenIAP PR #3
|
|
58
|
+
let request = ProductRequest(skus: skus, type: "all")
|
|
59
|
+
let products = try await self.iapModule.fetchProducts(request)
|
|
454
60
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
let products = await productStore.getAllProducts()
|
|
463
|
-
return products.map { serializeProduct($0) }.compactMap { $0 }
|
|
464
|
-
} catch {
|
|
465
|
-
print("Error fetching items: \(error)")
|
|
466
|
-
throw error
|
|
61
|
+
// Debug logging
|
|
62
|
+
for product in products {
|
|
63
|
+
logDebug("Product: id=\(product.id), title=\(product.title), description=\(product.description)")
|
|
64
|
+
logDebug("Product: price=\(product.price ?? 0), displayPrice=\(product.displayPrice), currency=\(product.currency)")
|
|
65
|
+
logDebug("Product: type=\(product.type), platform=\(product.platform)")
|
|
467
66
|
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
AsyncFunction("requestProducts") { (skus: [String]) -> [[String: Any?]?] in
|
|
471
|
-
print("WARNING: requestProducts is deprecated. Use fetchProducts instead. The 'request' prefix should only be used for event-based operations. This method will be removed in version 3.0.0.")
|
|
472
|
-
|
|
473
|
-
try self.ensureConnection()
|
|
474
|
-
|
|
475
|
-
let productStore = self.productStore!
|
|
476
67
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
fetchedProducts.forEach { product in
|
|
481
|
-
isolatedStore.addProduct(product)
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
let products = await productStore.getAllProducts()
|
|
485
|
-
return products.map { serializeProduct($0) }.compactMap { $0 }
|
|
486
|
-
} catch {
|
|
487
|
-
print("Error fetching items: \(error)")
|
|
488
|
-
throw error
|
|
489
|
-
}
|
|
68
|
+
let serializedProducts = products.map { self.serializeProduct($0) }
|
|
69
|
+
logDebug("Serialized products: \(serializedProducts)")
|
|
70
|
+
return serializedProducts
|
|
490
71
|
}
|
|
491
|
-
|
|
492
|
-
AsyncFunction("endConnection") { () -> Bool in
|
|
493
|
-
self.cleanupExistingState()
|
|
494
|
-
return true
|
|
495
|
-
}
|
|
496
|
-
|
|
72
|
+
|
|
497
73
|
AsyncFunction("getAvailableItems") {
|
|
498
|
-
(alsoPublishToEventListenerIOS: Bool
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if alsoPublishToEventListenerIOS {
|
|
509
|
-
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
for await verification in onlyIncludeActiveItemsIOS
|
|
514
|
-
? Transaction.currentEntitlements : Transaction.all
|
|
515
|
-
{
|
|
516
|
-
do {
|
|
517
|
-
let transaction = try self.checkVerified(verification)
|
|
518
|
-
if !onlyIncludeActiveItemsIOS {
|
|
519
|
-
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
520
|
-
continue
|
|
521
|
-
}
|
|
522
|
-
switch transaction.productType {
|
|
523
|
-
case .nonConsumable, .autoRenewable, .consumable:
|
|
524
|
-
if await self.productStore?.getProduct(productID: transaction.productID)
|
|
525
|
-
!= nil
|
|
526
|
-
{
|
|
527
|
-
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
528
|
-
}
|
|
529
|
-
case .nonRenewable:
|
|
530
|
-
if await self.productStore?.getProduct(productID: transaction.productID)
|
|
531
|
-
!= nil
|
|
532
|
-
{
|
|
533
|
-
let currentDate = Date()
|
|
534
|
-
let expirationDate = Calendar(identifier: .gregorian).date(
|
|
535
|
-
byAdding: DateComponents(year: 1), to: transaction.purchaseDate)!
|
|
536
|
-
if currentDate < expirationDate {
|
|
537
|
-
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
default:
|
|
541
|
-
break
|
|
542
|
-
}
|
|
543
|
-
} catch StoreError.failedVerification {
|
|
544
|
-
let err = [
|
|
545
|
-
"responseCode": IapErrorCode.transactionValidationFailed,
|
|
546
|
-
"debugMessage": StoreError.failedVerification.localizedDescription,
|
|
547
|
-
"code": IapErrorCode.transactionValidationFailed,
|
|
548
|
-
"message": StoreError.failedVerification.localizedDescription,
|
|
549
|
-
"productId": "unknown",
|
|
550
|
-
]
|
|
551
|
-
if alsoPublishToEventListenerIOS {
|
|
552
|
-
self.sendEvent(IapEvent.PurchaseError, err)
|
|
553
|
-
}
|
|
554
|
-
} catch {
|
|
555
|
-
let err = [
|
|
556
|
-
"responseCode": IapErrorCode.unknown,
|
|
557
|
-
"debugMessage": error.localizedDescription,
|
|
558
|
-
"code": IapErrorCode.unknown,
|
|
559
|
-
"message": error.localizedDescription,
|
|
560
|
-
"productId": "unknown",
|
|
561
|
-
]
|
|
562
|
-
if alsoPublishToEventListenerIOS {
|
|
563
|
-
self.sendEvent(IapEvent.PurchaseError, err)
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
return purchasedItemsSerialized
|
|
74
|
+
(alsoPublishToEventListenerIOS: Bool?, onlyIncludeActiveItemsIOS: Bool?) async throws -> [[String: Any?]] in
|
|
75
|
+
logDebug("getAvailableItems called")
|
|
76
|
+
// Use PurchaseOptions for OpenIAP PR #3
|
|
77
|
+
let options = PurchaseOptions(
|
|
78
|
+
alsoPublishToEventListenerIOS: alsoPublishToEventListenerIOS,
|
|
79
|
+
onlyIncludeActiveItemsIOS: onlyIncludeActiveItemsIOS
|
|
80
|
+
)
|
|
81
|
+
let transactions = try await self.iapModule.getAvailablePurchases(options)
|
|
82
|
+
return transactions.map { self.serializePurchase($0) }
|
|
568
83
|
}
|
|
569
|
-
|
|
84
|
+
|
|
570
85
|
AsyncFunction("requestPurchase") {
|
|
571
|
-
(
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
86
|
+
(sku: String,
|
|
87
|
+
andDangerouslyFinishTransactionAutomaticallyIOS: Bool?,
|
|
88
|
+
appAccountToken: String?,
|
|
89
|
+
quantity: Int?,
|
|
90
|
+
discountOffer: [String: String]?) async throws -> [String: Any?]? in
|
|
575
91
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
let
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
let uuidNonce = UUID(uuidString: nonce),
|
|
592
|
-
let signatureData = Data(base64Encoded: signature),
|
|
593
|
-
let timestampInt = Int(timestamp)
|
|
594
|
-
{
|
|
595
|
-
options.insert(
|
|
596
|
-
.promotionalOffer(
|
|
597
|
-
offerID: offerID, keyID: keyID, nonce: uuidNonce,
|
|
598
|
-
signature: signatureData, timestamp: timestampInt))
|
|
599
|
-
}
|
|
600
|
-
if let appAccountToken = appAccountToken,
|
|
601
|
-
let appAccountUUID = UUID(uuidString: appAccountToken)
|
|
602
|
-
{
|
|
603
|
-
options.insert(.appAccountToken(appAccountUUID))
|
|
604
|
-
}
|
|
605
|
-
guard let windowScene = await self.currentWindowScene() else {
|
|
606
|
-
let errorData = [
|
|
607
|
-
"responseCode": IapErrorCode.serviceError,
|
|
608
|
-
"debugMessage": "Could not find window scene",
|
|
609
|
-
"code": IapErrorCode.serviceError,
|
|
610
|
-
"message": "Could not find window scene",
|
|
611
|
-
"productId": sku,
|
|
612
|
-
]
|
|
613
|
-
self.sendEvent(IapEvent.PurchaseError, errorData)
|
|
614
|
-
throw Exception(name: "ExpoIapModule", description: "Could not find window scene", code: IapErrorCode.serviceError)
|
|
615
|
-
}
|
|
616
|
-
let result: Product.PurchaseResult
|
|
617
|
-
#if swift(>=5.9)
|
|
618
|
-
if #available(iOS 17.0, tvOS 17.0, *) {
|
|
619
|
-
result = try await product.purchase(
|
|
620
|
-
confirmIn: windowScene, options: options)
|
|
621
|
-
} else {
|
|
622
|
-
#if !os(visionOS)
|
|
623
|
-
result = try await product.purchase(options: options)
|
|
624
|
-
#endif
|
|
625
|
-
}
|
|
626
|
-
#elseif !os(visionOS)
|
|
627
|
-
result = try await product.purchase(options: options)
|
|
628
|
-
#endif
|
|
629
|
-
|
|
630
|
-
switch result {
|
|
631
|
-
case .success(let verification):
|
|
632
|
-
let transaction = try self.checkVerified(verification)
|
|
633
|
-
|
|
634
|
-
// Debug: Log JWS representation
|
|
635
|
-
let jwsRepresentation = verification.jwsRepresentation
|
|
636
|
-
if !jwsRepresentation.isEmpty {
|
|
637
|
-
logDebug("buyProduct JWS: exists")
|
|
638
|
-
logDebug("buyProduct JWS length: \(jwsRepresentation.count)")
|
|
639
|
-
} else {
|
|
640
|
-
logDebug("buyProduct JWS: empty string")
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
if andDangerouslyFinishTransactionAutomatically {
|
|
644
|
-
await transaction.finish()
|
|
645
|
-
return nil
|
|
646
|
-
} else {
|
|
647
|
-
self.transactions[String(transaction.id)] = transaction
|
|
648
|
-
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
649
|
-
|
|
650
|
-
// Debug: Check if jwsRepresentationIOS is included in serialized result
|
|
651
|
-
logDebug("buyProduct serialized includes JWS: \(serialized["jwsRepresentationIOS"] != nil)")
|
|
652
|
-
|
|
653
|
-
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
654
|
-
return serialized
|
|
655
|
-
}
|
|
656
|
-
case .userCancelled:
|
|
657
|
-
let errorData = [
|
|
658
|
-
"responseCode": IapErrorCode.userCancelled,
|
|
659
|
-
"debugMessage": "User cancelled the purchase",
|
|
660
|
-
"code": IapErrorCode.userCancelled,
|
|
661
|
-
"message": "User cancelled the purchase",
|
|
662
|
-
"productId": sku,
|
|
663
|
-
]
|
|
664
|
-
self.sendEvent(IapEvent.PurchaseError, errorData)
|
|
665
|
-
throw Exception(name: "ExpoIapModule", description: "User cancelled the purchase", code: IapErrorCode.userCancelled)
|
|
666
|
-
case .pending:
|
|
667
|
-
let errorData = [
|
|
668
|
-
"responseCode": IapErrorCode.deferredPayment,
|
|
669
|
-
"debugMessage": "The payment was deferred",
|
|
670
|
-
"code": IapErrorCode.deferredPayment,
|
|
671
|
-
"message": "The payment was deferred",
|
|
672
|
-
"productId": sku,
|
|
673
|
-
]
|
|
674
|
-
self.sendEvent(IapEvent.PurchaseError, errorData)
|
|
675
|
-
throw Exception(name: "ExpoIapModule", description: "The payment was deferred", code: IapErrorCode.deferredPayment)
|
|
676
|
-
@unknown default:
|
|
677
|
-
let errorData = [
|
|
678
|
-
"responseCode": IapErrorCode.unknown,
|
|
679
|
-
"debugMessage": "Unknown purchase result",
|
|
680
|
-
"code": IapErrorCode.unknown,
|
|
681
|
-
"message": "Unknown purchase result",
|
|
682
|
-
"productId": sku,
|
|
683
|
-
]
|
|
684
|
-
self.sendEvent(IapEvent.PurchaseError, errorData)
|
|
685
|
-
throw Exception(name: "ExpoIapModule", description: "Unknown purchase result", code: IapErrorCode.unknown)
|
|
686
|
-
}
|
|
687
|
-
} catch {
|
|
688
|
-
if error is Exception {
|
|
689
|
-
throw error
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Map StoreKit errors to proper error codes
|
|
693
|
-
var errorCode = IapErrorCode.purchaseError
|
|
694
|
-
var errorMessage = error.localizedDescription
|
|
695
|
-
|
|
696
|
-
// Check for specific StoreKit error types
|
|
697
|
-
if let nsError = error as NSError? {
|
|
698
|
-
switch nsError.domain {
|
|
699
|
-
case "SKErrorDomain":
|
|
700
|
-
// Handle SKError codes
|
|
701
|
-
switch nsError.code {
|
|
702
|
-
case 0: // SKError.unknown
|
|
703
|
-
errorCode = IapErrorCode.unknown
|
|
704
|
-
case 1: // SKError.clientInvalid
|
|
705
|
-
errorCode = IapErrorCode.serviceError
|
|
706
|
-
case 2: // SKError.paymentCancelled
|
|
707
|
-
errorCode = IapErrorCode.userCancelled
|
|
708
|
-
errorMessage = "User cancelled the purchase"
|
|
709
|
-
case 3: // SKError.paymentInvalid
|
|
710
|
-
errorCode = IapErrorCode.userError
|
|
711
|
-
case 4: // SKError.paymentNotAllowed
|
|
712
|
-
errorCode = IapErrorCode.userError
|
|
713
|
-
errorMessage = "Payment not allowed"
|
|
714
|
-
case 5: // SKError.storeProductNotAvailable
|
|
715
|
-
errorCode = IapErrorCode.itemUnavailable
|
|
716
|
-
case 6: // SKError.cloudServicePermissionDenied
|
|
717
|
-
errorCode = IapErrorCode.serviceError
|
|
718
|
-
case 7: // SKError.cloudServiceNetworkConnectionFailed
|
|
719
|
-
errorCode = IapErrorCode.networkError
|
|
720
|
-
case 8: // SKError.cloudServiceRevoked
|
|
721
|
-
errorCode = IapErrorCode.serviceError
|
|
722
|
-
default:
|
|
723
|
-
errorCode = IapErrorCode.purchaseError
|
|
724
|
-
}
|
|
725
|
-
case "NSURLErrorDomain":
|
|
726
|
-
errorCode = IapErrorCode.networkError
|
|
727
|
-
errorMessage = "Network error: \(error.localizedDescription)"
|
|
728
|
-
default:
|
|
729
|
-
errorCode = IapErrorCode.purchaseError
|
|
730
|
-
}
|
|
731
|
-
} else if error.localizedDescription.lowercased().contains("network") {
|
|
732
|
-
errorCode = IapErrorCode.networkError
|
|
733
|
-
} else if error.localizedDescription.lowercased().contains("cancelled") {
|
|
734
|
-
errorCode = IapErrorCode.userCancelled
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
let errorData = [
|
|
738
|
-
"responseCode": errorCode,
|
|
739
|
-
"debugMessage": "Purchase failed: \(error.localizedDescription)",
|
|
740
|
-
"code": errorCode,
|
|
741
|
-
"message": errorMessage,
|
|
742
|
-
"productId": sku,
|
|
743
|
-
]
|
|
744
|
-
self.sendEvent(IapEvent.PurchaseError, errorData)
|
|
745
|
-
throw Exception(name: "ExpoIapModule", description: errorMessage, code: errorCode)
|
|
746
|
-
}
|
|
747
|
-
} else {
|
|
748
|
-
let errorData = [
|
|
749
|
-
"responseCode": IapErrorCode.itemUnavailable,
|
|
750
|
-
"debugMessage": "Invalid product ID",
|
|
751
|
-
"code": IapErrorCode.itemUnavailable,
|
|
752
|
-
"message": "Invalid product ID",
|
|
753
|
-
"productId": sku,
|
|
754
|
-
]
|
|
755
|
-
self.sendEvent(IapEvent.PurchaseError, errorData)
|
|
756
|
-
throw Exception(name: "ExpoIapModule", description: "Invalid product ID", code: IapErrorCode.itemUnavailable)
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
AsyncFunction("isEligibleForIntroOfferIOS") { (groupID: String) -> Bool in
|
|
761
|
-
return await Product.SubscriptionInfo.isEligibleForIntroOffer(for: groupID)
|
|
92
|
+
logDebug("requestPurchase called for sku: \(sku)")
|
|
93
|
+
|
|
94
|
+
let finishAutomatically = andDangerouslyFinishTransactionAutomaticallyIOS ?? false
|
|
95
|
+
let qty = quantity ?? 1
|
|
96
|
+
|
|
97
|
+
// Use RequestPurchaseProps for OpenIAP PR #3
|
|
98
|
+
let props = RequestPurchaseProps(
|
|
99
|
+
sku: sku,
|
|
100
|
+
andDangerouslyFinishTransactionAutomatically: finishAutomatically,
|
|
101
|
+
appAccountToken: appAccountToken,
|
|
102
|
+
quantity: qty,
|
|
103
|
+
discountOffer: discountOffer
|
|
104
|
+
)
|
|
105
|
+
let purchase = try await self.iapModule.requestPurchase(props)
|
|
106
|
+
return self.serializePurchase(purchase)
|
|
762
107
|
}
|
|
763
|
-
|
|
764
|
-
AsyncFunction("
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
do {
|
|
769
|
-
let product = await productStore.getProduct(productID: sku)
|
|
770
|
-
let status: [Product.SubscriptionInfo.Status]? = try await product?.subscription?
|
|
771
|
-
.status
|
|
772
|
-
guard let status = status else {
|
|
773
|
-
return nil
|
|
774
|
-
}
|
|
775
|
-
return status.map { serializeSubscriptionStatus($0) }
|
|
776
|
-
} catch {
|
|
777
|
-
if error is Exception {
|
|
778
|
-
throw error
|
|
779
|
-
}
|
|
780
|
-
throw Exception(name: "ExpoIapModule", description: "Error getting subscription status: \(error.localizedDescription)", code: IapErrorCode.serviceError)
|
|
781
|
-
}
|
|
108
|
+
|
|
109
|
+
AsyncFunction("finishTransaction") { (transactionIdentifier: String) async throws -> Bool in
|
|
110
|
+
logDebug("finishTransaction called for id: \(transactionIdentifier)")
|
|
111
|
+
return try await self.iapModule.finishTransaction(transactionIdentifier: transactionIdentifier)
|
|
782
112
|
}
|
|
783
|
-
|
|
784
|
-
AsyncFunction("
|
|
785
|
-
|
|
786
|
-
let
|
|
787
|
-
|
|
788
|
-
if let product = await productStore.getProduct(productID: sku) {
|
|
789
|
-
if let result = await product.currentEntitlement {
|
|
790
|
-
do {
|
|
791
|
-
let transaction = try self.checkVerified(result)
|
|
792
|
-
return serializeTransaction(transaction)
|
|
793
|
-
} catch StoreError.failedVerification {
|
|
794
|
-
throw Exception(name: "ExpoIapModule", description: "Failed to verify transaction for sku \(sku)", code: IapErrorCode.transactionValidationFailed)
|
|
795
|
-
} catch {
|
|
796
|
-
if error is Exception {
|
|
797
|
-
throw error
|
|
798
|
-
}
|
|
799
|
-
throw Exception(name: "ExpoIapModule", description: "Error fetching entitlement for sku \(sku): \(error.localizedDescription)", code: IapErrorCode.serviceError)
|
|
800
|
-
}
|
|
801
|
-
} else {
|
|
802
|
-
throw Exception(name: "ExpoIapModule", description: "Can't find entitlement for sku \(sku)", code: IapErrorCode.itemUnavailable)
|
|
803
|
-
}
|
|
804
|
-
} else {
|
|
805
|
-
throw Exception(name: "ExpoIapModule", description: "Can't find product for sku \(sku)", code: IapErrorCode.itemUnavailable)
|
|
806
|
-
}
|
|
113
|
+
|
|
114
|
+
AsyncFunction("getPendingTransactionsIOS") { () async throws -> [[String: Any?]] in
|
|
115
|
+
logDebug("getPendingTransactionsIOS called")
|
|
116
|
+
let transactions = try await self.iapModule.getPendingTransactionsIOS()
|
|
117
|
+
return transactions.map { self.serializePurchase($0) }
|
|
807
118
|
}
|
|
808
|
-
|
|
809
|
-
AsyncFunction("
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if let product = await productStore.getProduct(productID: sku) {
|
|
814
|
-
if let result = await product.latestTransaction {
|
|
815
|
-
do {
|
|
816
|
-
let transaction = try self.checkVerified(result)
|
|
817
|
-
return serializeTransaction(transaction)
|
|
818
|
-
} catch StoreError.failedVerification {
|
|
819
|
-
throw Exception(name: "ExpoIapModule", description: "Failed to verify transaction for sku \(sku)", code: IapErrorCode.transactionValidationFailed)
|
|
820
|
-
} catch {
|
|
821
|
-
if error is Exception {
|
|
822
|
-
throw error
|
|
823
|
-
}
|
|
824
|
-
throw Exception(name: "ExpoIapModule", description: "Error fetching latest transaction for sku \(sku): \(error.localizedDescription)", code: IapErrorCode.serviceError)
|
|
825
|
-
}
|
|
826
|
-
} else {
|
|
827
|
-
throw Exception(name: "ExpoIapModule", description: "Can't find latest transaction for sku \(sku)", code: IapErrorCode.itemUnavailable)
|
|
828
|
-
}
|
|
829
|
-
} else {
|
|
830
|
-
throw Exception(name: "ExpoIapModule", description: "Can't find product for sku \(sku)", code: IapErrorCode.itemUnavailable)
|
|
831
|
-
}
|
|
119
|
+
|
|
120
|
+
AsyncFunction("clearTransactionIOS") { () async throws in
|
|
121
|
+
logDebug("clearTransactionIOS called")
|
|
122
|
+
try await self.iapModule.clearTransactionIOS()
|
|
832
123
|
}
|
|
833
|
-
|
|
834
|
-
AsyncFunction("
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
self.transactions.removeValue(forKey: transactionIdentifier)
|
|
838
|
-
return true
|
|
839
|
-
} else {
|
|
840
|
-
throw Exception(name: "ExpoIapModule", description: "Invalid transaction ID", code: IapErrorCode.developerError)
|
|
841
|
-
}
|
|
124
|
+
|
|
125
|
+
AsyncFunction("getReceiptDataIOS") { () async throws -> String? in
|
|
126
|
+
logDebug("getReceiptDataIOS called")
|
|
127
|
+
return try await self.iapModule.getReceiptDataIOS()
|
|
842
128
|
}
|
|
843
|
-
|
|
844
|
-
AsyncFunction("
|
|
845
|
-
|
|
129
|
+
|
|
130
|
+
AsyncFunction("getTransactionJwsIOS") { (sku: String) async throws -> String? in
|
|
131
|
+
logDebug("getTransactionJwsIOS called for sku: \(sku)")
|
|
132
|
+
return try await self.iapModule.getTransactionJwsIOS(sku: sku)
|
|
846
133
|
}
|
|
847
|
-
|
|
848
|
-
AsyncFunction("
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
134
|
+
|
|
135
|
+
AsyncFunction("validateReceiptIOS") { (sku: String) async throws -> [String: Any?] in
|
|
136
|
+
logDebug("validateReceiptIOS called for sku: \(sku)")
|
|
137
|
+
// Use ReceiptValidationProps for OpenIAP PR #3
|
|
138
|
+
let props = ReceiptValidationProps(sku: sku)
|
|
139
|
+
let validation = try await self.iapModule.validateReceiptIOS(props)
|
|
140
|
+
var result: [String: Any?] = [
|
|
141
|
+
"isValid": validation.isValid,
|
|
142
|
+
"receiptData": validation.receiptData,
|
|
143
|
+
"jwsRepresentation": validation.jwsRepresentation
|
|
144
|
+
]
|
|
145
|
+
if let latest = validation.latestTransaction {
|
|
146
|
+
result["latestTransaction"] = self.serializePurchase(latest)
|
|
857
147
|
}
|
|
148
|
+
return result
|
|
858
149
|
}
|
|
859
|
-
|
|
860
|
-
AsyncFunction("
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
return true
|
|
864
|
-
#else
|
|
865
|
-
throw Exception(name: "ExpoIapModule", description: "This method is not available on tvOS", code: IapErrorCode.serviceError)
|
|
866
|
-
#endif
|
|
150
|
+
|
|
151
|
+
AsyncFunction("getStorefrontIOS") { () async throws -> String in
|
|
152
|
+
logDebug("getStorefrontIOS called")
|
|
153
|
+
return try await self.iapModule.getStorefrontIOS()
|
|
867
154
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
}
|
|
874
|
-
// Get all subscription products before showing the management UI
|
|
875
|
-
let subscriptionSkus = await self.getAllSubscriptionProductIds()
|
|
876
|
-
self.pollingSkus = Set(subscriptionSkus)
|
|
877
|
-
// Show the management UI
|
|
878
|
-
try await AppStore.showManageSubscriptions(in: windowScene)
|
|
879
|
-
// Start polling for status changes
|
|
880
|
-
self.pollForSubscriptionStatusChanges()
|
|
881
|
-
return true
|
|
882
|
-
#else
|
|
883
|
-
throw Exception(name: "ExpoIapModule", description: "This method is not available on tvOS", code: IapErrorCode.serviceError)
|
|
884
|
-
#endif
|
|
155
|
+
|
|
156
|
+
// Deprecated: Keep for backward compatibility
|
|
157
|
+
AsyncFunction("getStorefront") { () async throws -> String in
|
|
158
|
+
logDebug("getStorefront called (deprecated)")
|
|
159
|
+
return try await self.iapModule.getStorefrontIOS()
|
|
885
160
|
}
|
|
886
|
-
|
|
887
|
-
AsyncFunction("
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
|
|
161
|
+
|
|
162
|
+
AsyncFunction("getAppTransactionIOS") { () async throws -> [String: Any?]? in
|
|
163
|
+
logDebug("getAppTransactionIOS called")
|
|
164
|
+
if #available(iOS 16.0, tvOS 16.0, *) {
|
|
165
|
+
if let appTransaction = try await self.iapModule.getAppTransactionIOS() {
|
|
166
|
+
return [
|
|
167
|
+
"appVersion": appTransaction.appVersion,
|
|
168
|
+
"originalAppVersion": appTransaction.originalAppVersion,
|
|
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
|
+
]
|
|
900
175
|
}
|
|
901
176
|
}
|
|
177
|
+
return nil
|
|
902
178
|
}
|
|
903
|
-
|
|
904
|
-
AsyncFunction("
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
let productStore = self.productStore!
|
|
908
|
-
|
|
909
|
-
guard let product = await productStore.getProduct(productID: sku),
|
|
910
|
-
let result = await product.latestTransaction
|
|
911
|
-
else {
|
|
912
|
-
throw Exception(name: "ExpoIapModule", description: "Can't find product or transaction for sku \(sku)", code: IapErrorCode.itemUnavailable)
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
do {
|
|
916
|
-
let transaction = try self.checkVerified(result)
|
|
917
|
-
guard let windowScene = await self.currentWindowScene() else {
|
|
918
|
-
throw Exception(name: "ExpoIapModule", description: "Cannot find window scene or not available on macOS", code: IapErrorCode.serviceError)
|
|
919
|
-
}
|
|
920
|
-
let refundStatus = try await transaction.beginRefundRequest(in: windowScene)
|
|
921
|
-
return serialize(refundStatus)
|
|
922
|
-
} catch StoreError.failedVerification {
|
|
923
|
-
throw Exception(name: "ExpoIapModule", description: "Failed to verify transaction for sku \(sku)", code: IapErrorCode.transactionValidationFailed)
|
|
924
|
-
} catch {
|
|
925
|
-
if error is Exception {
|
|
926
|
-
throw error
|
|
927
|
-
}
|
|
928
|
-
throw Exception(name: "ExpoIapModule", description: "Failed to refund purchase: \(error.localizedDescription)", code: IapErrorCode.serviceError)
|
|
929
|
-
}
|
|
930
|
-
#else
|
|
931
|
-
throw Exception(name: "ExpoIapModule", description: "This method is not available on tvOS", code: IapErrorCode.serviceError)
|
|
932
|
-
#endif
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// @deprecated - This function is deprecated and will be removed in v2.9.0
|
|
936
|
-
// The transaction observer is now managed automatically
|
|
937
|
-
Function("disable") { () -> Bool in
|
|
938
|
-
// No longer needed - observer management is automatic
|
|
939
|
-
return true
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
AsyncFunction("getReceiptDataIOS") { () -> String? in
|
|
943
|
-
return try self.getReceiptDataInternal()
|
|
179
|
+
|
|
180
|
+
AsyncFunction("isEligibleForIntroOfferIOS") { (groupID: String) async -> Bool in
|
|
181
|
+
logDebug("isEligibleForIntroOfferIOS called for groupID: \(groupID)")
|
|
182
|
+
return await self.iapModule.isEligibleForIntroOfferIOS(groupID: groupID)
|
|
944
183
|
}
|
|
945
|
-
|
|
946
|
-
AsyncFunction("
|
|
947
|
-
|
|
948
|
-
let
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
184
|
+
|
|
185
|
+
AsyncFunction("subscriptionStatusIOS") { (sku: String) async throws -> [[String: Any?]]? in
|
|
186
|
+
logDebug("subscriptionStatusIOS called for sku: \(sku)")
|
|
187
|
+
if let statuses = try await self.iapModule.subscriptionStatusIOS(sku: sku) {
|
|
188
|
+
return statuses.map { status in
|
|
189
|
+
[
|
|
190
|
+
"state": status.state,
|
|
191
|
+
"renewalInfo": status.renewalInfo != nil ? [
|
|
192
|
+
"autoRenewPreference": status.renewalInfo!.autoRenewPreference as Any,
|
|
193
|
+
"expirationReason": status.renewalInfo!.expirationReason as Any,
|
|
194
|
+
"gracePeriodExpirationDate": status.renewalInfo!.gracePeriodExpirationDate.map { $0.timeIntervalSince1970 * 1000 } as Any
|
|
195
|
+
] : nil
|
|
196
|
+
]
|
|
958
197
|
}
|
|
959
198
|
}
|
|
960
|
-
return
|
|
199
|
+
return nil
|
|
961
200
|
}
|
|
962
|
-
|
|
963
|
-
AsyncFunction("
|
|
964
|
-
|
|
965
|
-
let
|
|
966
|
-
|
|
967
|
-
if let product = await productStore.getProduct(productID: sku),
|
|
968
|
-
let result = await product.latestTransaction {
|
|
969
|
-
return result.jwsRepresentation
|
|
970
|
-
} else {
|
|
971
|
-
throw Exception(name: "ExpoIapModule", description: "Can't find transaction for sku \(sku)", code: IapErrorCode.itemUnavailable)
|
|
201
|
+
|
|
202
|
+
AsyncFunction("currentEntitlementIOS") { (sku: String) async throws -> [String: Any?]? in
|
|
203
|
+
logDebug("currentEntitlementIOS called for sku: \(sku)")
|
|
204
|
+
if let transaction = try await self.iapModule.currentEntitlementIOS(sku: sku) {
|
|
205
|
+
return self.serializePurchase(transaction)
|
|
972
206
|
}
|
|
207
|
+
return nil
|
|
973
208
|
}
|
|
974
|
-
|
|
975
|
-
AsyncFunction("
|
|
976
|
-
|
|
977
|
-
let
|
|
978
|
-
|
|
979
|
-
// Get receipt data
|
|
980
|
-
var receiptData: String = ""
|
|
981
|
-
do {
|
|
982
|
-
receiptData = try self.getReceiptDataInternal()
|
|
983
|
-
} catch {
|
|
984
|
-
// Continue with validation even if receipt retrieval fails
|
|
985
|
-
// Error will be reflected by empty receipt data
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
var isValid = false
|
|
989
|
-
var jwsRepresentation: String? = nil
|
|
990
|
-
var latestTransaction: [String: Any?]? = nil
|
|
991
|
-
|
|
992
|
-
// Get JWS representation and verify transaction
|
|
993
|
-
if let product = await productStore.getProduct(productID: sku),
|
|
994
|
-
let result = await product.latestTransaction {
|
|
995
|
-
jwsRepresentation = result.jwsRepresentation
|
|
996
|
-
|
|
997
|
-
do {
|
|
998
|
-
// If this doesn't throw, the transaction is verified
|
|
999
|
-
let transaction = try self.checkVerified(result)
|
|
1000
|
-
isValid = true
|
|
1001
|
-
latestTransaction = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
1002
|
-
} catch {
|
|
1003
|
-
isValid = false
|
|
1004
|
-
}
|
|
209
|
+
|
|
210
|
+
AsyncFunction("latestTransactionIOS") { (sku: String) async throws -> [String: Any?]? in
|
|
211
|
+
logDebug("latestTransactionIOS called for sku: \(sku)")
|
|
212
|
+
if let transaction = try await self.iapModule.latestTransactionIOS(sku: sku) {
|
|
213
|
+
return self.serializePurchase(transaction)
|
|
1005
214
|
}
|
|
1006
|
-
|
|
1007
|
-
return [
|
|
1008
|
-
"isValid": isValid,
|
|
1009
|
-
"receiptData": receiptData,
|
|
1010
|
-
"jwsRepresentation": jwsRepresentation ?? "",
|
|
1011
|
-
"latestTransaction": latestTransaction as Any
|
|
1012
|
-
]
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
// Similar to Android's ensureConnection pattern
|
|
1017
|
-
private func ensureConnection() throws {
|
|
1018
|
-
guard isInitialized else {
|
|
1019
|
-
throw Exception(
|
|
1020
|
-
name: "ExpoIapModule",
|
|
1021
|
-
description: "Connection not initialized. Call initConnection() first.",
|
|
1022
|
-
code: IapErrorCode.notPrepared
|
|
1023
|
-
)
|
|
215
|
+
return nil
|
|
1024
216
|
}
|
|
1025
217
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
description: "Product store not available",
|
|
1030
|
-
code: IapErrorCode.notPrepared
|
|
1031
|
-
)
|
|
218
|
+
AsyncFunction("beginRefundRequestIOS") { (sku: String) async throws -> String? in
|
|
219
|
+
logDebug("beginRefundRequestIOS called for sku: \(sku)")
|
|
220
|
+
return try await self.iapModule.beginRefundRequestIOS(sku: sku)
|
|
1032
221
|
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
private func cleanupExistingState() {
|
|
1036
|
-
// Cancel any existing tasks
|
|
1037
|
-
updateListenerTask?.cancel()
|
|
1038
|
-
updateListenerTask = nil
|
|
1039
222
|
|
|
1040
|
-
|
|
1041
|
-
|
|
223
|
+
AsyncFunction("getPromotedProductIOS") { () async throws -> [String: Any?]? in
|
|
224
|
+
logDebug("getPromotedProductIOS called")
|
|
225
|
+
if let promotedProduct = try await self.iapModule.getPromotedProductIOS() {
|
|
226
|
+
return [
|
|
227
|
+
"productId": promotedProduct.productIdentifier,
|
|
228
|
+
"paymentDiscount": nil
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
return nil
|
|
232
|
+
}
|
|
1042
233
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
234
|
+
AsyncFunction("requestPurchaseOnPromotedProductIOS") { () async throws in
|
|
235
|
+
logDebug("requestPurchaseOnPromotedProductIOS called")
|
|
236
|
+
try await self.iapModule.requestPurchaseOnPromotedProductIOS()
|
|
237
|
+
}
|
|
1046
238
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
239
|
+
AsyncFunction("syncIOS") { () async throws -> Bool in
|
|
240
|
+
logDebug("syncIOS called")
|
|
241
|
+
return try await self.iapModule.syncIOS()
|
|
242
|
+
}
|
|
1050
243
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
paymentObserver = nil
|
|
244
|
+
AsyncFunction("presentCodeRedemptionSheetIOS") { () async throws -> Bool in
|
|
245
|
+
logDebug("presentCodeRedemptionSheetIOS called")
|
|
246
|
+
return try await self.iapModule.presentCodeRedemptionSheetIOS()
|
|
1055
247
|
}
|
|
1056
248
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
await store.removeAll()
|
|
1061
|
-
}
|
|
249
|
+
AsyncFunction("showManageSubscriptionsIOS") { () async throws -> Bool in
|
|
250
|
+
logDebug("showManageSubscriptionsIOS called")
|
|
251
|
+
return try await self.iapModule.showManageSubscriptionsIOS()
|
|
1062
252
|
}
|
|
1063
|
-
productStore = nil
|
|
1064
253
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
private func addTransactionObserver() {
|
|
1069
|
-
if updateListenerTask == nil {
|
|
1070
|
-
updateListenerTask = listenForTransactions()
|
|
254
|
+
AsyncFunction("isTransactionVerifiedIOS") { (sku: String) async -> Bool in
|
|
255
|
+
logDebug("isTransactionVerifiedIOS called for sku: \(sku)")
|
|
256
|
+
return await self.iapModule.isTransactionVerifiedIOS(sku: sku)
|
|
1071
257
|
}
|
|
1072
258
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
private func listenForTransactions() -> Task<Void, Error> {
|
|
1080
|
-
return Task.detached { [weak self] in
|
|
1081
|
-
guard let self = self else { return }
|
|
1082
|
-
for await result in Transaction.updates {
|
|
1083
|
-
do {
|
|
1084
|
-
let transaction = try self.checkVerified(result)
|
|
1085
|
-
self.transactions[String(transaction.id)] = transaction
|
|
1086
|
-
if self.hasListeners {
|
|
1087
|
-
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
1088
|
-
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
1089
|
-
}
|
|
1090
|
-
} catch {
|
|
1091
|
-
if self.hasListeners {
|
|
1092
|
-
let err = [
|
|
1093
|
-
"responseCode": IapErrorCode.transactionValidationFailed,
|
|
1094
|
-
"debugMessage": error.localizedDescription,
|
|
1095
|
-
"code": IapErrorCode.transactionValidationFailed,
|
|
1096
|
-
"message": error.localizedDescription,
|
|
1097
|
-
]
|
|
1098
|
-
self.sendEvent(IapEvent.PurchaseError, err)
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
259
|
+
|
|
260
|
+
// MARK: - Purchase Listeners
|
|
261
|
+
|
|
262
|
+
private func setupPurchaseListeners() {
|
|
263
|
+
_ = iapModule.purchaseUpdatedListener { [weak self] purchase in
|
|
264
|
+
self?.handlePurchaseUpdated(purchase)
|
|
1102
265
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
public func startObserving() {
|
|
1106
|
-
hasListeners = true
|
|
1107
|
-
addTransactionObserver()
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
public func stopObserving() {
|
|
1111
|
-
hasListeners = false
|
|
1112
|
-
removeTransactionObserver()
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
private func currentWindowScene() async -> UIWindowScene? {
|
|
1116
|
-
await MainActor.run {
|
|
1117
|
-
return UIApplication.shared.connectedScenes.first as? UIWindowScene
|
|
266
|
+
_ = iapModule.purchaseErrorListener { [weak self] error in
|
|
267
|
+
self?.handlePurchaseError(error)
|
|
1118
268
|
}
|
|
1119
269
|
}
|
|
1120
|
-
|
|
1121
|
-
private func
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
case .verified(let item):
|
|
1126
|
-
return item
|
|
1127
|
-
}
|
|
270
|
+
|
|
271
|
+
private func handlePurchaseUpdated(_ purchase: OpenIapPurchase) {
|
|
272
|
+
logDebug("Purchase updated: \(purchase.productId)")
|
|
273
|
+
let serialized = serializePurchase(purchase)
|
|
274
|
+
sendEvent(OpenIapEvent.PurchaseUpdated, serialized)
|
|
1128
275
|
}
|
|
1129
|
-
|
|
1130
|
-
private func
|
|
1131
|
-
|
|
1132
|
-
let
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
276
|
+
|
|
277
|
+
private func handlePurchaseError(_ error: PurchaseError) {
|
|
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)
|
|
1139
285
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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,
|
|
1145
298
|
|
|
1146
|
-
|
|
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,
|
|
1147
305
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
// Compare with previous state
|
|
1185
|
-
if let previousWillAutoRenew = previousStatuses[sku],
|
|
1186
|
-
previousWillAutoRenew != currentWillAutoRenew {
|
|
1187
|
-
|
|
1188
|
-
// Use the jwsRepresentation when serializing the transaction
|
|
1189
|
-
var purchaseMap = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
1190
|
-
|
|
1191
|
-
if case .verified(let renewalInfo) = status.renewalInfo {
|
|
1192
|
-
if let renewalInfoDict = serializeRenewalInfo(.verified(renewalInfo)) {
|
|
1193
|
-
purchaseMap["renewalInfo"] = renewalInfoDict
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
self.sendEvent(IapEvent.PurchaseUpdated, purchaseMap)
|
|
1198
|
-
previousStatuses[sku] = currentWillAutoRenew
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
self.pollingSkus.removeAll()
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
private func getReceiptDataInternal() throws -> String {
|
|
1207
|
-
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
|
|
1208
|
-
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
|
|
1209
|
-
do {
|
|
1210
|
-
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
|
|
1211
|
-
return receiptData.base64EncodedString(options: [])
|
|
1212
|
-
} catch {
|
|
1213
|
-
throw Exception(name: "ExpoIapModule", description: "Error reading receipt data: \(error.localizedDescription)", code: IapErrorCode.receiptFailed)
|
|
1214
|
-
}
|
|
1215
|
-
} else {
|
|
1216
|
-
throw Exception(name: "ExpoIapModule", description: "App Store receipt not found", code: IapErrorCode.receiptFailed)
|
|
1217
|
-
}
|
|
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
|
+
]
|
|
1218
340
|
}
|
|
1219
341
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
+
]
|
|
1224
387
|
|
|
1225
|
-
if
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
"
|
|
1229
|
-
"
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
"currencyCode": product.priceLocale.currencyCode ?? "",
|
|
1233
|
-
"currencySymbol": product.priceLocale.currencySymbol ?? "",
|
|
1234
|
-
"countryCode": product.priceLocale.regionCode ?? ""
|
|
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
|
|
1235
395
|
]
|
|
1236
396
|
]
|
|
1237
|
-
|
|
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
|
|
1238
431
|
}
|
|
432
|
+
|
|
433
|
+
// Deprecated fields for backward compatibility
|
|
434
|
+
result["isFamilyShareable"] = product.isFamilyShareableIOS
|
|
435
|
+
result["jsonRepresentation"] = product.jsonRepresentationIOS
|
|
436
|
+
|
|
437
|
+
return result
|
|
1239
438
|
}
|
|
1240
439
|
|
|
1241
|
-
// Ensure cleanup when module is deallocated
|
|
1242
|
-
deinit {
|
|
1243
|
-
cleanupExistingState()
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
// PaymentObserver for handling promoted products
|
|
1248
|
-
@available(iOS 15.0, *)
|
|
1249
|
-
class PaymentObserver: NSObject, SKPaymentTransactionObserver {
|
|
1250
|
-
weak var module: ExpoIapModule?
|
|
1251
|
-
|
|
1252
|
-
init(module: ExpoIapModule) {
|
|
1253
|
-
self.module = module
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
// Required by SKPaymentTransactionObserver protocol but not used
|
|
1257
|
-
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
|
|
1258
|
-
// We don't handle transactions here as StoreKit 2 handles them in ExpoIapModule
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
// Handle promoted products from App Store
|
|
1262
|
-
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
|
|
1263
|
-
module?.handlePromotedProduct(payment: payment, product: product)
|
|
1264
|
-
// Return false to defer the payment
|
|
1265
|
-
return false
|
|
1266
|
-
}
|
|
1267
440
|
}
|