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