react-native-iap 16.4.0 → 16.5.0

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 (63) hide show
  1. package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +200 -142
  2. package/android/src/main/java/com/margelo/nitro/iap/TokenizedListenerRegistry.kt +0 -1
  3. package/ios/HybridRnIap.swift +832 -165
  4. package/ios/RnIapHelper.swift +1 -1
  5. package/lib/module/hooks/useIAP.js +48 -32
  6. package/lib/module/hooks/useIAP.js.map +1 -1
  7. package/lib/module/index.js +206 -152
  8. package/lib/module/index.js.map +1 -1
  9. package/lib/module/index.kepler.js +1 -0
  10. package/lib/module/index.kepler.js.map +1 -1
  11. package/lib/module/types.js +1 -0
  12. package/lib/module/types.js.map +1 -1
  13. package/lib/module/utils/error.js +5 -4
  14. package/lib/module/utils/error.js.map +1 -1
  15. package/lib/module/vega-adapter.js +4 -1
  16. package/lib/module/vega-adapter.js.map +1 -1
  17. package/lib/typescript/src/hooks/useIAP.d.ts +31 -15
  18. package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
  19. package/lib/typescript/src/index.d.ts +28 -14
  20. package/lib/typescript/src/index.d.ts.map +1 -1
  21. package/lib/typescript/src/index.kepler.d.ts.map +1 -1
  22. package/lib/typescript/src/specs/RnIap.nitro.d.ts +10 -0
  23. package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
  24. package/lib/typescript/src/types.d.ts +11 -2
  25. package/lib/typescript/src/types.d.ts.map +1 -1
  26. package/lib/typescript/src/utils/error.d.ts.map +1 -1
  27. package/lib/typescript/src/vega-adapter.d.ts.map +1 -1
  28. package/nitrogen/generated/android/NitroIap+autolinking.cmake +1 -0
  29. package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +9 -0
  30. package/nitrogen/generated/android/c++/JNitroPurchaseRequest.hpp +7 -1
  31. package/nitrogen/generated/android/c++/JNitroPurchaseRequestType.hpp +58 -0
  32. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitHorizonProps.hpp +66 -0
  33. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitProps.hpp +8 -1
  34. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithProviderProps.hpp +3 -0
  35. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.cpp +26 -0
  36. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.hpp +74 -0
  37. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitProps.hpp +3 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseRequest.kt +8 -3
  39. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseRequestType.kt +23 -0
  40. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitHorizonProps.kt +56 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitProps.kt +7 -2
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.kt +62 -0
  43. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +65 -0
  44. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +6 -0
  45. package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +6 -0
  46. package/nitrogen/generated/ios/swift/NitroPurchaseRequest.swift +13 -2
  47. package/nitrogen/generated/ios/swift/NitroPurchaseRequestType.swift +40 -0
  48. package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitHorizonProps.swift +66 -0
  49. package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitProps.swift +38 -1
  50. package/nitrogen/generated/ios/swift/Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.swift +30 -0
  51. package/nitrogen/generated/shared/c++/NitroPurchaseRequest.hpp +9 -2
  52. package/nitrogen/generated/shared/c++/NitroPurchaseRequestType.hpp +76 -0
  53. package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitHorizonProps.hpp +90 -0
  54. package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitProps.hpp +8 -1
  55. package/openiap-versions.json +3 -3
  56. package/package.json +1 -1
  57. package/src/hooks/useIAP.ts +96 -58
  58. package/src/index.kepler.ts +1 -0
  59. package/src/index.ts +248 -195
  60. package/src/specs/RnIap.nitro.ts +12 -0
  61. package/src/types.ts +12 -2
  62. package/src/utils/error.ts +7 -3
  63. package/src/vega-adapter.ts +9 -5
@@ -56,6 +56,7 @@ export { kitApi, KitApiError } from "./kit-api.js";
56
56
 
57
57
  // Create the RnIap HybridObject instance lazily to avoid early JSI crashes
58
58
  let iapRef = null;
59
+ let attachingPendingNativeListeners = false;
59
60
 
