expo-iap 2.7.14 → 2.8.1
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/.copilot-instructions.md +5 -5
- package/.cursorrules +31 -13
- package/CHANGELOG.md +53 -1
- package/CLAUDE.md +45 -4
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +52 -35
- package/build/ExpoIap.types.d.ts +21 -14
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js +0 -1
- package/build/ExpoIap.types.js.map +1 -1
- package/build/helpers/subscription.d.ts.map +1 -1
- package/build/helpers/subscription.js +24 -17
- package/build/helpers/subscription.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.js +10 -10
- package/build/index.js.map +1 -1
- package/build/modules/ios.d.ts +10 -23
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +9 -23
- package/build/modules/ios.js.map +1 -1
- package/build/types/ExpoIapAndroid.types.d.ts +50 -15
- package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
- package/build/types/ExpoIapAndroid.types.js +11 -6
- package/build/types/ExpoIapAndroid.types.js.map +1 -1
- package/build/types/{ExpoIapIos.types.d.ts → ExpoIapIOS.types.d.ts} +68 -39
- package/build/types/ExpoIapIOS.types.d.ts.map +1 -0
- package/build/types/ExpoIapIOS.types.js +2 -0
- package/build/types/ExpoIapIOS.types.js.map +1 -0
- package/build/{useIap.d.ts → useIAP.d.ts} +2 -2
- package/build/{useIap.d.ts.map → useIAP.d.ts.map} +1 -1
- package/build/{useIap.js → useIAP.js} +7 -6
- package/build/useIAP.js.map +1 -0
- package/bun.lock +677 -61
- package/ios/ExpoIapModule.swift +133 -51
- package/jest.config.js +43 -0
- package/package.json +8 -3
- package/src/ExpoIap.types.ts +44 -29
- package/src/helpers/subscription.ts +27 -20
- package/src/index.ts +13 -13
- package/src/modules/ios.ts +13 -40
- package/src/types/ExpoIapAndroid.types.ts +62 -15
- package/src/types/{ExpoIapIos.types.ts → ExpoIapIOS.types.ts} +73 -39
- package/src/{useIap.ts → useIAP.ts} +7 -6
- package/build/types/ExpoIapIos.types.d.ts.map +0 -1
- package/build/types/ExpoIapIos.types.js +0 -2
- package/build/types/ExpoIapIos.types.js.map +0 -1
- package/build/useIap.js.map +0 -1
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -22,12 +22,12 @@ struct IapEvent {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
@available(iOS 15.0, *)
|
|
25
|
-
func serializeTransaction(_ transaction: Transaction,
|
|
25
|
+
func serializeTransaction(_ transaction: Transaction, jwsRepresentationIOS: String? = nil) -> [String: Any?] {
|
|
26
26
|
let _ =
|
|
27
27
|
transaction.productType.rawValue.lowercased().contains("renewable")
|
|
28
28
|
|| transaction.expirationDate != nil
|
|
29
29
|
|
|
30
|
-
var
|
|
30
|
+
var transactionReasonIOS: String? = nil
|
|
31
31
|
var webOrderLineItemId: Int? = nil
|
|
32
32
|
var jsonData: [String: Any]? = nil
|
|
33
33
|
var jwsReceipt: String = ""
|
|
@@ -38,7 +38,7 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
|
|
|
38
38
|
do {
|
|
39
39
|
if let jsonObj = try JSONSerialization.jsonObject(with: jsonRep) as? [String: Any] {
|
|
40
40
|
jsonData = jsonObj
|
|
41
|
-
|
|
41
|
+
transactionReasonIOS = jsonObj["transactionReason"] as? String
|
|
42
42
|
if let webOrderId = jsonObj["webOrderLineItemID"] as? NSNumber {
|
|
43
43
|
webOrderLineItemId = webOrderId.intValue
|
|
44
44
|
}
|
|
@@ -56,47 +56,47 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
|
|
|
56
56
|
"transactionReceipt": jwsReceipt,
|
|
57
57
|
"platform": "ios",
|
|
58
58
|
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
59
|
+
"quantityIOS": transaction.purchasedQuantity,
|
|
60
|
+
"originalTransactionDateIOS": transaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
|
|
61
|
+
"originalTransactionIdentifierIOS": String(transaction.originalID),
|
|
62
62
|
"appAccountToken": transaction.appAccountToken?.uuidString,
|
|
63
63
|
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
64
|
+
"appBundleIdIOS": transaction.appBundleID,
|
|
65
|
+
"productTypeIOS": transaction.productType.rawValue,
|
|
66
|
+
"subscriptionGroupIdIOS": transaction.subscriptionGroupID,
|
|
67
67
|
|
|
68
|
-
"
|
|
68
|
+
"webOrderLineItemIdIOS": webOrderLineItemId,
|
|
69
69
|
|
|
70
|
-
"
|
|
70
|
+
"expirationDateIOS": transaction.expirationDate.map { $0.timeIntervalSince1970 * 1000 },
|
|
71
71
|
|
|
72
|
-
"
|
|
73
|
-
"
|
|
72
|
+
"isUpgradedIOS": transaction.isUpgraded,
|
|
73
|
+
"ownershipTypeIOS": transaction.ownershipType.rawValue,
|
|
74
74
|
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
75
|
+
"revocationDateIOS": transaction.revocationDate.map { $0.timeIntervalSince1970 * 1000 },
|
|
76
|
+
"revocationReasonIOS": transaction.revocationReason?.rawValue,
|
|
77
|
+
"transactionReasonIOS": transactionReasonIOS,
|
|
78
78
|
]
|
|
79
79
|
|
|
80
|
-
if (
|
|
81
|
-
logDebug("serializeTransaction adding
|
|
82
|
-
purchaseMap["
|
|
83
|
-
purchaseMap["purchaseToken"] =
|
|
80
|
+
if (jwsRepresentationIOS != nil) {
|
|
81
|
+
logDebug("serializeTransaction adding jwsRepresentationIOS with length: \(jwsRepresentationIOS!.count)")
|
|
82
|
+
purchaseMap["jwsRepresentationIOS"] = jwsRepresentationIOS
|
|
83
|
+
purchaseMap["purchaseToken"] = jwsRepresentationIOS
|
|
84
84
|
} else {
|
|
85
|
-
logDebug("serializeTransaction
|
|
85
|
+
logDebug("serializeTransaction jwsRepresentationIOS is nil")
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
if #available(iOS 16.0, *) {
|
|
89
|
-
purchaseMap["
|
|
89
|
+
purchaseMap["environmentIOS"] = transaction.environment.rawValue
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
if #available(iOS 17.0, *) {
|
|
93
|
-
purchaseMap["
|
|
94
|
-
purchaseMap["
|
|
93
|
+
purchaseMap["storefrontCountryCodeIOS"] = transaction.storefront.countryCode
|
|
94
|
+
purchaseMap["reasonIOS"] = transaction.reason.rawValue
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
if #available(iOS 17.2, *) {
|
|
98
98
|
if let offer = transaction.offer {
|
|
99
|
-
purchaseMap["
|
|
99
|
+
purchaseMap["offerIOS"] = [
|
|
100
100
|
"id": offer.id ?? "",
|
|
101
101
|
"type": offer.type.rawValue,
|
|
102
102
|
"paymentMode": offer.paymentMode?.rawValue ?? "",
|
|
@@ -106,10 +106,26 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
|
|
|
106
106
|
|
|
107
107
|
if #available(iOS 15.4, *), let jsonData = jsonData {
|
|
108
108
|
if let price = jsonData["price"] as? NSNumber {
|
|
109
|
-
|
|
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
|
|
110
113
|
}
|
|
111
114
|
if let currency = jsonData["currency"] as? String {
|
|
112
|
-
purchaseMap["
|
|
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
|
|
113
129
|
}
|
|
114
130
|
}
|
|
115
131
|
|
|
@@ -118,7 +134,7 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
|
|
|
118
134
|
|
|
119
135
|
private let DEFAULT_SUBSCRIPTION_PERIOD_UNIT = "DAY" // Default fallback unit for subscription periods.
|
|
120
136
|
|
|
121
|
-
func
|
|
137
|
+
func getPeriodIOS(_ unit: Product.SubscriptionPeriod.Unit) -> String {
|
|
122
138
|
return switch (unit) {
|
|
123
139
|
case .day: "DAY"
|
|
124
140
|
case .week: "WEEK"
|
|
@@ -135,7 +151,7 @@ func serializeOffer(_ offer: Product.SubscriptionOffer?) -> [String: Any?]? {
|
|
|
135
151
|
return [
|
|
136
152
|
"id": offer.id,
|
|
137
153
|
"period": [
|
|
138
|
-
"unit":
|
|
154
|
+
"unit": getPeriodIOS(offer.period.unit),
|
|
139
155
|
"value": offer.period.value
|
|
140
156
|
],
|
|
141
157
|
"periodCount": offer.periodCount,
|
|
@@ -151,9 +167,9 @@ func serializeSubscription(_ s: Product.SubscriptionInfo?) -> [String: Any?]? {
|
|
|
151
167
|
return [
|
|
152
168
|
"introductoryOffer": serializeOffer(s.introductoryOffer),
|
|
153
169
|
"promotionalOffers": s.promotionalOffers.map(serializeOffer),
|
|
154
|
-
"
|
|
170
|
+
"subscriptionGroupId": s.subscriptionGroupID,
|
|
155
171
|
"subscriptionPeriod": [
|
|
156
|
-
"unit":
|
|
172
|
+
"unit": getPeriodIOS(s.subscriptionPeriod.unit),
|
|
157
173
|
"value": s.subscriptionPeriod.value
|
|
158
174
|
],
|
|
159
175
|
]
|
|
@@ -162,20 +178,86 @@ func serializeSubscription(_ s: Product.SubscriptionInfo?) -> [String: Any?]? {
|
|
|
162
178
|
|
|
163
179
|
@available(iOS 15.0, *)
|
|
164
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
|
+
|
|
165
224
|
return [
|
|
166
225
|
"debugDescription": serializeDebug(p.debugDescription),
|
|
167
226
|
"description": p.description,
|
|
168
|
-
|
|
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,
|
|
169
237
|
"displayPrice": p.displayPrice,
|
|
170
238
|
"id": p.id,
|
|
171
239
|
"title": p.displayName,
|
|
172
|
-
"
|
|
173
|
-
"
|
|
240
|
+
"isFamilyShareableIOS": p.isFamilyShareable,
|
|
241
|
+
"jsonRepresentationIOS": String(data: p.jsonRepresentation, encoding: .utf8),
|
|
174
242
|
"price": p.price,
|
|
175
|
-
"
|
|
176
|
-
"type":
|
|
243
|
+
"subscriptionInfoIOS": serializeSubscription(p.subscription),
|
|
244
|
+
"type": productType,
|
|
177
245
|
"currency": p.priceFormatStyle.currencyCode,
|
|
178
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
|
|
179
261
|
]
|
|
180
262
|
}
|
|
181
263
|
|
|
@@ -290,7 +372,7 @@ public class ExpoIapModule: Module {
|
|
|
290
372
|
}
|
|
291
373
|
|
|
292
374
|
var result: [String: Any?] = [
|
|
293
|
-
"
|
|
375
|
+
"bundleId": appTransaction.bundleID,
|
|
294
376
|
"appVersion": appTransaction.appVersion,
|
|
295
377
|
"originalAppVersion": appTransaction.originalAppVersion,
|
|
296
378
|
"originalPurchaseDate": appTransaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
|
|
@@ -298,13 +380,13 @@ public class ExpoIapModule: Module {
|
|
|
298
380
|
"deviceVerificationNonce": appTransaction.deviceVerificationNonce.uuidString,
|
|
299
381
|
"environment": appTransaction.environment.rawValue,
|
|
300
382
|
"signedDate": appTransaction.signedDate.timeIntervalSince1970 * 1000,
|
|
301
|
-
"
|
|
302
|
-
"
|
|
383
|
+
"appId": appTransaction.appID,
|
|
384
|
+
"appVersionId": appTransaction.appVersionID,
|
|
303
385
|
"preorderDate": appTransaction.preorderDate.map { $0.timeIntervalSince1970 * 1000 }
|
|
304
386
|
]
|
|
305
387
|
|
|
306
388
|
if #available(iOS 18.4, *) {
|
|
307
|
-
result["
|
|
389
|
+
result["appTransactionId"] = appTransaction.appTransactionID
|
|
308
390
|
result["originalPlatform"] = appTransaction.originalPlatform.rawValue
|
|
309
391
|
}
|
|
310
392
|
|
|
@@ -393,8 +475,8 @@ public class ExpoIapModule: Module {
|
|
|
393
475
|
|
|
394
476
|
var purchasedItemsSerialized: [[String: Any?]] = []
|
|
395
477
|
|
|
396
|
-
func addTransaction(transaction: Transaction,
|
|
397
|
-
let serialized = serializeTransaction(transaction,
|
|
478
|
+
func addTransaction(transaction: Transaction, jwsRepresentationIOS: String? = nil) {
|
|
479
|
+
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: jwsRepresentationIOS)
|
|
398
480
|
purchasedItemsSerialized.append(serialized)
|
|
399
481
|
|
|
400
482
|
if alsoPublishToEventListener {
|
|
@@ -408,7 +490,7 @@ public class ExpoIapModule: Module {
|
|
|
408
490
|
do {
|
|
409
491
|
let transaction = try self.checkVerified(verification)
|
|
410
492
|
if !onlyIncludeActiveItems {
|
|
411
|
-
addTransaction(transaction: transaction,
|
|
493
|
+
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
412
494
|
continue
|
|
413
495
|
}
|
|
414
496
|
switch transaction.productType {
|
|
@@ -416,7 +498,7 @@ public class ExpoIapModule: Module {
|
|
|
416
498
|
if await self.productStore?.getProduct(productID: transaction.productID)
|
|
417
499
|
!= nil
|
|
418
500
|
{
|
|
419
|
-
addTransaction(transaction: transaction,
|
|
501
|
+
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
420
502
|
}
|
|
421
503
|
case .nonRenewable:
|
|
422
504
|
if await self.productStore?.getProduct(productID: transaction.productID)
|
|
@@ -426,7 +508,7 @@ public class ExpoIapModule: Module {
|
|
|
426
508
|
let expirationDate = Calendar(identifier: .gregorian).date(
|
|
427
509
|
byAdding: DateComponents(year: 1), to: transaction.purchaseDate)!
|
|
428
510
|
if currentDate < expirationDate {
|
|
429
|
-
addTransaction(transaction: transaction,
|
|
511
|
+
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
430
512
|
}
|
|
431
513
|
}
|
|
432
514
|
default:
|
|
@@ -537,10 +619,10 @@ public class ExpoIapModule: Module {
|
|
|
537
619
|
return nil
|
|
538
620
|
} else {
|
|
539
621
|
self.transactions[String(transaction.id)] = transaction
|
|
540
|
-
let serialized = serializeTransaction(transaction,
|
|
622
|
+
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
541
623
|
|
|
542
|
-
// Debug: Check if
|
|
543
|
-
logDebug("buyProduct serialized includes JWS: \(serialized["
|
|
624
|
+
// Debug: Check if jwsRepresentationIOS is included in serialized result
|
|
625
|
+
logDebug("buyProduct serialized includes JWS: \(serialized["jwsRepresentationIOS"] != nil)")
|
|
544
626
|
|
|
545
627
|
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
546
628
|
return serialized
|
|
@@ -888,7 +970,7 @@ public class ExpoIapModule: Module {
|
|
|
888
970
|
// If this doesn't throw, the transaction is verified
|
|
889
971
|
let transaction = try self.checkVerified(result)
|
|
890
972
|
isValid = true
|
|
891
|
-
latestTransaction = serializeTransaction(transaction,
|
|
973
|
+
latestTransaction = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
892
974
|
} catch {
|
|
893
975
|
isValid = false
|
|
894
976
|
}
|
|
@@ -974,7 +1056,7 @@ public class ExpoIapModule: Module {
|
|
|
974
1056
|
let transaction = try self.checkVerified(result)
|
|
975
1057
|
self.transactions[String(transaction.id)] = transaction
|
|
976
1058
|
if self.hasListeners {
|
|
977
|
-
let serialized = serializeTransaction(transaction,
|
|
1059
|
+
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
978
1060
|
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
979
1061
|
}
|
|
980
1062
|
} catch {
|
|
@@ -1076,7 +1158,7 @@ public class ExpoIapModule: Module {
|
|
|
1076
1158
|
previousWillAutoRenew != currentWillAutoRenew {
|
|
1077
1159
|
|
|
1078
1160
|
// Use the jwsRepresentation when serializing the transaction
|
|
1079
|
-
var purchaseMap = serializeTransaction(transaction,
|
|
1161
|
+
var purchaseMap = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
1080
1162
|
|
|
1081
1163
|
if case .verified(let renewalInfo) = status.renewalInfo {
|
|
1082
1164
|
if let renewalInfoDict = serializeRenewalInfo(.verified(renewalInfo)) {
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: 'ts-jest',
|
|
3
|
+
testEnvironment: 'node',
|
|
4
|
+
roots: ['<rootDir>/src'],
|
|
5
|
+
testMatch: [
|
|
6
|
+
'**/__tests__/**/*.+(ts|tsx|js)',
|
|
7
|
+
'**/?(*.)+(spec|test).+(ts|tsx|js)',
|
|
8
|
+
],
|
|
9
|
+
transform: {
|
|
10
|
+
'^.+\\.(ts|tsx)$': [
|
|
11
|
+
'ts-jest',
|
|
12
|
+
{
|
|
13
|
+
tsconfig: {
|
|
14
|
+
jsx: 'react',
|
|
15
|
+
esModuleInterop: true,
|
|
16
|
+
allowSyntheticDefaultImports: true,
|
|
17
|
+
moduleResolution: 'node',
|
|
18
|
+
skipLibCheck: true,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
moduleNameMapper: {
|
|
24
|
+
'^react-native$': '<rootDir>/src/__mocks__/react-native.js',
|
|
25
|
+
'^expo-modules-core$': '<rootDir>/src/__mocks__/expo-modules-core.js',
|
|
26
|
+
},
|
|
27
|
+
collectCoverageFrom: [
|
|
28
|
+
'src/**/*.{ts,tsx}',
|
|
29
|
+
'!src/**/*.d.ts',
|
|
30
|
+
'!src/**/__tests__/**',
|
|
31
|
+
'!src/**/__mocks__/**',
|
|
32
|
+
'!src/ExpoIapModule.ts',
|
|
33
|
+
'!src/ExpoIapModule.web.ts',
|
|
34
|
+
],
|
|
35
|
+
coverageThreshold: {
|
|
36
|
+
global: {
|
|
37
|
+
branches: 15,
|
|
38
|
+
functions: 15,
|
|
39
|
+
lines: 15,
|
|
40
|
+
statements: 15,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-iap",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"description": "In App Purchase module in Expo",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"lint:prettier": "prettier --write \"**/*.{md,js,jsx,ts,tsx}\"",
|
|
13
13
|
"lint:tsc": "tsc -p tsconfig.json --noEmit --skipLibCheck",
|
|
14
14
|
"lint:ci": "bun run lint:tsc && bun run lint:eslint && bun run lint:prettier",
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"test:coverage": "jest --coverage",
|
|
15
17
|
"prepare": "expo-module prepare",
|
|
16
18
|
"expo-module": "expo-module",
|
|
17
19
|
"open:ios": "xed example/ios",
|
|
@@ -38,15 +40,18 @@
|
|
|
38
40
|
"author": "hyochan <hyochan.dev@gmail.com> (https://github.com/hyochan)",
|
|
39
41
|
"license": "MIT",
|
|
40
42
|
"homepage": "https://github.com/hyochan/expo-iap#readme",
|
|
41
|
-
"dependencies": {},
|
|
42
43
|
"devDependencies": {
|
|
44
|
+
"@jest/globals": "^30.0.5",
|
|
45
|
+
"@types/jest": "^30.0.0",
|
|
43
46
|
"@types/react": "~19.1.7",
|
|
44
47
|
"eslint": "8.57.0",
|
|
45
48
|
"eslint-config-expo": "^9.2.0",
|
|
46
49
|
"eslint-config-prettier": "^10.1.5",
|
|
47
50
|
"eslint-plugin-prettier": "^5.4.1",
|
|
48
51
|
"expo-module-scripts": "^4.1.7",
|
|
49
|
-
"expo-modules-core": "^2.4.0"
|
|
52
|
+
"expo-modules-core": "^2.4.0",
|
|
53
|
+
"jest": "^29.7.0",
|
|
54
|
+
"ts-jest": "^29.4.1"
|
|
50
55
|
},
|
|
51
56
|
"peerDependencies": {
|
|
52
57
|
"expo": "*",
|
package/src/ExpoIap.types.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ProductAndroid,
|
|
3
3
|
ProductPurchaseAndroid,
|
|
4
|
-
|
|
4
|
+
ProductSubscriptionAndroid,
|
|
5
5
|
} from './types/ExpoIapAndroid.types';
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from './types/
|
|
7
|
+
ProductIOS,
|
|
8
|
+
ProductPurchaseIOS,
|
|
9
|
+
ProductSubscriptionIOS,
|
|
10
|
+
} from './types/ExpoIapIOS.types';
|
|
11
11
|
import {NATIVE_ERROR_CODES} from './ExpoIapModule';
|
|
12
12
|
|
|
13
13
|
export type ChangeEventPayload = {
|
|
@@ -16,7 +16,11 @@ export type ChangeEventPayload = {
|
|
|
16
16
|
|
|
17
17
|
export type ProductType = 'inapp' | 'subs';
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// COMMON TYPES (Base types shared across all platforms)
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
export type ProductCommon = {
|
|
20
24
|
id: string;
|
|
21
25
|
title: string;
|
|
22
26
|
description: string;
|
|
@@ -25,15 +29,23 @@ export type ProductBase = {
|
|
|
25
29
|
displayPrice: string;
|
|
26
30
|
currency: string;
|
|
27
31
|
price?: number;
|
|
32
|
+
debugDescription?: string;
|
|
33
|
+
platform?: string;
|
|
28
34
|
};
|
|
29
35
|
|
|
30
|
-
export type
|
|
36
|
+
export type PurchaseCommon = {
|
|
31
37
|
id: string; // Transaction identifier - used by finishTransaction
|
|
32
38
|
productId: string; // Product identifier - which product was purchased
|
|
39
|
+
ids?: string[]; // Product identifiers for purchases that include multiple products
|
|
33
40
|
transactionId?: string; // @deprecated - use id instead
|
|
34
41
|
transactionDate: number;
|
|
35
42
|
transactionReceipt: string;
|
|
36
43
|
purchaseToken?: string; // Unified purchase token (jwsRepresentation for iOS, purchaseToken for Android)
|
|
44
|
+
platform?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ProductSubscriptionCommon = ProductCommon & {
|
|
48
|
+
type: 'subs';
|
|
37
49
|
};
|
|
38
50
|
|
|
39
51
|
// Define literal platform types for better type discrimination
|
|
@@ -43,33 +55,38 @@ export type AndroidPlatform = {platform: 'android'};
|
|
|
43
55
|
// Platform-agnostic unified product types (public API)
|
|
44
56
|
export type Product =
|
|
45
57
|
| (ProductAndroid & AndroidPlatform)
|
|
46
|
-
| (
|
|
58
|
+
| (ProductIOS & IosPlatform);
|
|
47
59
|
|
|
48
60
|
export type SubscriptionProduct =
|
|
49
|
-
| (
|
|
50
|
-
| (
|
|
51
|
-
|
|
52
|
-
// ============================================================================
|
|
53
|
-
// Legacy Types (For backward compatibility with useIap hook)
|
|
54
|
-
// ============================================================================
|
|
55
|
-
|
|
56
|
-
// Re-export platform-specific purchase types for legacy compatibility
|
|
57
|
-
export type {ProductPurchaseAndroid} from './types/ExpoIapAndroid.types';
|
|
58
|
-
export type {ProductPurchaseIos} from './types/ExpoIapIos.types';
|
|
61
|
+
| (ProductSubscriptionAndroid & AndroidPlatform)
|
|
62
|
+
| (ProductSubscriptionIOS & IosPlatform);
|
|
59
63
|
|
|
60
|
-
//
|
|
64
|
+
// Re-export platform-specific types
|
|
65
|
+
export type {
|
|
66
|
+
ProductPurchaseAndroid,
|
|
67
|
+
PurchaseAndroid,
|
|
68
|
+
ProductSubscriptionAndroid,
|
|
69
|
+
SubscriptionProductAndroid, // Legacy
|
|
70
|
+
} from './types/ExpoIapAndroid.types';
|
|
71
|
+
export type {
|
|
72
|
+
ProductPurchaseIOS,
|
|
73
|
+
PurchaseIOS,
|
|
74
|
+
ProductSubscriptionIOS,
|
|
75
|
+
SubscriptionProductIOS, // Legacy
|
|
76
|
+
} from './types/ExpoIapIOS.types';
|
|
77
|
+
|
|
78
|
+
// Union type for platform-specific purchase types
|
|
61
79
|
export type ProductPurchase =
|
|
62
80
|
| (ProductPurchaseAndroid & AndroidPlatform)
|
|
63
|
-
| (
|
|
81
|
+
| (ProductPurchaseIOS & IosPlatform);
|
|
64
82
|
|
|
65
|
-
// Union type for platform-specific subscription purchase types
|
|
83
|
+
// Union type for platform-specific subscription purchase types
|
|
66
84
|
export type SubscriptionPurchase =
|
|
67
85
|
| (ProductPurchaseAndroid & AndroidPlatform & {autoRenewingAndroid: boolean})
|
|
68
|
-
| (
|
|
86
|
+
| (ProductPurchaseIOS & IosPlatform);
|
|
69
87
|
|
|
70
88
|
export type Purchase = ProductPurchase | SubscriptionPurchase;
|
|
71
89
|
|
|
72
|
-
// Legacy result type
|
|
73
90
|
export type PurchaseResult = {
|
|
74
91
|
responseCode?: number;
|
|
75
92
|
debugMessage?: string;
|
|
@@ -305,10 +322,10 @@ export interface UnifiedRequestPurchaseProps {
|
|
|
305
322
|
readonly skus?: string[]; // Multiple SKUs (Android native, iOS uses first item)
|
|
306
323
|
|
|
307
324
|
// iOS-specific properties (ignored on Android)
|
|
308
|
-
readonly
|
|
325
|
+
readonly andDangerouslyFinishTransactionAutomatically?: boolean;
|
|
309
326
|
readonly appAccountToken?: string;
|
|
310
327
|
readonly quantity?: number;
|
|
311
|
-
readonly withOffer?: import('./types/
|
|
328
|
+
readonly withOffer?: import('./types/ExpoIapIOS.types').PaymentDiscount;
|
|
312
329
|
|
|
313
330
|
// Android-specific properties (ignored on iOS)
|
|
314
331
|
readonly obfuscatedAccountIdAndroid?: string;
|
|
@@ -325,10 +342,10 @@ export interface UnifiedRequestPurchaseProps {
|
|
|
325
342
|
*/
|
|
326
343
|
export interface RequestPurchaseIosProps {
|
|
327
344
|
readonly sku: string;
|
|
328
|
-
readonly
|
|
345
|
+
readonly andDangerouslyFinishTransactionAutomatically?: boolean;
|
|
329
346
|
readonly appAccountToken?: string;
|
|
330
347
|
readonly quantity?: number;
|
|
331
|
-
readonly withOffer?: import('./types/
|
|
348
|
+
readonly withOffer?: import('./types/ExpoIapIOS.types').PaymentDiscount;
|
|
332
349
|
}
|
|
333
350
|
|
|
334
351
|
/**
|
|
@@ -382,5 +399,3 @@ export type RequestPurchaseProps = RequestPurchasePropsByPlatforms;
|
|
|
382
399
|
* This is the recommended API moving forward
|
|
383
400
|
*/
|
|
384
401
|
export type RequestSubscriptionProps = RequestSubscriptionPropsByPlatforms;
|
|
385
|
-
|
|
386
|
-
// Note: Type guard functions are exported from index.ts to avoid conflicts
|
|
@@ -28,15 +28,16 @@ export const getActiveSubscriptions = async (
|
|
|
28
28
|
const filteredPurchases = purchases.filter((purchase) => {
|
|
29
29
|
// If specific IDs provided, filter by them
|
|
30
30
|
if (subscriptionIds && subscriptionIds.length > 0) {
|
|
31
|
-
if (!subscriptionIds.includes(purchase.
|
|
31
|
+
if (!subscriptionIds.includes(purchase.productId)) {
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Check if this purchase has subscription-specific fields
|
|
37
37
|
const hasSubscriptionFields =
|
|
38
|
-
('
|
|
39
|
-
'autoRenewingAndroid' in purchase
|
|
38
|
+
('expirationDateIOS' in purchase && purchase.expirationDateIOS) ||
|
|
39
|
+
'autoRenewingAndroid' in purchase ||
|
|
40
|
+
('environmentIOS' in purchase && purchase.environmentIOS === 'Sandbox');
|
|
40
41
|
|
|
41
42
|
if (!hasSubscriptionFields) {
|
|
42
43
|
return false;
|
|
@@ -44,19 +45,25 @@ export const getActiveSubscriptions = async (
|
|
|
44
45
|
|
|
45
46
|
// Check if it's actually active
|
|
46
47
|
if (Platform.OS === 'ios') {
|
|
47
|
-
if ('
|
|
48
|
-
return purchase.
|
|
48
|
+
if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
|
|
49
|
+
return purchase.expirationDateIOS > currentTime;
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
) {
|
|
51
|
+
// For iOS purchases without expiration date (like Sandbox), we consider them active
|
|
52
|
+
// if they have the environmentIOS field and were created recently
|
|
53
|
+
if ('environmentIOS' in purchase && purchase.environmentIOS) {
|
|
54
54
|
const dayInMs = 24 * 60 * 60 * 1000;
|
|
55
|
+
// If no expiration date, consider active if transaction is recent (within 24 hours for Sandbox)
|
|
55
56
|
if (
|
|
56
|
-
purchase
|
|
57
|
-
|
|
57
|
+
!('expirationDateIOS' in purchase) ||
|
|
58
|
+
!purchase.expirationDateIOS
|
|
58
59
|
) {
|
|
59
|
-
|
|
60
|
+
if (
|
|
61
|
+
purchase.environmentIOS === 'Sandbox' &&
|
|
62
|
+
purchase.transactionDate &&
|
|
63
|
+
currentTime - purchase.transactionDate < dayInMs
|
|
64
|
+
) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
60
67
|
}
|
|
61
68
|
}
|
|
62
69
|
} else if (Platform.OS === 'android') {
|
|
@@ -70,26 +77,26 @@ export const getActiveSubscriptions = async (
|
|
|
70
77
|
// Convert to ActiveSubscription format
|
|
71
78
|
for (const purchase of filteredPurchases) {
|
|
72
79
|
const subscription: ActiveSubscription = {
|
|
73
|
-
productId: purchase.
|
|
80
|
+
productId: purchase.productId,
|
|
74
81
|
isActive: true,
|
|
75
82
|
};
|
|
76
83
|
|
|
77
84
|
// Add platform-specific details
|
|
78
85
|
if (Platform.OS === 'ios') {
|
|
79
|
-
if ('
|
|
80
|
-
const expirationDate = new Date(purchase.
|
|
86
|
+
if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
|
|
87
|
+
const expirationDate = new Date(purchase.expirationDateIOS);
|
|
81
88
|
subscription.expirationDateIOS = expirationDate;
|
|
82
89
|
|
|
83
|
-
// Calculate days until expiration
|
|
84
|
-
const daysUntilExpiration = Math.
|
|
85
|
-
(purchase.
|
|
90
|
+
// Calculate days until expiration (round to nearest day)
|
|
91
|
+
const daysUntilExpiration = Math.round(
|
|
92
|
+
(purchase.expirationDateIOS - currentTime) / (1000 * 60 * 60 * 24),
|
|
86
93
|
);
|
|
87
94
|
subscription.daysUntilExpirationIOS = daysUntilExpiration;
|
|
88
95
|
subscription.willExpireSoon = daysUntilExpiration <= 7;
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
if ('
|
|
92
|
-
subscription.environmentIOS = purchase.
|
|
98
|
+
if ('environmentIOS' in purchase) {
|
|
99
|
+
subscription.environmentIOS = purchase.environmentIOS;
|
|
93
100
|
}
|
|
94
101
|
} else if (Platform.OS === 'android') {
|
|
95
102
|
if ('autoRenewingAndroid' in purchase) {
|