expo-iap 3.0.2-rc.1 → 3.0.3

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +4 -6
  3. package/build/ExpoIap.types.d.ts +49 -36
  4. package/build/ExpoIap.types.d.ts.map +1 -1
  5. package/build/ExpoIap.types.js +79 -70
  6. package/build/ExpoIap.types.js.map +1 -1
  7. package/build/index.d.ts +2 -2
  8. package/build/index.js +2 -2
  9. package/build/index.js.map +1 -1
  10. package/build/types/ExpoIapAndroid.types.d.ts +2 -3
  11. package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
  12. package/build/types/ExpoIapAndroid.types.js.map +1 -1
  13. package/build/types/ExpoIapIOS.types.d.ts +7 -1
  14. package/build/types/ExpoIapIOS.types.d.ts.map +1 -1
  15. package/build/types/ExpoIapIOS.types.js +7 -1
  16. package/build/types/ExpoIapIOS.types.js.map +1 -1
  17. package/build/useIAP.d.ts +2 -2
  18. package/build/useIAP.d.ts.map +1 -1
  19. package/build/useIAP.js +13 -2
  20. package/build/useIAP.js.map +1 -1
  21. package/build/utils/errorMapping.d.ts +10 -4
  22. package/build/utils/errorMapping.d.ts.map +1 -1
  23. package/build/utils/errorMapping.js +54 -46
  24. package/build/utils/errorMapping.js.map +1 -1
  25. package/bun.lock +340 -137
  26. package/ios/ExpoIapModule.swift +24 -11
  27. package/package.json +1 -1
  28. package/plugin/src/withIAP.ts +5 -1
  29. package/plugin/src/withLocalOpenIAP.ts +3 -1
  30. package/plugin/tsconfig.tsbuildinfo +1 -1
  31. package/src/ExpoIap.types.ts +89 -74
  32. package/src/index.ts +6 -6
  33. package/src/types/ExpoIapAndroid.types.ts +2 -5
  34. package/src/types/ExpoIapIOS.types.ts +8 -3
  35. package/src/useIAP.ts +21 -10
  36. package/src/utils/errorMapping.ts +67 -54
  37. package/coverage/clover.xml +0 -358
  38. package/coverage/coverage-final.json +0 -7
  39. package/coverage/lcov-report/base.css +0 -224
  40. package/coverage/lcov-report/block-navigation.js +0 -87
  41. package/coverage/lcov-report/favicon.png +0 -0
  42. package/coverage/lcov-report/index.html +0 -176
  43. package/coverage/lcov-report/prettify.css +0 -1
  44. package/coverage/lcov-report/prettify.js +0 -2
  45. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  46. package/coverage/lcov-report/sorter.js +0 -196
  47. package/coverage/lcov-report/src/ExpoIap.types.ts.html +0 -1396
  48. package/coverage/lcov-report/src/helpers/index.html +0 -116
  49. package/coverage/lcov-report/src/helpers/subscription.ts.html +0 -532
  50. package/coverage/lcov-report/src/index.html +0 -116
  51. package/coverage/lcov-report/src/index.ts.html +0 -1945
  52. package/coverage/lcov-report/src/modules/android.ts.html +0 -496
  53. package/coverage/lcov-report/src/modules/index.html +0 -131
  54. package/coverage/lcov-report/src/modules/ios.ts.html +0 -1012
  55. package/coverage/lcov-report/src/types/ExpoIapAndroid.types.ts.html +0 -502
  56. package/coverage/lcov-report/src/types/index.html +0 -116
  57. package/coverage/lcov-report/src/useIAP.ts.html +0 -1654
  58. package/coverage/lcov-report/src/utils/constants.ts.html +0 -127
  59. package/coverage/lcov-report/src/utils/errorMapping.ts.html +0 -427
  60. package/coverage/lcov-report/src/utils/index.html +0 -116
  61. package/coverage/lcov.info +0 -685
  62. package/ios/expoiap.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  63. package/ios/expoiap.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
@@ -12,6 +12,19 @@ private func logDebug(_ message: String) {
12
12
  #endif
13
13
  }
14
14
 
15
+ // MARK: - Swift helpers for optional dictionary compaction
16
+ private extension Sequence where Element == [String: Any?] {
17
+ func compactingValues() -> [[String: Any]] {
18
+ return self.map { $0.compactMapValues { $0 } }
19
+ }
20
+ }
21
+
22
+ private extension Dictionary where Key == String, Value == Any? {
23
+ func compactingValues() -> [String: Any] {
24
+ return self.compactMapValues { $0 }
25
+ }
26
+ }
27
+
15
28
  // Event names
