react-native-iap 14.3.2-rc.9 → 14.3.3-rc.1

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 (60) hide show
  1. package/lib/index.d.ts +8 -0
  2. package/lib/index.js +36 -0
  3. package/lib/module/helpers/subscription.js +2 -2
  4. package/lib/module/helpers/subscription.js.map +1 -1
  5. package/lib/module/hooks/useIAP.js +14 -8
  6. package/lib/module/hooks/useIAP.js.map +1 -1
  7. package/lib/module/index.js +87 -30
  8. package/lib/module/index.js.map +1 -1
  9. package/lib/module/types.js +90 -190
  10. package/lib/module/types.js.map +1 -1
  11. package/lib/module/utils/error.js +4 -4
  12. package/lib/module/utils/error.js.map +1 -1
  13. package/lib/module/utils/errorMapping.js +34 -10
  14. package/lib/module/utils/errorMapping.js.map +1 -1
  15. package/lib/module/utils/type-bridge.js +217 -173
  16. package/lib/module/utils/type-bridge.js.map +1 -1
  17. package/lib/specs/RnIap.nitro.d.ts +7 -0
  18. package/lib/specs/RnIap.nitro.js +1 -0
  19. package/lib/typescript/plugin/src/withIAP.d.ts.map +1 -1
  20. package/lib/typescript/src/helpers/subscription.d.ts.map +1 -1
  21. package/lib/typescript/src/hooks/useIAP.d.ts +8 -11
  22. package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
  23. package/lib/typescript/src/index.d.ts +11 -10
  24. package/lib/typescript/src/index.d.ts.map +1 -1
  25. package/lib/typescript/src/specs/RnIap.nitro.d.ts +2 -2
  26. package/lib/typescript/src/types.d.ts +606 -518
  27. package/lib/typescript/src/types.d.ts.map +1 -1
  28. package/lib/typescript/src/utils/errorMapping.d.ts +2 -1
  29. package/lib/typescript/src/utils/errorMapping.d.ts.map +1 -1
  30. package/lib/typescript/src/utils/type-bridge.d.ts +13 -14
  31. package/lib/typescript/src/utils/type-bridge.d.ts.map +1 -1
  32. package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +4 -4
  33. package/nitrogen/generated/android/c++/{JNitroAndroidReceiptValidationOptions.hpp → JNitroReceiptValidationAndroidOptions.hpp} +9 -9
  34. package/nitrogen/generated/android/c++/JNitroReceiptValidationParams.hpp +5 -5
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/{NitroAndroidReceiptValidationOptions.kt → NitroReceiptValidationAndroidOptions.kt} +3 -3
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroReceiptValidationParams.kt +1 -1
  37. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +10 -10
  38. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +3 -3
  39. package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +3 -3
  40. package/nitrogen/generated/ios/swift/{NitroAndroidReceiptValidationOptions.swift → NitroReceiptValidationAndroidOptions.swift} +5 -5
  41. package/nitrogen/generated/ios/swift/NitroReceiptValidationParams.swift +9 -9
  42. package/nitrogen/generated/shared/c++/{NitroAndroidReceiptValidationOptions.hpp → NitroReceiptValidationAndroidOptions.hpp} +10 -10
  43. package/nitrogen/generated/shared/c++/NitroReceiptValidationParams.hpp +8 -8
  44. package/package.json +1 -1
  45. package/plugin/src/withIAP.ts +4 -2
  46. package/src/helpers/subscription.ts +8 -9
  47. package/src/hooks/useIAP.ts +52 -47
  48. package/src/index.ts +141 -45
  49. package/src/specs/RnIap.nitro.ts +2 -2
  50. package/src/types.ts +651 -616
  51. package/src/utils/error.ts +4 -4
  52. package/src/utils/errorMapping.ts +47 -19
  53. package/src/utils/type-bridge.ts +308 -204
  54. package/lib/commonjs/index.js +0 -36
  55. package/lib/commonjs/index.js.map +0 -1
  56. package/lib/commonjs/package.json +0 -1
  57. package/lib/commonjs/specs/RnIap.nitro.js +0 -6
  58. package/lib/commonjs/specs/RnIap.nitro.js.map +0 -1
  59. package/lib/commonjs/types.js +0 -118
  60. package/lib/commonjs/types.js.map +0 -1
