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.
@@ -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
- // Add a flag to track initialization state
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
- for await verification in onlyIncludeActiveItemsIOS
492
- ? Transaction.currentEntitlements : Transaction.all
493
- {
494
- do {
495
- let transaction = try self.checkVerified(verification)
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
- case .nonRenewable:
508
- if await self.productStore?.getProduct(productID: transaction.productID)
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
- } catch {
533
- let err = [
534
- "responseCode": IapErrorCode.unknown,
535
- "debugMessage": error.localizedDescription,
536
- "code": IapErrorCode.unknown,
537
- "message": error.localizedDescription,
538
- "productId": "unknown",
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") { () -> Bool in
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
- // Get all subscription products before showing the management UI
816
+
817
+ var beforeStatuses: [String: Bool] = [:]
853
818
  let subscriptionSkus = await self.getAllSubscriptionProductIds()
854
- self.pollingSkus = Set(subscriptionSkus)
855
- // Show the management UI
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
- // Start polling for status changes
858
- self.pollForSubscriptionStatusChanges()
859
- return true
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.6",
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 `requestProducts({ skus, type: 'inapp' })` instead. This function will be removed in version 3.0.0.",
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.requestProducts(skus);
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.requestProducts('inapp', skus);
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 `requestProducts({ skus, type: 'subs' })` instead. This function will be removed in version 3.0.0.",
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.requestProducts(skus);
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.requestProducts('subs', skus);
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
- * Request products with unified API (v2.7.0+)
203
+ * Fetch products with unified API (v2.7.0+)
204
204
  *
205
- * @param params - Product request configuration
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 requestProducts({
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 requestProducts({
218
+ * const subscriptions = await fetchProducts({
219
219
  * skus: ['sub1', 'sub2'],
220
220
  * type: 'subs'
221
221
  * });
222
222
  * ```
223
223
  */
224
- export const requestProducts = async ({
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.requestProducts(skus);
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.requestProducts(type, skus);
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
  */
@@ -168,15 +168,14 @@ export const beginRefundRequestIOS = (
168
168
 
169
169
  /**
170
170
  * Shows the system UI for managing subscriptions.
171
- * When the user changes subscription renewal status, the system will emit events to
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 resolving to null on success
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<null> => {
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<null> => {
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
  );