react-native-iap 14.3.6 → 14.3.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.
Files changed (98) hide show
  1. package/README.md +1 -1
  2. package/android/build.gradle +1 -3
  3. package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +61 -11
  4. package/ios/HybridRnIap.swift +47 -12
  5. package/lib/module/hooks/useIAP.js +31 -21
  6. package/lib/module/hooks/useIAP.js.map +1 -1
  7. package/lib/module/index.js +629 -700
  8. package/lib/module/index.js.map +1 -1
  9. package/lib/module/types.js +12 -0
  10. package/lib/module/types.js.map +1 -1
  11. package/lib/module/utils/purchase.js +22 -0
  12. package/lib/module/utils/purchase.js.map +1 -0
  13. package/lib/module/utils.js +43 -0
  14. package/lib/module/utils.js.map +1 -0
  15. package/lib/typescript/plugin/src/withIAP.d.ts +1 -0
  16. package/lib/typescript/plugin/src/withIAP.d.ts.map +1 -1
  17. package/lib/typescript/src/hooks/useIAP.d.ts +4 -5
  18. package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
  19. package/lib/typescript/src/index.d.ts +57 -176
  20. package/lib/typescript/src/index.d.ts.map +1 -1
  21. package/lib/typescript/src/specs/RnIap.nitro.d.ts +113 -154
  22. package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
  23. package/lib/typescript/src/types.d.ts +99 -76
  24. package/lib/typescript/src/types.d.ts.map +1 -1
  25. package/lib/typescript/src/utils/purchase.d.ts +4 -0
  26. package/lib/typescript/src/utils/purchase.d.ts.map +1 -0
  27. package/lib/typescript/src/utils.d.ts +8 -0
  28. package/lib/typescript/src/utils.d.ts.map +1 -0
  29. package/nitrogen/generated/android/NitroIap+autolinking.cmake +1 -1
  30. package/nitrogen/generated/android/c++/{JNitroSubscriptionOffer.hpp → JAndroidSubscriptionOfferInput.hpp} +15 -15
  31. package/nitrogen/generated/android/c++/JFunc_void_NitroProduct.hpp +2 -0
  32. package/nitrogen/generated/android/c++/JFunc_void_NitroPurchase.hpp +4 -0
  33. package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +16 -16
  34. package/nitrogen/generated/android/c++/JHybridRnIapSpec.hpp +1 -1
  35. package/nitrogen/generated/android/c++/JNitroAvailablePurchasesAndroidOptions.hpp +6 -5
  36. package/nitrogen/generated/android/c++/JNitroAvailablePurchasesAndroidType.hpp +59 -0
  37. package/nitrogen/generated/android/c++/JNitroAvailablePurchasesOptions.hpp +2 -1
  38. package/nitrogen/generated/android/c++/JNitroProduct.hpp +22 -20
  39. package/nitrogen/generated/android/c++/JNitroPurchase.hpp +12 -8
  40. package/nitrogen/generated/android/c++/JNitroPurchaseRequest.hpp +2 -2
  41. package/nitrogen/generated/android/c++/JNitroReceiptValidationAndroidOptions.hpp +10 -10
  42. package/nitrogen/generated/android/c++/JNitroReceiptValidationResultAndroid.hpp +6 -6
  43. package/nitrogen/generated/android/c++/JNitroReceiptValidationResultIOS.hpp +4 -0
  44. package/nitrogen/generated/android/c++/JNitroRequestPurchaseAndroid.hpp +7 -7
  45. package/nitrogen/generated/android/c++/JRequestPurchaseResult.cpp +39 -0
  46. package/nitrogen/generated/android/c++/JRequestPurchaseResult.hpp +68 -53
  47. package/nitrogen/generated/android/c++/JVariant_NitroReceiptValidationResultIOS_NitroReceiptValidationResultAndroid.hpp +4 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/{NitroSubscriptionOffer.kt → AndroidSubscriptionOfferInput.kt} +5 -5
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/HybridRnIapSpec.kt +1 -1
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroAvailablePurchasesAndroidOptions.kt +1 -1
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroAvailablePurchasesAndroidType.kt +21 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroProduct.kt +11 -11
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchase.kt +2 -2
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroReceiptValidationAndroidOptions.kt +4 -4
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroReceiptValidationResultAndroid.kt +2 -2
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroRequestPurchaseAndroid.kt +1 -1
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/RequestPurchaseResult.kt +31 -13
  58. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.cpp +4 -4
  59. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +99 -64
  60. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +6 -6
  61. package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +10 -10
  62. package/nitrogen/generated/ios/swift/{NitroSubscriptionOffer.swift → AndroidSubscriptionOfferInput.swift} +13 -13
  63. package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_PurchaseAndroid__PurchaseIOS__std__vector_std__variant_PurchaseAndroid__PurchaseIOS____.swift +81 -0
  64. package/nitrogen/generated/ios/swift/HybridRnIapSpec.swift +1 -1
  65. package/nitrogen/generated/ios/swift/HybridRnIapSpec_cxx.swift +35 -7
  66. package/nitrogen/generated/ios/swift/NitroAvailablePurchasesAndroidOptions.swift +7 -14
  67. package/nitrogen/generated/ios/swift/NitroAvailablePurchasesAndroidType.swift +40 -0
  68. package/nitrogen/generated/ios/swift/NitroProduct.swift +72 -72
  69. package/nitrogen/generated/ios/swift/NitroPurchase.swift +8 -8
  70. package/nitrogen/generated/ios/swift/NitroReceiptValidationAndroidOptions.swift +21 -21
  71. package/nitrogen/generated/ios/swift/NitroReceiptValidationResultAndroid.swift +37 -11
  72. package/nitrogen/generated/ios/swift/NitroRequestPurchaseAndroid.swift +11 -11
  73. package/nitrogen/generated/ios/swift/RequestPurchaseResult.swift +8 -137
  74. package/nitrogen/generated/shared/c++/{NitroSubscriptionOffer.hpp → AndroidSubscriptionOfferInput.hpp} +15 -15
  75. package/nitrogen/generated/shared/c++/HybridRnIapSpec.hpp +9 -6
  76. package/nitrogen/generated/shared/c++/NitroAvailablePurchasesAndroidOptions.hpp +8 -7
  77. package/nitrogen/generated/shared/c++/NitroAvailablePurchasesAndroidType.hpp +76 -0
  78. package/nitrogen/generated/shared/c++/NitroProduct.hpp +24 -22
  79. package/nitrogen/generated/shared/c++/NitroPurchase.hpp +15 -10
  80. package/nitrogen/generated/shared/c++/NitroReceiptValidationAndroidOptions.hpp +10 -10
  81. package/nitrogen/generated/shared/c++/NitroReceiptValidationResultAndroid.hpp +9 -9
  82. package/nitrogen/generated/shared/c++/NitroRequestPurchaseAndroid.hpp +8 -8
  83. package/package.json +2 -2
  84. package/plugin/build/withIAP.d.ts +1 -0
  85. package/plugin/build/withIAP.js +8 -2
  86. package/plugin/src/withIAP.ts +13 -3
  87. package/src/hooks/useIAP.ts +63 -32
  88. package/src/index.ts +832 -796
  89. package/src/specs/RnIap.nitro.ts +131 -163
  90. package/src/types.ts +131 -85
  91. package/src/utils/purchase.ts +32 -0
  92. package/src/utils.ts +68 -0
  93. package/nitrogen/generated/android/c++/JVariant_PurchaseAndroid_PurchaseIOS.cpp +0 -26
  94. package/nitrogen/generated/android/c++/JVariant_PurchaseAndroid_PurchaseIOS.hpp +0 -80
  95. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_PurchaseAndroid_PurchaseIOS.kt +0 -42
  96. package/nitrogen/generated/ios/swift/Func_void_RequestPurchaseResult.swift +0 -47
  97. package/nitrogen/generated/ios/swift/Variant_PurchaseAndroid_PurchaseIOS.swift +0 -18
  98. package/nitrogen/generated/shared/c++/RequestPurchaseResult.hpp +0 -78
@@ -8,73 +8,29 @@ import { NitroModules } from 'react-native-nitro-modules';
8
8
 
9
9
  // Internal modules
10
10
 
11
- import { convertNitroProductToProduct, convertNitroPurchaseToPurchase, validateNitroProduct, validateNitroPurchase, convertNitroSubscriptionStatusToSubscriptionStatusIOS } from "./utils/type-bridge.js";
11
+ import { convertNitroProductToProduct, convertNitroPurchaseToPurchase, convertProductToProductSubscription, validateNitroProduct, validateNitroPurchase, convertNitroSubscriptionStatusToSubscriptionStatusIOS } from "./utils/type-bridge.js";
12
12
  import { parseErrorStringToJsonObj } from "./utils/error.js";
13
13
  import { normalizeErrorCodeFromNative } from "./utils/errorMapping.js";
14
+ import { getSuccessFromPurchaseVariant } from "./utils/purchase.js";
15
+ import { parseAppTransactionPayload } from "./utils.js";
14
16
 
15
17
  // Export all types
16
18
 
17
19
  export * from "./types.js";
18
20
  export * from "./utils/error.js";