@@ -33,21 +33,20 @@ export const getActiveSubscriptions = async (
33
33
  purchaseToken: purchase.purchaseToken,
34
34
  transactionDate: purchase.transactionDate,
35
35
  // Platform-specific fields
36
- expirationDateIOS: iosPurchase.expirationDateIOS
37
- ? new Date(iosPurchase.expirationDateIOS)
38
- : undefined,
36
+ expirationDateIOS: iosPurchase.expirationDateIOS ?? null,
39
37
  autoRenewingAndroid:
40
38
  androidPurchase.autoRenewingAndroid ??
41
39
  androidPurchase.isAutoRenewing, // deprecated - use isAutoRenewing instead
42
40
  environmentIOS: iosPurchase.environmentIOS,
43
41
  // Convenience fields
44
42
  willExpireSoon: false, // Would need to calculate based on expiration date
45
- daysUntilExpirationIOS: iosPurchase.expirationDateIOS
46
- ? Math.ceil(
47
- (iosPurchase.expirationDateIOS - Date.now()) /
48
- (1000 * 60 * 60 * 24),
49
- )
50
- : undefined,
43
+ daysUntilExpirationIOS:
44
+ iosPurchase.expirationDateIOS != null
45
+ ? Math.ceil(
46
+ (iosPurchase.expirationDateIOS - Date.now()) /
47
+ (1000 * 60 * 60 * 24),
48
+ )
49
+ : undefined,
51
50
  };
52
51
  });
53
52
 
@@ -16,20 +16,23 @@ import {
16
16
  getActiveSubscriptions,
17
17
  hasActiveSubscriptions,
18
18
  restorePurchases as restorePurchasesTopLevel,
19
+ getPromotedProductIOS,
20
+ requestPurchaseOnPromotedProductIOS,
19
21
  } from '../';
20
- import {getPromotedProductIOS, requestPurchaseOnPromotedProductIOS} from '../';
21
22
 
22
23
  // Types
24
+ import {ProductQueryType, ErrorCode} from '../types';
23
25
  import type {
26
+ ActiveSubscription,
24
27
  Product,
25
28
  Purchase,
26
29
  PurchaseError,
27
- PurchaseResult,
28
- SubscriptionProduct,
29
- RequestPurchaseProps,
30
- RequestSubscriptionProps,
31
- ActiveSubscription,
30
+ ProductSubscription,
31
+ PurchaseParams,
32
32
  } from '../types';
33
+ import type {FinishTransactionParams} from '../';
34
+ import type {NitroPurchaseResult} from '../specs/RnIap.nitro';
35
+ import {normalizeErrorCodeFromNative} from '../utils/errorMapping';
33
36
 
34
37
  // Types for event subscriptions
