react-native-iap 14.3.6 → 14.3.8

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