60
61
  /**
61
62
  * Check if Nitro runtime is ready for IAP operations.
@@ -101,29 +102,41 @@ export const isStandardIOS = () => {
101
102
  const isAndroidStoreRuntime = () => {
102
103
  return Platform.OS === 'android' || isVegaOS();
103
104
  };
104
- const IAP = {
105
- get instance() {
106
- if (iapRef) return iapRef;
107
- if (isVegaOS()) {
108
- const vegaModule = getVegaIapModule();
109
- if (!vegaModule) {
110
- throw new Error('Amazon Vega IAP module is unavailable. Install @amazon-devices/keplerscript-appstore-iap-lib in the Vega app target and build with the React Native for Vega kepler platform.');
111
- }
112
- iapRef = vegaModule;
113
- return iapRef;
105
+ function getRawIapInstance() {
106
+ if (iapRef) return iapRef;
107
+ if (isVegaOS()) {
108
+ const vegaModule = getVegaIapModule();
109
+ if (!vegaModule) {
110
+ throw new Error('Amazon Vega IAP module is unavailable. Install @amazon-devices/keplerscript-appstore-iap-lib in the Vega app target and build with the React Native for Vega kepler platform.');
114
111
  }
112
+ iapRef = vegaModule;
113
+ return iapRef;
114
+ }
115
115
 
116
- // Attempt to create the HybridObject and map common Nitro/JSI readiness errors
117
- try {
118
- iapRef = NitroModules.createHybridObject('RnIap');
119
- } catch (e) {
120
- const msg = toErrorMessage(e);
121
- if (msg.includes('Nitro') || msg.includes('JSI') || msg.includes('dispatcher') || msg.includes('HybridObject')) {
122
- throw new Error('Nitro runtime not installed yet. Ensure react-native-nitro-modules is initialized before calling IAP.');
116
+ // Attempt to create the HybridObject and map common Nitro/JSI readiness errors
117
+ try {
118
+ iapRef = NitroModules.createHybridObject('RnIap');
119
+ } catch (e) {
120
+ const msg = toErrorMessage(e);
121
+ if (msg.includes('Nitro') || msg.includes('JSI') || msg.includes('dispatcher') || msg.includes('HybridObject')) {
122
+ throw new Error('Nitro runtime not installed yet. Ensure react-native-nitro-modules is initialized before calling IAP.');
123
+ }
124
+ throw e;
125
+ }
126
+ return iapRef;
127
+ }
128
+ const IAP = {
129
+ get instance() {
130
+ const instance = getRawIapInstance();
131
+ if (!attachingPendingNativeListeners) {
132
+ attachingPendingNativeListeners = true;
133
+ try {
134
+ tryAttachPendingNativeListeners();
135
+ } finally {
136
+ attachingPendingNativeListeners = false;
123
137
  }
124
- throw e;
125
138
  }
126
- return iapRef;
139
+ return instance;
127
140
  }
128
141
  };
129
142
 
@@ -135,6 +148,17 @@ const IAP = {
135
148
  // hyodotdev/openiap#382.
136
149
  // ============================================================================
137
150
 
151
+ function attachNativeListenerOrDefer(label, attach) {
152
+ try {
153
+ attach();
154
+ } catch (error) {
155
+ if (toErrorMessage(error).includes('Nitro runtime not installed')) {
156
+ RnIapConsole.warn(`[${label}] Nitro not ready yet; will retry on the next native operation`);
157
+ return;
158
+ }
159
+ throw error;
160
+ }
161
+ }
138
162
  const purchaseUpdateJsListeners = new Set();
139
163
  const purchaseUpdateDuplicateJsListeners = new Set();
140
164
  let purchaseUpdateNativeAttached = false;
@@ -161,6 +185,23 @@ const purchaseUpdateNativeHandler = nitroPurchase => {
161
185
  const purchaseUpdateDuplicateNativeHandler = nitroPurchase => {
162
186
  emitPurchaseUpdateToListeners(nitroPurchase, purchaseUpdateDuplicateJsListeners);
163
187
  };
188
+ function tryAttachPurchaseUpdateNative(receiveDuplicateTransactionUpdatesIOS) {
189
+ const alreadyAttached = receiveDuplicateTransactionUpdatesIOS ? purchaseUpdateDuplicateNativeAttached : purchaseUpdateNativeAttached;
190
+ if (alreadyAttached) return;
191
+ attachNativeListenerOrDefer('purchaseUpdatedListener', () => {
192
+ const nativeOptions = receiveDuplicateTransactionUpdatesIOS ? {
193
+ dedupeTransactionIOS: false
194
+ } : undefined;
195
+ const token = getRawIapInstance().addPurchaseUpdatedListener(receiveDuplicateTransactionUpdatesIOS ? purchaseUpdateDuplicateNativeHandler : purchaseUpdateNativeHandler, nativeOptions);
196
+ if (receiveDuplicateTransactionUpdatesIOS) {
197
+ purchaseUpdateDuplicateNativeToken = typeof token === 'number' ? token : null;
198
+ purchaseUpdateDuplicateNativeAttached = true;
199
+ } else {
200
+ purchaseUpdateNativeToken = typeof token === 'number' ? token : null;
201
+ purchaseUpdateNativeAttached = true;
202
+ }
203
+ });
204
+ }
164
205
  const purchaseErrorJsListeners = new Set();
165
206
  let purchaseErrorNativeAttached = false;
166
207
  const purchaseErrorNativeHandler = error => {
@@ -184,6 +225,13 @@ const purchaseErrorNativeHandler = error => {
184
225
  }
185
226
  }
186
227
  };
228
+ function tryAttachPurchaseErrorNative() {
229
+ if (purchaseErrorNativeAttached) return;
230
+ attachNativeListenerOrDefer('purchaseErrorListener', () => {
231
+ getRawIapInstance().addPurchaseErrorListener(purchaseErrorNativeHandler);
232
+ purchaseErrorNativeAttached = true;
233
+ });
234
+ }
187
235
  const promotedProductJsListeners = new Set();
188
236
  let promotedProductNativeAttached = false;
189
237
  const promotedProductNativeHandler = nitroProduct => {
@@ -200,6 +248,13 @@ const promotedProductNativeHandler = nitroProduct => {
200
248
  RnIapConsole.error('Invalid promoted product data received from native — id:', nitroProduct?.id ?? 'unknown');
201
249
  }
202
250
  };
251
+ function tryAttachPromotedProductNative() {
252
+ if (promotedProductNativeAttached) return;
253
+ attachNativeListenerOrDefer('promotedProductListenerIOS', () => {
254
+ getRawIapInstance().addPromotedProductListenerIOS(promotedProductNativeHandler);
255
+ promotedProductNativeAttached = true;
256
+ });
257
+ }
203
258
 
204
259
  /**
205
260
  * Reset all JS-level listener tracking state.
@@ -228,36 +283,11 @@ export const purchaseUpdatedListener = (listener, options) => {
228
283
  const receiveDuplicateTransactionUpdatesIOS = Platform.OS === 'ios' && options?.dedupeTransactionIOS === false;
229
284
  const listeners = receiveDuplicateTransactionUpdatesIOS ? purchaseUpdateDuplicateJsListeners : purchaseUpdateJsListeners;
230
285
  listeners.add(listener);
231
- if (!purchaseUpdateNativeAttached && !receiveDuplicateTransactionUpdatesIOS) {
232
- try {
233
- const token = IAP.instance.addPurchaseUpdatedListener(purchaseUpdateNativeHandler);
234
- purchaseUpdateNativeToken = typeof token === 'number' ? token : null;
235
- purchaseUpdateNativeAttached = true;
236
- } catch (e) {
237
- const msg = toErrorMessage(e);
238
- if (msg.includes('Nitro runtime not installed')) {
239
- RnIapConsole.warn('[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()');
240
- } else {
241
- throw e;
242
- }
243
- }
244
- }
245
- if (!purchaseUpdateDuplicateNativeAttached && receiveDuplicateTransactionUpdatesIOS) {
246
- try {
247
- const nativeOptions = {
248
- dedupeTransactionIOS: false
249
- };
250
- const token = IAP.instance.addPurchaseUpdatedListener(purchaseUpdateDuplicateNativeHandler, nativeOptions);
251
- purchaseUpdateDuplicateNativeToken = typeof token === 'number' ? token : null;
252
- purchaseUpdateDuplicateNativeAttached = true;
253
- } catch (e) {
254
- const msg = toErrorMessage(e);
255
- if (msg.includes('Nitro runtime not installed')) {
256
- RnIapConsole.warn('[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()');
257
- } else {
258
- throw e;
259
- }
260
- }
286
+ try {
287
+ tryAttachPurchaseUpdateNative(receiveDuplicateTransactionUpdatesIOS);
288
+ } catch (error) {
289
+ listeners.delete(listener);
290
+ throw error;
261
291
  }
262
292
  let removed = false;
263
293
  return {
@@ -282,7 +312,7 @@ export const purchaseUpdatedListener = (listener, options) => {
282
312
  return;
283
313
  }
284
314
  try {
285
- IAP.instance.removePurchaseUpdatedListener(token);
315
+ getRawIapInstance().removePurchaseUpdatedListener(token);
286
316
  if (receiveDuplicateTransactionUpdatesIOS) {
287
317
  purchaseUpdateDuplicateNativeToken = null;
288
318
  purchaseUpdateDuplicateNativeAttached = false;
@@ -298,18 +328,11 @@ export const purchaseUpdatedListener = (listener, options) => {
298
328
  };
299
329
  export const purchaseErrorListener = listener => {
300
330
  purchaseErrorJsListeners.add(listener);
301
- if (!purchaseErrorNativeAttached) {
302
- try {
303
- IAP.instance.addPurchaseErrorListener(purchaseErrorNativeHandler);
304
- purchaseErrorNativeAttached = true;
305
- } catch (e) {
306
- const msg = toErrorMessage(e);
307
- if (msg.includes('Nitro runtime not installed')) {
308
- RnIapConsole.warn('[purchaseErrorListener] Nitro not ready yet; listener inert until initConnection()');
309
- } else {
310
- throw e;
311
- }
312
- }
331
+ try {
332
+ tryAttachPurchaseErrorNative();
333
+ } catch (error) {
334
+ purchaseErrorJsListeners.delete(listener);
335
+ throw error;
313
336
  }
314
337
  return {
315
338
  remove: () => {
@@ -327,7 +350,7 @@ export const purchaseErrorListener = listener => {
327
350
  }
328
351
  if (purchaseErrorNativeAttached) {
329
352
  try {
330
- IAP.instance.removePurchaseErrorListener(purchaseErrorNativeHandler);
353
+ getRawIapInstance().removePurchaseErrorListener(purchaseErrorNativeHandler);
331
354
  purchaseErrorNativeAttached = false;
332
355
  } catch (e) {
333
356
  RnIapConsole.warn('[purchaseErrorListener] native remove failed:', e);
@@ -352,18 +375,11 @@ export const promotedProductListenerIOS = listener => {
352
375
  };
353
376
  }
354
377
  promotedProductJsListeners.add(listener);
355
- if (!promotedProductNativeAttached) {
356
- try {
357
- IAP.instance.addPromotedProductListenerIOS(promotedProductNativeHandler);
358
- promotedProductNativeAttached = true;
359
- } catch (e) {
360
- const msg = toErrorMessage(e);
361
- if (msg.includes('Nitro runtime not installed')) {
362
- RnIapConsole.warn('[promotedProductListenerIOS] Nitro not ready yet; listener inert until initConnection()');
363
- } else {
364
- throw e;
365
- }
366
- }
378
+ try {
379
+ tryAttachPromotedProductNative();
380
+ } catch (error) {
381
+ promotedProductJsListeners.delete(listener);
382
+ throw error;
367
383
  }
368
384
  return {
369
385
  remove: () => {
@@ -388,7 +404,9 @@ export const promotedProductListenerIOS = listener => {
388
404
  * console.log('External transaction token received; send it to your backend without logging it.');
389
405
  *
390
406
  * // Send token to backend for Google Play reporting
391
- * await reportToGooglePlay(details.externalTransactionToken);
407
+ * void reportToGooglePlay(details.externalTransactionToken).catch((error) => {
408
+ * console.warn('Alternative billing report failed', error);
409
+ * });
392
410
  * });
393
411
  *
394
412
  * // Later, remove the listener
@@ -407,6 +425,13 @@ const userChoiceBillingNativeHandler = details => {
407
425
  }
408
426
  }
409
427
  };
428
+ function tryAttachUserChoiceBillingNative() {
429
+ if (userChoiceBillingNativeAttached) return;
430
+ attachNativeListenerOrDefer('userChoiceBillingListenerAndroid', () => {
431
+ getRawIapInstance().addUserChoiceBillingListenerAndroid(userChoiceBillingNativeHandler);
432
+ userChoiceBillingNativeAttached = true;
433
+ });
434
+ }
410
435
  export const userChoiceBillingListenerAndroid = listener => {
411
436
  if (Platform.OS !== 'android') {
412
437
  RnIapConsole.warn('userChoiceBillingListenerAndroid: This listener is only available on Android');
@@ -415,18 +440,11 @@ export const userChoiceBillingListenerAndroid = listener => {
415
440
  };
416
441
  }
417
442
  userChoiceBillingJsListeners.add(listener);
418
- if (!userChoiceBillingNativeAttached) {
419
- try {
420
- IAP.instance.addUserChoiceBillingListenerAndroid(userChoiceBillingNativeHandler);
421
- userChoiceBillingNativeAttached = true;
422
- } catch (e) {
423
- const msg = toErrorMessage(e);
424
- if (msg.includes('Nitro runtime not installed')) {
425
- RnIapConsole.warn('[userChoiceBillingListenerAndroid] Nitro not ready yet; listener inert until initConnection()');
426
- } else {
427
- throw e;
428
- }
429
- }
443
+ try {
444
+ tryAttachUserChoiceBillingNative();
445
+ } catch (error) {
446
+ userChoiceBillingJsListeners.delete(listener);
447
+ throw error;
430
448
  }
431
449
  let removed = false;
432
450
  return {
@@ -438,7 +456,7 @@ export const userChoiceBillingListenerAndroid = listener => {
438
456
  return;
439
457
  }
440
458
  try {
441
- IAP.instance.removeUserChoiceBillingListenerAndroid(userChoiceBillingNativeHandler);
459
+ getRawIapInstance().removeUserChoiceBillingListenerAndroid(userChoiceBillingNativeHandler);
442
460
  userChoiceBillingNativeAttached = false;
443
461
  } catch (e) {
444
462
  RnIapConsole.warn('[userChoiceBillingListenerAndroid] native remove failed:', e);
@@ -462,10 +480,11 @@ export const userChoiceBillingListenerAndroid = listener => {
462
480
  * @example
463
481
  * ```typescript
464
482
  * const subscription = developerProvidedBillingListenerAndroid((details) => {
465
- * await processExternalPayment(details.products, details.linkUri);
466
- * if (details.externalTransactionToken) {
467
- * await reportToGooglePlay(details.externalTransactionToken);
468
- * }
483
+ * void processExternalPayment(details.products, details.linkUri)
484
+ * .then(() => details.externalTransactionToken
485
+ * ? reportToGooglePlay(details.externalTransactionToken)
486
+ * : undefined)
487
+ * .catch((error) => console.warn('Developer billing failed', error));
469
488
  * });
470
489
  *
471
490
  * // Later, remove the listener
@@ -484,6 +503,13 @@ const developerProvidedBillingNativeHandler = details => {
484
503
  }
485
504
  }
486
505
  };
506
+ function tryAttachDeveloperProvidedBillingNative() {
507
+ if (developerProvidedBillingNativeAttached) return;
508
+ attachNativeListenerOrDefer('developerProvidedBillingListenerAndroid', () => {
509
+ getRawIapInstance().addDeveloperProvidedBillingListenerAndroid(developerProvidedBillingNativeHandler);
510
+ developerProvidedBillingNativeAttached = true;
511
+ });
512
+ }
487
513
  export const developerProvidedBillingListenerAndroid = listener => {
488
514
  if (Platform.OS !== 'android') {
489
515
  RnIapConsole.warn('developerProvidedBillingListenerAndroid: This listener is only available on Android');
@@ -492,18 +518,11 @@ export const developerProvidedBillingListenerAndroid = listener => {
492
518
  };
493
519
  }
494
520
  developerProvidedBillingJsListeners.add(listener);
495
- if (!developerProvidedBillingNativeAttached) {
496
- try {
497
- IAP.instance.addDeveloperProvidedBillingListenerAndroid(developerProvidedBillingNativeHandler);
498
- developerProvidedBillingNativeAttached = true;
499
- } catch (e) {
500
- const msg = toErrorMessage(e);
501
- if (msg.includes('Nitro runtime not installed')) {
502
- RnIapConsole.warn('[developerProvidedBillingListenerAndroid] Nitro not ready yet; listener inert until initConnection()');
503
- } else {
504
- throw e;
505
- }
506
- }
521
+ try {
522
+ tryAttachDeveloperProvidedBillingNative();
523
+ } catch (error) {
524
+ developerProvidedBillingJsListeners.delete(listener);
525
+ throw error;
507
526
  }
508
527
  return {
509
528
  remove: () => {
@@ -555,17 +574,10 @@ const subscriptionBillingIssueNativeHandler = nitroPurchase => {
555
574
  };
556
575
  function tryAttachSubscriptionBillingIssueNative() {
557
576
  if (subscriptionBillingIssueNativeAttached) return;
558
- try {
559
- IAP.instance.addSubscriptionBillingIssueListener(subscriptionBillingIssueNativeHandler);
577
+ attachNativeListenerOrDefer('subscriptionBillingIssueListener', () => {
578
+ getRawIapInstance().addSubscriptionBillingIssueListener(subscriptionBillingIssueNativeHandler);
560
579
  subscriptionBillingIssueNativeAttached = true;
561
- } catch (e) {
562
- const msg = toErrorMessage(e);
563
- if (msg.includes('Nitro runtime not installed')) {
564
- RnIapConsole.warn('[subscriptionBillingIssueListener] Nitro not ready yet; will retry after initConnection()');
565
- } else {
566
- throw e;
567
- }
568
- }
580
+ });
569
581
  }
570
582
  export const subscriptionBillingIssueListener = listener => {
571
583
  subscriptionBillingIssueJsListeners.add(listener);
@@ -583,6 +595,29 @@ export const subscriptionBillingIssueListener = listener => {
583
595
  }
584
596
  };
585
597
  };
598
+ function tryAttachPendingNativeListeners() {
599
+ if (purchaseUpdateJsListeners.size > 0) {
600
+ tryAttachPurchaseUpdateNative(false);
601
+ }
602
+ if (purchaseUpdateDuplicateJsListeners.size > 0) {
603
+ tryAttachPurchaseUpdateNative(true);
604
+ }
605
+ if (purchaseErrorJsListeners.size > 0) {
606
+ tryAttachPurchaseErrorNative();
607
+ }
608
+ if (promotedProductJsListeners.size > 0) {
609
+ tryAttachPromotedProductNative();
610
+ }
611
+ if (userChoiceBillingJsListeners.size > 0) {
612
+ tryAttachUserChoiceBillingNative();
613
+ }
614
+ if (developerProvidedBillingJsListeners.size > 0) {
615
+ tryAttachDeveloperProvidedBillingNative();
616
+ }
617
+ if (subscriptionBillingIssueJsListeners.size > 0) {
618
+ tryAttachSubscriptionBillingIssueNative();
619
+ }
620
+ }
586
621
 
587
622
  // ------------------------------
588
623
  // Query API
@@ -852,8 +887,13 @@ export const getAppTransactionIOS = async () => {
852
887
  }
853
888
  return null;
854
889
  } catch (error) {
855
- RnIapConsole.error('Failed to get app transaction:', error);
856
- throw error;
890
+ const parsedError = parseErrorAndLogIfNeeded('Failed to get app transaction:', error);
891
+ throw createPurchaseError({
892
+ code: parsedError.code,
893
+ message: parsedError.message,
894
+ responseCode: parsedError.responseCode,
895
+ debugMessage: parsedError.debugMessage
896
+ });
857
897
  }
858
898
  };
859
899
 
@@ -1152,25 +1192,29 @@ export const getTransactionJwsIOS = async sku => {
1152
1192
  *
1153
1193
  * @example
1154
1194
  * ```ts
1155
- * await initConnection();
1156
- * await initConnection({ enableBillingProgramAndroid: 'external-offer' });
1157
- * await initConnection({
1158
- * enableBillingProgramAndroid: 'billing-choice',
1159
- * billingChoiceScreenTypeAndroid: 'developer-rendered',
1160
- * });
1195
+ * // Choose exactly one connection call for the session.
1196
+ * const connected = await initConnection();
1197
+ * // Or replace the call above with one Android billing program:
1198
+ * // const connected = await initConnection({
1199
+ * // enableBillingProgramAndroid: 'external-offer',
1200
+ * // });
1201
+ * // const connected = await initConnection({
1202
+ * // enableBillingProgramAndroid: 'billing-choice',
1203
+ * // billingChoiceScreenTypeAndroid: 'developer-rendered',
1204
+ * // });
1205
+ * if (!connected) throw new Error('Store connection failed');
1161
1206
  * ```
1162
1207
  *
1163
- * @remarks When using `useIAP()`, connection is auto-managed on mount/unmount
1164
- * pass options to the hook instead of calling this directly.
1208
+ * @remarks When using `useIAP()`, the connection initializes on mount. On unmount,
1209
+ * React Native removes hook listeners but keeps the native connection open across
1210
+ * screens. Pass options to the hook instead of calling this directly.
1165
1211
  *
1166
1212
  * @see {@link https://openiap.dev/docs/apis/init-connection}
1167
1213
  */