19
21
  const LEGACY_INAPP_WARNING = "[react-native-iap] `type: 'inapp'` is deprecated and will be removed in v14.4.0. Use 'in-app' instead.";
20
- function toNitroProductType(type) {
21
- if (type === 'subs') {
22
- return 'subs';
23
- }
24
- if (type === 'inapp') {
25
- console.warn(LEGACY_INAPP_WARNING);
26
- return 'inapp';
27
- }
28
- if (type === 'all') {
29
- return 'inapp';
30
- }
31
- return 'inapp';
32
- }
33
- function isSubscriptionQuery(type) {
34
- return type === 'subs';
35
- }
36
- function normalizeProductQueryType(type) {
37
- if (type === 'all' || type === 'subs' || type === 'in-app') {
38
- return type;
22
+ const toErrorMessage = error => {
23
+ if (typeof error === 'object' && error !== null && 'message' in error && error.message != null) {
24
+ return String(error.message);
39
25
  }
40
- if (typeof type === 'string') {
41
- const normalized = type.trim().toLowerCase().replace(/_/g, '-');
42
- if (normalized === 'all') {
43
- return 'all';
44
- }
45
- if (normalized === 'subs') {
46
- return 'subs';
47
- }
48
- if (normalized === 'inapp') {
49
- console.warn(LEGACY_INAPP_WARNING);
50
- return 'in-app';
51
- }
52
- if (normalized === 'in-app') {
53
- return 'in-app';
54
- }
55
- }
56
- return 'in-app';
57
- }
26
+ return String(error ?? '');
27
+ };
58
28
  // ActiveSubscription and PurchaseError types are already exported via 'export * from ./types'
59
29
 
60
30
  // Export hooks
61
31
  export { useIAP } from "./hooks/useIAP.js";
62
32
 
63
- // iOS promoted product aliases for API parity
64
- export const getPromotedProductIOS = async () => requestPromotedProductIOS();
65
- export const requestPurchaseOnPromotedProductIOS = async () => buyPromotedProductIOS();
66
-
67
33
  // Restore completed transactions (cross-platform)
68
- export const restorePurchases = async (options = {
69
- alsoPublishToEventListenerIOS: false,
70
- onlyIncludeActiveItemsIOS: true
71
- }) => {
72
- if (Platform.OS === 'ios') {
73
- await syncIOS();
74
- }
75
- return getAvailablePurchases(options);
76
- };
77
-
78
34
  // Development utilities removed - use type bridge functions directly if needed
79
35
 
80
36
  // Create the RnIap HybridObject instance lazily to avoid early JSI crashes
@@ -87,7 +43,7 @@ const IAP = {
87
43
  try {
88
44
  iapRef = NitroModules.createHybridObject('RnIap');
89
45
  } catch (e) {
90
- const msg = String(e?.message ?? e ?? '');
46
+ const msg = toErrorMessage(e);
91
47
  if (msg.includes('Nitro') || msg.includes('JSI') || msg.includes('dispatcher') || msg.includes('HybridObject')) {
92
48
  throw new Error('Nitro runtime not installed yet. Ensure react-native-nitro-modules is initialized before calling IAP.');
93
49
  }
@@ -97,6 +53,430 @@ const IAP = {
97
53
  }
98
54
  };
99
55
 
56
+ // ============================================================================
57
+ // EVENT LISTENERS
58
+ // ============================================================================
59
+
60
+ const purchaseUpdatedListenerMap = new WeakMap();
61
+ const purchaseErrorListenerMap = new WeakMap();
62
+ const promotedProductListenerMap = new WeakMap();
63
+ export const purchaseUpdatedListener = listener => {
64
+ const wrappedListener = nitroPurchase => {
65
+ if (validateNitroPurchase(nitroPurchase)) {
66
+ const convertedPurchase = convertNitroPurchaseToPurchase(nitroPurchase);
67
+ listener(convertedPurchase);
68
+ } else {
69
+ console.error('Invalid purchase data received from native:', nitroPurchase);
70
+ }
71
+ };
72
+ purchaseUpdatedListenerMap.set(listener, wrappedListener);
73
+ let attached = false;
74
+ try {
75
+ IAP.instance.addPurchaseUpdatedListener(wrappedListener);
76
+ attached = true;
77
+ } catch (e) {
78
+ const msg = toErrorMessage(e);
79
+ if (msg.includes('Nitro runtime not installed')) {
80
+ console.warn('[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()');
81
+ } else {
82
+ throw e;
83
+ }
84
+ }
85
+ return {
86
+ remove: () => {
87
+ const wrapped = purchaseUpdatedListenerMap.get(listener);
88
+ if (wrapped) {
89
+ if (attached) {
90
+ try {
91
+ IAP.instance.removePurchaseUpdatedListener(wrapped);
92
+ } catch {}
93
+ }
94
+ purchaseUpdatedListenerMap.delete(listener);
95
+ }
96
+ }
97
+ };
98
+ };
99
+ export const purchaseErrorListener = listener => {
100
+ const wrapped = error => {
101
+ listener({
102
+ code: normalizeErrorCodeFromNative(error.code),
103
+ message: error.message,
104
+ productId: undefined
105
+ });
106
+ };
107
+ purchaseErrorListenerMap.set(listener, wrapped);
108
+ let attached = false;
109
+ try {
110
+ IAP.instance.addPurchaseErrorListener(wrapped);
111
+ attached = true;
112
+ } catch (e) {
113
+ const msg = toErrorMessage(e);
114
+ if (msg.includes('Nitro runtime not installed')) {
115
+ console.warn('[purchaseErrorListener] Nitro not ready yet; listener inert until initConnection()');
116
+ } else {
117
+ throw e;
118
+ }
119
+ }
120
+ return {
121
+ remove: () => {
122
+ const stored = purchaseErrorListenerMap.get(listener);
123
+ if (stored) {
124
+ if (attached) {
125
+ try {
126
+ IAP.instance.removePurchaseErrorListener(stored);
127
+ } catch {}
128
+ }
129
+ purchaseErrorListenerMap.delete(listener);
130
+ }
131
+ }
132
+ };
133
+ };
134
+ export const promotedProductListenerIOS = listener => {
135
+ if (Platform.OS !== 'ios') {
136
+ console.warn('promotedProductListenerIOS: This listener is only available on iOS');
137
+ return {
138
+ remove: () => {}
139
+ };
140
+ }
141
+ const wrappedListener = nitroProduct => {
142
+ if (validateNitroProduct(nitroProduct)) {
143
+ const convertedProduct = convertNitroProductToProduct(nitroProduct);
144
+ listener(convertedProduct);
145
+ } else {
146
+ console.error('Invalid promoted product data received from native:', nitroProduct);
147
+ }
148
+ };
149
+ promotedProductListenerMap.set(listener, wrappedListener);
150
+ let attached = false;
151
+ try {
152
+ IAP.instance.addPromotedProductListenerIOS(wrappedListener);
153
+ attached = true;
154
+ } catch (e) {
155
+ const msg = toErrorMessage(e);
156
+ if (msg.includes('Nitro runtime not installed')) {
157
+ console.warn('[promotedProductListenerIOS] Nitro not ready yet; listener inert until initConnection()');
158
+ } else {
159
+ throw e;
160
+ }
161
+ }
162
+ return {
163
+ remove: () => {
164
+ const wrapped = promotedProductListenerMap.get(listener);
165
+ if (wrapped) {
166
+ if (attached) {
167
+ try {
168
+ IAP.instance.removePromotedProductListenerIOS(wrapped);
169
+ } catch {}
170
+ }
171
+ promotedProductListenerMap.delete(listener);
172
+ }
173
+ }
174
+ };
175
+ };
176
+
177
+ // ------------------------------
178
+ // Query API
179
+ // ------------------------------
180
+
181
+ /**
182
+ * Fetch products from the store
183
+ * @param params - Product request configuration
184
+ * @param params.skus - Array of product SKUs to fetch
185
+ * @param params.type - Optional filter: 'in-app' (default) for products, 'subs' for subscriptions, or 'all' for both.
186
+ * @returns Promise<Product[]> - Array of products from the store
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * // Regular products
191
+ * const products = await fetchProducts({ skus: ['product1', 'product2'] });
192
+ *
193
+ * // Subscriptions
194
+ * const subscriptions = await fetchProducts({ skus: ['sub1', 'sub2'], type: 'subs' });
195
+ * ```
196
+ */
197
+ export const fetchProducts = async request => {
198
+ const {
199
+ skus,
200
+ type
201
+ } = request;
202
+ try {
203
+ if (!skus?.length) {
204
+ throw new Error('No SKUs provided');
205
+ }
206
+ const normalizedType = normalizeProductQueryType(type);
207
+ const fetchAndConvert = async nitroType => {
208
+ const nitroProducts = await IAP.instance.fetchProducts(skus, nitroType);
209
+ const validProducts = nitroProducts.filter(validateNitroProduct);
210
+ if (validProducts.length !== nitroProducts.length) {
211
+ console.warn(`[fetchProducts] Some products failed validation: ${nitroProducts.length - validProducts.length} invalid`);
212
+ }
213
+ return validProducts.map(convertNitroProductToProduct);
214
+ };
215
+ if (normalizedType === 'all') {
216
+ const converted = await fetchAndConvert('all');
217
+ const productItems = converted.filter(item => item.type === 'in-app');
218
+ const subscriptionItems = converted.filter(item => item.type === 'subs').map(convertProductToProductSubscription);
219
+ return [...productItems, ...subscriptionItems];
220
+ }
221
+ const convertedProducts = await fetchAndConvert(toNitroProductType(normalizedType));
222
+ if (normalizedType === 'subs') {
223
+ return convertedProducts.map(convertProductToProductSubscription);
224
+ }
225
+ return convertedProducts;
226
+ } catch (error) {
227
+ console.error('[fetchProducts] Failed:', error);
228
+ throw error;
229
+ }
230
+ };
231
+
232
+ /**
233
+ * Get available purchases (purchased items not yet consumed/finished)
234
+ * @param params - Options for getting available purchases
235
+ * @param params.alsoPublishToEventListener - Whether to also publish to event listener
236
+ * @param params.onlyIncludeActiveItems - Whether to only include active items
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * const purchases = await getAvailablePurchases({
241
+ * onlyIncludeActiveItemsIOS: true
242
+ * });
243
+ * ```
244
+ */
245
+ export const getAvailablePurchases = async options => {
246
+ const alsoPublishToEventListenerIOS = Boolean(options?.alsoPublishToEventListenerIOS ?? false);
247
+ const onlyIncludeActiveItemsIOS = Boolean(options?.onlyIncludeActiveItemsIOS ?? true);
248
+ try {
249
+ if (Platform.OS === 'ios') {
250
+ const nitroOptions = {
251
+ ios: {
252
+ alsoPublishToEventListenerIOS,
253
+ onlyIncludeActiveItemsIOS,
254
+ alsoPublishToEventListener: alsoPublishToEventListenerIOS,
255
+ onlyIncludeActiveItems: onlyIncludeActiveItemsIOS
256
+ }
257
+ };
258
+ const nitroPurchases = await IAP.instance.getAvailablePurchases(nitroOptions);
259
+ const validPurchases = nitroPurchases.filter(validateNitroPurchase);
260
+ if (validPurchases.length !== nitroPurchases.length) {
261
+ console.warn(`[getAvailablePurchases] Some purchases failed validation: ${nitroPurchases.length - validPurchases.length} invalid`);
262
+ }
263
+ return validPurchases.map(convertNitroPurchaseToPurchase);
264
+ } else if (Platform.OS === 'android') {
265
+ // For Android, we need to call twice for inapp and subs
266
+ const inappNitroPurchases = await IAP.instance.getAvailablePurchases({
267
+ android: {
268
+ type: 'inapp'
269
+ }
270
+ });
271
+ const subsNitroPurchases = await IAP.instance.getAvailablePurchases({
272
+ android: {
273
+ type: 'subs'
274
+ }
275
+ });
276
+
277
+ // Validate and convert both sets of purchases
278
+ const allNitroPurchases = [...inappNitroPurchases, ...subsNitroPurchases];
279
+ const validPurchases = allNitroPurchases.filter(validateNitroPurchase);
280
+ if (validPurchases.length !== allNitroPurchases.length) {
281
+ console.warn(`[getAvailablePurchases] Some Android purchases failed validation: ${allNitroPurchases.length - validPurchases.length} invalid`);
282
+ }
283
+ return validPurchases.map(convertNitroPurchaseToPurchase);
284
+ } else {
285
+ throw new Error('Unsupported platform');
286
+ }
287
+ } catch (error) {
288
+ console.error('Failed to get available purchases:', error);
289
+ throw error;
290
+ }
291
+ };
292
+
293
+ /**
294
+ * Request the promoted product from the App Store (iOS only)
295
+ * @returns Promise<Product | null> - The promoted product or null if none available
296
+ * @platform iOS
297
+ */
298
+ export const getPromotedProductIOS = async () => {
299
+ if (Platform.OS !== 'ios') {
300
+ return null;
301
+ }
302
+ try {
303
+ const nitroProduct = await IAP.instance.requestPromotedProductIOS();
304
+ if (!nitroProduct) {
305
+ return null;
306
+ }
307
+ const converted = convertNitroProductToProduct(nitroProduct);
308
+ return converted.platform === 'ios' ? converted : null;
309
+ } catch (error) {
310
+ console.error('[getPromotedProductIOS] Failed:', error);
311
+ const errorJson = parseErrorStringToJsonObj(error);
312
+ throw new Error(errorJson.message);
313
+ }
314
+ };
315
+ export const requestPromotedProductIOS = getPromotedProductIOS;
316
+ export const getStorefrontIOS = async () => {
317
+ if (Platform.OS !== 'ios') {
318
+ throw new Error('getStorefrontIOS is only available on iOS');
319
+ }
320
+ try {
321
+ const storefront = await IAP.instance.getStorefrontIOS();
322
+ return storefront;
323
+ } catch (error) {
324
+ console.error('Failed to get storefront:', error);
325
+ throw error;
326
+ }
327
+ };
328
+ export const getAppTransactionIOS = async () => {
329
+ if (Platform.OS !== 'ios') {
330
+ throw new Error('getAppTransactionIOS is only available on iOS');
331
+ }
332
+ try {
333
+ const appTransaction = await IAP.instance.getAppTransactionIOS();
334
+ if (appTransaction == null) {
335
+ return null;
336
+ }
337
+ if (typeof appTransaction === 'string') {
338
+ const parsed = parseAppTransactionPayload(appTransaction);
339
+ if (parsed) {
340
+ return parsed;
341
+ }
342
+ throw new Error('Unable to parse app transaction payload');
343
+ }
344
+ if (typeof appTransaction === 'object' && appTransaction !== null) {
345
+ return appTransaction;
346
+ }
347
+ return null;
348
+ } catch (error) {
349
+ console.error('Failed to get app transaction:', error);
350
+ throw error;
351
+ }
352
+ };
353
+ export const subscriptionStatusIOS = async sku => {
354
+ if (Platform.OS !== 'ios') {
355
+ throw new Error('subscriptionStatusIOS is only available on iOS');
356
+ }
357
+ try {
358
+ const statuses = await IAP.instance.subscriptionStatusIOS(sku);
359
+ if (!Array.isArray(statuses)) return [];
360
+ return statuses.filter(status => status != null).map(convertNitroSubscriptionStatusToSubscriptionStatusIOS);
361
+ } catch (error) {
362
+ console.error('[subscriptionStatusIOS] Failed:', error);
363
+ const errorJson = parseErrorStringToJsonObj(error);
364
+ throw new Error(errorJson.message);
365
+ }
366
+ };
367
+ export const currentEntitlementIOS = async sku => {
368
+ if (Platform.OS !== 'ios') {
369
+ return null;
370
+ }
371
+ try {
372
+ const nitroPurchase = await IAP.instance.currentEntitlementIOS(sku);
373
+ if (nitroPurchase) {
374
+ const converted = convertNitroPurchaseToPurchase(nitroPurchase);
375
+ return converted.platform === 'ios' ? converted : null;
376
+ }
377
+ return null;
378
+ } catch (error) {
379
+ console.error('[currentEntitlementIOS] Failed:', error);
380
+ const errorJson = parseErrorStringToJsonObj(error);
381
+ throw new Error(errorJson.message);
382
+ }
383
+ };
384
+ export const latestTransactionIOS = async sku => {
385
+ if (Platform.OS !== 'ios') {
386
+ return null;
387
+ }
388
+ try {
389
+ const nitroPurchase = await IAP.instance.latestTransactionIOS(sku);
390
+ if (nitroPurchase) {
391
+ const converted = convertNitroPurchaseToPurchase(nitroPurchase);
392
+ return converted.platform === 'ios' ? converted : null;
393
+ }
394
+ return null;
395
+ } catch (error) {
396
+ console.error('[latestTransactionIOS] Failed:', error);
397
+ const errorJson = parseErrorStringToJsonObj(error);
398
+ throw new Error(errorJson.message);
399
+ }
400
+ };
401
+ export const getPendingTransactionsIOS = async () => {
402
+ if (Platform.OS !== 'ios') {
403
+ return [];
404
+ }
405
+ try {
406
+ const nitroPurchases = await IAP.instance.getPendingTransactionsIOS();
407
+ return nitroPurchases.map(convertNitroPurchaseToPurchase).filter(purchase => purchase.platform === 'ios');
408
+ } catch (error) {
409
+ console.error('[getPendingTransactionsIOS] Failed:', error);
410
+ const errorJson = parseErrorStringToJsonObj(error);
411
+ throw new Error(errorJson.message);
412
+ }
413
+ };
414
+ export const showManageSubscriptionsIOS = async () => {
415
+ if (Platform.OS !== 'ios') {
416
+ return [];
417
+ }
418
+ try {
419
+ const nitroPurchases = await IAP.instance.showManageSubscriptionsIOS();
420
+ return nitroPurchases.map(convertNitroPurchaseToPurchase).filter(purchase => purchase.platform === 'ios');
421
+ } catch (error) {
422
+ console.error('[showManageSubscriptionsIOS] Failed:', error);
423
+ const errorJson = parseErrorStringToJsonObj(error);
424
+ throw new Error(errorJson.message);
425
+ }
426
+ };
427
+ export const isEligibleForIntroOfferIOS = async groupID => {
428
+ if (Platform.OS !== 'ios') {
429
+ return false;
430
+ }
431
+ try {
432
+ return await IAP.instance.isEligibleForIntroOfferIOS(groupID);
433
+ } catch (error) {
434
+ console.error('[isEligibleForIntroOfferIOS] Failed:', error);
435
+ const errorJson = parseErrorStringToJsonObj(error);
436
+ throw new Error(errorJson.message);
437
+ }
438
+ };
439
+ export const getReceiptDataIOS = async () => {
440
+ if (Platform.OS !== 'ios') {
441
+ throw new Error('getReceiptDataIOS is only available on iOS');
442
+ }
443
+ try {
444
+ return await IAP.instance.getReceiptDataIOS();
445
+ } catch (error) {
446
+ console.error('[getReceiptDataIOS] Failed:', error);
447
+ const errorJson = parseErrorStringToJsonObj(error);
448
+ throw new Error(errorJson.message);
449
+ }
450
+ };
451
+ export const isTransactionVerifiedIOS = async sku => {
452
+ if (Platform.OS !== 'ios') {
453
+ return false;
454
+ }
455
+ try {
456
+ return await IAP.instance.isTransactionVerifiedIOS(sku);
457
+ } catch (error) {
458
+ console.error('[isTransactionVerifiedIOS] Failed:', error);
459
+ const errorJson = parseErrorStringToJsonObj(error);
460
+ throw new Error(errorJson.message);
461
+ }
462
+ };
463
+ export const getTransactionJwsIOS = async sku => {
464
+ if (Platform.OS !== 'ios') {
465
+ return null;
466
+ }
467
+ try {
468
+ return await IAP.instance.getTransactionJwsIOS(sku);
469
+ } catch (error) {
470
+ console.error('[getTransactionJwsIOS] Failed:', error);
471
+ const errorJson = parseErrorStringToJsonObj(error);
472
+ throw new Error(errorJson.message);
473
+ }
474
+ };
475
+
476
+ // ------------------------------
477
+ // Mutation API
478
+ // ------------------------------
479
+
100
480
  /**
101
481
  * Initialize connection to the store
102
482
  */
@@ -114,7 +494,6 @@ export const initConnection = async () => {
114
494
  */
115
495
  export const endConnection = async () => {
116
496
  try {
117
- // If never initialized, treat as ended
118
497
  if (!iapRef) return true;
119
498
  return await IAP.instance.endConnection();
120
499
  } catch (error) {
@@ -122,215 +501,106 @@ export const endConnection = async () => {
122
501
  throw error;
123
502
  }
124
503
  };
125
-
126
- /**
127
- * Fetch products from the store
128
- * @param params - Product request configuration
129
- * @param params.skus - Array of product SKUs to fetch
130
- * @param params.type - Optional filter: 'in-app' (default) for products, 'subs' for subscriptions, or 'all' for both.
131
- * @returns Promise<Product[]> - Array of products from the store
132
- *
133
- * @example
134
- * ```typescript
135
- * // Regular products
136
- * const products = await fetchProducts({ skus: ['product1', 'product2'] });
137
- *
138
- * // Subscriptions
139
- * const subscriptions = await fetchProducts({ skus: ['sub1', 'sub2'], type: 'subs' });
140
- * ```
141
- */
142
- export const fetchProducts = async ({
143
- skus,
144
- type = 'in-app'
145
- }) => {
504
+ export const restorePurchases = async () => {
146
505
  try {
147
- if (!skus || skus.length === 0) {
148
- throw new Error('No SKUs provided');
149
- }
150
- const normalizedType = normalizeProductQueryType(type);
151
- if (normalizedType === 'all') {
152
- const [inappNitro, subsNitro] = await Promise.all([IAP.instance.fetchProducts(skus, 'inapp'), IAP.instance.fetchProducts(skus, 'subs')]);
153
- const allNitro = [...inappNitro, ...subsNitro];
154
- const validAll = allNitro.filter(validateNitroProduct);
155
- if (validAll.length !== allNitro.length) {
156
- console.warn(`[fetchProducts] Some products failed validation: ${allNitro.length - validAll.length} invalid`);
157
- }
158
- return validAll.map(convertNitroProductToProduct);
159
- }
160
- const nitroProducts = await IAP.instance.fetchProducts(skus, toNitroProductType(normalizedType));
161
-
162
- // Validate and convert NitroProducts to TypeScript Products
163
- const validProducts = nitroProducts.filter(validateNitroProduct);
164
- if (validProducts.length !== nitroProducts.length) {
165
- console.warn(`[fetchProducts] Some products failed validation: ${nitroProducts.length - validProducts.length} invalid`);
506
+ if (Platform.OS === 'ios') {
507
+ await syncIOS();
166
508
  }
167
- const typedProducts = validProducts.map(convertNitroProductToProduct);
168
- return typedProducts;
509
+ await getAvailablePurchases({
510
+ alsoPublishToEventListenerIOS: false,
511
+ onlyIncludeActiveItemsIOS: true
512
+ });
169
513
  } catch (error) {
170
- console.error('[fetchProducts] Failed:', error);
514
+ console.error('Failed to restore purchases:', error);
171
515
  throw error;
172
516
  }
173
517
  };
174
518
 
175
- /**
176
- * Request a purchase for products or subscriptions
177
- * @param params - Purchase request configuration
178
- * @param params.request - Platform-specific purchase parameters
179
- * @param params.type - Type of purchase: 'in-app' for products (default) or 'subs' for subscriptions
180
- *
181
- * @example
182
- * ```typescript
183
- * // Product purchase
184
- * await requestPurchase({
185
- * request: {
186
- * ios: { sku: productId },
187
- * android: { skus: [productId] }
188
- * },
189
- * type: 'in-app'
190
- * });
191
- *
192
- * // Subscription purchase
193
- * await requestPurchase({
194
- * request: {
195
- * ios: { sku: subscriptionId },
196
- * android: {
197
- * skus: [subscriptionId],
198
- * subscriptionOffers: [{ sku: subscriptionId, offerToken: 'token' }]
199
- * }
200
- * },
201
- * type: 'subs'
202
- * });
203
- * ```
204
- */
205
519
  /**
206
520
  * Request a purchase for products or subscriptions
207
521
  * ⚠️ Important: This is an event-based operation, not promise-based.
208
522
  * Listen for events through purchaseUpdatedListener or purchaseErrorListener.
209
- * @param params - Purchase request configuration
210
- * @param params.request - Platform-specific request parameters
211
- * @param params.type - Type of purchase (defaults to in-app)
212
523
  */
213
- export const requestPurchase = async params => {
524
+ export const requestPurchase = async request => {
214
525
  try {
215
- const normalizedType = normalizeProductQueryType(params.type);
526
+ const {
527
+ request: platformRequest,
528
+ type
529
+ } = request;
530
+ const normalizedType = normalizeProductQueryType(type ?? 'in-app');
216
531
  const isSubs = isSubscriptionQuery(normalizedType);
217
- const request = params.request;
218
- if (!request) {
532
+ const perPlatformRequest = platformRequest;
533
+ if (!perPlatformRequest) {
219
534
  throw new Error('Missing purchase request configuration');
220
535
  }
221
-
222
- // Validate platform-specific requests
223
536
  if (Platform.OS === 'ios') {
224
- const iosRequest = request.ios;
537
+ const iosRequest = perPlatformRequest.ios;
225
538
  if (!iosRequest?.sku) {
226
539
  throw new Error('Invalid request for iOS. The `sku` property is required.');
227
540
  }
228
541
  } else if (Platform.OS === 'android') {
229
- const androidRequest = request.android;
542
+ const androidRequest = perPlatformRequest.android;
230
543
  if (!androidRequest?.skus?.length) {
231
544
  throw new Error('Invalid request for Android. The `skus` property is required and must be a non-empty array.');
232
545
  }
233
546
  } else {
234
547
  throw new Error('Unsupported platform');
235
548
  }
236
-
237
- // Transform the request for the unified interface
238
549
  const unifiedRequest = {};
239
- if (Platform.OS === 'ios' && request.ios) {
240
- if (isSubs) {
241
- const iosReq = request.ios;
242
- const autoFinishSubs = iosReq.andDangerouslyFinishTransactionAutomatically == null;
243
- unifiedRequest.ios = {
244
- ...iosReq,
245
- ...(autoFinishSubs ? {
246
- andDangerouslyFinishTransactionAutomatically: true
247
- } : {})
248
- };
249
- } else {
250
- unifiedRequest.ios = request.ios;
550
+ if (Platform.OS === 'ios' && perPlatformRequest.ios) {
551
+ const iosRequest = isSubs ? perPlatformRequest.ios : perPlatformRequest.ios;
552
+ const iosPayload = {
553
+ sku: iosRequest.sku
554
+ };
555
+ const explicitAutoFinish = iosRequest.andDangerouslyFinishTransactionAutomatically ?? undefined;
556
+ const autoFinish = explicitAutoFinish !== undefined ? explicitAutoFinish : isSubs ? true : undefined;
557
+ if (autoFinish !== undefined) {
558
+ iosPayload.andDangerouslyFinishTransactionAutomatically = autoFinish;
251
559
  }
252
- }
253
- if (Platform.OS === 'android' && request.android) {
254
- if (isSubs) {
255
- const subsRequest = request.android;
256
- unifiedRequest.android = {
257
- ...subsRequest,
258
- subscriptionOffers: subsRequest.subscriptionOffers || []
259
- };
260
- } else {
261
- unifiedRequest.android = request.android;
560
+ if (iosRequest.appAccountToken) {
561
+ iosPayload.appAccountToken = iosRequest.appAccountToken;
262
562
  }
563
+ if (typeof iosRequest.quantity === 'number') {
564
+ iosPayload.quantity = iosRequest.quantity;
565
+ }
566
+ const offerRecord = toDiscountOfferRecordIOS(iosRequest.withOffer);
567
+ if (offerRecord) {
568
+ iosPayload.withOffer = offerRecord;
569
+ }
570
+ unifiedRequest.ios = iosPayload;
263
571
  }
264
-
265
- // Call unified method - returns void, listen for events instead
266
- return await IAP.instance.requestPurchase(unifiedRequest);
267
- } catch (error) {
268
- console.error('Failed to request purchase:', error);
269
- throw error;
270
- }
271
- };
272
-
273
- /**
274
- * Get available purchases (purchased items not yet consumed/finished)
275
- * @param params - Options for getting available purchases
276
- * @param params.alsoPublishToEventListener - Whether to also publish to event listener
277
- * @param params.onlyIncludeActiveItems - Whether to only include active items
278
- *
279
- * @example
280
- * ```typescript
281
- * const purchases = await getAvailablePurchases({
282
- * onlyIncludeActiveItemsIOS: true
283
- * });
284
- * ```
285
- */
286
- export const getAvailablePurchases = async ({
287
- alsoPublishToEventListenerIOS = false,
288
- onlyIncludeActiveItemsIOS = true
289
- } = {}) => {
290
- try {
291
- // Create unified options
292
- const options = {};
293
- if (Platform.OS === 'ios') {
294
- // Provide both new and deprecated keys for compatibility
295
- options.ios = {
296
- alsoPublishToEventListenerIOS,
297
- onlyIncludeActiveItemsIOS,
298
- alsoPublishToEventListener: alsoPublishToEventListenerIOS,
299
- onlyIncludeActiveItems: onlyIncludeActiveItemsIOS
572
+ if (Platform.OS === 'android' && perPlatformRequest.android) {
573
+ const androidRequest = isSubs ? perPlatformRequest.android : perPlatformRequest.android;
574
+ const androidPayload = {
575
+ skus: androidRequest.skus
300
576
  };
301
- } else if (Platform.OS === 'android') {
302
- // For Android, we need to call twice for inapp and subs
303
- const inappNitroPurchases = await IAP.instance.getAvailablePurchases({
304
- android: {
305
- type: 'inapp'
577
+ if (androidRequest.obfuscatedAccountIdAndroid) {
578
+ androidPayload.obfuscatedAccountIdAndroid = androidRequest.obfuscatedAccountIdAndroid;
579
+ }
580
+ if (androidRequest.obfuscatedProfileIdAndroid) {
581
+ androidPayload.obfuscatedProfileIdAndroid = androidRequest.obfuscatedProfileIdAndroid;
582
+ }
583
+ if (androidRequest.isOfferPersonalized != null) {
584
+ androidPayload.isOfferPersonalized = androidRequest.isOfferPersonalized;
585
+ }
586
+ if (isSubs) {
587
+ const subsRequest = androidRequest;
588
+ if (subsRequest.purchaseTokenAndroid) {
589
+ androidPayload.purchaseTokenAndroid = subsRequest.purchaseTokenAndroid;
306
590
  }
307
- });
308
- const subsNitroPurchases = await IAP.instance.getAvailablePurchases({
309
- android: {
310
- type: 'subs'
591
+ if (subsRequest.replacementModeAndroid != null) {
592
+ androidPayload.replacementModeAndroid = subsRequest.replacementModeAndroid;
311
593
  }
312
- });
313
-
314
- // Validate and convert both sets of purchases
315
- const allNitroPurchases = [...inappNitroPurchases, ...subsNitroPurchases];
316
- const validPurchases = allNitroPurchases.filter(validateNitroPurchase);
317
- if (validPurchases.length !== allNitroPurchases.length) {
318
- console.warn(`[getAvailablePurchases] Some Android purchases failed validation: ${allNitroPurchases.length - validPurchases.length} invalid`);
594
+ androidPayload.subscriptionOffers = (subsRequest.subscriptionOffers ?? []).filter(offer => offer != null).map(offer => ({
595
+ sku: offer.sku,
596
+ offerToken: offer.offerToken
597
+ }));
319
598
  }
320
- return validPurchases.map(convertNitroPurchaseToPurchase);
321
- } else {
322
- throw new Error('Unsupported platform');
599
+ unifiedRequest.android = androidPayload;
323
600
  }
324
- const nitroPurchases = await IAP.instance.getAvailablePurchases(options);
325
-
326
- // Validate and convert NitroPurchases to TypeScript Purchases
327
- const validPurchases = nitroPurchases.filter(validateNitroPurchase);
328
- if (validPurchases.length !== nitroPurchases.length) {
329
- console.warn(`[getAvailablePurchases] Some purchases failed validation: ${nitroPurchases.length - validPurchases.length} invalid`);
330
- }
331
- return validPurchases.map(convertNitroPurchaseToPurchase);
601
+ return await IAP.instance.requestPurchase(unifiedRequest);
332
602
  } catch (error) {
333
- console.error('Failed to get available purchases:', error);
603
+ console.error('Failed to request purchase:', error);
334
604
  throw error;
335
605
  }
336
606
  };
@@ -340,6 +610,7 @@ export const getAvailablePurchases = async ({
340
610
  * @param params - Transaction finish parameters
341
611
  * @param params.purchase - The purchase to finish
342
612
  * @param params.isConsumable - Whether this is a consumable product (Android only)
613
+ * @returns Promise<void> - Resolves when the transaction is successfully finished
343
614
  *
344
615
  * @example
345
616
  * ```typescript
@@ -349,41 +620,42 @@ export const getAvailablePurchases = async ({
349
620
  * });
350
621
  * ```
351
622
  */
352
- export const finishTransaction = async ({
353
- purchase,
354
- isConsumable = false
355
- }) => {
623
+ export const finishTransaction = async args => {
624
+ const {
625
+ purchase,
626
+ isConsumable
627
+ } = args;
356
628
  try {
357
- // Create unified params
358
- const params = {};
629
+ let params;
359
630
  if (Platform.OS === 'ios') {
360
631
  if (!purchase.id) {
361
632
  throw new Error('purchase.id required to finish iOS transaction');
362
633
  }
363
- params.ios = {
364
- transactionId: purchase.id
634
+ params = {
635
+ ios: {
636
+ transactionId: purchase.id
637
+ }
365
638
  };
366
639
  } else if (Platform.OS === 'android') {
367
- const androidPurchase = purchase;
368
- const token = androidPurchase.purchaseToken;
640
+ const token = purchase.purchaseToken ?? undefined;
369
641
  if (!token) {
370
642
  throw new Error('purchaseToken required to finish Android transaction');
371
643
  }
372
- params.android = {
373
- purchaseToken: token,
374
- isConsumable
644
+ params = {
645
+ android: {
646
+ purchaseToken: token,
647
+ isConsumable: isConsumable ?? false
648
+ }
375
649
  };
376
650
  } else {
377
651
  throw new Error('Unsupported platform');
378
652
  }
379
653
  const result = await IAP.instance.finishTransaction(params);
380
-
381
- // Handle variant return type
382
- if (typeof result === 'boolean') {
383
- return result;
654
+ const success = getSuccessFromPurchaseVariant(result, 'finishTransaction');
655
+ if (!success) {
656
+ throw new Error('Failed to finish transaction');
384
657
  }
385
- // It's a PurchaseResult
386
- return result;
658
+ return;
387
659
  } catch (error) {
388
660
  // If iOS transaction has already been auto-finished natively, treat as success
389
661
  if (Platform.OS === 'ios') {
@@ -392,7 +664,7 @@ export const finishTransaction = async ({
392
664
  const code = (err?.code || '').toString();
393
665
  if (msg.includes('Transaction not found') || code === 'E_ITEM_UNAVAILABLE') {
394
666
  // Consider already finished
395
- return true;
667
+ return;
396
668
  }
397
669
  }
398
670
  console.error('Failed to finish transaction:', error);
@@ -403,6 +675,7 @@ export const finishTransaction = async ({
403
675
  /**
404
676
  * Acknowledge a purchase (Android only)
405
677
  * @param purchaseToken - The purchase token to acknowledge
678
+ * @returns Promise<boolean> - Indicates whether the acknowledgement succeeded
406
679
  *
407
680
  * @example
408
681
  * ```typescript
@@ -420,18 +693,7 @@ export const acknowledgePurchaseAndroid = async purchaseToken => {
420
693
  isConsumable: false
421
694
  }
422
695
  });
423
-
424
- // Result is a variant, extract PurchaseResult
425
- if (typeof result === 'boolean') {
426
- // This shouldn't happen for Android, but handle it
427
- return {
428
- responseCode: 0,
429
- code: '0',
430
- message: 'Success',
431
- purchaseToken
432
- };
433
- }
434
- return result;
696
+ return getSuccessFromPurchaseVariant(result, 'acknowledgePurchaseAndroid');
435
697
  } catch (error) {
436
698
  console.error('Failed to acknowledge purchase Android:', error);
437
699
  throw error;
@@ -441,236 +703,29 @@ export const acknowledgePurchaseAndroid = async purchaseToken => {
441
703
  /**
442
704
  * Consume a purchase (Android only)
443
705
  * @param purchaseToken - The purchase token to consume
706
+ * @returns Promise<boolean> - Indicates whether the consumption succeeded
444
707
  *
445
708
  * @example
446
709
  * ```typescript
447
710
  * await consumePurchaseAndroid('purchase_token_here');
448
711
  * ```
449
712
  */
450
- export const consumePurchaseAndroid = async purchaseToken => {
451
- try {
452
- if (Platform.OS !== 'android') {
453
- throw new Error('consumePurchaseAndroid is only available on Android');
454
- }
455
- const result = await IAP.instance.finishTransaction({
456
- android: {
457
- purchaseToken,
458
- isConsumable: true
459
- }
460
- });
461
-
462
- // Result is a variant, extract PurchaseResult
463
- if (typeof result === 'boolean') {
464
- // This shouldn't happen for Android, but handle it
465
- return {
466
- responseCode: 0,
467
- code: '0',
468
- message: 'Success',
469
- purchaseToken
470
- };
471
- }
472
- return result;
473
- } catch (error) {
474
- console.error('Failed to consume purchase Android:', error);
475
- throw error;
476
- }
477
- };
478
-
479
- // ============================================================================
480
- // EVENT LISTENERS
481
- // ============================================================================
482
-
483
- // Store wrapped listeners for proper removal
484
- const listenerMap = new WeakMap();
485
-
486
- /**
487
- * Purchase updated event listener
488
- * Fired when a purchase is successful or when a pending purchase is completed.
489
- *
490
- * @param listener - Function to call when a purchase is updated
491
- * @returns EventSubscription object with remove method
492
- *
493
- * @example
494
- * ```typescript
495
- * const subscription = purchaseUpdatedListener((purchase) => {
496
- * console.log('Purchase successful:', purchase);
497
- * // 1. Validate receipt with backend
498
- * // 2. Deliver content to user
499
- * // 3. Call finishTransaction to acknowledge
500
- * });
501
- *
502
- * // Later, clean up
503
- * subscription.remove();
504
- * ```
505
- */
506
- export const purchaseUpdatedListener = listener => {
507
- // Wrap the listener to convert NitroPurchase to Purchase
508
- const wrappedListener = nitroPurchase => {
509
- if (validateNitroPurchase(nitroPurchase)) {
510
- const convertedPurchase = convertNitroPurchaseToPurchase(nitroPurchase);
511
- listener(convertedPurchase);
512
- } else {
513
- console.error('Invalid purchase data received from native:', nitroPurchase);
514
- }
515
- };
516
-
517
- // Store the wrapped listener for removal
518
- listenerMap.set(listener, wrappedListener);
519
- let attached = false;
520
- try {
521
- IAP.instance.addPurchaseUpdatedListener(wrappedListener);
522
- attached = true;
523
- } catch (e) {
524
- const msg = String(e ?? '');
525
- if (msg.includes('Nitro runtime not installed')) {
526
- console.warn('[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()');
527
- } else {
528
- throw e;
529
- }
530
- }
531
- return {
532
- remove: () => {
533
- const wrapped = listenerMap.get(listener);
534
- if (wrapped) {
535
- if (attached) {
536
- try {
537
- IAP.instance.removePurchaseUpdatedListener(wrapped);
538
- } catch {}
539
- }
540
- listenerMap.delete(listener);
541
- }
542
- }
543
- };
544
- };
545
-
546
- /**
547
- * Purchase error event listener
548
- * Fired when a purchase fails or is cancelled by the user.
549
- *
550
- * @param listener - Function to call when a purchase error occurs
551
- * @returns EventSubscription object with remove method
552
- *
553
- * @example
554
- * ```typescript
555
- * const subscription = purchaseErrorListener((error) => {
556
- * switch (error.code) {
557
- * case 'E_USER_CANCELLED':
558
- * // User cancelled - no action needed
559
- * break;
560
- * case 'E_ITEM_UNAVAILABLE':
561
- * // Product not available
562
- * break;
563
- * case 'E_NETWORK_ERROR':
564
- * // Retry with backoff
565
- * break;
566
- * }
567
- * });
568
- *
569
- * // Later, clean up
570
- * subscription.remove();
571
- * ```
572
- */
573
- export const purchaseErrorListener = listener => {
574
- const wrapped = error => {
575
- listener({
576
- code: normalizeErrorCodeFromNative(error.code),
577
- message: error.message,
578
- productId: undefined
579
- });
580
- };
581
- listenerMap.set(listener, wrapped);
582
- let attached = false;
583
- try {
584
- IAP.instance.addPurchaseErrorListener(wrapped);
585
- attached = true;
586
- } catch (e) {
587
- const msg = String(e ?? '');
588
- if (msg.includes('Nitro runtime not installed')) {
589
- console.warn('[purchaseErrorListener] Nitro not ready yet; listener inert until initConnection()');
590
- } else {
591
- throw e;
592
- }
593
- }
594
- return {
595
- remove: () => {
596
- const stored = listenerMap.get(listener);
597
- if (stored) {
598
- if (attached) {
599
- try {
600
- IAP.instance.removePurchaseErrorListener(stored);
601
- } catch {}
602
- }
603
- listenerMap.delete(listener);
604
- }
605
- }
606
- };
607
- };
608
-
609
- /**
610
- * iOS-only listener for App Store promoted product events.
611
- * Fired when a user clicks on a promoted in-app purchase in the App Store.
612
- *
613
- * @param listener - Callback function that receives the promoted product
614
- * @returns EventSubscription object with remove method
615
- *
616
- * @example
617
- * ```typescript
618
- * const subscription = promotedProductListenerIOS((product) => {
619
- * console.log('Promoted product:', product);
620
- * // Trigger purchase flow for the promoted product
621
- * });
622
- *
623
- * // Later, clean up
624
- * subscription.remove();
625
- * ```
626
- *
627
- * @platform iOS
628
- */
629
- export const promotedProductListenerIOS = listener => {
630
- if (Platform.OS !== 'ios') {
631
- console.warn('promotedProductListenerIOS: This listener is only available on iOS');
632
- return {
633
- remove: () => {}
634
- };
635
- }
636
-
637
- // Wrap the listener to convert NitroProduct to Product
638
- const wrappedListener = nitroProduct => {
639
- if (validateNitroProduct(nitroProduct)) {
640
- const convertedProduct = convertNitroProductToProduct(nitroProduct);
641
- listener(convertedProduct);
642
- } else {
643
- console.error('Invalid promoted product data received from native:', nitroProduct);
644
- }
645
- };
646
-
647
- // Store the wrapped listener for removal
648
- listenerMap.set(listener, wrappedListener);
649
- let attached = false;
713
+ export const consumePurchaseAndroid = async purchaseToken => {
650
714
  try {
651
- IAP.instance.addPromotedProductListenerIOS(wrappedListener);
652
- attached = true;
653
- } catch (e) {
654
- const msg = String(e ?? '');
655
- if (msg.includes('Nitro runtime not installed')) {
656
- console.warn('[promotedProductListenerIOS] Nitro not ready yet; listener inert until initConnection()');
657
- } else {
658
- throw e;
715
+ if (Platform.OS !== 'android') {
716
+ throw new Error('consumePurchaseAndroid is only available on Android');
659
717
  }
660
- }
661
- return {
662
- remove: () => {
663
- const wrapped = listenerMap.get(listener);
664
- if (wrapped) {
665
- if (attached) {
666
- try {
667
- IAP.instance.removePromotedProductListenerIOS(wrapped);
668
- } catch {}
669
- }
670
- listenerMap.delete(listener);
718
+ const result = await IAP.instance.finishTransaction({
719
+ android: {
720
+ purchaseToken,
721
+ isConsumable: true
671
722
  }
672
- }
673
- };
723
+ });
724
+ return getSuccessFromPurchaseVariant(result, 'consumePurchaseAndroid');
725
+ } catch (error) {
726
+ console.error('Failed to consume purchase Android:', error);
727
+ throw error;
728
+ }
674
729
  };
675
730
 
676
731
  // ============================================================================
@@ -683,11 +738,19 @@ export const promotedProductListenerIOS = listener => {
683
738
  * @param androidOptions - Android-specific validation options (required for Android)
684
739
  * @returns Promise<ReceiptValidationResultIOS | ReceiptValidationResultAndroid> - Platform-specific receipt validation result
685
740
  */
686
- export const validateReceipt = async (sku, androidOptions) => {
741
+ export const validateReceipt = async options => {
742
+ const {
743
+ sku,
744
+ androidOptions
745
+ } = options;
687
746
  try {
747
+ const normalizedAndroidOptions = androidOptions != null ? {
748
+ ...androidOptions,
749
+ isSub: androidOptions.isSub == null ? undefined : Boolean(androidOptions.isSub)
750
+ } : undefined;
688
751
  const params = {
689
752
  sku,
690
- androidOptions
753
+ androidOptions: normalizedAndroidOptions
691
754
  };
692
755
  const nitroResult = await IAP.instance.validateReceipt(params);
693
756
 
@@ -743,7 +806,8 @@ export const syncIOS = async () => {
743
806
  throw new Error('syncIOS is only available on iOS');
744
807
  }
745
808
  try {
746
- return await IAP.instance.syncIOS();
809
+ const result = await IAP.instance.syncIOS();
810
+ return Boolean(result);
747
811
  } catch (error) {
748
812
  console.error('[syncIOS] Failed:', error);
749
813
  const errorJson = parseErrorStringToJsonObj(error);
@@ -751,31 +815,9 @@ export const syncIOS = async () => {
751
815
  }
752
816
  };
753
817
 
754
- /**
755
- * Request the promoted product from the App Store (iOS only)
756
- * @returns Promise<Product | null> - The promoted product or null if none available
757
- * @platform iOS
758
- */
759
- export const requestPromotedProductIOS = async () => {
760
- if (Platform.OS !== 'ios') {
761
- return null;
762
- }
763
- try {
764
- const nitroProduct = await IAP.instance.requestPromotedProductIOS();
765
- if (nitroProduct) {
766
- return convertNitroProductToProduct(nitroProduct);
767
- }
768
- return null;
769
- } catch (error) {
770
- console.error('[getPromotedProductIOS] Failed:', error);
771
- const errorJson = parseErrorStringToJsonObj(error);
772
- throw new Error(errorJson.message);
773
- }
774
- };
775
-
776
818
  /**
777
819
  * Present the code redemption sheet for offer codes (iOS only)
778
- * @returns Promise<boolean> - True if the sheet was presented successfully
820
+ * @returns Promise<boolean> - Indicates whether the redemption sheet was presented
779
821
  * @platform iOS
780
822
  */
781
823
  export const presentCodeRedemptionSheetIOS = async () => {
@@ -783,7 +825,8 @@ export const presentCodeRedemptionSheetIOS = async () => {
783
825
  return false;
784
826
  }
785
827
  try {
786
- return await IAP.instance.presentCodeRedemptionSheetIOS();
828
+ const result = await IAP.instance.presentCodeRedemptionSheetIOS();
829
+ return Boolean(result);
787
830
  } catch (error) {
788
831
  console.error('[presentCodeRedemptionSheetIOS] Failed:', error);
789
832
  const errorJson = parseErrorStringToJsonObj(error);
@@ -793,33 +836,43 @@ export const presentCodeRedemptionSheetIOS = async () => {
793
836
 
794
837
  /**
795
838
  * Buy promoted product on iOS
796
- * @returns Promise<void>
839
+ * @returns Promise<boolean> - true when the request triggers successfully
797
840
  * @platform iOS
798
841
  */
799
- export const buyPromotedProductIOS = async () => {
842
+ export const requestPurchaseOnPromotedProductIOS = async () => {
800
843
  if (Platform.OS !== 'ios') {
801
- throw new Error('buyPromotedProductIOS is only available on iOS');
844
+ throw new Error('requestPurchaseOnPromotedProductIOS is only available on iOS');
802
845
  }
803
846
  try {
804
847
  await IAP.instance.buyPromotedProductIOS();
848
+ const pending = await IAP.instance.getPendingTransactionsIOS();
849
+ const latest = pending.find(purchase => purchase != null);
850
+ if (!latest) {
851
+ throw new Error('No promoted purchase available after request');
852
+ }
853
+ const converted = convertNitroPurchaseToPurchase(latest);
854
+ if (converted.platform !== 'ios') {
855
+ throw new Error('Promoted purchase result not available for iOS');
856
+ }
857
+ return true;
805
858
  } catch (error) {
806
- console.error('[buyPromotedProductIOS] Failed:', error);
807
- const errorJson = parseErrorStringToJsonObj(error);
808
- throw new Error(errorJson.message);
859
+ console.error('[requestPurchaseOnPromotedProductIOS] Failed:', error);
860
+ throw error;
809
861
  }
810
862
  };
811
863
 
812
864
  /**
813
865
  * Clear unfinished transactions on iOS
814
- * @returns Promise<void>
866
+ * @returns Promise<boolean>
815
867
  * @platform iOS
816
868
  */
817
869
  export const clearTransactionIOS = async () => {
818
870
  if (Platform.OS !== 'ios') {
819
- return;
871
+ return false;
820
872
  }
821
873
  try {
822
874
  await IAP.instance.clearTransactionIOS();
875
+ return true;
823
876
  } catch (error) {
824
877
  console.error('[clearTransactionIOS] Failed:', error);
825
878
  const errorJson = parseErrorStringToJsonObj(error);
@@ -835,10 +888,11 @@ export const clearTransactionIOS = async () => {
835
888
  */
836
889
  export const beginRefundRequestIOS = async sku => {
837
890
  if (Platform.OS !== 'ios') {
838
- return null;
891
+ throw new Error('beginRefundRequestIOS is only available on iOS');
839
892
  }
840
893
  try {
841
- return await IAP.instance.beginRefundRequestIOS(sku);
894
+ const status = await IAP.instance.beginRefundRequestIOS(sku);
895
+ return status ?? null;
842
896
  } catch (error) {
843
897
  console.error('[beginRefundRequestIOS] Failed:', error);
844
898
  const errorJson = parseErrorStringToJsonObj(error);
@@ -853,180 +907,51 @@ export const beginRefundRequestIOS = async sku => {
853
907
  * @throws Error when called on non-iOS platforms or when IAP is not initialized
854
908
  * @platform iOS
855
909
  */
856
- export const subscriptionStatusIOS = async sku => {
857
- if (Platform.OS !== 'ios') {
858
- throw new Error('subscriptionStatusIOS is only available on iOS');
859
- }
860
- try {
861
- const statuses = await IAP.instance.subscriptionStatusIOS(sku);
862
- if (!statuses || !Array.isArray(statuses)) return [];
863
- return statuses.map(s => convertNitroSubscriptionStatusToSubscriptionStatusIOS(s));
864
- } catch (error) {
865
- console.error('[subscriptionStatusIOS] Failed:', error);
866
- const errorJson = parseErrorStringToJsonObj(error);
867
- throw new Error(errorJson.message);
868
- }
869
- };
870
-
871
910
  /**
872
911
  * Get current entitlement for a product (iOS only)
873
912
  * @param sku - The product SKU
874
913
  * @returns Promise<Purchase | null> - Current entitlement or null
875
914
  * @platform iOS
876
915
  */
877
- export const currentEntitlementIOS = async sku => {
878
- if (Platform.OS !== 'ios') {
879
- return null;
880
- }
881
- try {
882
- const nitroPurchase = await IAP.instance.currentEntitlementIOS(sku);
883
- if (nitroPurchase) {
884
- return convertNitroPurchaseToPurchase(nitroPurchase);
885
- }
886
- return null;
887
- } catch (error) {
888
- console.error('[currentEntitlementIOS] Failed:', error);
889
- const errorJson = parseErrorStringToJsonObj(error);
890
- throw new Error(errorJson.message);
891
- }
892
- };
893
-
894
916
  /**
895
917
  * Get latest transaction for a product (iOS only)
896
918
  * @param sku - The product SKU
897
919
  * @returns Promise<Purchase | null> - Latest transaction or null
898
920
  * @platform iOS
899
921
  */
900
- export const latestTransactionIOS = async sku => {
901
- if (Platform.OS !== 'ios') {
902
- return null;
903
- }
904
- try {
905
- const nitroPurchase = await IAP.instance.latestTransactionIOS(sku);
906
- if (nitroPurchase) {
907
- return convertNitroPurchaseToPurchase(nitroPurchase);
908
- }
909
- return null;
910
- } catch (error) {
911
- console.error('[latestTransactionIOS] Failed:', error);
912
- const errorJson = parseErrorStringToJsonObj(error);
913
- throw new Error(errorJson.message);
914
- }
915
- };
916
-
917
922
  /**
918
923
  * Get pending transactions (iOS only)
919
924
  * @returns Promise<Purchase[]> - Array of pending transactions
920
925
  * @platform iOS
921
926
  */
922
- export const getPendingTransactionsIOS = async () => {
923
- if (Platform.OS !== 'ios') {
924
- return [];
925
- }
926
- try {
927
- const nitroPurchases = await IAP.instance.getPendingTransactionsIOS();
928
- return nitroPurchases.map(convertNitroPurchaseToPurchase);
929
- } catch (error) {
930
- console.error('[getPendingTransactionsIOS] Failed:', error);
931
- const errorJson = parseErrorStringToJsonObj(error);
932
- throw new Error(errorJson.message);
933
- }
934
- };
935
-
936
927
  /**
937
928
  * Show manage subscriptions screen (iOS only)
938
929
  * @returns Promise<Purchase[]> - Subscriptions where auto-renewal status changed
939
930
  * @platform iOS
940
931
  */
941
- export const showManageSubscriptionsIOS = async () => {
942
- if (Platform.OS !== 'ios') {
943
- return [];
944
- }
945
- try {
946
- const nitroPurchases = await IAP.instance.showManageSubscriptionsIOS();
947
- return nitroPurchases.map(convertNitroPurchaseToPurchase);
948
- } catch (error) {
949
- console.error('[showManageSubscriptionsIOS] Failed:', error);
950
- const errorJson = parseErrorStringToJsonObj(error);
951
- throw new Error(errorJson.message);
952
- }
953
- };
954
-
955
932
  /**
956
933
  * Check if user is eligible for intro offer (iOS only)
957
934
  * @param groupID - The subscription group ID
958
935
  * @returns Promise<boolean> - Eligibility status
959
936
  * @platform iOS
960
937
  */
961
- export const isEligibleForIntroOfferIOS = async groupID => {
962
- if (Platform.OS !== 'ios') {
963
- return false;
964
- }
965
- try {
966
- return await IAP.instance.isEligibleForIntroOfferIOS(groupID);
967
- } catch (error) {
968
- console.error('[isEligibleForIntroOfferIOS] Failed:', error);
969
- const errorJson = parseErrorStringToJsonObj(error);
970
- throw new Error(errorJson.message);
971
- }
972
- };
973
-
974
938
  /**
975
939
  * Get receipt data (iOS only)
976
940
  * @returns Promise<string> - Base64 encoded receipt data
977
941
  * @platform iOS
978
942
  */
979
- export const getReceiptDataIOS = async () => {
980
- if (Platform.OS !== 'ios') {
981
- throw new Error('getReceiptDataIOS is only available on iOS');
982
- }
983
- try {
984
- return await IAP.instance.getReceiptDataIOS();
985
- } catch (error) {
986
- console.error('[getReceiptDataIOS] Failed:', error);
987
- const errorJson = parseErrorStringToJsonObj(error);
988
- throw new Error(errorJson.message);
989
- }
990
- };
991
-
992
943
  /**
993
944
  * Check if transaction is verified (iOS only)
994
945
  * @param sku - The product SKU
995
946
  * @returns Promise<boolean> - Verification status
996
947
  * @platform iOS
997
948
  */
998
- export const isTransactionVerifiedIOS = async sku => {
999
- if (Platform.OS !== 'ios') {
1000
- return false;
1001
- }
1002
- try {
1003
- return await IAP.instance.isTransactionVerifiedIOS(sku);
1004
- } catch (error) {
1005
- console.error('[isTransactionVerifiedIOS] Failed:', error);
1006
- const errorJson = parseErrorStringToJsonObj(error);
1007
- throw new Error(errorJson.message);
1008
- }
1009
- };
1010
-
1011
949
  /**
1012
950
  * Get transaction JWS representation (iOS only)
1013
951
  * @param sku - The product SKU
1014
952
  * @returns Promise<string | null> - JWS representation or null
1015
953
  * @platform iOS
1016
954
  */
1017
- export const getTransactionJwsIOS = async sku => {
1018
- if (Platform.OS !== 'ios') {
1019
- return null;
1020
- }
1021
- try {
1022
- return await IAP.instance.getTransactionJwsIOS(sku);
1023
- } catch (error) {
1024
- console.error('[getTransactionJwsIOS] Failed:', error);
1025
- const errorJson = parseErrorStringToJsonObj(error);
1026
- throw new Error(errorJson.message);
1027
- }
1028
- };
1029
-
1030
955
  /**
1031
956
  * Get the storefront identifier for the user's App Store account (iOS only)
1032
957
  * @returns Promise<string> - The storefront identifier (e.g., 'USA' for United States)
@@ -1038,61 +963,26 @@ export const getTransactionJwsIOS = async sku => {
1038
963
  * console.log('User storefront:', storefront); // e.g., 'USA', 'GBR', 'KOR'
1039
964
  * ```
1040
965
  */
1041
- export const getStorefrontIOS = async () => {
1042
- if (Platform.OS !== 'ios') {
1043
- throw new Error('getStorefrontIOS is only available on iOS');
1044
- }
1045
- try {
1046
- // Call the native method to get storefront
1047
- const storefront = await IAP.instance.getStorefrontIOS();
1048
- return storefront;
1049
- } catch (error) {
1050
- console.error('Failed to get storefront:', error);
1051
- throw error;
1052
- }
1053
- };
1054
-
1055
- /**
1056
- * Gets the storefront country code from the underlying native store.
1057
- * Returns a two-letter country code such as 'US', 'KR', or empty string on failure.
1058
- *
1059
- * Cross-platform alias aligning with expo-iap.
1060
- */
1061
- export const getStorefront = async () => {
1062
- if (Platform.OS === 'android') {
1063
- try {
1064
- // Optional since older builds may not have the method
1065
- const result = await IAP.instance.getStorefrontAndroid?.();
1066
- return result ?? '';
1067
- } catch {
1068
- return '';
1069
- }
1070
- }
1071
- return getStorefrontIOS();
1072
- };
1073
-
1074
966
  /**
1075
967
  * Deeplinks to native interface that allows users to manage their subscriptions
1076
968
  * Cross-platform alias aligning with expo-iap
1077
969
  */
1078
- export const deepLinkToSubscriptions = async (options = {}) => {
970
+ export const deepLinkToSubscriptions = async options => {
971
+ const resolvedOptions = options ?? undefined;
1079
972
  if (Platform.OS === 'android') {
1080
973
  await IAP.instance.deepLinkToSubscriptionsAndroid?.({
1081
- skuAndroid: options.skuAndroid,
1082
- packageNameAndroid: options.packageNameAndroid
974
+ skuAndroid: resolvedOptions?.skuAndroid ?? undefined,
975
+ packageNameAndroid: resolvedOptions?.packageNameAndroid ?? undefined
1083
976
  });
1084
977
  return;
1085
978
  }
1086
- // iOS: Use manage subscriptions sheet (ignore returned purchases for deeplink parity)
1087
979
  if (Platform.OS === 'ios') {
1088
980
  try {
1089
981
  await IAP.instance.showManageSubscriptionsIOS();
1090
- } catch {
1091
- // no-op
982
+ } catch (error) {
983
+ console.warn('[deepLinkToSubscriptions] Failed on iOS:', error);
1092
984
  }
1093
- return;
1094
985
  }
1095
- return;
1096
986
  };
1097
987
 
1098
988
  /**
@@ -1114,20 +1004,6 @@ export const deepLinkToSubscriptions = async (options = {}) => {
1114
1004
  * }
1115
1005
  * ```
1116
1006
  */
1117
- export const getAppTransactionIOS = async () => {
1118
- if (Platform.OS !== 'ios') {
1119
- throw new Error('getAppTransactionIOS is only available on iOS');
1120
- }
1121
- try {
1122
- // Call the native method to get app transaction
1123
- const appTransaction = await IAP.instance.getAppTransactionIOS();
1124
- return appTransaction;
1125
- } catch (error) {
1126
- console.error('Failed to get app transaction:', error);
1127
- throw error;
1128
- }
1129
- };
1130
-
1131
1007
  // Export subscription helpers
1132
1008
  export { getActiveSubscriptions, hasActiveSubscriptions } from "./helpers/subscription.js";
1133
1009
 
@@ -1144,4 +1020,57 @@ export const acknowledgePurchase = acknowledgePurchaseAndroid;
1144
1020
  * @deprecated Use consumePurchaseAndroid instead
1145
1021
  */
1146
1022
  export const consumePurchase = consumePurchaseAndroid;
1023
+
1024
+ // ============================================================================
1025
+ // Internal Helpers
1026
+ // ============================================================================
1027
+
1028
+ const toDiscountOfferRecordIOS = offer => {
1029
+ if (!offer) {
1030
+ return undefined;
1031
+ }
1032
+ return {
1033
+ identifier: offer.identifier,
1034
+ keyIdentifier: offer.keyIdentifier,
1035
+ nonce: offer.nonce,
1036
+ signature: offer.signature,
1037
+ timestamp: String(offer.timestamp)
1038
+ };
1039
+ };
1040
+ const toNitroProductType = type => {
1041
+ if (type === 'subs') {
1042
+ return 'subs';
1043
+ }
1044
+ if (type === 'all') {
1045
+ return 'all';
1046
+ }
1047
+ if (type === 'inapp') {
1048
+ console.warn(LEGACY_INAPP_WARNING);
1049
+ return 'inapp';
1050
+ }
1051
+ return 'inapp';
1052
+ };
1053
+ const isSubscriptionQuery = type => type === 'subs';
1054
+ const normalizeProductQueryType = type => {
1055
+ if (type === 'all' || type === 'subs' || type === 'in-app') {
1056
+ return type;
1057
+ }
1058
+ if (typeof type === 'string') {
1059
+ const normalized = type.trim().toLowerCase().replace(/_/g, '-');
1060
+ if (normalized === 'all') {
1061
+ return 'all';
1062
+ }
1063
+ if (normalized === 'subs') {
1064
+ return 'subs';
1065
+ }
1066
+ if (normalized === 'inapp') {
1067
+ console.warn(LEGACY_INAPP_WARNING);
1068
+ return 'in-app';
1069
+ }
1070
+ if (normalized === 'in-app') {
1071
+ return 'in-app';
1072
+ }
1073
+ }
1074
+ return 'in-app';
1075
+ };
1147
1076
  //# sourceMappingURL=index.js.map