16
29
  struct OpenIapEvent {
17
30
  static let PurchaseUpdated = "purchase-updated"
@@ -145,7 +158,7 @@ public class ExpoIapModule: Module {
145
158
  logDebug("Product: \(product.id) - \(product.title) - \(product.displayPrice)")
146
159
  }
147
160
  // Ensure non-optional values for Expo bridge
148
- return OpenIapSerialization.products(products).map { $0.compactMapValues { $0 } }
161
+ return OpenIapSerialization.products(products).compactingValues()
149
162
  }
150
163
 
151
164
  // MARK: - Purchase Operations
@@ -233,7 +246,7 @@ public class ExpoIapModule: Module {
233
246
  )
234
247
  }
235
248
  let purchases = try await OpenIapModule.shared.getAvailablePurchases(purchaseOptions)
236
- return OpenIapSerialization.purchases(purchases).map { $0.compactMapValues { $0 } }
249
+ return OpenIapSerialization.purchases(purchases).compactingValues()
237
250
  }
238
251
 
239
252
  // Legacy function for backward compatibility
@@ -246,7 +259,7 @@ public class ExpoIapModule: Module {
246
259
  onlyIncludeActiveItemsIOS: onlyIncludeActiveItems
247
260
  )
248
261
  let purchases = try await OpenIapModule.shared.getAvailablePurchases(purchaseOptions)
249
- return OpenIapSerialization.purchases(purchases).map { $0.compactMapValues { $0 } }
262
+ return OpenIapSerialization.purchases(purchases).compactingValues()
250
263
  }
251
264
 
252
265
  AsyncFunction("getPendingTransactionsIOS") { () async throws -> [[String: Any]] in
@@ -254,7 +267,7 @@ public class ExpoIapModule: Module {
254
267
  logDebug("getPendingTransactionsIOS called")
255
268
 
256
269
  let pendingTransactions = try await OpenIapModule.shared.getPendingTransactionsIOS()
257
- return OpenIapSerialization.purchases(pendingTransactions).map { $0.compactMapValues { $0 } }
270
+ return OpenIapSerialization.purchases(pendingTransactions).compactingValues()
258
271
  }
259
272
 
260
273
  AsyncFunction("clearTransactionIOS") { () async throws -> Bool in
@@ -299,9 +312,9 @@ public class ExpoIapModule: Module {
299
312
  "jwsRepresentation": result.jwsRepresentation,
300
313
  // Populate unified purchaseToken for iOS as alias of JWS
301
314
  "purchaseToken": result.jwsRepresentation,
302
- "latestTransaction": result.latestTransaction.map { OpenIapSerialization.purchase($0).compactMapValues { $0 } },
315
+ "latestTransaction": result.latestTransaction.map { OpenIapSerialization.purchase($0).compactingValues() },
303
316
  ]
304
- return dict.compactMapValues { $0 }
317
+ return dict.compactingValues()
305
318
  } catch {
306
319
  throw OpenIapError.make(code: OpenIapError.E_RECEIPT_FAILED)
307
320
  }
@@ -321,7 +334,7 @@ public class ExpoIapModule: Module {
321
334
  logDebug("showManageSubscriptionsIOS called")
322
335
  // OpenIAP 1.1.9 returns already-serialized dictionaries here.
323
336
  let purchases = try await OpenIapModule.shared.showManageSubscriptionsIOS()
324
- return purchases.map { $0.compactMapValues { $0 } }
337
+ return purchases.compactingValues()
325
338
  }
326
339
 
327
340
  AsyncFunction("deepLinkToSubscriptionsIOS") { () async throws in
@@ -350,7 +363,7 @@ public class ExpoIapModule: Module {
350
363
  // Fetch full product info by SKU to conform to OpenIapProduct
351
364
  let request = OpenIapProductRequest(skus: [promoted.productIdentifier], type: .all)
352
365
  let products = try await OpenIapModule.shared.fetchProducts(request)
353
- let serialized = OpenIapSerialization.products(products).map { $0.compactMapValues { $0 } }
366
+ let serialized = OpenIapSerialization.products(products).compactingValues()
354
367
  return serialized.first
355
368
  }
356
369
  return nil
@@ -408,7 +421,7 @@ public class ExpoIapModule: Module {
408
421
  dict["renewalInfo"] = renewalInfo
409
422
  }
410
423
 
411
- return dict.compactMapValues { $0 }
424
+ return dict.compactingValues()
412
425
  }
413
426
  }
414
427
  return nil
