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
package/src/index.ts CHANGED
@@ -6,27 +6,25 @@ import {NitroModules} from 'react-native-nitro-modules';
6
6
 
7
7
  // Internal modules
8
8
  import type {
9
- NitroPurchaseResult,
10
9
  NitroReceiptValidationParams,
11
10
  NitroReceiptValidationResultIOS,
12
11
  NitroReceiptValidationResultAndroid,
13
12
  NitroSubscriptionStatus,
14
13
  RnIap,
15
14
  } from './specs/RnIap.nitro';
16
- import type {
17
- ProductQueryType,
18
- RequestPurchaseProps,
19
- RequestPurchaseResult,
20
- } from './types';
21
15
  import type {
22
16
  AndroidSubscriptionOfferInput,
23
17
  DiscountOfferInputIOS,
18
+ FetchProductsResult,
19
+ MutationField,
24
20
  Product,
25
- ProductRequest,
21
+ ProductIOS,
22
+ ProductQueryType,
26
23
  Purchase,
27
- PurchaseAndroid,
28
- PurchaseOptions,
29
24
  PurchaseError,
25
+ PurchaseIOS,
26
+ QueryField,
27
+ AppTransaction,
30
28
  ReceiptValidationResultAndroid,
31
29
  ReceiptValidationResultIOS,
32
30
  RequestPurchaseAndroidProps,
@@ -35,17 +33,19 @@ import type {
35
33
  RequestSubscriptionAndroidProps,
36
34
  RequestSubscriptionIosProps,
37
35
  RequestSubscriptionPropsByPlatforms,
38
- SubscriptionStatusIOS,
39
36
  } from './types';
40
37
  import {
41
38
  convertNitroProductToProduct,
42
39
  convertNitroPurchaseToPurchase,
40
+ convertProductToProductSubscription,
43
41
  validateNitroProduct,
44
42
  validateNitroPurchase,
45
43
  convertNitroSubscriptionStatusToSubscriptionStatusIOS,
46
44
  } from './utils/type-bridge';
47
45
  import {parseErrorStringToJsonObj} from './utils/error';
48
46
  import {normalizeErrorCodeFromNative} from './utils/errorMapping';
47
+ import {getSuccessFromPurchaseVariant} from './utils/purchase';
48
+ import {parseAppTransactionPayload} from './utils';
49
49
 
50
50
  // Export all types
51
51
  export type {
@@ -89,105 +89,16 @@ const toErrorMessage = (error: unknown): string => {
89
89
  return String(error ?? '');
90
90
  };
91
91
 
92
- type NitroDiscountOfferRecord = NonNullable<
93
- NonNullable<NitroPurchaseRequest['ios']>['withOffer']
94
- >;
95
-
96
- const toDiscountOfferRecordIOS = (
97
- offer: DiscountOfferInputIOS | null | undefined,
98
- ): NitroDiscountOfferRecord | undefined => {
99
- if (!offer) {
100
- return undefined;
101
- }
102
- return {
103
- identifier: offer.identifier,
104
- keyIdentifier: offer.keyIdentifier,
105
- nonce: offer.nonce,
106
- signature: offer.signature,
107
- timestamp: String(offer.timestamp),
108
- };
109
- };
110
-
111
- function toNitroProductType(
112
- type?: ProductTypeInput | ProductQueryType | null,
113
- ): 'inapp' | 'subs' {
114
- if (type === 'subs') {
115
- return 'subs';
116
- }
117
- if (type === 'inapp') {
118
- console.warn(LEGACY_INAPP_WARNING);
119
- return 'inapp';
120
- }
121
- if (type === 'all') {
122
- return 'inapp';
123
- }
124
- return 'inapp';
125
- }
126
-
127
- function isSubscriptionQuery(type?: ProductQueryType | null): boolean {
128
- return type === 'subs';
129
- }
130
-
131
- function normalizeProductQueryType(
132
- type?: ProductQueryType | string | null,
133
- ): ProductQueryType {
134
- if (type === 'all' || type === 'subs' || type === 'in-app') {
135
- return type;
136
- }
137
-
138
- if (typeof type === 'string') {
139
- const normalized = type.trim().toLowerCase().replace(/_/g, '-');
140
-
141
- if (normalized === 'all') {
142
- return 'all';
143
- }
144
- if (normalized === 'subs') {
145
- return 'subs';
146
- }
147
- if (normalized === 'inapp') {
148
- console.warn(LEGACY_INAPP_WARNING);
149
- return 'in-app';
150
- }
151
- if (normalized === 'in-app') {
152
- return 'in-app';
153
- }
154
- }
155
- return 'in-app';
156
- }
157
-
158
92
  export interface EventSubscription {
159
93
  remove(): void;
160
94
  }
161
95
 
162
- export type FinishTransactionParams = {
163
- purchase: Purchase;
164
- isConsumable?: boolean;
165
- };
166
-
167
96
  // ActiveSubscription and PurchaseError types are already exported via 'export * from ./types'
168
97
 
169
98
  // Export hooks
170
99
  export {useIAP} from './hooks/useIAP';
171
100
 
172
- // iOS promoted product aliases for API parity
173
- export const getPromotedProductIOS = async (): Promise<Product | null> =>
174
- requestPromotedProductIOS();
175
- export const requestPurchaseOnPromotedProductIOS = async (): Promise<void> =>
176
- buyPromotedProductIOS();
177
-
178
101
  // Restore completed transactions (cross-platform)
179
- export const restorePurchases = async (
180
- options: PurchaseOptions = {
181
- alsoPublishToEventListenerIOS: false,
182
- onlyIncludeActiveItemsIOS: true,
183
- },
184
- ): Promise<Purchase[]> => {
185
- if (Platform.OS === 'ios') {
186
- await syncIOS();
187
- }
188
- return getAvailablePurchases(options);
189
- };
190
-
191
102
  // Development utilities removed - use type bridge functions directly if needed
192
103
 
193
104
  // Create the RnIap HybridObject instance lazily to avoid early JSI crashes
@@ -218,32 +129,168 @@ const IAP = {
218
129
  },
219
130
  };
220
131
 
221
- /**
222
- * Initialize connection to the store
223
- */
224
- export const initConnection = async (): Promise<boolean> => {
132
+ // ============================================================================
133
+ // EVENT LISTENERS
134
+ // ============================================================================
135
+
136
+ const purchaseUpdatedListenerMap = new WeakMap<
137
+ (purchase: Purchase) => void,
138
+ NitroPurchaseListener
139
+ >();
140
+ const purchaseErrorListenerMap = new WeakMap<
141
+ (error: PurchaseError) => void,
142
+ NitroPurchaseErrorListener
143
+ >();
144
+ const promotedProductListenerMap = new WeakMap<
145
+ (product: Product) => void,
146
+ NitroPromotedProductListener
147
+ >();
148
+
149
+ export const purchaseUpdatedListener = (
150
+ listener: (purchase: Purchase) => void,
151
+ ): EventSubscription => {
152
+ const wrappedListener: NitroPurchaseListener = (nitroPurchase) => {
153
+ if (validateNitroPurchase(nitroPurchase)) {
154
+ const convertedPurchase = convertNitroPurchaseToPurchase(nitroPurchase);
155
+ listener(convertedPurchase);
156
+ } else {
157
+ console.error(
158
+ 'Invalid purchase data received from native:',
159
+ nitroPurchase,
160
+ );
161
+ }
162
+ };
163
+
164
+ purchaseUpdatedListenerMap.set(listener, wrappedListener);
165
+ let attached = false;
225
166
  try {
226
- return await IAP.instance.initConnection();
227
- } catch (error) {
228
- console.error('Failed to initialize IAP connection:', error);
229
- throw error;
167
+ IAP.instance.addPurchaseUpdatedListener(wrappedListener);
168
+ attached = true;
169
+ } catch (e) {
170
+ const msg = toErrorMessage(e);
171
+ if (msg.includes('Nitro runtime not installed')) {
172
+ console.warn(
173
+ '[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()',
174
+ );
175
+ } else {
176
+ throw e;
177
+ }
178
+ }
179
+
180
+ return {
181
+ remove: () => {
182
+ const wrapped = purchaseUpdatedListenerMap.get(listener);
183
+ if (wrapped) {
184
+ if (attached) {
185
+ try {
186
+ IAP.instance.removePurchaseUpdatedListener(wrapped);
187
+ } catch {}
188
+ }
189
+ purchaseUpdatedListenerMap.delete(listener);
190
+ }
191
+ },
192
+ };
193
+ };
194
+
195
+ export const purchaseErrorListener = (
196
+ listener: (error: PurchaseError) => void,
197
+ ): EventSubscription => {
198
+ const wrapped: NitroPurchaseErrorListener = (error) => {
199
+ listener({
200
+ code: normalizeErrorCodeFromNative(error.code),
201
+ message: error.message,
202
+ productId: undefined,
203
+ });
204
+ };
205
+
206
+ purchaseErrorListenerMap.set(listener, wrapped);
207
+ let attached = false;
208
+ try {
209
+ IAP.instance.addPurchaseErrorListener(wrapped);
210
+ attached = true;
211
+ } catch (e) {
212
+ const msg = toErrorMessage(e);
213
+ if (msg.includes('Nitro runtime not installed')) {
214
+ console.warn(
215
+ '[purchaseErrorListener] Nitro not ready yet; listener inert until initConnection()',
216
+ );
217
+ } else {
218
+ throw e;
219
+ }
230
220
  }
221
+
222
+ return {
223
+ remove: () => {
224
+ const stored = purchaseErrorListenerMap.get(listener);
225
+ if (stored) {
226
+ if (attached) {
227
+ try {
228
+ IAP.instance.removePurchaseErrorListener(stored);
229
+ } catch {}
230
+ }
231
+ purchaseErrorListenerMap.delete(listener);
232
+ }
233
+ },
234
+ };
231
235
  };
232
236
 
233
- /**
234
- * End connection to the store
235
- */
236
- export const endConnection = async (): Promise<boolean> => {
237
+ export const promotedProductListenerIOS = (
238
+ listener: (product: Product) => void,
239
+ ): EventSubscription => {
240
+ if (Platform.OS !== 'ios') {
241
+ console.warn(
242
+ 'promotedProductListenerIOS: This listener is only available on iOS',
243
+ );
244
+ return {remove: () => {}};
245
+ }
246
+
247
+ const wrappedListener: NitroPromotedProductListener = (nitroProduct) => {
248
+ if (validateNitroProduct(nitroProduct)) {
249
+ const convertedProduct = convertNitroProductToProduct(nitroProduct);
250
+ listener(convertedProduct);
251
+ } else {
252
+ console.error(
253
+ 'Invalid promoted product data received from native:',
254
+ nitroProduct,
255
+ );
256
+ }
257
+ };
258
+
259
+ promotedProductListenerMap.set(listener, wrappedListener);
260
+ let attached = false;
237
261
  try {
238
- // If never initialized, treat as ended
239
- if (!iapRef) return true;
240
- return await IAP.instance.endConnection();
241
- } catch (error) {
242
- console.error('Failed to end IAP connection:', error);
243
- throw error;
262
+ IAP.instance.addPromotedProductListenerIOS(wrappedListener);
263
+ attached = true;
264
+ } catch (e) {
265
+ const msg = toErrorMessage(e);
266
+ if (msg.includes('Nitro runtime not installed')) {
267
+ console.warn(
268
+ '[promotedProductListenerIOS] Nitro not ready yet; listener inert until initConnection()',
269
+ );
270
+ } else {
271
+ throw e;
272
+ }
244
273
  }
274
+
275
+ return {
276
+ remove: () => {
277
+ const wrapped = promotedProductListenerMap.get(listener);
278
+ if (wrapped) {
279
+ if (attached) {
280
+ try {
281
+ IAP.instance.removePromotedProductListenerIOS(wrapped);
282
+ } catch {}
283
+ }
284
+ promotedProductListenerMap.delete(listener);
285
+ }
286
+ },
287
+ };
245
288
  };
246
289
 
290
+ // ------------------------------
291
+ // Query API
292
+ // ------------------------------
293
+
247
294
  /**
248
295
  * Fetch products from the store
249
296
  * @param params - Product request configuration
@@ -260,47 +307,52 @@ export const endConnection = async (): Promise<boolean> => {
260
307
  * const subscriptions = await fetchProducts({ skus: ['sub1', 'sub2'], type: 'subs' });
261
308
  * ```
262
309
  */
263
- export const fetchProducts = async ({
264
- skus,
265
- type = 'in-app',
266
- }: ProductRequest): Promise<Product[]> => {
310
+ export const fetchProducts: QueryField<'fetchProducts'> = async (request) => {
311
+ const {skus, type} = request;
312
+
267
313
  try {
268
- if (!skus || skus.length === 0) {
314
+ if (!skus?.length) {
269
315
  throw new Error('No SKUs provided');
270
316
  }
271
317
 
272
318
  const normalizedType = normalizeProductQueryType(type);
273
319
 
274
- if (normalizedType === 'all') {
275
- const [inappNitro, subsNitro] = await Promise.all([
276
- IAP.instance.fetchProducts(skus, 'inapp'),
277
- IAP.instance.fetchProducts(skus, 'subs'),
278
- ]);
279
- const allNitro = [...inappNitro, ...subsNitro];
280
- const validAll = allNitro.filter(validateNitroProduct);
281
- if (validAll.length !== allNitro.length) {
320
+ const fetchAndConvert = async (
321
+ nitroType: ReturnType<typeof toNitroProductType> | 'all',
322
+ ) => {
323
+ const nitroProducts = await IAP.instance.fetchProducts(skus, nitroType);
324
+ const validProducts = nitroProducts.filter(validateNitroProduct);
325
+ if (validProducts.length !== nitroProducts.length) {
282
326
  console.warn(
283
- `[fetchProducts] Some products failed validation: ${allNitro.length - validAll.length} invalid`,
327
+ `[fetchProducts] Some products failed validation: ${nitroProducts.length - validProducts.length} invalid`,
284
328
  );
285
329
  }
286
- return validAll.map(convertNitroProductToProduct);
330
+ return validProducts.map(convertNitroProductToProduct);
331
+ };
332
+
333
+ if (normalizedType === 'all') {
334
+ const converted = await fetchAndConvert('all');
335
+ const productItems = converted.filter(
336
+ (item): item is Product => item.type === 'in-app',
337
+ );
338
+ const subscriptionItems = converted
339
+ .filter((item) => item.type === 'subs')
340
+ .map(convertProductToProductSubscription);
341
+
342
+ return [...productItems, ...subscriptionItems] as FetchProductsResult;
287
343
  }
288
344
 
289
- const nitroProducts = await IAP.instance.fetchProducts(
290
- skus,
345
+ const convertedProducts = await fetchAndConvert(
291
346
  toNitroProductType(normalizedType),
292
347
  );
293
348
 
294
- // Validate and convert NitroProducts to TypeScript Products
295
- const validProducts = nitroProducts.filter(validateNitroProduct);
296
- if (validProducts.length !== nitroProducts.length) {
297
- console.warn(
298
- `[fetchProducts] Some products failed validation: ${nitroProducts.length - validProducts.length} invalid`,
299
- );
349
+ if (normalizedType === 'subs') {
350
+ return convertedProducts.map(
351
+ convertProductToProductSubscription,
352
+ ) as FetchProductsResult;
300
353
  }
301
354
 
302
- const typedProducts = validProducts.map(convertNitroProductToProduct);
303
- return typedProducts;
355
+ return convertedProducts as FetchProductsResult;
304
356
  } catch (error) {
305
357
  console.error('[fetchProducts] Failed:', error);
306
358
  throw error;
@@ -308,68 +360,391 @@ export const fetchProducts = async ({
308
360
  };
309
361
 
310
362
  /**
311
- * Request a purchase for products or subscriptions
312
- * @param params - Purchase request configuration
313
- * @param params.request - Platform-specific purchase parameters
314
- * @param params.type - Type of purchase: 'in-app' for products (default) or 'subs' for subscriptions
363
+ * Get available purchases (purchased items not yet consumed/finished)
364
+ * @param params - Options for getting available purchases
365
+ * @param params.alsoPublishToEventListener - Whether to also publish to event listener
366
+ * @param params.onlyIncludeActiveItems - Whether to only include active items
315
367
  *
316
368
  * @example
317
369
  * ```typescript
318
- * // Product purchase
319
- * await requestPurchase({
320
- * request: {
321
- * ios: { sku: productId },
322
- * android: { skus: [productId] }
323
- * },
324
- * type: 'in-app'
325
- * });
326
- *
327
- * // Subscription purchase
328
- * await requestPurchase({
329
- * request: {
330
- * ios: { sku: subscriptionId },
331
- * android: {
332
- * skus: [subscriptionId],
333
- * subscriptionOffers: [{ sku: subscriptionId, offerToken: 'token' }]
334
- * }
335
- * },
336
- * type: 'subs'
370
+ * const purchases = await getAvailablePurchases({
371
+ * onlyIncludeActiveItemsIOS: true
337
372
  * });
338
373
  * ```
339
374
  */
375
+ export const getAvailablePurchases: QueryField<
376
+ 'getAvailablePurchases'
377
+ > = async (options) => {
378
+ const alsoPublishToEventListenerIOS = Boolean(
379
+ options?.alsoPublishToEventListenerIOS ?? false,
380
+ );
381
+ const onlyIncludeActiveItemsIOS = Boolean(
382
+ options?.onlyIncludeActiveItemsIOS ?? true,
383
+ );
384
+ try {
385
+ if (Platform.OS === 'ios') {
386
+ const nitroOptions: NitroAvailablePurchasesOptions = {
387
+ ios: {
388
+ alsoPublishToEventListenerIOS,
389
+ onlyIncludeActiveItemsIOS,
390
+ alsoPublishToEventListener: alsoPublishToEventListenerIOS,
391
+ onlyIncludeActiveItems: onlyIncludeActiveItemsIOS,
392
+ },
393
+ };
394
+ const nitroPurchases =
395
+ await IAP.instance.getAvailablePurchases(nitroOptions);
396
+
397
+ const validPurchases = nitroPurchases.filter(validateNitroPurchase);
398
+ if (validPurchases.length !== nitroPurchases.length) {
399
+ console.warn(
400
+ `[getAvailablePurchases] Some purchases failed validation: ${nitroPurchases.length - validPurchases.length} invalid`,
401
+ );
402
+ }
403
+
404
+ return validPurchases.map(convertNitroPurchaseToPurchase);
405
+ } else if (Platform.OS === 'android') {
406
+ // For Android, we need to call twice for inapp and subs
407
+ const inappNitroPurchases = await IAP.instance.getAvailablePurchases({
408
+ android: {type: 'inapp'},
409
+ });
410
+ const subsNitroPurchases = await IAP.instance.getAvailablePurchases({
411
+ android: {type: 'subs'},
412
+ });
413
+
414
+ // Validate and convert both sets of purchases
415
+ const allNitroPurchases = [...inappNitroPurchases, ...subsNitroPurchases];
416
+ const validPurchases = allNitroPurchases.filter(validateNitroPurchase);
417
+ if (validPurchases.length !== allNitroPurchases.length) {
418
+ console.warn(
419
+ `[getAvailablePurchases] Some Android purchases failed validation: ${allNitroPurchases.length - validPurchases.length} invalid`,
420
+ );
421
+ }
422
+
423
+ return validPurchases.map(convertNitroPurchaseToPurchase);
424
+ } else {
425
+ throw new Error('Unsupported platform');
426
+ }
427
+ } catch (error) {
428
+ console.error('Failed to get available purchases:', error);
429
+ throw error;
430
+ }
431
+ };
432
+
433
+ /**
434
+ * Request the promoted product from the App Store (iOS only)
435
+ * @returns Promise<Product | null> - The promoted product or null if none available
436
+ * @platform iOS
437
+ */
438
+ export const getPromotedProductIOS: QueryField<
439
+ 'getPromotedProductIOS'
440
+ > = async () => {
441
+ if (Platform.OS !== 'ios') {
442
+ return null;
443
+ }
444
+
445
+ try {
446
+ const nitroProduct = await IAP.instance.requestPromotedProductIOS();
447
+ if (!nitroProduct) {
448
+ return null;
449
+ }
450
+ const converted = convertNitroProductToProduct(nitroProduct);
451
+ return converted.platform === 'ios' ? (converted as ProductIOS) : null;
452
+ } catch (error) {
453
+ console.error('[getPromotedProductIOS] Failed:', error);
454
+ const errorJson = parseErrorStringToJsonObj(error);
455
+ throw new Error(errorJson.message);
456
+ }
457
+ };
458
+
459
+ export const requestPromotedProductIOS = getPromotedProductIOS;
460
+
461
+ export const getStorefrontIOS: QueryField<'getStorefrontIOS'> = async () => {
462
+ if (Platform.OS !== 'ios') {
463
+ throw new Error('getStorefrontIOS is only available on iOS');
464
+ }
465
+
466
+ try {
467
+ const storefront = await IAP.instance.getStorefrontIOS();
468
+ return storefront;
469
+ } catch (error) {
470
+ console.error('Failed to get storefront:', error);
471
+ throw error;
472
+ }
473
+ };
474
+
475
+ export const getAppTransactionIOS: QueryField<
476
+ 'getAppTransactionIOS'
477
+ > = async () => {
478
+ if (Platform.OS !== 'ios') {
479
+ throw new Error('getAppTransactionIOS is only available on iOS');
480
+ }
481
+
482
+ try {
483
+ const appTransaction = await IAP.instance.getAppTransactionIOS();
484
+ if (appTransaction == null) {
485
+ return null;
486
+ }
487
+
488
+ if (typeof appTransaction === 'string') {
489
+ const parsed = parseAppTransactionPayload(appTransaction);
490
+ if (parsed) {
491
+ return parsed;
492
+ }
493
+ throw new Error('Unable to parse app transaction payload');
494
+ }
495
+
496
+ if (typeof appTransaction === 'object' && appTransaction !== null) {
497
+ return appTransaction as AppTransaction;
498
+ }
499
+
500
+ return null;
501
+ } catch (error) {
502
+ console.error('Failed to get app transaction:', error);
503
+ throw error;
504
+ }
505
+ };
506
+
507
+ export const subscriptionStatusIOS: QueryField<
508
+ 'subscriptionStatusIOS'
509
+ > = async (sku) => {
510
+ if (Platform.OS !== 'ios') {
511
+ throw new Error('subscriptionStatusIOS is only available on iOS');
512
+ }
513
+
514
+ try {
515
+ const statuses = await IAP.instance.subscriptionStatusIOS(sku);
516
+ if (!Array.isArray(statuses)) return [];
517
+ return statuses
518
+ .filter((status): status is NitroSubscriptionStatus => status != null)
519
+ .map(convertNitroSubscriptionStatusToSubscriptionStatusIOS);
520
+ } catch (error) {
521
+ console.error('[subscriptionStatusIOS] Failed:', error);
522
+ const errorJson = parseErrorStringToJsonObj(error);
523
+ throw new Error(errorJson.message);
524
+ }
525
+ };
526
+
527
+ export const currentEntitlementIOS: QueryField<
528
+ 'currentEntitlementIOS'
529
+ > = async (sku) => {
530
+ if (Platform.OS !== 'ios') {
531
+ return null;
532
+ }
533
+
534
+ try {
535
+ const nitroPurchase = await IAP.instance.currentEntitlementIOS(sku);
536
+ if (nitroPurchase) {
537
+ const converted = convertNitroPurchaseToPurchase(nitroPurchase);
538
+ return converted.platform === 'ios' ? (converted as PurchaseIOS) : null;
539
+ }
540
+ return null;
541
+ } catch (error) {
542
+ console.error('[currentEntitlementIOS] Failed:', error);
543
+ const errorJson = parseErrorStringToJsonObj(error);
544
+ throw new Error(errorJson.message);
545
+ }
546
+ };
547
+
548
+ export const latestTransactionIOS: QueryField<'latestTransactionIOS'> = async (
549
+ sku,
550
+ ) => {
551
+ if (Platform.OS !== 'ios') {
552
+ return null;
553
+ }
554
+
555
+ try {
556
+ const nitroPurchase = await IAP.instance.latestTransactionIOS(sku);
557
+ if (nitroPurchase) {
558
+ const converted = convertNitroPurchaseToPurchase(nitroPurchase);
559
+ return converted.platform === 'ios' ? (converted as PurchaseIOS) : null;
560
+ }
561
+ return null;
562
+ } catch (error) {
563
+ console.error('[latestTransactionIOS] Failed:', error);
564
+ const errorJson = parseErrorStringToJsonObj(error);
565
+ throw new Error(errorJson.message);
566
+ }
567
+ };
568
+
569
+ export const getPendingTransactionsIOS: QueryField<
570
+ 'getPendingTransactionsIOS'
571
+ > = async () => {
572
+ if (Platform.OS !== 'ios') {
573
+ return [];
574
+ }
575
+
576
+ try {
577
+ const nitroPurchases = await IAP.instance.getPendingTransactionsIOS();
578
+ return nitroPurchases
579
+ .map(convertNitroPurchaseToPurchase)
580
+ .filter(
581
+ (purchase): purchase is PurchaseIOS => purchase.platform === 'ios',
582
+ );
583
+ } catch (error) {
584
+ console.error('[getPendingTransactionsIOS] Failed:', error);
585
+ const errorJson = parseErrorStringToJsonObj(error);
586
+ throw new Error(errorJson.message);
587
+ }
588
+ };
589
+
590
+ export const showManageSubscriptionsIOS: MutationField<
591
+ 'showManageSubscriptionsIOS'
592
+ > = async () => {
593
+ if (Platform.OS !== 'ios') {
594
+ return [];
595
+ }
596
+
597
+ try {
598
+ const nitroPurchases = await IAP.instance.showManageSubscriptionsIOS();
599
+ return nitroPurchases
600
+ .map(convertNitroPurchaseToPurchase)
601
+ .filter(
602
+ (purchase): purchase is PurchaseIOS => purchase.platform === 'ios',
603
+ );
604
+ } catch (error) {
605
+ console.error('[showManageSubscriptionsIOS] Failed:', error);
606
+ const errorJson = parseErrorStringToJsonObj(error);
607
+ throw new Error(errorJson.message);
608
+ }
609
+ };
610
+
611
+ export const isEligibleForIntroOfferIOS: QueryField<
612
+ 'isEligibleForIntroOfferIOS'
613
+ > = async (groupID) => {
614
+ if (Platform.OS !== 'ios') {
615
+ return false;
616
+ }
617
+
618
+ try {
619
+ return await IAP.instance.isEligibleForIntroOfferIOS(groupID);
620
+ } catch (error) {
621
+ console.error('[isEligibleForIntroOfferIOS] Failed:', error);
622
+ const errorJson = parseErrorStringToJsonObj(error);
623
+ throw new Error(errorJson.message);
624
+ }
625
+ };
626
+
627
+ export const getReceiptDataIOS: QueryField<'getReceiptDataIOS'> = async () => {
628
+ if (Platform.OS !== 'ios') {
629
+ throw new Error('getReceiptDataIOS is only available on iOS');
630
+ }
631
+
632
+ try {
633
+ return await IAP.instance.getReceiptDataIOS();
634
+ } catch (error) {
635
+ console.error('[getReceiptDataIOS] Failed:', error);
636
+ const errorJson = parseErrorStringToJsonObj(error);
637
+ throw new Error(errorJson.message);
638
+ }
639
+ };
640
+
641
+ export const isTransactionVerifiedIOS: QueryField<
642
+ 'isTransactionVerifiedIOS'
643
+ > = async (sku) => {
644
+ if (Platform.OS !== 'ios') {
645
+ return false;
646
+ }
647
+
648
+ try {
649
+ return await IAP.instance.isTransactionVerifiedIOS(sku);
650
+ } catch (error) {
651
+ console.error('[isTransactionVerifiedIOS] Failed:', error);
652
+ const errorJson = parseErrorStringToJsonObj(error);
653
+ throw new Error(errorJson.message);
654
+ }
655
+ };
656
+
657
+ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async (
658
+ sku,
659
+ ) => {
660
+ if (Platform.OS !== 'ios') {
661
+ return null;
662
+ }
663
+
664
+ try {
665
+ return await IAP.instance.getTransactionJwsIOS(sku);
666
+ } catch (error) {
667
+ console.error('[getTransactionJwsIOS] Failed:', error);
668
+ const errorJson = parseErrorStringToJsonObj(error);
669
+ throw new Error(errorJson.message);
670
+ }
671
+ };
672
+
673
+ // ------------------------------
674
+ // Mutation API
675
+ // ------------------------------
676
+
677
+ /**
678
+ * Initialize connection to the store
679
+ */
680
+ export const initConnection: MutationField<'initConnection'> = async () => {
681
+ try {
682
+ return await IAP.instance.initConnection();
683
+ } catch (error) {
684
+ console.error('Failed to initialize IAP connection:', error);
685
+ throw error;
686
+ }
687
+ };
688
+
689
+ /**
690
+ * End connection to the store
691
+ */
692
+ export const endConnection: MutationField<'endConnection'> = async () => {
693
+ try {
694
+ if (!iapRef) return true;
695
+ return await IAP.instance.endConnection();
696
+ } catch (error) {
697
+ console.error('Failed to end IAP connection:', error);
698
+ throw error;
699
+ }
700
+ };
701
+
702
+ export const restorePurchases: MutationField<'restorePurchases'> = async () => {
703
+ try {
704
+ if (Platform.OS === 'ios') {
705
+ await syncIOS();
706
+ }
707
+
708
+ await getAvailablePurchases({
709
+ alsoPublishToEventListenerIOS: false,
710
+ onlyIncludeActiveItemsIOS: true,
711
+ });
712
+ } catch (error) {
713
+ console.error('Failed to restore purchases:', error);
714
+ throw error;
715
+ }
716
+ };
717
+
340
718
  /**
341
719
  * Request a purchase for products or subscriptions
342
720
  * ⚠️ Important: This is an event-based operation, not promise-based.
343
721
  * Listen for events through purchaseUpdatedListener or purchaseErrorListener.
344
- * @param params - Purchase request configuration
345
- * @param params.request - Platform-specific request parameters
346
- * @param params.type - Type of purchase (defaults to in-app)
347
722
  */
348
- export const requestPurchase = async (
349
- params: RequestPurchaseProps,
350
- ): Promise<RequestPurchaseResult> => {
723
+ export const requestPurchase: MutationField<'requestPurchase'> = async (
724
+ request,
725
+ ) => {
351
726
  try {
352
- const normalizedType = normalizeProductQueryType(params.type);
727
+ const {request: platformRequest, type} = request;
728
+ const normalizedType = normalizeProductQueryType(type ?? 'in-app');
353
729
  const isSubs = isSubscriptionQuery(normalizedType);
354
- const request = params.request as
730
+ const perPlatformRequest = platformRequest as
355
731
  | RequestPurchasePropsByPlatforms
356
732
  | RequestSubscriptionPropsByPlatforms
357
733
  | undefined;
358
734
 
359
- if (!request) {
735
+ if (!perPlatformRequest) {
360
736
  throw new Error('Missing purchase request configuration');
361
737
  }
362
738
 
363
- // Validate platform-specific requests
364
739
  if (Platform.OS === 'ios') {
365
- const iosRequest = request.ios;
740
+ const iosRequest = perPlatformRequest.ios;
366
741
  if (!iosRequest?.sku) {
367
742
  throw new Error(
368
743
  'Invalid request for iOS. The `sku` property is required.',
369
744
  );
370
745
  }
371
746
  } else if (Platform.OS === 'android') {
372
- const androidRequest = request.android;
747
+ const androidRequest = perPlatformRequest.android;
373
748
  if (!androidRequest?.skus?.length) {
374
749
  throw new Error(
375
750
  'Invalid request for Android. The `skus` property is required and must be a non-empty array.',
@@ -381,10 +756,10 @@ export const requestPurchase = async (
381
756
 
382
757
  const unifiedRequest: NitroPurchaseRequest = {};
383
758
 
384
- if (Platform.OS === 'ios' && request.ios) {
759
+ if (Platform.OS === 'ios' && perPlatformRequest.ios) {
385
760
  const iosRequest = isSubs
386
- ? (request.ios as RequestSubscriptionIosProps)
387
- : (request.ios as RequestPurchaseIosProps);
761
+ ? (perPlatformRequest.ios as RequestSubscriptionIosProps)
762
+ : (perPlatformRequest.ios as RequestPurchaseIosProps);
388
763
 
389
764
  const iosPayload: NonNullable<NitroPurchaseRequest['ios']> = {
390
765
  sku: iosRequest.sku,
@@ -415,10 +790,10 @@ export const requestPurchase = async (
415
790
  unifiedRequest.ios = iosPayload;
416
791
  }
417
792
 
418
- if (Platform.OS === 'android' && request.android) {
793
+ if (Platform.OS === 'android' && perPlatformRequest.android) {
419
794
  const androidRequest = isSubs
420
- ? (request.android as RequestSubscriptionAndroidProps)
421
- : (request.android as RequestPurchaseAndroidProps);
795
+ ? (perPlatformRequest.android as RequestSubscriptionAndroidProps)
796
+ : (perPlatformRequest.android as RequestPurchaseAndroidProps);
422
797
 
423
798
  const androidPayload: NonNullable<NitroPurchaseRequest['android']> = {
424
799
  skus: androidRequest.skus,
@@ -468,78 +843,12 @@ export const requestPurchase = async (
468
843
  }
469
844
  };
470
845
 
471
- /**
472
- * Get available purchases (purchased items not yet consumed/finished)
473
- * @param params - Options for getting available purchases
474
- * @param params.alsoPublishToEventListener - Whether to also publish to event listener
475
- * @param params.onlyIncludeActiveItems - Whether to only include active items
476
- *
477
- * @example
478
- * ```typescript
479
- * const purchases = await getAvailablePurchases({
480
- * onlyIncludeActiveItemsIOS: true
481
- * });
482
- * ```
483
- */
484
- export const getAvailablePurchases = async ({
485
- alsoPublishToEventListenerIOS = false,
486
- onlyIncludeActiveItemsIOS = true,
487
- }: PurchaseOptions = {}): Promise<Purchase[]> => {
488
- try {
489
- if (Platform.OS === 'ios') {
490
- const iosAlsoPublish = Boolean(alsoPublishToEventListenerIOS);
491
- const iosOnlyActive = Boolean(onlyIncludeActiveItemsIOS);
492
- const options: NitroAvailablePurchasesOptions = {
493
- ios: {
494
- alsoPublishToEventListenerIOS: iosAlsoPublish,
495
- onlyIncludeActiveItemsIOS: iosOnlyActive,
496
- alsoPublishToEventListener: iosAlsoPublish,
497
- onlyIncludeActiveItems: iosOnlyActive,
498
- },
499
- };
500
- const nitroPurchases = await IAP.instance.getAvailablePurchases(options);
501
-
502
- const validPurchases = nitroPurchases.filter(validateNitroPurchase);
503
- if (validPurchases.length !== nitroPurchases.length) {
504
- console.warn(
505
- `[getAvailablePurchases] Some purchases failed validation: ${nitroPurchases.length - validPurchases.length} invalid`,
506
- );
507
- }
508
-
509
- return validPurchases.map(convertNitroPurchaseToPurchase);
510
- } else if (Platform.OS === 'android') {
511
- // For Android, we need to call twice for inapp and subs
512
- const inappNitroPurchases = await IAP.instance.getAvailablePurchases({
513
- android: {type: 'inapp'},
514
- });
515
- const subsNitroPurchases = await IAP.instance.getAvailablePurchases({
516
- android: {type: 'subs'},
517
- });
518
-
519
- // Validate and convert both sets of purchases
520
- const allNitroPurchases = [...inappNitroPurchases, ...subsNitroPurchases];
521
- const validPurchases = allNitroPurchases.filter(validateNitroPurchase);
522
- if (validPurchases.length !== allNitroPurchases.length) {
523
- console.warn(
524
- `[getAvailablePurchases] Some Android purchases failed validation: ${allNitroPurchases.length - validPurchases.length} invalid`,
525
- );
526
- }
527
-
528
- return validPurchases.map(convertNitroPurchaseToPurchase);
529
- } else {
530
- throw new Error('Unsupported platform');
531
- }
532
- } catch (error) {
533
- console.error('Failed to get available purchases:', error);
534
- throw error;
535
- }
536
- };
537
-
538
846
  /**
539
847
  * Finish a transaction (consume or acknowledge)
540
848
  * @param params - Transaction finish parameters
541
849
  * @param params.purchase - The purchase to finish
542
850
  * @param params.isConsumable - Whether this is a consumable product (Android only)
851
+ * @returns Promise<void> - Resolves when the transaction is successfully finished
543
852
  *
544
853
  * @example
545
854
  * ```typescript
@@ -549,10 +858,10 @@ export const getAvailablePurchases = async ({
549
858
  * });
550
859
  * ```
551
860
  */
552
- export const finishTransaction = async ({
553
- purchase,
554
- isConsumable = false,
555
- }: FinishTransactionParams): Promise<NitroPurchaseResult | boolean> => {
861
+ export const finishTransaction: MutationField<'finishTransaction'> = async (
862
+ args,
863
+ ) => {
864
+ const {purchase, isConsumable} = args;
556
865
  try {
557
866
  let params: NitroFinishTransactionParamsInternal;
558
867
  if (Platform.OS === 'ios') {
@@ -565,8 +874,7 @@ export const finishTransaction = async ({
565
874
  },
566
875
  };
567
876
  } else if (Platform.OS === 'android') {
568
- const androidPurchase = purchase as PurchaseAndroid;
569
- const token = androidPurchase.purchaseToken;
877
+ const token = purchase.purchaseToken ?? undefined;
570
878
 
571
879
  if (!token) {
572
880
  throw new Error('purchaseToken required to finish Android transaction');
@@ -575,7 +883,7 @@ export const finishTransaction = async ({
575
883
  params = {
576
884
  android: {
577
885
  purchaseToken: token,
578
- isConsumable,
886
+ isConsumable: isConsumable ?? false,
579
887
  },
580
888
  };
581
889
  } else {
@@ -583,13 +891,11 @@ export const finishTransaction = async ({
583
891
  }
584
892
 
585
893
  const result = await IAP.instance.finishTransaction(params);
586
-
587
- // Handle variant return type
588
- if (typeof result === 'boolean') {
589
- return result;
894
+ const success = getSuccessFromPurchaseVariant(result, 'finishTransaction');
895
+ if (!success) {
896
+ throw new Error('Failed to finish transaction');
590
897
  }
591
- // It's a PurchaseResult
592
- return result as NitroPurchaseResult;
898
+ return;
593
899
  } catch (error) {
594
900
  // If iOS transaction has already been auto-finished natively, treat as success
595
901
  if (Platform.OS === 'ios') {
@@ -601,7 +907,7 @@ export const finishTransaction = async ({
601
907
  code === 'E_ITEM_UNAVAILABLE'
602
908
  ) {
603
909
  // Consider already finished
604
- return true;
910
+ return;
605
911
  }
606
912
  }
607
913
  console.error('Failed to finish transaction:', error);
@@ -612,15 +918,16 @@ export const finishTransaction = async ({
612
918
  /**
613
919
  * Acknowledge a purchase (Android only)
614
920
  * @param purchaseToken - The purchase token to acknowledge
921
+ * @returns Promise<boolean> - Indicates whether the acknowledgement succeeded
615
922
  *
616
923
  * @example
617
924
  * ```typescript
618
925
  * await acknowledgePurchaseAndroid('purchase_token_here');
619
926
  * ```
620
927
  */
621
- export const acknowledgePurchaseAndroid = async (
622
- purchaseToken: string,
623
- ): Promise<NitroPurchaseResult> => {
928
+ export const acknowledgePurchaseAndroid: MutationField<
929
+ 'acknowledgePurchaseAndroid'
930
+ > = async (purchaseToken) => {
624
931
  try {
625
932
  if (Platform.OS !== 'android') {
626
933
  throw new Error(
@@ -634,18 +941,7 @@ export const acknowledgePurchaseAndroid = async (
634
941
  isConsumable: false,
635
942
  },
636
943
  });
637
-
638
- // Result is a variant, extract PurchaseResult
639
- if (typeof result === 'boolean') {
640
- // This shouldn't happen for Android, but handle it
641
- return {
642
- responseCode: 0,
643
- code: '0',
644
- message: 'Success',
645
- purchaseToken,
646
- };
647
- }
648
- return result as NitroPurchaseResult;
944
+ return getSuccessFromPurchaseVariant(result, 'acknowledgePurchaseAndroid');
649
945
  } catch (error) {
650
946
  console.error('Failed to acknowledge purchase Android:', error);
651
947
  throw error;
@@ -655,15 +951,16 @@ export const acknowledgePurchaseAndroid = async (
655
951
  /**
656
952
  * Consume a purchase (Android only)
657
953
  * @param purchaseToken - The purchase token to consume
954
+ * @returns Promise<boolean> - Indicates whether the consumption succeeded
658
955
  *
659
956
  * @example
660
957
  * ```typescript
661
958
  * await consumePurchaseAndroid('purchase_token_here');
662
959
  * ```
663
960
  */
664
- export const consumePurchaseAndroid = async (
665
- purchaseToken: string,
666
- ): Promise<NitroPurchaseResult> => {
961
+ export const consumePurchaseAndroid: MutationField<
962
+ 'consumePurchaseAndroid'
963
+ > = async (purchaseToken) => {
667
964
  try {
668
965
  if (Platform.OS !== 'android') {
669
966
  throw new Error('consumePurchaseAndroid is only available on Android');
@@ -675,254 +972,13 @@ export const consumePurchaseAndroid = async (
675
972
  isConsumable: true,
676
973
  },
677
974
  });
678
-
679
- // Result is a variant, extract PurchaseResult
680
- if (typeof result === 'boolean') {
681
- // This shouldn't happen for Android, but handle it
682
- return {
683
- responseCode: 0,
684
- code: '0',
685
- message: 'Success',
686
- purchaseToken,
687
- };
688
- }
689
- return result as NitroPurchaseResult;
975
+ return getSuccessFromPurchaseVariant(result, 'consumePurchaseAndroid');
690
976
  } catch (error) {
691
977
  console.error('Failed to consume purchase Android:', error);
692
978
  throw error;
693
979
  }
694
980
  };
695
981
 
696
- // ============================================================================
697
- // EVENT LISTENERS
698
- // ============================================================================
699
-
700
- // Store wrapped listeners for proper removal
701
- const purchaseUpdatedListenerMap = new WeakMap<
702
- (purchase: Purchase) => void,
703
- NitroPurchaseListener
704
- >();
705
- const purchaseErrorListenerMap = new WeakMap<
706
- (error: PurchaseError) => void,
707
- NitroPurchaseErrorListener
708
- >();
709
- const promotedProductListenerMap = new WeakMap<
710
- (product: Product) => void,
711
- NitroPromotedProductListener
712
- >();
713
-
714
- /**
715
- * Purchase updated event listener
716
- * Fired when a purchase is successful or when a pending purchase is completed.
717
- *
718
- * @param listener - Function to call when a purchase is updated
719
- * @returns EventSubscription object with remove method
720
- *
721
- * @example
722
- * ```typescript
723
- * const subscription = purchaseUpdatedListener((purchase) => {
724
- * console.log('Purchase successful:', purchase);
725
- * // 1. Validate receipt with backend
726
- * // 2. Deliver content to user
727
- * // 3. Call finishTransaction to acknowledge
728
- * });
729
- *
730
- * // Later, clean up
731
- * subscription.remove();
732
- * ```
733
- */
734
- export const purchaseUpdatedListener = (
735
- listener: (purchase: Purchase) => void,
736
- ): EventSubscription => {
737
- // Wrap the listener to convert NitroPurchase to Purchase
738
- const wrappedListener: NitroPurchaseListener = (nitroPurchase) => {
739
- if (validateNitroPurchase(nitroPurchase)) {
740
- const convertedPurchase = convertNitroPurchaseToPurchase(nitroPurchase);
741
- listener(convertedPurchase);
742
- } else {
743
- console.error(
744
- 'Invalid purchase data received from native:',
745
- nitroPurchase,
746
- );
747
- }
748
- };
749
-
750
- // Store the wrapped listener for removal
751
- purchaseUpdatedListenerMap.set(listener, wrappedListener);
752
- let attached = false;
753
- try {
754
- IAP.instance.addPurchaseUpdatedListener(wrappedListener);
755
- attached = true;
756
- } catch (e) {
757
- const msg = toErrorMessage(e);
758
- if (msg.includes('Nitro runtime not installed')) {
759
- console.warn(
760
- '[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()',
761
- );
762
- } else {
763
- throw e;
764
- }
765
- }
766
-
767
- return {
768
- remove: () => {
769
- const wrapped = purchaseUpdatedListenerMap.get(listener);
770
- if (wrapped) {
771
- if (attached) {
772
- try {
773
- IAP.instance.removePurchaseUpdatedListener(wrapped);
774
- } catch {}
775
- }
776
- purchaseUpdatedListenerMap.delete(listener);
777
- }
778
- },
779
- };
780
- };
781
-
782
- /**
783
- * Purchase error event listener
784
- * Fired when a purchase fails or is cancelled by the user.
785
- *
786
- * @param listener - Function to call when a purchase error occurs
787
- * @returns EventSubscription object with remove method
788
- *
789
- * @example
790
- * ```typescript
791
- * const subscription = purchaseErrorListener((error) => {
792
- * switch (error.code) {
793
- * case 'E_USER_CANCELLED':
794
- * // User cancelled - no action needed
795
- * break;
796
- * case 'E_ITEM_UNAVAILABLE':
797
- * // Product not available
798
- * break;
799
- * case 'E_NETWORK_ERROR':
800
- * // Retry with backoff
801
- * break;
802
- * }
803
- * });
804
- *
805
- * // Later, clean up
806
- * subscription.remove();
807
- * ```
808
- */
809
- export const purchaseErrorListener = (
810
- listener: (error: PurchaseError) => void,
811
- ): EventSubscription => {
812
- const wrapped: NitroPurchaseErrorListener = (error) => {
813
- listener({
814
- code: normalizeErrorCodeFromNative(error.code),
815
- message: error.message,
816
- productId: undefined,
817
- });
818
- };
819
-
820
- purchaseErrorListenerMap.set(listener, wrapped);
821
- let attached = false;
822
- try {
823
- IAP.instance.addPurchaseErrorListener(wrapped);
824
- attached = true;
825
- } catch (e) {
826
- const msg = toErrorMessage(e);
827
- if (msg.includes('Nitro runtime not installed')) {
828
- console.warn(
829
- '[purchaseErrorListener] Nitro not ready yet; listener inert until initConnection()',
830
- );
831
- } else {
832
- throw e;
833
- }
834
- }
835
-
836
- return {
837
- remove: () => {
838
- const stored = purchaseErrorListenerMap.get(listener);
839
- if (stored) {
840
- if (attached) {
841
- try {
842
- IAP.instance.removePurchaseErrorListener(stored);
843
- } catch {}
844
- }
845
- purchaseErrorListenerMap.delete(listener);
846
- }
847
- },
848
- };
849
- };
850
-
851
- /**
852
- * iOS-only listener for App Store promoted product events.
853
- * Fired when a user clicks on a promoted in-app purchase in the App Store.
854
- *
855
- * @param listener - Callback function that receives the promoted product
856
- * @returns EventSubscription object with remove method
857
- *
858
- * @example
859
- * ```typescript
860
- * const subscription = promotedProductListenerIOS((product) => {
861
- * console.log('Promoted product:', product);
862
- * // Trigger purchase flow for the promoted product
863
- * });
864
- *
865
- * // Later, clean up
866
- * subscription.remove();
867
- * ```
868
- *
869
- * @platform iOS
870
- */
871
- export const promotedProductListenerIOS = (
872
- listener: (product: Product) => void,
873
- ): EventSubscription => {
874
- if (Platform.OS !== 'ios') {
875
- console.warn(
876
- 'promotedProductListenerIOS: This listener is only available on iOS',
877
- );
878
- return {remove: () => {}};
879
- }
880
-
881
- // Wrap the listener to convert NitroProduct to Product
882
- const wrappedListener: NitroPromotedProductListener = (nitroProduct) => {
883
- if (validateNitroProduct(nitroProduct)) {
884
- const convertedProduct = convertNitroProductToProduct(nitroProduct);
885
- listener(convertedProduct);
886
- } else {
887
- console.error(
888
- 'Invalid promoted product data received from native:',
889
- nitroProduct,
890
- );
891
- }
892
- };
893
-
894
- // Store the wrapped listener for removal
895
- promotedProductListenerMap.set(listener, wrappedListener);
896
- let attached = false;
897
- try {
898
- IAP.instance.addPromotedProductListenerIOS(wrappedListener);
899
- attached = true;
900
- } catch (e) {
901
- const msg = toErrorMessage(e);
902
- if (msg.includes('Nitro runtime not installed')) {
903
- console.warn(
904
- '[promotedProductListenerIOS] Nitro not ready yet; listener inert until initConnection()',
905
- );
906
- } else {
907
- throw e;
908
- }
909
- }
910
-
911
- return {
912
- remove: () => {
913
- const wrapped = promotedProductListenerMap.get(listener);
914
- if (wrapped) {
915
- if (attached) {
916
- try {
917
- IAP.instance.removePromotedProductListenerIOS(wrapped);
918
- } catch {}
919
- }
920
- promotedProductListenerMap.delete(listener);
921
- }
922
- },
923
- };
924
- };
925
-
926
982
  // ============================================================================
927
983
  // iOS-SPECIFIC FUNCTIONS
928
984
  // ============================================================================
@@ -933,19 +989,25 @@ export const promotedProductListenerIOS = (
933
989
  * @param androidOptions - Android-specific validation options (required for Android)
934
990
  * @returns Promise<ReceiptValidationResultIOS | ReceiptValidationResultAndroid> - Platform-specific receipt validation result
935
991
  */
936
- export const validateReceipt = async (
937
- sku: string,
938
- androidOptions?: {
939
- packageName: string;
940
- productToken: string;
941
- accessToken: string;
942
- isSub?: boolean;
943
- },
944
- ): Promise<import('./types').ReceiptValidationResult> => {
992
+ export const validateReceipt: MutationField<'validateReceipt'> = async (
993
+ options,
994
+ ) => {
995
+ const {sku, androidOptions} = options;
945
996
  try {
997
+ const normalizedAndroidOptions =
998
+ androidOptions != null
999
+ ? {
1000
+ ...androidOptions,
1001
+ isSub:
1002
+ androidOptions.isSub == null
1003
+ ? undefined
1004
+ : Boolean(androidOptions.isSub),
1005
+ }
1006
+ : undefined;
1007
+
946
1008
  const params: NitroReceiptValidationParams = {
947
1009
  sku,
948
- androidOptions,
1010
+ androidOptions: normalizedAndroidOptions,
949
1011
  };
950
1012
 
951
1013
  const nitroResult = await IAP.instance.validateReceipt(params);
@@ -999,13 +1061,14 @@ export const validateReceipt = async (
999
1061
  * @returns Promise<boolean>
1000
1062
  * @platform iOS
1001
1063
  */
1002
- export const syncIOS = async (): Promise<boolean> => {
1064
+ export const syncIOS: MutationField<'syncIOS'> = async () => {
1003
1065
  if (Platform.OS !== 'ios') {
1004
1066
  throw new Error('syncIOS is only available on iOS');
1005
1067
  }
1006
1068
 
1007
1069
  try {
1008
- return await IAP.instance.syncIOS();
1070
+ const result = await IAP.instance.syncIOS();
1071
+ return Boolean(result);
1009
1072
  } catch (error) {
1010
1073
  console.error('[syncIOS] Failed:', error);
1011
1074
  const errorJson = parseErrorStringToJsonObj(error);
@@ -1013,41 +1076,21 @@ export const syncIOS = async (): Promise<boolean> => {
1013
1076
  }
1014
1077
  };
1015
1078
 
1016
- /**
1017
- * Request the promoted product from the App Store (iOS only)
1018
- * @returns Promise<Product | null> - The promoted product or null if none available
1019
- * @platform iOS
1020
- */
1021
- export const requestPromotedProductIOS = async (): Promise<Product | null> => {
1022
- if (Platform.OS !== 'ios') {
1023
- return null;
1024
- }
1025
-
1026
- try {
1027
- const nitroProduct = await IAP.instance.requestPromotedProductIOS();
1028
- if (nitroProduct) {
1029
- return convertNitroProductToProduct(nitroProduct);
1030
- }
1031
- return null;
1032
- } catch (error) {
1033
- console.error('[getPromotedProductIOS] Failed:', error);
1034
- const errorJson = parseErrorStringToJsonObj(error);
1035
- throw new Error(errorJson.message);
1036
- }
1037
- };
1038
-
1039
1079
  /**
1040
1080
  * Present the code redemption sheet for offer codes (iOS only)
1041
- * @returns Promise<boolean> - True if the sheet was presented successfully
1081
+ * @returns Promise<boolean> - Indicates whether the redemption sheet was presented
1042
1082
  * @platform iOS
1043
1083
  */
1044
- export const presentCodeRedemptionSheetIOS = async (): Promise<boolean> => {
1084
+ export const presentCodeRedemptionSheetIOS: MutationField<
1085
+ 'presentCodeRedemptionSheetIOS'
1086
+ > = async () => {
1045
1087
  if (Platform.OS !== 'ios') {
1046
1088
  return false;
1047
1089
  }
1048
1090
 
1049
1091
  try {
1050
- return await IAP.instance.presentCodeRedemptionSheetIOS();
1092
+ const result = await IAP.instance.presentCodeRedemptionSheetIOS();
1093
+ return Boolean(result);
1051
1094
  } catch (error) {
1052
1095
  console.error('[presentCodeRedemptionSheetIOS] Failed:', error);
1053
1096
  const errorJson = parseErrorStringToJsonObj(error);
@@ -1057,35 +1100,53 @@ export const presentCodeRedemptionSheetIOS = async (): Promise<boolean> => {
1057
1100
 
1058
1101
  /**
1059
1102
  * Buy promoted product on iOS
1060
- * @returns Promise<void>
1103
+ * @returns Promise<boolean> - true when the request triggers successfully
1061
1104
  * @platform iOS
1062
1105
  */
1063
- export const buyPromotedProductIOS = async (): Promise<void> => {
1106
+ export const requestPurchaseOnPromotedProductIOS: MutationField<
1107
+ 'requestPurchaseOnPromotedProductIOS'
1108
+ > = async () => {
1064
1109
  if (Platform.OS !== 'ios') {
1065
- throw new Error('buyPromotedProductIOS is only available on iOS');
1110
+ throw new Error(
1111
+ 'requestPurchaseOnPromotedProductIOS is only available on iOS',
1112
+ );
1066
1113
  }
1067
1114
 
1068
1115
  try {
1069
1116
  await IAP.instance.buyPromotedProductIOS();
1117
+ const pending = await IAP.instance.getPendingTransactionsIOS();
1118
+ const latest = pending.find((purchase) => purchase != null);
1119
+ if (!latest) {
1120
+ throw new Error('No promoted purchase available after request');
1121
+ }
1122
+
1123
+ const converted = convertNitroPurchaseToPurchase(latest);
1124
+ if (converted.platform !== 'ios') {
1125
+ throw new Error('Promoted purchase result not available for iOS');
1126
+ }
1127
+
1128
+ return true;
1070
1129
  } catch (error) {
1071
- console.error('[buyPromotedProductIOS] Failed:', error);
1072
- const errorJson = parseErrorStringToJsonObj(error);
1073
- throw new Error(errorJson.message);
1130
+ console.error('[requestPurchaseOnPromotedProductIOS] Failed:', error);
1131
+ throw error;
1074
1132
  }
1075
1133
  };
1076
1134
 
1077
1135
  /**
1078
1136
  * Clear unfinished transactions on iOS
1079
- * @returns Promise<void>
1137
+ * @returns Promise<boolean>
1080
1138
  * @platform iOS
1081
1139
  */
1082
- export const clearTransactionIOS = async (): Promise<void> => {
1140
+ export const clearTransactionIOS: MutationField<
1141
+ 'clearTransactionIOS'
1142
+ > = async () => {
1083
1143
  if (Platform.OS !== 'ios') {
1084
- return;
1144
+ return false;
1085
1145
  }
1086
1146
 
1087
1147
  try {
1088
1148
  await IAP.instance.clearTransactionIOS();
1149
+ return true;
1089
1150
  } catch (error) {
1090
1151
  console.error('[clearTransactionIOS] Failed:', error);
1091
1152
  const errorJson = parseErrorStringToJsonObj(error);
@@ -1099,15 +1160,16 @@ export const clearTransactionIOS = async (): Promise<void> => {
1099
1160
  * @returns Promise<string | null> - The refund status or null if not available
1100
1161
  * @platform iOS
1101
1162
  */
1102
- export const beginRefundRequestIOS = async (
1103
- sku: string,
1104
- ): Promise<string | null> => {
1163
+ export const beginRefundRequestIOS: MutationField<
1164
+ 'beginRefundRequestIOS'
1165
+ > = async (sku) => {
1105
1166
  if (Platform.OS !== 'ios') {
1106
- return null;
1167
+ throw new Error('beginRefundRequestIOS is only available on iOS');
1107
1168
  }
1108
1169
 
1109
1170
  try {
1110
- return await IAP.instance.beginRefundRequestIOS(sku);
1171
+ const status = await IAP.instance.beginRefundRequestIOS(sku);
1172
+ return status ?? null;
1111
1173
  } catch (error) {
1112
1174
  console.error('[beginRefundRequestIOS] Failed:', error);
1113
1175
  const errorJson = parseErrorStringToJsonObj(error);
@@ -1122,203 +1184,51 @@ export const beginRefundRequestIOS = async (
1122
1184
  * @throws Error when called on non-iOS platforms or when IAP is not initialized
1123
1185
  * @platform iOS
1124
1186
  */
1125
- export const subscriptionStatusIOS = async (
1126
- sku: string,
1127
- ): Promise<SubscriptionStatusIOS[]> => {
1128
- if (Platform.OS !== 'ios') {
1129
- throw new Error('subscriptionStatusIOS is only available on iOS');
1130
- }
1131
-
1132
- try {
1133
- const statuses = await IAP.instance.subscriptionStatusIOS(sku);
1134
- if (!Array.isArray(statuses)) return [];
1135
- return statuses
1136
- .filter((status): status is NitroSubscriptionStatus => status != null)
1137
- .map(convertNitroSubscriptionStatusToSubscriptionStatusIOS);
1138
- } catch (error) {
1139
- console.error('[subscriptionStatusIOS] Failed:', error);
1140
- const errorJson = parseErrorStringToJsonObj(error);
1141
- throw new Error(errorJson.message);
1142
- }
1143
- };
1144
-
1145
1187
  /**
1146
1188
  * Get current entitlement for a product (iOS only)
1147
1189
  * @param sku - The product SKU
1148
1190
  * @returns Promise<Purchase | null> - Current entitlement or null
1149
1191
  * @platform iOS
1150
1192
  */
1151
- export const currentEntitlementIOS = async (
1152
- sku: string,
1153
- ): Promise<Purchase | null> => {
1154
- if (Platform.OS !== 'ios') {
1155
- return null;
1156
- }
1157
-
1158
- try {
1159
- const nitroPurchase = await IAP.instance.currentEntitlementIOS(sku);
1160
- if (nitroPurchase) {
1161
- return convertNitroPurchaseToPurchase(nitroPurchase);
1162
- }
1163
- return null;
1164
- } catch (error) {
1165
- console.error('[currentEntitlementIOS] Failed:', error);
1166
- const errorJson = parseErrorStringToJsonObj(error);
1167
- throw new Error(errorJson.message);
1168
- }
1169
- };
1170
-
1171
1193
  /**
1172
1194
  * Get latest transaction for a product (iOS only)
1173
1195
  * @param sku - The product SKU
1174
1196
  * @returns Promise<Purchase | null> - Latest transaction or null
1175
1197
  * @platform iOS
1176
1198
  */
1177
- export const latestTransactionIOS = async (
1178
- sku: string,
1179
- ): Promise<Purchase | null> => {
1180
- if (Platform.OS !== 'ios') {
1181
- return null;
1182
- }
1183
-
1184
- try {
1185
- const nitroPurchase = await IAP.instance.latestTransactionIOS(sku);
1186
- if (nitroPurchase) {
1187
- return convertNitroPurchaseToPurchase(nitroPurchase);
1188
- }
1189
- return null;
1190
- } catch (error) {
1191
- console.error('[latestTransactionIOS] Failed:', error);
1192
- const errorJson = parseErrorStringToJsonObj(error);
1193
- throw new Error(errorJson.message);
1194
- }
1195
- };
1196
-
1197
1199
  /**
1198
1200
  * Get pending transactions (iOS only)
1199
1201
  * @returns Promise<Purchase[]> - Array of pending transactions
1200
1202
  * @platform iOS
1201
1203
  */
1202
- export const getPendingTransactionsIOS = async (): Promise<Purchase[]> => {
1203
- if (Platform.OS !== 'ios') {
1204
- return [];
1205
- }
1206
-
1207
- try {
1208
- const nitroPurchases = await IAP.instance.getPendingTransactionsIOS();
1209
- return nitroPurchases.map(convertNitroPurchaseToPurchase);
1210
- } catch (error) {
1211
- console.error('[getPendingTransactionsIOS] Failed:', error);
1212
- const errorJson = parseErrorStringToJsonObj(error);
1213
- throw new Error(errorJson.message);
1214
- }
1215
- };
1216
-
1217
1204
  /**
1218
1205
  * Show manage subscriptions screen (iOS only)
1219
1206
  * @returns Promise<Purchase[]> - Subscriptions where auto-renewal status changed
1220
1207
  * @platform iOS
1221
1208
  */
1222
- export const showManageSubscriptionsIOS = async (): Promise<Purchase[]> => {
1223
- if (Platform.OS !== 'ios') {
1224
- return [];
1225
- }
1226
-
1227
- try {
1228
- const nitroPurchases = await IAP.instance.showManageSubscriptionsIOS();
1229
- return nitroPurchases.map(convertNitroPurchaseToPurchase);
1230
- } catch (error) {
1231
- console.error('[showManageSubscriptionsIOS] Failed:', error);
1232
- const errorJson = parseErrorStringToJsonObj(error);
1233
- throw new Error(errorJson.message);
1234
- }
1235
- };
1236
-
1237
1209
  /**
1238
1210
  * Check if user is eligible for intro offer (iOS only)
1239
1211
  * @param groupID - The subscription group ID
1240
1212
  * @returns Promise<boolean> - Eligibility status
1241
1213
  * @platform iOS
1242
1214
  */
1243
- export const isEligibleForIntroOfferIOS = async (
1244
- groupID: string,
1245
- ): Promise<boolean> => {
1246
- if (Platform.OS !== 'ios') {
1247
- return false;
1248
- }
1249
-
1250
- try {
1251
- return await IAP.instance.isEligibleForIntroOfferIOS(groupID);
1252
- } catch (error) {
1253
- console.error('[isEligibleForIntroOfferIOS] Failed:', error);
1254
- const errorJson = parseErrorStringToJsonObj(error);
1255
- throw new Error(errorJson.message);
1256
- }
1257
- };
1258
-
1259
1215
  /**
1260
1216
  * Get receipt data (iOS only)
1261
1217
  * @returns Promise<string> - Base64 encoded receipt data
1262
1218
  * @platform iOS
1263
1219
  */
1264
- export const getReceiptDataIOS = async (): Promise<string> => {
1265
- if (Platform.OS !== 'ios') {
1266
- throw new Error('getReceiptDataIOS is only available on iOS');
1267
- }
1268
-
1269
- try {
1270
- return await IAP.instance.getReceiptDataIOS();
1271
- } catch (error) {
1272
- console.error('[getReceiptDataIOS] Failed:', error);
1273
- const errorJson = parseErrorStringToJsonObj(error);
1274
- throw new Error(errorJson.message);
1275
- }
1276
- };
1277
-
1278
1220
  /**
1279
1221
  * Check if transaction is verified (iOS only)
1280
1222
  * @param sku - The product SKU
1281
1223
  * @returns Promise<boolean> - Verification status
1282
1224
  * @platform iOS
1283
1225
  */
1284
- export const isTransactionVerifiedIOS = async (
1285
- sku: string,
1286
- ): Promise<boolean> => {
1287
- if (Platform.OS !== 'ios') {
1288
- return false;
1289
- }
1290
-
1291
- try {
1292
- return await IAP.instance.isTransactionVerifiedIOS(sku);
1293
- } catch (error) {
1294
- console.error('[isTransactionVerifiedIOS] Failed:', error);
1295
- const errorJson = parseErrorStringToJsonObj(error);
1296
- throw new Error(errorJson.message);
1297
- }
1298
- };
1299
-
1300
1226
  /**
1301
1227
  * Get transaction JWS representation (iOS only)
1302
1228
  * @param sku - The product SKU
1303
1229
  * @returns Promise<string | null> - JWS representation or null
1304
1230
  * @platform iOS
1305
1231
  */
1306
- export const getTransactionJwsIOS = async (
1307
- sku: string,
1308
- ): Promise<string | null> => {
1309
- if (Platform.OS !== 'ios') {
1310
- return null;
1311
- }
1312
-
1313
- try {
1314
- return await IAP.instance.getTransactionJwsIOS(sku);
1315
- } catch (error) {
1316
- console.error('[getTransactionJwsIOS] Failed:', error);
1317
- const errorJson = parseErrorStringToJsonObj(error);
1318
- throw new Error(errorJson.message);
1319
- }
1320
- };
1321
-
1322
1232
  /**
1323
1233
  * Get the storefront identifier for the user's App Store account (iOS only)
1324
1234
  * @returns Promise<string> - The storefront identifier (e.g., 'USA' for United States)
@@ -1330,67 +1240,29 @@ export const getTransactionJwsIOS = async (
1330
1240
  * console.log('User storefront:', storefront); // e.g., 'USA', 'GBR', 'KOR'
1331
1241
  * ```
1332
1242
  */
1333
- export const getStorefrontIOS = async (): Promise<string> => {
1334
- if (Platform.OS !== 'ios') {
1335
- throw new Error('getStorefrontIOS is only available on iOS');
1336
- }
1337
-
1338
- try {
1339
- // Call the native method to get storefront
1340
- const storefront = await IAP.instance.getStorefrontIOS();
1341
- return storefront;
1342
- } catch (error) {
1343
- console.error('Failed to get storefront:', error);
1344
- throw error;
1345
- }
1346
- };
1347
-
1348
- /**
1349
- * Gets the storefront country code from the underlying native store.
1350
- * Returns a two-letter country code such as 'US', 'KR', or empty string on failure.
1351
- *
1352
- * Cross-platform alias aligning with expo-iap.
1353
- */
1354
- export const getStorefront = async (): Promise<string> => {
1355
- if (Platform.OS === 'android') {
1356
- try {
1357
- // Optional since older builds may not have the method
1358
- const result = await IAP.instance.getStorefrontAndroid?.();
1359
- return result ?? '';
1360
- } catch {
1361
- return '';
1362
- }
1363
- }
1364
- return getStorefrontIOS();
1365
- };
1366
-
1367
1243
  /**
1368
1244
  * Deeplinks to native interface that allows users to manage their subscriptions
1369
1245
  * Cross-platform alias aligning with expo-iap
1370
1246
  */
1371
- export const deepLinkToSubscriptions = async (
1372
- options: {
1373
- skuAndroid?: string;
1374
- packageNameAndroid?: string;
1375
- } = {},
1376
- ): Promise<void> => {
1247
+ export const deepLinkToSubscriptions: MutationField<
1248
+ 'deepLinkToSubscriptions'
1249
+ > = async (options) => {
1250
+ const resolvedOptions = options ?? undefined;
1251
+
1377
1252
  if (Platform.OS === 'android') {
1378
1253
  await IAP.instance.deepLinkToSubscriptionsAndroid?.({
1379
- skuAndroid: options.skuAndroid,
1380
- packageNameAndroid: options.packageNameAndroid,
1254
+ skuAndroid: resolvedOptions?.skuAndroid ?? undefined,
1255
+ packageNameAndroid: resolvedOptions?.packageNameAndroid ?? undefined,
1381
1256
  });
1382
1257
  return;
1383
1258
  }
1384
- // iOS: Use manage subscriptions sheet (ignore returned purchases for deeplink parity)
1385
1259
  if (Platform.OS === 'ios') {
1386
1260
  try {
1387
1261
  await IAP.instance.showManageSubscriptionsIOS();
1388
- } catch {
1389
- // no-op
1262
+ } catch (error) {
1263
+ console.warn('[deepLinkToSubscriptions] Failed on iOS:', error);
1390
1264
  }
1391
- return;
1392
1265
  }
1393
- return;
1394
1266
  };
1395
1267
 
1396
1268
  /**
@@ -1412,21 +1284,6 @@ export const deepLinkToSubscriptions = async (
1412
1284
  * }
1413
1285
  * ```
1414
1286
  */
1415
- export const getAppTransactionIOS = async (): Promise<string | null> => {
1416
- if (Platform.OS !== 'ios') {
1417
- throw new Error('getAppTransactionIOS is only available on iOS');
1418
- }
1419
-
1420
- try {
1421
- // Call the native method to get app transaction
1422
- const appTransaction = await IAP.instance.getAppTransactionIOS();
1423
- return appTransaction;
1424
- } catch (error) {
1425
- console.error('Failed to get app transaction:', error);
1426
- throw error;
1427
- }
1428
- };
1429
-
1430
1287
  // Export subscription helpers
1431
1288
  export {
1432
1289
  getActiveSubscriptions,
@@ -1453,3 +1310,72 @@ export const acknowledgePurchase = acknowledgePurchaseAndroid;
1453
1310
  * @deprecated Use consumePurchaseAndroid instead
1454
1311
  */
1455
1312
  export const consumePurchase = consumePurchaseAndroid;
1313
+
1314
+ // ============================================================================
1315
+ // Internal Helpers
1316
+ // ============================================================================
1317
+
1318
+ type NitroDiscountOfferRecord = NonNullable<
1319
+ NonNullable<NitroPurchaseRequest['ios']>['withOffer']
1320
+ >;
1321
+
1322
+ const toDiscountOfferRecordIOS = (
1323
+ offer: DiscountOfferInputIOS | null | undefined,
1324
+ ): NitroDiscountOfferRecord | undefined => {
1325
+ if (!offer) {
1326
+ return undefined;
1327
+ }
1328
+ return {
1329
+ identifier: offer.identifier,
1330
+ keyIdentifier: offer.keyIdentifier,
1331
+ nonce: offer.nonce,
1332
+ signature: offer.signature,
1333
+ timestamp: String(offer.timestamp),
1334
+ };
1335
+ };
1336
+
1337
+ const toNitroProductType = (
1338
+ type?: ProductTypeInput | ProductQueryType | null,
1339
+ ): 'inapp' | 'subs' | 'all' => {
1340
+ if (type === 'subs') {
1341
+ return 'subs';
1342
+ }
1343
+ if (type === 'all') {
1344
+ return 'all';
1345
+ }
1346
+ if (type === 'inapp') {
1347
+ console.warn(LEGACY_INAPP_WARNING);
1348
+ return 'inapp';
1349
+ }
1350
+ return 'inapp';
1351
+ };
1352
+
1353
+ const isSubscriptionQuery = (type?: ProductQueryType | null): boolean =>
1354
+ type === 'subs';
1355
+
1356
+ const normalizeProductQueryType = (
1357
+ type?: ProductQueryType | string | null,
1358
+ ): ProductQueryType => {
1359
+ if (type === 'all' || type === 'subs' || type === 'in-app') {
1360
+ return type;
1361
+ }
1362
+
1363
+ if (typeof type === 'string') {
1364
+ const normalized = type.trim().toLowerCase().replace(/_/g, '-');
1365
+
1366
+ if (normalized === 'all') {
1367
+ return 'all';
1368
+ }
1369
+ if (normalized === 'subs') {
1370
+ return 'subs';
1371
+ }
1372
+ if (normalized === 'inapp') {
1373
+ console.warn(LEGACY_INAPP_WARNING);
1374
+ return 'in-app';
1375
+ }
1376
+ if (normalized === 'in-app') {
1377
+ return 'in-app';
1378
+ }
1379
+ }
1380
+ return 'in-app';
1381
+ };