35
38
  interface EventSubscription {
@@ -41,7 +44,7 @@ type UseIap = {
41
44
  products: Product[];
42
45
  promotedProductsIOS: Purchase[];
43
46
  promotedProductIdIOS?: string;
44
- subscriptions: SubscriptionProduct[];
47
+ subscriptions: ProductSubscription[];
45
48
  availablePurchases: Purchase[];
46
49
  currentPurchase?: Purchase;
47
50
  currentPurchaseError?: PurchaseError;
@@ -52,14 +55,11 @@ type UseIap = {
52
55
  finishTransaction: ({
53
56
  purchase,
54
57
  isConsumable,
55
- }: {
56
- purchase: Purchase;
57
- isConsumable?: boolean;
58
- }) => Promise<PurchaseResult | boolean>;
58
+ }: FinishTransactionParams) => Promise<NitroPurchaseResult | boolean>;
59
59
  getAvailablePurchases: (skus?: string[]) => Promise<void>;
60
60
  fetchProducts: (params: {
61
61
  skus: string[];
62
- type?: 'inapp' | 'subs';
62
+ type?: ProductQueryType | null;
63
63
  }) => Promise<void>;
64
64
  /**
65
65
  * @deprecated Use fetchProducts({ skus, type: 'inapp' }) instead. This method will be removed in version 3.0.0.
@@ -71,10 +71,7 @@ type UseIap = {
71
71
  * Note: This method internally uses fetchProducts, so no deprecation warning is shown.
72
72
  */
73
73
  getSubscriptions: (skus: string[]) => Promise<void>;
74
- requestPurchase: (params: {
75
- request: RequestPurchaseProps | RequestSubscriptionProps;
76
- type?: 'inapp' | 'subs';
77
- }) => Promise<any>;
74
+ requestPurchase: (params: PurchaseParams) => Promise<any>;
78
75
  validateReceipt: (
79
76
  sku: string,
80
77
  androidOptions?: {
@@ -109,7 +106,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
109
106
  const [connected, setConnected] = useState<boolean>(false);
110
107
  const [products, setProducts] = useState<Product[]>([]);
111
108
  const [promotedProductsIOS] = useState<Purchase[]>([]);
112
- const [subscriptions, setSubscriptions] = useState<SubscriptionProduct[]>([]);
109
+ const [subscriptions, setSubscriptions] = useState<ProductSubscription[]>([]);
113
110
  const [availablePurchases, setAvailablePurchases] = useState<Purchase[]>([]);
114
111
  const [currentPurchase, setCurrentPurchase] = useState<Purchase>();
115
112
  const [promotedProductIOS, setPromotedProductIOS] = useState<Product>();
@@ -159,7 +156,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
159
156
  promotedProductIOS?: EventSubscription;
160
157
  }>({});
161
158
 
162
- const subscriptionsRefState = useRef<SubscriptionProduct[]>([]);
159
+ const subscriptionsRefState = useRef<ProductSubscription[]>([]);
163
160
 
164
161
  useEffect(() => {
165
162
  subscriptionsRefState.current = subscriptions;
@@ -176,7 +173,10 @@ export function useIAP(options?: UseIapOptions): UseIap {
176
173
  const getProductsInternal = useCallback(
177
174
  async (skus: string[]): Promise<void> => {
178
175
  try {
179
- const result = await fetchProducts({skus, type: 'inapp'});
176
+ const result = await fetchProducts({
177
+ skus,
178
+ type: ProductQueryType.InApp,
179
+ });
180
180
  setProducts((prevProducts: Product[]) =>
181
181
  mergeWithDuplicateCheck(
182
182
  prevProducts,
@@ -194,12 +194,15 @@ export function useIAP(options?: UseIapOptions): UseIap {
194
194
  const getSubscriptionsInternal = useCallback(
195
195
  async (skus: string[]): Promise<void> => {
196
196
  try {
197
- const result = await fetchProducts({skus, type: 'subs'});
198
- setSubscriptions((prevSubscriptions: SubscriptionProduct[]) =>
197
+ const result = await fetchProducts({
198
+ skus,
199
+ type: ProductQueryType.Subs,
200
+ });
201
+ setSubscriptions((prevSubscriptions: ProductSubscription[]) =>
199
202
  mergeWithDuplicateCheck(
200
203
  prevSubscriptions,
201
- result as SubscriptionProduct[],
202
- (subscription: SubscriptionProduct) => subscription.id,
204
+ result as ProductSubscription[],
205
+ (subscription: ProductSubscription) => subscription.id,
203
206
  ),
204
207
  );
205
208
  } catch (error) {
@@ -212,7 +215,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
212
215
  const fetchProductsInternal = useCallback(
213
216
  async (params: {
214
217
  skus: string[];
215
- type?: 'inapp' | 'subs';
218
+ type?: ProductQueryType | null;
216
219
  }): Promise<void> => {
217
220
  if (!connectedRef.current) {
218
221
  console.warn(
@@ -222,12 +225,12 @@ export function useIAP(options?: UseIapOptions): UseIap {
222
225
  }
223
226
  try {
224
227
  const result = await fetchProducts(params);
225
- if (params.type === 'subs') {
226
- setSubscriptions((prevSubscriptions: SubscriptionProduct[]) =>
228
+ if (params.type === ProductQueryType.Subs) {
229
+ setSubscriptions((prevSubscriptions: ProductSubscription[]) =>
227
230
  mergeWithDuplicateCheck(
228
231
  prevSubscriptions,
229
- result as SubscriptionProduct[],
230
- (subscription: SubscriptionProduct) => subscription.id,
232
+ result as ProductSubscription[],
233
+ (subscription: ProductSubscription) => subscription.id,
231
234
  ),
232
235
  );
233
236
  } else {
@@ -296,7 +299,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
296
299
  }: {
297
300
  purchase: Purchase;
298
301
  isConsumable?: boolean;
299
- }): Promise<PurchaseResult | boolean> => {
302
+ }): Promise<NitroPurchaseResult | boolean> => {
300
303
  try {
301
304
  return await finishTransactionInternal({
302
305
  purchase,
@@ -322,7 +325,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
322
325
  );
323
326
 
324
327
  const requestPurchaseWithReset = useCallback(
325
- async (requestObj: {request: any; type?: 'inapp' | 'subs'}) => {
328
+ async (requestObj: PurchaseParams) => {
326
329
  clearCurrentPurchase();
327
330
  clearCurrentPurchaseError();
328
331
 
@@ -371,23 +374,25 @@ export function useIAP(options?: UseIapOptions): UseIap {
371
374
  },
372
375
  );
373
376
 
374
- subscriptionsRef.current.purchaseError = purchaseErrorListener(
375
- (error: PurchaseError) => {
376
- // Ignore init error until connected
377
- if (
378
- error &&
379
- (error as any).code === 'E_INIT_CONNECTION' &&
380
- !connectedRef.current
381
- ) {
382
- return;
383
- }
384
- setCurrentPurchase(undefined);
385
- setCurrentPurchaseError(error);
386
- if (optionsRef.current?.onPurchaseError) {
387
- optionsRef.current.onPurchaseError(error);
388
- }
389
- },
390
- );
377
+ subscriptionsRef.current.purchaseError = purchaseErrorListener((error) => {
378
+ const mappedError: PurchaseError = {
379
+ code: normalizeErrorCodeFromNative(error.code),
380
+ message: error.message,
381
+ productId: undefined,
382
+ };
383
+ // Ignore init error until connected
384
+ if (
385
+ mappedError.code === ErrorCode.InitConnection &&
386
+ !connectedRef.current
387
+ ) {
388
+ return;
389
+ }
390
+ setCurrentPurchase(undefined);
391
+ setCurrentPurchaseError(mappedError);
392
+ if (optionsRef.current?.onPurchaseError) {
393
+ optionsRef.current.onPurchaseError(mappedError);
394
+ }
395
+ });
391
396
 
392
397
  if (Platform.OS === 'ios') {
393
398
  subscriptionsRef.current.promotedProductsIOS = promotedProductListenerIOS(
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  // External dependencies
2
2
  import {Platform} from 'react-native';
3
- // Side-effect import ensures Nitro installs its dispatcher before IAP is used
3
+ // Side-effect import ensures Nitro installs its dispatcher before IAP is used (no-op in tests)
4
4
  import 'react-native-nitro-modules';
5
- import {NitroModules, isRuntimeAlive} from 'react-native-nitro-modules';
5
+ import {NitroModules} from 'react-native-nitro-modules';
6
6
 
7
7
  // Internal modules
8
8
  import type {
@@ -12,22 +12,24 @@ import type {
12
12
  NitroReceiptValidationResultIOS,
13
13
  NitroReceiptValidationResultAndroid,
14
14
  } from './specs/RnIap.nitro';
15
+ import {ProductQueryType} from './types';
15
16
  import type {
16
17
  Product,
18
+ ProductRequest,
17
19
  Purchase,
18
20
  PurchaseAndroid,
19
- RequestPurchaseProps,
20
- RequestSubscriptionProps,
21
- RequestSubscriptionAndroidProps,
22
21
  PurchaseOptions,
23
- FinishTransactionParams,
24
- ReceiptValidationResultIOS,
22
+ PurchaseParams,
23
+ PurchaseError,
25
24
  ReceiptValidationResultAndroid,
26
- RequestPurchaseIosProps,
25
+ ReceiptValidationResultIOS,
27
26
  RequestPurchaseAndroidProps,
27
+ RequestPurchaseIosProps,
28
+ RequestPurchasePropsByPlatforms,
29
+ RequestSubscriptionAndroidProps,
30
+ RequestSubscriptionPropsByPlatforms,
28
31
  SubscriptionStatusIOS,
29
32
  } from './types';
30
- import type {ProductRequest} from './types';
31
33
  import {
32
34
  convertNitroProductToProduct,
33
35
  convertNitroPurchaseToPurchase,
@@ -36,6 +38,7 @@ import {
36
38
  convertNitroSubscriptionStatusToSubscriptionStatusIOS,
37
39
  } from './utils/type-bridge';
38
40
  import {parseErrorStringToJsonObj} from './utils/error';
41
+ import {normalizeErrorCodeFromNative} from './utils/errorMapping';
39
42
 
40
43
  // Export all types
41
44
  export type {
@@ -47,11 +50,46 @@ export type {
47
50
  export * from './types';
48
51
  export * from './utils/error';
49
52
 
50
- // Types for event listeners
53
+ // Internal constants/helpers for bridging legacy Nitro expectations
54
+ const NITRO_PRODUCT_TYPE_INAPP = 'inapp';
55
+ const NITRO_PRODUCT_TYPE_SUBS = 'subs';
56
+
57
+ function toNitroProductType(
58
+ type?: ProductQueryType | null,
59
+ ): typeof NITRO_PRODUCT_TYPE_INAPP | typeof NITRO_PRODUCT_TYPE_SUBS {
60
+ return type === ProductQueryType.Subs
61
+ ? NITRO_PRODUCT_TYPE_SUBS
62
+ : NITRO_PRODUCT_TYPE_INAPP;
63
+ }
64
+
65
+ function isSubscriptionQuery(type?: ProductQueryType | null): boolean {
66
+ return type === ProductQueryType.Subs;
67
+ }
68
+
69
+ function normalizeProductQueryType(
70
+ type?: ProductQueryType | string | null,
71
+ ): ProductQueryType {
72
+ if (type === ProductQueryType.All || type === 'all') {
73
+ return ProductQueryType.All;
74
+ }
75
+ if (type === ProductQueryType.Subs || type === 'subs') {
76
+ return ProductQueryType.Subs;
77
+ }
78
+ if (type === ProductQueryType.InApp || type === 'inapp') {
79
+ return ProductQueryType.InApp;
80
+ }
81
+ return ProductQueryType.InApp;
82
+ }
83
+
51
84
  export interface EventSubscription {
52
85
  remove(): void;
53
86
  }
54
87
 
88
+ export type FinishTransactionParams = {
89
+ purchase: Purchase;
90
+ isConsumable?: boolean;
91
+ };
92
+
55
93
  // ActiveSubscription and PurchaseError types are already exported via 'export * from ./types'
56
94
 
57
95
  // Export hooks
@@ -85,15 +123,23 @@ const IAP = {
85
123
  get instance(): RnIap {
86
124
  if (iapRef) return iapRef;
87
125
 
88
- // Guard against accessing Nitro before it's installed into the JS runtime
89
- const hasNitroDispatcher = isRuntimeAlive();
90
- if (!hasNitroDispatcher) {
91
- throw new Error(
92
- 'Nitro runtime not installed yet. Ensure react-native-nitro-modules is initialized before calling IAP.',
93
- );
126
+ // Attempt to create the HybridObject and map common Nitro/JSI readiness errors
127
+ try {
128
+ iapRef = NitroModules.createHybridObject<RnIap>('RnIap');
129
+ } catch (e) {
130
+ const msg = String((e as any)?.message ?? e ?? '');
131
+ if (
132
+ msg.includes('Nitro') ||
133
+ msg.includes('JSI') ||
134
+ msg.includes('dispatcher') ||
135
+ msg.includes('HybridObject')
136
+ ) {
137
+ throw new Error(
138
+ 'Nitro runtime not installed yet. Ensure react-native-nitro-modules is initialized before calling IAP.',
139
+ );
140
+ }
141
+ throw e;
94
142
  }
95
-
96
- iapRef = NitroModules.createHybridObject<RnIap>('RnIap');
97
143
  return iapRef;
98
144
  },
99
145
  };
@@ -142,17 +188,19 @@ export const endConnection = async (): Promise<boolean> => {
142
188
  */
143
189
  export const fetchProducts = async ({
144
190
  skus,
145
- type = 'inapp',
191
+ type = ProductQueryType.InApp,
146
192
  }: ProductRequest): Promise<Product[]> => {
147
193
  try {
148
194
  if (!skus || skus.length === 0) {
149
195
  throw new Error('No SKUs provided');
150
196
  }
151
197
 
152
- if (type === 'all') {
198
+ const normalizedType = normalizeProductQueryType(type);
199
+
200
+ if (normalizedType === ProductQueryType.All) {
153
201
  const [inappNitro, subsNitro] = await Promise.all([
154
- IAP.instance.fetchProducts(skus, 'inapp'),
155
- IAP.instance.fetchProducts(skus, 'subs'),
202
+ IAP.instance.fetchProducts(skus, NITRO_PRODUCT_TYPE_INAPP),
203
+ IAP.instance.fetchProducts(skus, NITRO_PRODUCT_TYPE_SUBS),
156
204
  ]);
157
205
  const allNitro = [...inappNitro, ...subsNitro];
158
206
  const validAll = allNitro.filter(validateNitroProduct);
@@ -164,7 +212,10 @@ export const fetchProducts = async ({
164
212
  return validAll.map(convertNitroProductToProduct);
165
213
  }
166
214
 
167
- const nitroProducts = await IAP.instance.fetchProducts(skus, type);
215
+ const nitroProducts = await IAP.instance.fetchProducts(
216
+ skus,
217
+ toNitroProductType(normalizedType),
218
+ );
168
219
 
169
220
  // Validate and convert NitroProducts to TypeScript Products
170
221
  const validProducts = nitroProducts.filter(validateNitroProduct);
@@ -217,17 +268,53 @@ export const fetchProducts = async ({
217
268
  * ⚠️ Important: This is an event-based operation, not promise-based.
218
269
  * Listen for events through purchaseUpdatedListener or purchaseErrorListener.
219
270
  * @param params - Purchase request configuration
220
- * @param params.request - Platform-specific purchase parameters
221
- * @param params.type - Type of purchase: 'inapp' for products (default) or 'subs' for subscriptions
271
+ * @param params.requestPurchase - Platform-specific purchase parameters (in-app)
272
+ * @param params.requestSubscription - Platform-specific subscription parameters (subs)
273
+ * @param params.type - Type of purchase (defaults to in-app)
222
274
  */
223
- export const requestPurchase = async ({
224
- request,
225
- type = 'inapp',
226
- }: {
227
- request: RequestPurchaseProps | RequestSubscriptionProps;
228
- type?: 'inapp' | 'subs';
229
- }): Promise<void> => {
275
+ export const requestPurchase = async (
276
+ params: PurchaseParams,
277
+ ): Promise<void> => {
230
278
  try {
279
+ const {requestPurchase: purchaseRequest, requestSubscription} = params;
280
+ const normalizedPurchaseRequest = purchaseRequest ?? undefined;
281
+ const normalizedSubscriptionRequest = requestSubscription ?? undefined;
282
+
283
+ const effectiveType = normalizeProductQueryType(params.type);
284
+ const isSubs = isSubscriptionQuery(effectiveType);
285
+ let request:
286
+ | RequestPurchasePropsByPlatforms
287
+ | RequestSubscriptionPropsByPlatforms
288
+ | undefined;
289
+
290
+ if (isSubs) {
291
+ if (
292
+ __DEV__ &&
293
+ normalizedPurchaseRequest &&
294
+ !normalizedSubscriptionRequest
295
+ ) {
296
+ console.warn(
297
+ '[react-native-iap] `requestPurchase` was provided for a subscription request. Did you mean to use `requestSubscription`?',
298
+ );
299
+ }
300
+ request = normalizedSubscriptionRequest ?? normalizedPurchaseRequest;
301
+ } else {
302
+ if (
303
+ __DEV__ &&
304
+ normalizedSubscriptionRequest &&
305
+ !normalizedPurchaseRequest
306
+ ) {
307
+ console.warn(
308
+ '[react-native-iap] `requestSubscription` was provided for an in-app purchase request. Did you mean to use `requestPurchase`?',
309
+ );
310
+ }
311
+ request = normalizedPurchaseRequest ?? normalizedSubscriptionRequest;
312
+ }
313
+
314
+ if (!request) {
315
+ throw new Error('Missing purchase request configuration');
316
+ }
317
+
231
318
  // Validate platform-specific requests
232
319
  if (Platform.OS === 'ios') {
233
320
  const iosRequest = request.ios;
@@ -253,8 +340,7 @@ export const requestPurchase = async ({
253
340
  if (Platform.OS === 'ios' && request.ios) {
254
341
  const iosReq = request.ios as RequestPurchaseIosProps;
255
342
  const autoFinishSubs =
256
- type === 'subs' &&
257
- iosReq.andDangerouslyFinishTransactionAutomatically == null;
343
+ isSubs && iosReq.andDangerouslyFinishTransactionAutomatically == null;
258
344
  unifiedRequest.ios = {
259
345
  ...iosReq,
260
346
  // Align with native SwiftUI flow: auto-finish subscriptions by default
@@ -265,7 +351,7 @@ export const requestPurchase = async ({
265
351
  }
266
352
 
267
353
  if (Platform.OS === 'android' && request.android) {
268
- if (type === 'subs') {
354
+ if (isSubs) {
269
355
  const subsRequest = request.android as RequestSubscriptionAndroidProps;
270
356
  unifiedRequest.android = {
271
357
  ...subsRequest,
@@ -612,13 +698,20 @@ export const purchaseUpdatedListener = (
612
698
  * ```
613
699
  */
614
700
  export const purchaseErrorListener = (
615
- listener: (error: NitroPurchaseResult) => void,
701
+ listener: (error: PurchaseError) => void,
616
702
  ): EventSubscription => {
617
- // Store the listener for removal
618
- listenerMap.set(listener, listener);
703
+ const wrapped = (error: NitroPurchaseResult) => {
704
+ listener({
705
+ code: normalizeErrorCodeFromNative(error.code),
706
+ message: error.message,
707
+ productId: undefined,
708
+ });
709
+ };
710
+
711
+ listenerMap.set(listener, wrapped);
619
712
  let attached = false;
620
713
  try {
621
- IAP.instance.addPurchaseErrorListener(listener as any);
714
+ IAP.instance.addPurchaseErrorListener(wrapped as any);
622
715
  attached = true;
623
716
  } catch (e) {
624
717
  const msg = String(e ?? '');
@@ -633,12 +726,15 @@ export const purchaseErrorListener = (
633
726
 
634
727
  return {
635
728
  remove: () => {
636
- if (attached) {
637
- try {
638
- IAP.instance.removePurchaseErrorListener(listener as any);
639
- } catch {}
729
+ const stored = listenerMap.get(listener);
730
+ if (stored) {
731
+ if (attached) {
732
+ try {
733
+ IAP.instance.removePurchaseErrorListener(stored as any);
734
+ } catch {}
735
+ }
736
+ listenerMap.delete(listener);
640
737
  }
641
- listenerMap.delete(listener);
642
738
  },
643
739
  };
644
740
  };
@@ -1232,7 +1328,7 @@ export {
1232
1328
  export {
1233
1329
  convertNitroProductToProduct,
1234
1330
  convertNitroPurchaseToPurchase,
1235
- convertProductToSubscriptionProduct,
1331
+ convertProductToProductSubscription,
1236
1332
  validateNitroProduct,
1237
1333
  validateNitroPurchase,
1238
1334
  checkTypeSynchronization,
@@ -9,7 +9,7 @@ import type {HybridObject} from 'react-native-nitro-modules';
9
9
  /**
10
10
  * Android-specific receipt validation options
11
11
  */
12
- export interface NitroAndroidReceiptValidationOptions {
12
+ export interface NitroReceiptValidationAndroidOptions {
13
13
  packageName: string;
14
14
  productToken: string;
15
15
  accessToken: string;
@@ -21,7 +21,7 @@ export interface NitroAndroidReceiptValidationOptions {
21
21
  */
22
22
  export interface NitroReceiptValidationParams {
23
23
  sku: string;
24
- androidOptions?: NitroAndroidReceiptValidationOptions;
24
+ androidOptions?: NitroReceiptValidationAndroidOptions;
25
25
  }
26
26
 
27
27
  // Purchase request parameters