@@ -419,7 +432,7 @@ public class ExpoIapModule: Module {
419
432
  logDebug("currentEntitlementIOS called for sku: \(sku)")
420
433
  do {
421
434
  if let entitlement = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku) {
422
- return OpenIapSerialization.purchase(entitlement).compactMapValues { $0 }
435
+ return OpenIapSerialization.purchase(entitlement).compactingValues()
423
436
  }
424
437
  return nil
425
438
  } catch {
@@ -432,7 +445,7 @@ public class ExpoIapModule: Module {
432
445
  logDebug("latestTransactionIOS called for sku: \(sku)")
433
446
  do {
434
447
  if let transaction = try await OpenIapModule.shared.latestTransactionIOS(sku: sku) {
435
- return OpenIapSerialization.purchase(transaction).compactMapValues { $0 }
448
+ return OpenIapSerialization.purchase(transaction).compactingValues()
436
449
  }
437
450
  return nil
438
451
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-iap",
3
- "version": "3.0.2-rc.1",
3
+ "version": "3.0.3",
4
4
  "description": "In App Purchase module in Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -66,7 +66,11 @@ const modifyAppBuildGradle = (
66
66
  }
67
67
 
68
68
  // Ensure the desired dependency line is present
69
- if (!new RegExp(String.raw`io\.github\.hyochan\.openiap:openiap-google:1\.1\.0`).test(modified)) {
69
+ if (
70
+ !new RegExp(
71
+ String.raw`io\.github\.hyochan\.openiap:openiap-google:1\.1\.0`,
72
+ ).test(modified)
73
+ ) {
70
74
  // Insert just after the opening `dependencies {` line
71
75
  modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep, 1);
72
76
  logOnce(
@@ -213,7 +213,9 @@ const withLocalOpenIAP: ConfigPlugin<{localPath?: LocalPathOption} | void> = (
213
213
  }
214
214
  if (removedAny) {
215
215
  gradle.contents = contents;
216
- console.log('🧹 Removed Maven openiap-google to use local :openiap-google');
216
+ console.log(
217
+ '🧹 Removed Maven openiap-google to use local :openiap-google',
218
+ );
217
219
  }
218
220
  if (!gradle.contents.includes(dependencyLine)) {
219
221
  const anchor = /dependencies\s*\{/m;
@@ -1 +1 @@
1
- {"root":["./src/withiap.ts","./src/withlocalopeniap.ts"],"version":"5.9.2"}
1
+ {"root":["./src/withIAP.ts","./src/withLocalOpenIAP.ts"],"version":"5.9.2"}
@@ -16,6 +16,15 @@ export type ChangeEventPayload = {
16
16
 
17
17
  export type ProductType = 'inapp' | 'subs';
18
18
 
19
+ export enum PurchaseState {
20
+ Pending = 'pending',
21
+ Purchased = 'purchased',
22
+ Failed = 'failed',
23
+ Restored = 'restored',
24
+ Deferred = 'deferred',
25
+ Unknown = 'unknown',
26
+ }
27
+
19
28
  // =============================================================================
20
29
  // COMMON TYPES (Base types shared across all platforms)
21
30
  // =============================================================================
@@ -37,10 +46,13 @@ export type PurchaseCommon = {
37
46
  id: string; // Transaction identifier - used by finishTransaction
38
47
  productId: string; // Product identifier - which product was purchased
39
48
  ids?: string[]; // Product identifiers for purchases that include multiple products
49
+ transactionId?: string; // Legacy identifier
40
50
  transactionDate: number;
41
- transactionReceipt: string;
42
- purchaseToken?: string; // Unified purchase token (jwsRepresentation for iOS, purchaseToken for Android)
51
+ purchaseToken?: string; // Unified token (iOS: JWS, Android: purchaseToken)
43
52
  platform?: string;
53
+ quantity?: number;
54
+ purchaseState?: PurchaseState;
55
+ isAutoRenewing?: boolean;
44
56
  };
45
57
 
46
58
  export type ProductSubscriptionCommon = ProductCommon & {
@@ -56,10 +68,13 @@ export type Product =
56
68
  | (ProductAndroid & AndroidPlatform)
57
69
  | (ProductIOS & IosPlatform);
58
70
 
59
- export type SubscriptionProduct =
71
+ export type ProductSubscription =
60
72
  | (ProductSubscriptionAndroid & AndroidPlatform)
61
73
  | (ProductSubscriptionIOS & IosPlatform);
62
74
 
75
+ // Legacy naming retained for backward compatibility
76
+ export type SubscriptionProduct = ProductSubscription;
77
+
63
78
  // Re-export all platform-specific types to avoid deep imports
64
79
  export * from './types/ExpoIapAndroid.types';
65
80
  export * from './types/ExpoIapIOS.types';
@@ -81,41 +96,41 @@ export type PurchaseResult = {
81
96
  * These are mapped to platform-specific error codes and provide consistent error handling
82
97
  */
83
98
  export enum ErrorCode {
84
- E_UNKNOWN = 'E_UNKNOWN',
85
- E_USER_CANCELLED = 'E_USER_CANCELLED',
86
- E_USER_ERROR = 'E_USER_ERROR',
87
- E_ITEM_UNAVAILABLE = 'E_ITEM_UNAVAILABLE',
88
- E_REMOTE_ERROR = 'E_REMOTE_ERROR',
89
- E_NETWORK_ERROR = 'E_NETWORK_ERROR',
90
- E_SERVICE_ERROR = 'E_SERVICE_ERROR',
91
- E_RECEIPT_FAILED = 'E_RECEIPT_FAILED',
92
- E_RECEIPT_FINISHED = 'E_RECEIPT_FINISHED',
93
- E_RECEIPT_FINISHED_FAILED = 'E_RECEIPT_FINISHED_FAILED',
94
- E_NOT_PREPARED = 'E_NOT_PREPARED',
95
- E_NOT_ENDED = 'E_NOT_ENDED',
96
- E_ALREADY_OWNED = 'E_ALREADY_OWNED',
97
- E_DEVELOPER_ERROR = 'E_DEVELOPER_ERROR',
98
- E_BILLING_RESPONSE_JSON_PARSE_ERROR = 'E_BILLING_RESPONSE_JSON_PARSE_ERROR',
99
- E_DEFERRED_PAYMENT = 'E_DEFERRED_PAYMENT',
100
- E_INTERRUPTED = 'E_INTERRUPTED',
101
- E_IAP_NOT_AVAILABLE = 'E_IAP_NOT_AVAILABLE',
102
- E_PURCHASE_ERROR = 'E_PURCHASE_ERROR',
103
- E_SYNC_ERROR = 'E_SYNC_ERROR',
104
- E_TRANSACTION_VALIDATION_FAILED = 'E_TRANSACTION_VALIDATION_FAILED',
105
- E_ACTIVITY_UNAVAILABLE = 'E_ACTIVITY_UNAVAILABLE',
106
- E_ALREADY_PREPARED = 'E_ALREADY_PREPARED',
107
- E_PENDING = 'E_PENDING',
108
- E_CONNECTION_CLOSED = 'E_CONNECTION_CLOSED',
99
+ Unknown = 'E_UNKNOWN',
100
+ UserCancelled = 'E_USER_CANCELLED',
101
+ UserError = 'E_USER_ERROR',
102
+ ItemUnavailable = 'E_ITEM_UNAVAILABLE',
103
+ RemoteError = 'E_REMOTE_ERROR',
104
+ NetworkError = 'E_NETWORK_ERROR',
105
+ ServiceError = 'E_SERVICE_ERROR',
106
+ ReceiptFailed = 'E_RECEIPT_FAILED',
107
+ ReceiptFinished = 'E_RECEIPT_FINISHED',
108
+ ReceiptFinishedFailed = 'E_RECEIPT_FINISHED_FAILED',
109
+ NotPrepared = 'E_NOT_PREPARED',
110
+ NotEnded = 'E_NOT_ENDED',
111
+ AlreadyOwned = 'E_ALREADY_OWNED',
112
+ DeveloperError = 'E_DEVELOPER_ERROR',
113
+ BillingResponseJsonParseError = 'E_BILLING_RESPONSE_JSON_PARSE_ERROR',
114
+ DeferredPayment = 'E_DEFERRED_PAYMENT',
115
+ Interrupted = 'E_INTERRUPTED',
116
+ IapNotAvailable = 'E_IAP_NOT_AVAILABLE',
117
+ PurchaseError = 'E_PURCHASE_ERROR',
118
+ SyncError = 'E_SYNC_ERROR',
119
+ TransactionValidationFailed = 'E_TRANSACTION_VALIDATION_FAILED',
120
+ ActivityUnavailable = 'E_ACTIVITY_UNAVAILABLE',
121
+ AlreadyPrepared = 'E_ALREADY_PREPARED',
122
+ Pending = 'E_PENDING',
123
+ ConnectionClosed = 'E_CONNECTION_CLOSED',
109
124
  // Additional detailed errors (Android-focused, kept cross-platform)
110
- E_INIT_CONNECTION = 'E_INIT_CONNECTION',
111
- E_SERVICE_DISCONNECTED = 'E_SERVICE_DISCONNECTED',
112
- E_QUERY_PRODUCT = 'E_QUERY_PRODUCT',
113
- E_SKU_NOT_FOUND = 'E_SKU_NOT_FOUND',
114
- E_SKU_OFFER_MISMATCH = 'E_SKU_OFFER_MISMATCH',
115
- E_ITEM_NOT_OWNED = 'E_ITEM_NOT_OWNED',
116
- E_BILLING_UNAVAILABLE = 'E_BILLING_UNAVAILABLE',
117
- E_FEATURE_NOT_SUPPORTED = 'E_FEATURE_NOT_SUPPORTED',
118
- E_EMPTY_SKU_LIST = 'E_EMPTY_SKU_LIST',
125
+ InitConnection = 'E_INIT_CONNECTION',
126
+ ServiceDisconnected = 'E_SERVICE_DISCONNECTED',
127
+ QueryProduct = 'E_QUERY_PRODUCT',
128
+ SkuNotFound = 'E_SKU_NOT_FOUND',
129
+ SkuOfferMismatch = 'E_SKU_OFFER_MISMATCH',
130
+ ItemNotOwned = 'E_ITEM_NOT_OWNED',
131
+ BillingUnavailable = 'E_BILLING_UNAVAILABLE',
132
+ FeatureNotSupported = 'E_FEATURE_NOT_SUPPORTED',
133
+ EmptySkuList = 'E_EMPTY_SKU_LIST',
119
134
  }
120
135
 
121
136
  // Fast lookup set for validating standardized error code strings
@@ -129,42 +144,41 @@ const OPENIAP_ERROR_CODE_SET: Set<string> = new Set(
129
144
  */
130
145
  // Shared OpenIAP string code mapping for both platforms
131
146
  const COMMON_ERROR_CODE_MAP = {
132
- [ErrorCode.E_UNKNOWN]: 'E_UNKNOWN',
133
- [ErrorCode.E_USER_CANCELLED]: 'E_USER_CANCELLED',
134
- [ErrorCode.E_USER_ERROR]: 'E_USER_ERROR',
135
- [ErrorCode.E_ITEM_UNAVAILABLE]: 'E_ITEM_UNAVAILABLE',
136
- [ErrorCode.E_REMOTE_ERROR]: 'E_REMOTE_ERROR',
137
- [ErrorCode.E_NETWORK_ERROR]: 'E_NETWORK_ERROR',
138
- [ErrorCode.E_SERVICE_ERROR]: 'E_SERVICE_ERROR',
139
- [ErrorCode.E_RECEIPT_FAILED]: 'E_RECEIPT_FAILED',
140
- [ErrorCode.E_RECEIPT_FINISHED]: 'E_RECEIPT_FINISHED',
141
- [ErrorCode.E_RECEIPT_FINISHED_FAILED]: 'E_RECEIPT_FINISHED_FAILED',
142
- [ErrorCode.E_NOT_PREPARED]: 'E_NOT_PREPARED',
143
- [ErrorCode.E_NOT_ENDED]: 'E_NOT_ENDED',
144
- [ErrorCode.E_ALREADY_OWNED]: 'E_ALREADY_OWNED',
145
- [ErrorCode.E_DEVELOPER_ERROR]: 'E_DEVELOPER_ERROR',
146
- [ErrorCode.E_BILLING_RESPONSE_JSON_PARSE_ERROR]:
147
+ [ErrorCode.Unknown]: 'E_UNKNOWN',
148
+ [ErrorCode.UserCancelled]: 'E_USER_CANCELLED',
149
+ [ErrorCode.UserError]: 'E_USER_ERROR',
150
+ [ErrorCode.ItemUnavailable]: 'E_ITEM_UNAVAILABLE',
151
+ [ErrorCode.RemoteError]: 'E_REMOTE_ERROR',
152
+ [ErrorCode.NetworkError]: 'E_NETWORK_ERROR',
153
+ [ErrorCode.ServiceError]: 'E_SERVICE_ERROR',
154
+ [ErrorCode.ReceiptFailed]: 'E_RECEIPT_FAILED',
155
+ [ErrorCode.ReceiptFinished]: 'E_RECEIPT_FINISHED',
156
+ [ErrorCode.ReceiptFinishedFailed]: 'E_RECEIPT_FINISHED_FAILED',
157
+ [ErrorCode.NotPrepared]: 'E_NOT_PREPARED',
158
+ [ErrorCode.NotEnded]: 'E_NOT_ENDED',
159
+ [ErrorCode.AlreadyOwned]: 'E_ALREADY_OWNED',
160
+ [ErrorCode.DeveloperError]: 'E_DEVELOPER_ERROR',
161
+ [ErrorCode.BillingResponseJsonParseError]:
147
162
  'E_BILLING_RESPONSE_JSON_PARSE_ERROR',
148
- [ErrorCode.E_DEFERRED_PAYMENT]: 'E_DEFERRED_PAYMENT',
149
- [ErrorCode.E_INTERRUPTED]: 'E_INTERRUPTED',
150
- [ErrorCode.E_IAP_NOT_AVAILABLE]: 'E_IAP_NOT_AVAILABLE',
151
- [ErrorCode.E_PURCHASE_ERROR]: 'E_PURCHASE_ERROR',
152
- [ErrorCode.E_SYNC_ERROR]: 'E_SYNC_ERROR',
153
- [ErrorCode.E_TRANSACTION_VALIDATION_FAILED]:
154
- 'E_TRANSACTION_VALIDATION_FAILED',
155
- [ErrorCode.E_ACTIVITY_UNAVAILABLE]: 'E_ACTIVITY_UNAVAILABLE',
156
- [ErrorCode.E_ALREADY_PREPARED]: 'E_ALREADY_PREPARED',
157
- [ErrorCode.E_PENDING]: 'E_PENDING',
158
- [ErrorCode.E_CONNECTION_CLOSED]: 'E_CONNECTION_CLOSED',
159
- [ErrorCode.E_INIT_CONNECTION]: 'E_INIT_CONNECTION',
160
- [ErrorCode.E_SERVICE_DISCONNECTED]: 'E_SERVICE_DISCONNECTED',
161
- [ErrorCode.E_QUERY_PRODUCT]: 'E_QUERY_PRODUCT',
162
- [ErrorCode.E_SKU_NOT_FOUND]: 'E_SKU_NOT_FOUND',
163
- [ErrorCode.E_SKU_OFFER_MISMATCH]: 'E_SKU_OFFER_MISMATCH',
164
- [ErrorCode.E_ITEM_NOT_OWNED]: 'E_ITEM_NOT_OWNED',
165
- [ErrorCode.E_BILLING_UNAVAILABLE]: 'E_BILLING_UNAVAILABLE',
166
- [ErrorCode.E_FEATURE_NOT_SUPPORTED]: 'E_FEATURE_NOT_SUPPORTED',
167
- [ErrorCode.E_EMPTY_SKU_LIST]: 'E_EMPTY_SKU_LIST',
163
+ [ErrorCode.DeferredPayment]: 'E_DEFERRED_PAYMENT',
164
+ [ErrorCode.Interrupted]: 'E_INTERRUPTED',
165
+ [ErrorCode.IapNotAvailable]: 'E_IAP_NOT_AVAILABLE',
166
+ [ErrorCode.PurchaseError]: 'E_PURCHASE_ERROR',
167
+ [ErrorCode.SyncError]: 'E_SYNC_ERROR',
168
+ [ErrorCode.TransactionValidationFailed]: 'E_TRANSACTION_VALIDATION_FAILED',
169
+ [ErrorCode.ActivityUnavailable]: 'E_ACTIVITY_UNAVAILABLE',
170
+ [ErrorCode.AlreadyPrepared]: 'E_ALREADY_PREPARED',
171
+ [ErrorCode.Pending]: 'E_PENDING',
172
+ [ErrorCode.ConnectionClosed]: 'E_CONNECTION_CLOSED',
173
+ [ErrorCode.InitConnection]: 'E_INIT_CONNECTION',
174
+ [ErrorCode.ServiceDisconnected]: 'E_SERVICE_DISCONNECTED',
175
+ [ErrorCode.QueryProduct]: 'E_QUERY_PRODUCT',
176
+ [ErrorCode.SkuNotFound]: 'E_SKU_NOT_FOUND',
177
+ [ErrorCode.SkuOfferMismatch]: 'E_SKU_OFFER_MISMATCH',
178
+ [ErrorCode.ItemNotOwned]: 'E_ITEM_NOT_OWNED',
179
+ [ErrorCode.BillingUnavailable]: 'E_BILLING_UNAVAILABLE',
180
+ [ErrorCode.FeatureNotSupported]: 'E_FEATURE_NOT_SUPPORTED',
181
+ [ErrorCode.EmptySkuList]: 'E_EMPTY_SKU_LIST',
168
182
  } as const;
169
183
 
170
184
  export const ErrorCodeMapping = {
@@ -229,7 +243,7 @@ export class PurchaseError implements Error {
229
243
  ): PurchaseError {
230
244
  const errorCode = errorData.code
231
245
  ? ErrorCodeUtils.fromPlatformCode(errorData.code, platform)
232
- : ErrorCode.E_UNKNOWN;
246
+ : ErrorCode.Unknown;
233
247
 
234
248
  return new PurchaseError({
235
249
  message: errorData.message || 'Unknown error occurred',
@@ -299,7 +313,7 @@ export const ErrorCodeUtils = {
299
313
  }
300
314
  }
301
315
 
302
- return ErrorCode.E_UNKNOWN;
316
+ return ErrorCode.Unknown;
303
317
  },
304
318
 
305
319
  /**
@@ -392,6 +406,7 @@ export interface RequestPurchaseAndroidProps {
392
406
  */
393
407
  export interface RequestSubscriptionAndroidProps
394
408
  extends RequestPurchaseAndroidProps {
409
+ readonly purchaseTokenAndroid?: string;
395
410
  readonly replacementModeAndroid?: number;
396
411
  readonly subscriptionOffers: {
397
412
  sku: string;
package/src/index.ts CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  PurchaseResult,
26
26
  RequestSubscriptionProps,
27
27
  RequestPurchaseProps,
28
- SubscriptionProduct,
28
+ ProductSubscription,
29
29
  // Bring platform types from the barrel to avoid deep imports
30
30
  PurchaseAndroid,
31
31
  PaymentDiscount,
@@ -169,11 +169,11 @@ export const fetchProducts = async ({
169
169
  }: {
170
170
  skus: string[];
171
171
  type?: 'inapp' | 'subs';
172
- }): Promise<Product[] | SubscriptionProduct[]> => {
172
+ }): Promise<Product[] | ProductSubscription[]> => {
173
173
  if (!skus?.length) {
174
174
  throw new PurchaseError({
175
175
  message: 'No SKUs provided',
176
- code: ErrorCode.E_EMPTY_SKU_LIST,
176
+ code: ErrorCode.EmptySkuList,
177
177
  });
178
178
  }
179
179
 
@@ -195,7 +195,7 @@ export const fetchProducts = async ({
195
195
 
196
196
  return type === 'inapp'
197
197
  ? (filteredItems as Product[])
198
- : (filteredItems as SubscriptionProduct[]);
198
+ : (filteredItems as ProductSubscription[]);
199
199
  }
200
200
 
201
201
  if (Platform.OS === 'android') {
@@ -213,7 +213,7 @@ export const fetchProducts = async ({
213
213
 
214
214
  return type === 'inapp'
215
215
  ? (filteredItems as Product[])
216
- : (filteredItems as SubscriptionProduct[]);
216
+ : (filteredItems as ProductSubscription[]);
217
217
  }
218
218
 
219
219
  throw new Error('Unsupported platform');
@@ -466,7 +466,7 @@ export const finishTransaction = ({
466
466
  return Promise.reject(
467
467
  new PurchaseError({
468
468
  message: 'Purchase token is required to finish transaction',
469
- code: 'E_DEVELOPER_ERROR' as ErrorCode,
469
+ code: ErrorCode.DeveloperError,
470
470
  productId: androidPurchase.productId,
471
471
  platform: 'android',
472
472
  }),
@@ -22,7 +22,7 @@ type PricingPhasesAndroid = {
22
22
 
23
23
  type ProductSubscriptionAndroidOfferDetail = {
24
24
  basePlanId: string;
25
- offerId: string | null;
25
+ offerId?: string | null;
26
26
  offerToken: string;
27
27
  offerTags: string[];
28
28
  pricingPhases: PricingPhasesAndroid;
@@ -37,7 +37,7 @@ export type ProductAndroid = ProductCommon & {
37
37
 
38
38
  type ProductSubscriptionAndroidOfferDetails = {
39
39
  basePlanId: string;
40
- offerId: string | null;
40
+ offerId?: string | null;
41
41
  offerToken: string;
42
42
  pricingPhases: PricingPhasesAndroid;
43
43
  offerTags: string[];
@@ -47,9 +47,6 @@ export type ProductSubscriptionAndroid = ProductAndroid & {
47
47
  subscriptionOfferDetailsAndroid: ProductSubscriptionAndroidOfferDetails[];
48
48
  };
49
49
 
50
- // Legacy naming for backward compatibility
51
- export type SubscriptionProductAndroid = ProductSubscriptionAndroid;
52
-
53
50
  export type RequestPurchaseAndroidProps = {
54
51
  skus: string[];
55
52
  obfuscatedAccountIdAndroid?: string;
@@ -1,5 +1,12 @@
1
1
  import {PurchaseCommon, ProductCommon} from '../ExpoIap.types';
2
2
 
3
+ export enum ProductTypeIOS {
4
+ Consumable = 'consumable',
5
+ NonConsumable = 'nonConsumable',
6
+ AutoRenewableSubscription = 'autoRenewableSubscription',
7
+ NonRenewingSubscription = 'nonRenewingSubscription',
8
+ }
9
+
3
10
  type SubscriptionIosPeriod = 'DAY' | 'WEEK' | 'MONTH' | 'YEAR' | '';
4
11
  type PaymentMode = '' | 'FREETRIAL' | 'PAYASYOUGO' | 'PAYUPFRONT';
5
12
 
@@ -31,6 +38,7 @@ export type ProductIOS = ProductCommon & {
31
38
  isFamilyShareableIOS: boolean;
32
39
  jsonRepresentationIOS: string;
33
40
  platform: 'ios';
41
+ typeIOS: ProductTypeIOS;
34
42
  subscriptionInfoIOS?: SubscriptionInfo;
35
43
  introductoryPriceNumberOfPeriodsIOS?: string;
36
44
  introductoryPriceSubscriptionPeriodIOS?: SubscriptionIosPeriod;
@@ -58,9 +66,6 @@ export type ProductSubscriptionIOS = ProductIOS & {
58
66
  subscriptionPeriodUnitIOS?: SubscriptionIosPeriod;
59
67
  };
60
68
 
61
- // Legacy naming for backward compatibility
62
- export type SubscriptionProductIOS = ProductSubscriptionIOS;
63
-
64
69
  export type PaymentDiscount = {
65
70
  /**
66
71
  * A string used to uniquely identify a discount offer for a product.
package/src/useIAP.ts CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  Purchase,
32
32
  PurchaseError,
33
33
  PurchaseResult,
34
- SubscriptionProduct,
34
+ ProductSubscription,
35
35
  RequestPurchaseProps,
36
36
  RequestSubscriptionProps,
37
37
  ErrorCode,
@@ -42,12 +42,16 @@ import {
42
42
  isRecoverableError,
43
43
  } from './utils/errorMapping';
44
44
 
45
+ // Deduplicate purchase success events across re-mounts (dev StrictMode, nav returns)
46
+ // Keep minimal in-memory state; safe for subscriptions since renewals use new ids
47
+ const handledPurchaseIds = new Set<string>();
48
+
45
49
  type UseIap = {
46
50
  connected: boolean;
47
51
  products: Product[];
48
52
  promotedProductsIOS: Purchase[];
49
53
  promotedProductIdIOS?: string;
50
- subscriptions: SubscriptionProduct[];
54
+ subscriptions: ProductSubscription[];
51
55
  availablePurchases: Purchase[];
52
56
  currentPurchase?: Purchase;
53
57
  currentPurchaseError?: PurchaseError;
@@ -104,7 +108,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
104
108
  const [connected, setConnected] = useState<boolean>(false);
105
109
  const [products, setProducts] = useState<Product[]>([]);
106
110
  const [promotedProductsIOS] = useState<Purchase[]>([]);
107
- const [subscriptions, setSubscriptions] = useState<SubscriptionProduct[]>([]);
111
+ const [subscriptions, setSubscriptions] = useState<ProductSubscription[]>([]);
108
112
 
109
113
  const [availablePurchases, setAvailablePurchases] = useState<Purchase[]>([]);
110
114
  const [currentPurchase, setCurrentPurchase] = useState<Purchase>();
@@ -155,7 +159,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
155
159
  promotedProductIOS?: EventSubscription;
156
160
  }>({});
157
161
 
158
- const subscriptionsRefState = useRef<SubscriptionProduct[]>([]);
162
+ const subscriptionsRefState = useRef<ProductSubscription[]>([]);
159
163
 
160
164
  useEffect(() => {
161
165
  subscriptionsRefState.current = subscriptions;
@@ -176,7 +180,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
176
180
  setSubscriptions((prevSubscriptions) =>
177
181
  mergeWithDuplicateCheck(
178
182
  prevSubscriptions,
179
- result as SubscriptionProduct[],
183
+ result as ProductSubscription[],
180
184
  (subscription) => subscription.id,
181
185
  ),
182
186
  );
@@ -199,7 +203,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
199
203
  setSubscriptions((prevSubscriptions) =>
200
204
  mergeWithDuplicateCheck(
201
205
  prevSubscriptions,
202
- result as SubscriptionProduct[],
206
+ result as ProductSubscription[],
203
207
  (subscription) => subscription.id,
204
208
  ),
205
209
  );
@@ -355,6 +359,15 @@ export function useIAP(options?: UseIAPOptions): UseIap {
355
359
  subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(
356
360
  async (purchase: Purchase) => {
357
361
  console.log('[useIAP] Purchase success callback triggered:', purchase);
362
+
363
+ // Guard against duplicate emissions for the same transaction
364
+ const dedupeKey = purchase.id;
365
+ if (dedupeKey && handledPurchaseIds.has(dedupeKey)) {
366
+ console.log('[useIAP] Duplicate purchase event ignored:', dedupeKey);
367
+ return;
368
+ }
369
+ if (dedupeKey) handledPurchaseIds.add(dedupeKey);
370
+
358
371
  setCurrentPurchaseError(undefined);
359
372
  setCurrentPurchase(purchase);
360
373
 
@@ -371,10 +384,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
371
384
  // Register purchase error listener EARLY. Ignore init-related errors until connected.
372
385
  subscriptionsRef.current.purchaseError = purchaseErrorListener(
373
386
  (error: PurchaseError) => {
374
- if (
375
- !connectedRef.current &&
376
- error.code === ErrorCode.E_INIT_CONNECTION
377
- ) {
387
+ if (!connectedRef.current && error.code === ErrorCode.InitConnection) {
378
388
  return; // Ignore initialization error before connected
379
389
  }
380
390
  const friendly = getUserFriendlyErrorMessage(error);
@@ -440,6 +450,7 @@ export function useIAP(options?: UseIAPOptions): UseIap {
440
450
  currentSubscriptions.promotedProductIOS?.remove();
441
451
  endConnection();
442
452
  setConnected(false);
453
+ handledPurchaseIds.clear();
443
454
  };
444
455
  }, [initIapWithSubscriptions]);
445
456