expo-iap 2.8.6 → 2.8.8
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 +38 -0
- package/CLAUDE.md +7 -0
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +113 -0
- package/build/helpers/subscription.d.ts +3 -0
- package/build/helpers/subscription.d.ts.map +1 -1
- package/build/helpers/subscription.js +3 -0
- package/build/helpers/subscription.js.map +1 -1
- package/build/index.d.ts +30 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +39 -13
- package/build/index.js.map +1 -1
- package/build/modules/ios.d.ts +4 -5
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +2 -3
- package/build/modules/ios.js.map +1 -1
- package/build/useIAP.d.ts +12 -4
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +14 -6
- package/build/useIAP.js.map +1 -1
- package/build/utils/constants.d.ts +4 -0
- package/build/utils/constants.d.ts.map +1 -0
- package/build/utils/constants.js +12 -0
- package/build/utils/constants.js.map +1 -0
- package/ios/ExpoIapModule.swift +102 -150
- package/package.json +3 -2
- package/src/helpers/subscription.ts +6 -0
- package/src/index.ts +48 -13
- package/src/modules/ios.ts +4 -5
- package/src/useIAP.ts +35 -10
- package/src/utils/constants.ts +14 -0
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -114,7 +114,6 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIOS: Stri
|
|
|
114
114
|
if let currency = jsonData["currency"] as? String {
|
|
115
115
|
purchaseMap["currencyCodeIOS"] = currency
|
|
116
116
|
|
|
117
|
-
// Try to get currency symbol from locale
|
|
118
117
|
let locale = Locale(identifier: Locale.identifier(fromComponents: [NSLocale.Key.currencyCode.rawValue: currency]))
|
|
119
118
|
purchaseMap["currencySymbolIOS"] = locale.currencySymbol
|
|
120
119
|
|
|
@@ -123,7 +122,6 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIOS: Stri
|
|
|
123
122
|
purchaseMap["currencyIOS"] = currency
|
|
124
123
|
// END: Deprecated - will be removed in v2.9.0
|
|
125
124
|
}
|
|
126
|
-
// Extract country code from storefront if available
|
|
127
125
|
if let storefront = jsonData["storefront"] as? String {
|
|
128
126
|
purchaseMap["countryCodeIOS"] = storefront
|
|
129
127
|
}
|
|
@@ -178,10 +176,8 @@ func serializeSubscription(_ s: Product.SubscriptionInfo?) -> [String: Any?]? {
|
|
|
178
176
|
|
|
179
177
|
@available(iOS 15.0, *)
|
|
180
178
|
func serializeProduct(_ p: Product) -> [String: Any?] {
|
|
181
|
-
// Convert Product.ProductType to our expected 'inapp' or 'subs' string
|
|
182
179
|
let productType: String = p.subscription != nil ? "subs" : "inapp"
|
|
183
180
|
|
|
184
|
-
// For subscription products, add discounts and introductory price
|
|
185
181
|
var discounts: [[String: Any?]]? = nil
|
|
186
182
|
var introductoryPrice: String? = nil
|
|
187
183
|
var introductoryPriceAsAmountIOS: String? = nil
|
|
@@ -192,7 +188,6 @@ func serializeProduct(_ p: Product) -> [String: Any?] {
|
|
|
192
188
|
var subscriptionPeriodUnitIOS: String? = nil
|
|
193
189
|
|
|
194
190
|
if let subscription = p.subscription {
|
|
195
|
-
// Extract discount information from promotional offers
|
|
196
191
|
if !subscription.promotionalOffers.isEmpty {
|
|
197
192
|
discounts = subscription.promotionalOffers.compactMap { offer in
|
|
198
193
|
return [
|
|
@@ -207,7 +202,6 @@ func serializeProduct(_ p: Product) -> [String: Any?] {
|
|
|
207
202
|
}
|
|
208
203
|
}
|
|
209
204
|
|
|
210
|
-
// Extract introductory price from introductory offer
|
|
211
205
|
if let introOffer = subscription.introductoryOffer {
|
|
212
206
|
introductoryPrice = introOffer.displayPrice
|
|
213
207
|
introductoryPriceAsAmountIOS = "\(introOffer.price)"
|
|
@@ -216,7 +210,6 @@ func serializeProduct(_ p: Product) -> [String: Any?] {
|
|
|
216
210
|
introductoryPriceSubscriptionPeriodIOS = getPeriodIOS(introOffer.period.unit)
|
|
217
211
|
}
|
|
218
212
|
|
|
219
|
-
// Extract subscription period information
|
|
220
213
|
subscriptionPeriodNumberIOS = "\(subscription.subscriptionPeriod.value)"
|
|
221
214
|
subscriptionPeriodUnitIOS = getPeriodIOS(subscription.subscriptionPeriod.unit)
|
|
222
215
|
}
|
|
@@ -224,7 +217,6 @@ func serializeProduct(_ p: Product) -> [String: Any?] {
|
|
|
224
217
|
return [
|
|
225
218
|
"debugDescription": serializeDebug(p.debugDescription),
|
|
226
219
|
"description": p.description,
|
|
227
|
-
// New iOS-suffixed fields
|
|
228
220
|
"displayNameIOS": p.displayName,
|
|
229
221
|
"discountsIOS": discounts,
|
|
230
222
|
"introductoryPriceIOS": introductoryPrice,
|
|
@@ -308,13 +300,11 @@ public class ExpoIapModule: Module {
|
|
|
308
300
|
private var productStore: ProductStore?
|
|
309
301
|
private var hasListeners = false
|
|
310
302
|
private var updateListenerTask: Task<Void, Error>?
|
|
311
|
-
private var subscriptionPollingTask: Task<Void, Error>?
|
|
312
|
-
private var pollingSkus: Set<String> = []
|
|
313
303
|
private var paymentObserver: PaymentObserver?
|
|
314
304
|
private var promotedPayment: SKPayment?
|
|
315
305
|
private var promotedProduct: SKProduct?
|
|
316
306
|
|
|
317
|
-
|
|
307
|
+
private let subscriptionChangePropagationDelay: UInt64 = 1_500_000_000 // 1.5 seconds in nanoseconds
|
|
318
308
|
private var isInitialized = false
|
|
319
309
|
|
|
320
310
|
public func definition() -> ModuleDefinition {
|
|
@@ -337,13 +327,10 @@ public class ExpoIapModule: Module {
|
|
|
337
327
|
}
|
|
338
328
|
|
|
339
329
|
Function("initConnection") { () -> Bool in
|
|
340
|
-
// Clean up any existing state first (important for hot reload)
|
|
341
330
|
self.cleanupExistingState()
|
|
342
331
|
|
|
343
|
-
// Initialize fresh state
|
|
344
332
|
self.productStore = ProductStore()
|
|
345
333
|
|
|
346
|
-
// Set up PaymentObserver for promoted products
|
|
347
334
|
if self.paymentObserver == nil {
|
|
348
335
|
self.paymentObserver = PaymentObserver(module: self)
|
|
349
336
|
SKPaymentQueue.default().add(self.paymentObserver!)
|
|
@@ -416,7 +403,6 @@ public class ExpoIapModule: Module {
|
|
|
416
403
|
return nil
|
|
417
404
|
}
|
|
418
405
|
|
|
419
|
-
// Convert SKProduct to dictionary
|
|
420
406
|
return [
|
|
421
407
|
"productIdentifier": product.productIdentifier,
|
|
422
408
|
"localizedTitle": product.localizedTitle,
|
|
@@ -439,15 +425,35 @@ public class ExpoIapModule: Module {
|
|
|
439
425
|
)
|
|
440
426
|
}
|
|
441
427
|
|
|
442
|
-
// Add the deferred payment to the queue
|
|
443
428
|
SKPaymentQueue.default().add(payment)
|
|
444
429
|
|
|
445
|
-
// Clear the promoted product data
|
|
446
430
|
self.promotedPayment = nil
|
|
447
431
|
self.promotedProduct = nil
|
|
448
432
|
}
|
|
449
433
|
|
|
434
|
+
AsyncFunction("fetchProducts") { (skus: [String]) -> [[String: Any?]?] in
|
|
435
|
+
try self.ensureConnection()
|
|
436
|
+
|
|
437
|
+
let productStore = self.productStore!
|
|
438
|
+
|
|
439
|
+
do {
|
|
440
|
+
let fetchedProducts = try await Product.products(for: skus)
|
|
441
|
+
await productStore.performOnActor { isolatedStore in
|
|
442
|
+
fetchedProducts.forEach { product in
|
|
443
|
+
isolatedStore.addProduct(product)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
let products = await productStore.getAllProducts()
|
|
447
|
+
return products.map { serializeProduct($0) }.compactMap { $0 }
|
|
448
|
+
} catch {
|
|
449
|
+
print("Error fetching items: \(error)")
|
|
450
|
+
throw error
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
450
454
|
AsyncFunction("requestProducts") { (skus: [String]) -> [[String: Any?]?] in
|
|
455
|
+
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.")
|
|
456
|
+
|
|
451
457
|
try self.ensureConnection()
|
|
452
458
|
|
|
453
459
|
let productStore = self.productStore!
|
|
@@ -482,63 +488,26 @@ public class ExpoIapModule: Module {
|
|
|
482
488
|
func addTransaction(transaction: Transaction, jwsRepresentationIOS: String? = nil) {
|
|
483
489
|
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: jwsRepresentationIOS)
|
|
484
490
|
purchasedItemsSerialized.append(serialized)
|
|
485
|
-
|
|
486
|
-
if alsoPublishToEventListenerIOS {
|
|
487
|
-
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
488
|
-
}
|
|
489
491
|
}
|
|
490
492
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if !onlyIncludeActiveItemsIOS {
|
|
497
|
-
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
498
|
-
continue
|
|
499
|
-
}
|
|
500
|
-
switch transaction.productType {
|
|
501
|
-
case .nonConsumable, .autoRenewable, .consumable:
|
|
502
|
-
if await self.productStore?.getProduct(productID: transaction.productID)
|
|
503
|
-
!= nil
|
|
504
|
-
{
|
|
493
|
+
if onlyIncludeActiveItemsIOS {
|
|
494
|
+
for await verification in Transaction.currentEntitlements {
|
|
495
|
+
do {
|
|
496
|
+
let transaction = try self.checkVerified(verification)
|
|
497
|
+
if await self.productStore?.getProduct(productID: transaction.productID) != nil {
|
|
505
498
|
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
506
499
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
!= nil
|
|
510
|
-
{
|
|
511
|
-
let currentDate = Date()
|
|
512
|
-
let expirationDate = Calendar(identifier: .gregorian).date(
|
|
513
|
-
byAdding: DateComponents(year: 1), to: transaction.purchaseDate)!
|
|
514
|
-
if currentDate < expirationDate {
|
|
515
|
-
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
default:
|
|
519
|
-
break
|
|
520
|
-
}
|
|
521
|
-
} catch StoreError.failedVerification {
|
|
522
|
-
let err = [
|
|
523
|
-
"responseCode": IapErrorCode.transactionValidationFailed,
|
|
524
|
-
"debugMessage": StoreError.failedVerification.localizedDescription,
|
|
525
|
-
"code": IapErrorCode.transactionValidationFailed,
|
|
526
|
-
"message": StoreError.failedVerification.localizedDescription,
|
|
527
|
-
"productId": "unknown",
|
|
528
|
-
]
|
|
529
|
-
if alsoPublishToEventListenerIOS {
|
|
530
|
-
self.sendEvent(IapEvent.PurchaseError, err)
|
|
500
|
+
} catch {
|
|
501
|
+
print("[ExpoIapModule] Failed to verify transaction: \(error)")
|
|
531
502
|
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if alsoPublishToEventListenerIOS {
|
|
541
|
-
self.sendEvent(IapEvent.PurchaseError, err)
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
for await verification in Transaction.all {
|
|
506
|
+
do {
|
|
507
|
+
let transaction = try self.checkVerified(verification)
|
|
508
|
+
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
509
|
+
} catch {
|
|
510
|
+
print("[ExpoIapModule] Failed to verify transaction: \(error)")
|
|
542
511
|
}
|
|
543
512
|
}
|
|
544
513
|
}
|
|
@@ -609,7 +578,6 @@ public class ExpoIapModule: Module {
|
|
|
609
578
|
case .success(let verification):
|
|
610
579
|
let transaction = try self.checkVerified(verification)
|
|
611
580
|
|
|
612
|
-
// Debug: Log JWS representation
|
|
613
581
|
let jwsRepresentation = verification.jwsRepresentation
|
|
614
582
|
if !jwsRepresentation.isEmpty {
|
|
615
583
|
logDebug("buyProduct JWS: exists")
|
|
@@ -625,7 +593,6 @@ public class ExpoIapModule: Module {
|
|
|
625
593
|
self.transactions[String(transaction.id)] = transaction
|
|
626
594
|
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
627
595
|
|
|
628
|
-
// Debug: Check if jwsRepresentationIOS is included in serialized result
|
|
629
596
|
logDebug("buyProduct serialized includes JWS: \(serialized["jwsRepresentationIOS"] != nil)")
|
|
630
597
|
|
|
631
598
|
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
@@ -667,15 +634,12 @@ public class ExpoIapModule: Module {
|
|
|
667
634
|
throw error
|
|
668
635
|
}
|
|
669
636
|
|
|
670
|
-
// Map StoreKit errors to proper error codes
|
|
671
637
|
var errorCode = IapErrorCode.purchaseError
|
|
672
638
|
var errorMessage = error.localizedDescription
|
|
673
639
|
|
|
674
|
-
// Check for specific StoreKit error types
|
|
675
640
|
if let nsError = error as NSError? {
|
|
676
641
|
switch nsError.domain {
|
|
677
642
|
case "SKErrorDomain":
|
|
678
|
-
// Handle SKError codes
|
|
679
643
|
switch nsError.code {
|
|
680
644
|
case 0: // SKError.unknown
|
|
681
645
|
errorCode = IapErrorCode.unknown
|
|
@@ -844,19 +808,76 @@ public class ExpoIapModule: Module {
|
|
|
844
808
|
#endif
|
|
845
809
|
}
|
|
846
810
|
|
|
847
|
-
AsyncFunction("showManageSubscriptionsIOS") { () ->
|
|
811
|
+
AsyncFunction("showManageSubscriptionsIOS") { () -> [[String: Any?]] in
|
|
848
812
|
#if !os(tvOS)
|
|
849
813
|
guard let windowScene = await self.currentWindowScene() else {
|
|
850
814
|
throw Exception(name: "ExpoIapModule", description: "Cannot find window scene or not available on macOS", code: IapErrorCode.serviceError)
|
|
851
815
|
}
|
|
852
|
-
|
|
816
|
+
|
|
817
|
+
var beforeStatuses: [String: Bool] = [:]
|
|
853
818
|
let subscriptionSkus = await self.getAllSubscriptionProductIds()
|
|
854
|
-
|
|
855
|
-
|
|
819
|
+
|
|
820
|
+
for sku in subscriptionSkus {
|
|
821
|
+
if let product = await self.productStore?.getProduct(productID: sku),
|
|
822
|
+
let subscription = product.subscription {
|
|
823
|
+
do {
|
|
824
|
+
let statuses = try await subscription.status
|
|
825
|
+
if let s = statuses.first(where: { status in
|
|
826
|
+
if case .verified(let info) = status.renewalInfo {
|
|
827
|
+
return info.currentProductID == sku
|
|
828
|
+
}
|
|
829
|
+
return false
|
|
830
|
+
}) {
|
|
831
|
+
var willAutoRenew = false
|
|
832
|
+
if case .verified(let info) = s.renewalInfo {
|
|
833
|
+
willAutoRenew = info.willAutoRenew
|
|
834
|
+
}
|
|
835
|
+
beforeStatuses[sku] = willAutoRenew
|
|
836
|
+
}
|
|
837
|
+
} catch {
|
|
838
|
+
// ignore
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
856
843
|
try await AppStore.showManageSubscriptions(in: windowScene)
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
844
|
+
|
|
845
|
+
try? await Task.sleep(nanoseconds: subscriptionChangePropagationDelay)
|
|
846
|
+
|
|
847
|
+
var updatedSubscriptions: [[String: Any?]] = []
|
|
848
|
+
|
|
849
|
+
for sku in subscriptionSkus {
|
|
850
|
+
if let product = await self.productStore?.getProduct(productID: sku),
|
|
851
|
+
let subscription = product.subscription,
|
|
852
|
+
let result = await product.latestTransaction {
|
|
853
|
+
let statuses = try? await subscription.status
|
|
854
|
+
let matchedStatus = statuses?.first(where: { status in
|
|
855
|
+
if case .verified(let info) = status.renewalInfo {
|
|
856
|
+
return info.currentProductID == sku
|
|
857
|
+
}
|
|
858
|
+
return false
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
var currentWillAutoRenew = false
|
|
862
|
+
if let s = matchedStatus, case .verified(let info) = s.renewalInfo {
|
|
863
|
+
currentWillAutoRenew = info.willAutoRenew
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
let previousWillAutoRenew = beforeStatuses[sku] ?? false
|
|
867
|
+
if previousWillAutoRenew != currentWillAutoRenew {
|
|
868
|
+
do {
|
|
869
|
+
let transaction = try self.checkVerified(result)
|
|
870
|
+
var purchaseMap = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
871
|
+
purchaseMap["willAutoRenewIOS"] = currentWillAutoRenew
|
|
872
|
+
updatedSubscriptions.append(purchaseMap)
|
|
873
|
+
} catch {
|
|
874
|
+
print("[ExpoIapModule] Failed to verify subscription change: \(error)")
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return updatedSubscriptions
|
|
860
881
|
#else
|
|
861
882
|
throw Exception(name: "ExpoIapModule", description: "This method is not available on tvOS", code: IapErrorCode.serviceError)
|
|
862
883
|
#endif
|
|
@@ -1015,12 +1036,8 @@ public class ExpoIapModule: Module {
|
|
|
1015
1036
|
updateListenerTask?.cancel()
|
|
1016
1037
|
updateListenerTask = nil
|
|
1017
1038
|
|
|
1018
|
-
subscriptionPollingTask?.cancel()
|
|
1019
|
-
subscriptionPollingTask = nil
|
|
1020
|
-
|
|
1021
1039
|
// Clear collections
|
|
1022
1040
|
transactions.removeAll()
|
|
1023
|
-
pollingSkus.removeAll()
|
|
1024
1041
|
|
|
1025
1042
|
// Reset promoted products
|
|
1026
1043
|
promotedPayment = nil
|
|
@@ -1116,71 +1133,6 @@ public class ExpoIapModule: Module {
|
|
|
1116
1133
|
}
|
|
1117
1134
|
}
|
|
1118
1135
|
|
|
1119
|
-
private func pollForSubscriptionStatusChanges() {
|
|
1120
|
-
subscriptionPollingTask?.cancel()
|
|
1121
|
-
subscriptionPollingTask = Task {
|
|
1122
|
-
try? await Task.sleep(nanoseconds: 1_500_000_000) // 1.5 seconds
|
|
1123
|
-
|
|
1124
|
-
var previousStatuses: [String: Bool] = [:] // Track auto-renewal state with Bool
|
|
1125
|
-
|
|
1126
|
-
for sku in self.pollingSkus {
|
|
1127
|
-
guard let product = await self.productStore?.getProduct(productID: sku),
|
|
1128
|
-
let status = try? await product.subscription?.status.first else { continue }
|
|
1129
|
-
|
|
1130
|
-
// Track willAutoRenew as a bool value
|
|
1131
|
-
var willAutoRenew = false
|
|
1132
|
-
if case .verified(let info) = status.renewalInfo {
|
|
1133
|
-
willAutoRenew = info.willAutoRenew
|
|
1134
|
-
}
|
|
1135
|
-
previousStatuses[sku] = willAutoRenew
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
for _ in 1...5 {
|
|
1139
|
-
try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
|
|
1140
|
-
if Task.isCancelled {
|
|
1141
|
-
return
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
for sku in self.pollingSkus {
|
|
1145
|
-
guard let product = await self.productStore?.getProduct(productID: sku),
|
|
1146
|
-
let status = try? await product.subscription?.status.first,
|
|
1147
|
-
let result = await product.latestTransaction else { continue }
|
|
1148
|
-
// Try to verify the transaction
|
|
1149
|
-
let transaction: Transaction
|
|
1150
|
-
do {
|
|
1151
|
-
transaction = try self.checkVerified(result)
|
|
1152
|
-
} catch {
|
|
1153
|
-
continue // Skip if verification fails
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
// Track current auto-renewal state
|
|
1157
|
-
var currentWillAutoRenew = false
|
|
1158
|
-
if case .verified(let info) = status.renewalInfo {
|
|
1159
|
-
currentWillAutoRenew = info.willAutoRenew
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// Compare with previous state
|
|
1163
|
-
if let previousWillAutoRenew = previousStatuses[sku],
|
|
1164
|
-
previousWillAutoRenew != currentWillAutoRenew {
|
|
1165
|
-
|
|
1166
|
-
// Use the jwsRepresentation when serializing the transaction
|
|
1167
|
-
var purchaseMap = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
1168
|
-
|
|
1169
|
-
if case .verified(let renewalInfo) = status.renewalInfo {
|
|
1170
|
-
if let renewalInfoDict = serializeRenewalInfo(.verified(renewalInfo)) {
|
|
1171
|
-
purchaseMap["renewalInfo"] = renewalInfoDict
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
self.sendEvent(IapEvent.PurchaseUpdated, purchaseMap)
|
|
1176
|
-
previousStatuses[sku] = currentWillAutoRenew
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
self.pollingSkus.removeAll()
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
1136
|
private func getReceiptDataInternal() throws -> String {
|
|
1185
1137
|
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
|
|
1186
1138
|
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-iap",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.8",
|
|
4
4
|
"description": "In App Purchase module in Expo",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"docs:start": "cd docs && bun run start",
|
|
26
26
|
"docs:build": "cd docs && bun run build",
|
|
27
27
|
"docs:serve": "cd docs && bun run serve",
|
|
28
|
-
"docs:install": "cd docs && bun install"
|
|
28
|
+
"docs:install": "cd docs && bun install",
|
|
29
|
+
"generate:icon": "npx sharp-cli resize 32 32 -i docs/static/img/icon.png -o docs/static/img/favicon-32x32.png && npx sharp-cli resize 16 16 -i docs/static/img/icon.png -o docs/static/img/favicon-16x16.png && npx sharp-cli resize 180 180 -i docs/static/img/icon.png -o docs/static/img/apple-touch-icon.png && npx sharp-cli resize 192 192 -i docs/static/img/icon.png -o docs/static/img/android-chrome-192x192.png && npx sharp-cli resize 512 512 -i docs/static/img/icon.png -o docs/static/img/android-chrome-512x512.png && npx sharp-cli resize 150 150 -i docs/static/img/icon.png -o docs/static/img/mstile-150x150.png && npx sharp-cli resize 1200 630 -i docs/static/img/icon.png -o docs/static/img/og-image.png && npx sharp-cli resize 1200 600 -i docs/static/img/icon.png -o docs/static/img/twitter-card.png && npx sharp-cli resize 16 16 -i docs/static/img/icon.png -o docs/static/img/favicon.png && cp docs/static/img/favicon-16x16.png docs/static/img/favicon.ico"
|
|
29
30
|
},
|
|
30
31
|
"keywords": [
|
|
31
32
|
"react-native",
|
|
@@ -4,6 +4,9 @@ import {getAvailablePurchases} from '../index';
|
|
|
4
4
|
export interface ActiveSubscription {
|
|
5
5
|
productId: string;
|
|
6
6
|
isActive: boolean;
|
|
7
|
+
transactionId: string; // Transaction identifier for backend validation
|
|
8
|
+
purchaseToken?: string; // JWT token (iOS) or purchase token (Android) for backend validation
|
|
9
|
+
transactionDate: number; // Transaction timestamp
|
|
7
10
|
expirationDateIOS?: Date;
|
|
8
11
|
autoRenewingAndroid?: boolean;
|
|
9
12
|
environmentIOS?: string;
|
|
@@ -79,6 +82,9 @@ export const getActiveSubscriptions = async (
|
|
|
79
82
|
const subscription: ActiveSubscription = {
|
|
80
83
|
productId: purchase.productId,
|
|
81
84
|
isActive: true,
|
|
85
|
+
transactionId: purchase.transactionId || purchase.id,
|
|
86
|
+
purchaseToken: purchase.purchaseToken,
|
|
87
|
+
transactionDate: purchase.transactionDate,
|
|
82
88
|
};
|
|
83
89
|
|
|
84
90
|
// Add platform-specific details
|
package/src/index.ts
CHANGED
|
@@ -124,7 +124,7 @@ export function initConnection(): Promise<boolean> {
|
|
|
124
124
|
|
|
125
125
|
export const getProducts = async (skus: string[]): Promise<Product[]> => {
|
|
126
126
|
console.warn(
|
|
127
|
-
"`getProducts` is deprecated. Use `
|
|
127
|
+
"`getProducts` is deprecated. Use `fetchProducts({ skus, type: 'inapp' })` instead. This function will be removed in version 3.0.0.",
|
|
128
128
|
);
|
|
129
129
|
if (!skus?.length) {
|
|
130
130
|
return Promise.reject(new Error('"skus" is required'));
|
|
@@ -132,7 +132,7 @@ export const getProducts = async (skus: string[]): Promise<Product[]> => {
|
|
|
132
132
|
|
|
133
133
|
return Platform.select({
|
|
134
134
|
ios: async () => {
|
|
135
|
-
const rawItems = await ExpoIapModule.
|
|
135
|
+
const rawItems = await ExpoIapModule.fetchProducts(skus);
|
|
136
136
|
return rawItems.filter((item: unknown) => {
|
|
137
137
|
if (!isProductIOS(item)) return false;
|
|
138
138
|
return (
|
|
@@ -145,7 +145,7 @@ export const getProducts = async (skus: string[]): Promise<Product[]> => {
|
|
|
145
145
|
}) as Product[];
|
|
146
146
|
},
|
|
147
147
|
android: async () => {
|
|
148
|
-
const products = await ExpoIapModule.
|
|
148
|
+
const products = await ExpoIapModule.fetchProducts('inapp', skus);
|
|
149
149
|
return products.filter((product: unknown) =>
|
|
150
150
|
isProductAndroid<Product>(product),
|
|
151
151
|
);
|
|
@@ -158,7 +158,7 @@ export const getSubscriptions = async (
|
|
|
158
158
|
skus: string[],
|
|
159
159
|
): Promise<SubscriptionProduct[]> => {
|
|
160
160
|
console.warn(
|
|
161
|
-
"`getSubscriptions` is deprecated. Use `
|
|
161
|
+
"`getSubscriptions` is deprecated. Use `fetchProducts({ skus, type: 'subs' })` instead. This function will be removed in version 3.0.0.",
|
|
162
162
|
);
|
|
163
163
|
if (!skus?.length) {
|
|
164
164
|
return Promise.reject(new Error('"skus" is required'));
|
|
@@ -166,7 +166,7 @@ export const getSubscriptions = async (
|
|
|
166
166
|
|
|
167
167
|
return Platform.select({
|
|
168
168
|
ios: async () => {
|
|
169
|
-
const rawItems = await ExpoIapModule.
|
|
169
|
+
const rawItems = await ExpoIapModule.fetchProducts(skus);
|
|
170
170
|
return rawItems.filter((item: unknown) => {
|
|
171
171
|
if (!isProductIOS(item)) return false;
|
|
172
172
|
return (
|
|
@@ -179,7 +179,7 @@ export const getSubscriptions = async (
|
|
|
179
179
|
}) as SubscriptionProduct[];
|
|
180
180
|
},
|
|
181
181
|
android: async () => {
|
|
182
|
-
const rawItems = await ExpoIapModule.
|
|
182
|
+
const rawItems = await ExpoIapModule.fetchProducts('subs', skus);
|
|
183
183
|
return rawItems.filter((item: unknown) => {
|
|
184
184
|
if (!isProductAndroid(item)) return false;
|
|
185
185
|
return (
|
|
@@ -200,28 +200,28 @@ export async function endConnection(): Promise<boolean> {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
|
-
*
|
|
203
|
+
* Fetch products with unified API (v2.7.0+)
|
|
204
204
|
*
|
|
205
|
-
* @param params - Product
|
|
205
|
+
* @param params - Product fetch configuration
|
|
206
206
|
* @param params.skus - Array of product SKUs to fetch
|
|
207
207
|
* @param params.type - Type of products: 'inapp' for regular products (default) or 'subs' for subscriptions
|
|
208
208
|
*
|
|
209
209
|
* @example
|
|
210
210
|
* ```typescript
|
|
211
211
|
* // Regular products
|
|
212
|
-
* const products = await
|
|
212
|
+
* const products = await fetchProducts({
|
|
213
213
|
* skus: ['product1', 'product2'],
|
|
214
214
|
* type: 'inapp'
|
|
215
215
|
* });
|
|
216
216
|
*
|
|
217
217
|
* // Subscriptions
|
|
218
|
-
* const subscriptions = await
|
|
218
|
+
* const subscriptions = await fetchProducts({
|
|
219
219
|
* skus: ['sub1', 'sub2'],
|
|
220
220
|
* type: 'subs'
|
|
221
221
|
* });
|
|
222
222
|
* ```
|
|
223
223
|
*/
|
|
224
|
-
export const
|
|
224
|
+
export const fetchProducts = async ({
|
|
225
225
|
skus,
|
|
226
226
|
type = 'inapp',
|
|
227
227
|
}: {
|
|
@@ -233,7 +233,7 @@ export const requestProducts = async ({
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
if (Platform.OS === 'ios') {
|
|
236
|
-
const rawItems = await ExpoIapModule.
|
|
236
|
+
const rawItems = await ExpoIapModule.fetchProducts(skus);
|
|
237
237
|
const filteredItems = rawItems.filter((item: unknown) => {
|
|
238
238
|
if (!isProductIOS(item)) return false;
|
|
239
239
|
return (
|
|
@@ -251,7 +251,7 @@ export const requestProducts = async ({
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
if (Platform.OS === 'android') {
|
|
254
|
-
const items = await ExpoIapModule.
|
|
254
|
+
const items = await ExpoIapModule.fetchProducts(type, skus);
|
|
255
255
|
const filteredItems = items.filter((item: unknown) => {
|
|
256
256
|
if (!isProductAndroid(item)) return false;
|
|
257
257
|
return (
|
|
@@ -271,6 +271,41 @@ export const requestProducts = async ({
|
|
|
271
271
|
throw new Error('Unsupported platform');
|
|
272
272
|
};
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* @deprecated Use `fetchProducts` instead. This method will be removed in version 3.0.0.
|
|
276
|
+
*
|
|
277
|
+
* The 'request' prefix should only be used for event-based operations that trigger
|
|
278
|
+
* purchase flows. Since this function simply fetches product information, it has been
|
|
279
|
+
* renamed to `fetchProducts` to follow OpenIAP terminology guidelines.
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```typescript
|
|
283
|
+
* // Old way (deprecated)
|
|
284
|
+
* const products = await requestProducts({
|
|
285
|
+
* skus: ['com.example.product1'],
|
|
286
|
+
* type: 'inapp'
|
|
287
|
+
* });
|
|
288
|
+
*
|
|
289
|
+
* // New way (recommended)
|
|
290
|
+
* const products = await fetchProducts({
|
|
291
|
+
* skus: ['com.example.product1'],
|
|
292
|
+
* type: 'inapp'
|
|
293
|
+
* });
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
export const requestProducts = async ({
|
|
297
|
+
skus,
|
|
298
|
+
type = 'inapp',
|
|
299
|
+
}: {
|
|
300
|
+
skus: string[];
|
|
301
|
+
type?: 'inapp' | 'subs';
|
|
302
|
+
}): Promise<Product[] | SubscriptionProduct[]> => {
|
|
303
|
+
console.warn(
|
|
304
|
+
"`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.",
|
|
305
|
+
);
|
|
306
|
+
return fetchProducts({skus, type});
|
|
307
|
+
};
|
|
308
|
+
|
|
274
309
|
/**
|
|
275
310
|
* @deprecated Use `getPurchaseHistories` instead. This function will be removed in version 3.0.0.
|
|
276
311
|
*/
|
package/src/modules/ios.ts
CHANGED
|
@@ -168,15 +168,14 @@ export const beginRefundRequestIOS = (
|
|
|
168
168
|
|
|
169
169
|
/**
|
|
170
170
|
* Shows the system UI for managing subscriptions.
|
|
171
|
-
*
|
|
172
|
-
* purchaseUpdatedListener and transactionUpdatedIOS listeners.
|
|
171
|
+
* Returns an array of subscriptions that had status changes after the UI is closed.
|
|
173
172
|
*
|
|
174
|
-
* @returns Promise
|
|
173
|
+
* @returns Promise<Purchase[]> - Array of subscriptions with status changes (e.g., auto-renewal toggled)
|
|
175
174
|
* @throws Error if called on non-iOS platform
|
|
176
175
|
*
|
|
177
176
|
* @platform iOS
|
|
178
177
|
*/
|
|
179
|
-
export const showManageSubscriptionsIOS = (): Promise<
|
|
178
|
+
export const showManageSubscriptionsIOS = (): Promise<Purchase[]> => {
|
|
180
179
|
return ExpoIapModule.showManageSubscriptionsIOS();
|
|
181
180
|
};
|
|
182
181
|
|
|
@@ -415,7 +414,7 @@ export const beginRefundRequest = (
|
|
|
415
414
|
/**
|
|
416
415
|
* @deprecated Use `showManageSubscriptionsIOS` instead. This function will be removed in version 3.0.0.
|
|
417
416
|
*/
|
|
418
|
-
export const showManageSubscriptions = (): Promise<
|
|
417
|
+
export const showManageSubscriptions = (): Promise<Purchase[]> => {
|
|
419
418
|
console.warn(
|
|
420
419
|
'`showManageSubscriptions` is deprecated. Use `showManageSubscriptionsIOS` instead. This function will be removed in version 3.0.0.',
|
|
421
420
|
);
|