1168
1214
  export const initConnection = async config => {
1169
1215
  try {
1170
1216
  const result = await IAP.instance.initConnection(config);
1171
- if (subscriptionBillingIssueJsListeners.size > 0) {
1172
- tryAttachSubscriptionBillingIssueNative();
1173
- }
1217
+ tryAttachPendingNativeListeners();
1174
1218
  return result;
1175
1219
  } catch (error) {
1176
1220
  const parsedError = parseErrorAndLogIfNeeded('Failed to initialize IAP connection:', error);
@@ -1190,8 +1234,9 @@ export const initConnection = async config => {
1190
1234
  */
1191
1235
  export const endConnection = async () => {
1192
1236
  try {
1193
- if (!iapRef) return true;
1194
- return await IAP.instance.endConnection();
1237
+ const result = iapRef ? await IAP.instance.endConnection() : true;
1238
+ resetListenerState();
1239
+ return result;
1195
1240
  } catch (error) {
1196
1241
  const parsedError = parseErrorAndLogIfNeeded('Failed to end IAP connection:', error);
1197
1242
  throw createPurchaseError({
@@ -1200,8 +1245,6 @@ export const endConnection = async () => {
1200
1245
  responseCode: parsedError.responseCode,
1201
1246
  debugMessage: parsedError.debugMessage
1202
1247
  });
1203
- } finally {
1204
- resetListenerState();
1205
1248
  }
1206
1249
  };
1207
1250
 
@@ -1245,10 +1288,14 @@ export const restorePurchases = async () => {
1245
1288
  * - `type: 'in-app'` — pass `request.apple.sku` (iOS) and/or `request.google.skus` (Android).
1246
1289
  * - `type: 'subs'` — same shape, plus `request.google.subscriptionOffers: [{ sku, offerToken }]`.
1247
1290
  * @returns The dispatched purchase payload. **Do not rely on it** for the actual outcome.
1248
- * @throws Synchronous rejection from the store (e.g. `E_NOT_PREPARED`, validation failure).
1291
+ * @throws Invalid arguments or failures that prevent dispatch. Store outcomes are event-based;
1292
+ * on Android, `not-prepared` is delivered through `purchaseErrorListener`.
1249
1293
  *
1250
1294
  * @example
1251
1295
  * ```ts
1296
+ * const connected = await initConnection();
1297
+ * if (!connected) return;
1298
+ *
1252
1299
  * await requestPurchase({
1253
1300
  * request: {
1254
1301
  * apple: { sku: 'com.app.premium' },
@@ -1301,7 +1348,9 @@ export const requestPurchase = async request => {
1301
1348
  } else {
1302
1349
  throw unsupportedPlatformError();
1303
1350
  }
1304
- const unifiedRequest = {};
1351
+ const unifiedRequest = {
1352
+ type: normalizedType
1353
+ };
1305
1354
  if (Platform.OS === 'ios' && iosRequestSource) {
1306
1355
  const iosRequest = isSubs ? iosRequestSource : iosRequestSource;
1307
1356
  const iosPayload = {
@@ -1429,10 +1478,12 @@ export const requestPurchase = async request => {
1429
1478
  *
1430
1479
  * @example
1431
1480
  * ```ts
1432
- * purchaseUpdatedListener(async (purchase) => {
1433
- * if (await verifyOnServer(purchase)) {
1434
- * await finishTransaction({ purchase, isConsumable: false });
1435
- * }
1481
+ * purchaseUpdatedListener((purchase) => {
1482
+ * void verifyOnServer(purchase)
1483
+ * .then((verified) => verified
1484
+ * ? finishTransaction({ purchase, isConsumable: false })
1485
+ * : undefined)
1486
+ * .catch((error) => console.warn('Transaction finalization failed:', error));
1436
1487
  * });
1437
1488
  * ```
1438
1489
  *
@@ -1478,18 +1529,25 @@ export const finishTransaction = async args => {
1478
1529
  }
1479
1530
  return;
1480
1531
  } catch (error) {
1532
+ const parsedError = parseErrorStringToJsonObj(error);
1481
1533
  // If iOS transaction has already been auto-finished natively, treat as success
1482
1534
  if (Platform.OS === 'ios') {
1483
- const err = parseErrorStringToJsonObj(error);
1484
- const msg = (err?.message || '').toString();
1485
- const code = (err?.code || '').toString();
1535
+ const msg = (parsedError.message || '').toString();
1536
+ const code = (parsedError.code || '').toString();
1486
1537
  if (msg.includes('Transaction not found') || code === 'E_ITEM_UNAVAILABLE') {
1487
1538
  // Consider already finished
1488
1539
  return;
1489
1540
  }
1490
1541
  }
1491
- RnIapConsole.error('Failed to finish transaction:', error);
1492
- throw error;
1542
+ if (!isUserCancelledError(parsedError)) {
1543
+ RnIapConsole.error('Failed to finish transaction:', error);
1544
+ }
1545
+ throw createPurchaseError({
1546
+ code: parsedError.code,
1547
+ message: parsedError.message,
1548
+ responseCode: parsedError.responseCode,
1549
+ debugMessage: parsedError.debugMessage
1550
+ });
1493
1551
  }
1494
1552
  };
1495
1553
 
@@ -2060,18 +2118,13 @@ export const getActiveSubscriptions = async subscriptionIds => {
2060
2118
  *
2061
2119
  * @param subscriptionIds - Optional array of subscription IDs to check
2062
2120
  * @returns Promise<boolean> - True if there are active subscriptions
2121
+ * @throws When the store cannot determine subscription status
2063
2122
  *
2064
2123
  * @see {@link https://openiap.dev/docs/apis/has-active-subscriptions}
2065
2124
  */
2066
2125
  export const hasActiveSubscriptions = async subscriptionIds => {
2067
- try {
2068
- const activeSubscriptions = await getActiveSubscriptions(subscriptionIds);
2069
- return activeSubscriptions.length > 0;
2070
- } catch (error) {
2071
- // If there's an error getting subscriptions, return false
2072
- RnIapConsole.warn('Error checking active subscriptions:', error);
2073
- return false;
2074
- }
2126
+ const activeSubscriptions = await getActiveSubscriptions(subscriptionIds);
2127
+ return activeSubscriptions.length > 0;
2075
2128
  };
2076
2129
 
2077
2130
  // Type conversion utilities
@@ -2128,7 +2181,8 @@ const normalizeProductQueryType = type => {
2128
2181
  * ```typescript
2129
2182
  * // Enable external offers before connecting
2130
2183
  * enableBillingProgramAndroid('external-offer');
2131
- * await initConnection();
2184
+ * const connected = await initConnection();
2185
+ * if (!connected) throw new Error('Store connection failed');
2132
2186
  * ```
2133
2187
  */
2134
2188
  export const enableBillingProgramAndroid = program => {