react-native-iap 16.4.1 → 16.5.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 (71) hide show
  1. package/README.md +2 -0
  2. package/android/build.gradle +19 -21
  3. package/android/fix-prefab.gradle +11 -24
  4. package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +200 -142
  5. package/android/src/main/java/com/margelo/nitro/iap/TokenizedListenerRegistry.kt +0 -1
  6. package/ios/HybridRnIap.swift +832 -165
  7. package/ios/RnIapHelper.swift +1 -1
  8. package/lib/module/hooks/useIAP.js +48 -32
  9. package/lib/module/hooks/useIAP.js.map +1 -1
  10. package/lib/module/index.js +207 -153
  11. package/lib/module/index.js.map +1 -1
  12. package/lib/module/index.kepler.js +1 -0
  13. package/lib/module/index.kepler.js.map +1 -1
  14. package/lib/module/kit-api.js +9 -5
  15. package/lib/module/kit-api.js.map +1 -1
  16. package/lib/module/types.js +1 -0
  17. package/lib/module/types.js.map +1 -1
  18. package/lib/module/utils/error.js +5 -4
  19. package/lib/module/utils/error.js.map +1 -1
  20. package/lib/module/vega-adapter.js +4 -1
  21. package/lib/module/vega-adapter.js.map +1 -1
  22. package/lib/typescript/src/hooks/useIAP.d.ts +31 -15
  23. package/lib/typescript/src/hooks/useIAP.d.ts.map +1 -1
  24. package/lib/typescript/src/index.d.ts +28 -14
  25. package/lib/typescript/src/index.d.ts.map +1 -1
  26. package/lib/typescript/src/index.kepler.d.ts.map +1 -1
  27. package/lib/typescript/src/kit-api.d.ts +9 -5
  28. package/lib/typescript/src/kit-api.d.ts.map +1 -1
  29. package/lib/typescript/src/specs/RnIap.nitro.d.ts +10 -0
  30. package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
  31. package/lib/typescript/src/types.d.ts +12 -2
  32. package/lib/typescript/src/types.d.ts.map +1 -1
  33. package/lib/typescript/src/utils/error.d.ts.map +1 -1
  34. package/lib/typescript/src/vega-adapter.d.ts.map +1 -1
  35. package/nitrogen/generated/android/NitroIap+autolinking.cmake +1 -0
  36. package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +9 -0
  37. package/nitrogen/generated/android/c++/JNitroPurchaseRequest.hpp +7 -1
  38. package/nitrogen/generated/android/c++/JNitroPurchaseRequestType.hpp +58 -0
  39. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitHorizonProps.hpp +66 -0
  40. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitProps.hpp +8 -1
  41. package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithProviderProps.hpp +3 -0
  42. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.cpp +26 -0
  43. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.hpp +74 -0
  44. package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitProps.hpp +3 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseRequest.kt +8 -3
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroPurchaseRequestType.kt +23 -0
  47. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitHorizonProps.kt +56 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitProps.kt +7 -2
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.kt +62 -0
  50. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +65 -0
  51. package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +6 -0
  52. package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +6 -0
  53. package/nitrogen/generated/ios/swift/NitroPurchaseRequest.swift +13 -2
  54. package/nitrogen/generated/ios/swift/NitroPurchaseRequestType.swift +40 -0
  55. package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitHorizonProps.swift +66 -0
  56. package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitProps.swift +38 -1
  57. package/nitrogen/generated/ios/swift/Variant_NullType_NitroVerifyPurchaseWithIapkitHorizonProps.swift +30 -0
  58. package/nitrogen/generated/shared/c++/NitroPurchaseRequest.hpp +9 -2
  59. package/nitrogen/generated/shared/c++/NitroPurchaseRequestType.hpp +76 -0
  60. package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitHorizonProps.hpp +90 -0
  61. package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitProps.hpp +8 -1
  62. package/openiap-versions.json +3 -3
  63. package/package.json +1 -1
  64. package/src/hooks/useIAP.ts +96 -58
  65. package/src/index.kepler.ts +1 -0
  66. package/src/index.ts +249 -196
  67. package/src/kit-api.ts +10 -6
  68. package/src/specs/RnIap.nitro.ts +12 -0
  69. package/src/types.ts +13 -2
  70. package/src/utils/error.ts +7 -3
  71. package/src/vega-adapter.ts +9 -5
package/src/index.ts CHANGED
@@ -80,7 +80,7 @@ import {getVegaIapModule, isVegaOS} from './vega';
80
80
  // ------------------------------
81
81
 
82
82
  // BillingProgramAndroid, ExternalLinkLaunchModeAndroid, and ExternalLinkTypeAndroid
83
- // are exported from './types' (auto-generated from openiap-gql).
83
+ // are exported from './types' (generated from the OpenIAP client spec).
84
84
 
85
85
  // Export all types
86
86
  export type {
@@ -170,6 +170,7 @@ export type {
170
170
 
171
171
  // Create the RnIap HybridObject instance lazily to avoid early JSI crashes
172
172
  let iapRef: RnIap | null = null;
173
+ let attachingPendingNativeListeners = false;
173
174
 
174
175
  /**
175
176
  * Check if Nitro runtime is ready for IAP operations.
@@ -220,39 +221,52 @@ const isAndroidStoreRuntime = (): boolean => {
220
221
  return Platform.OS === 'android' || isVegaOS();
221
222
  };
222
223
 
223
- const IAP = {
224
- get instance(): RnIap {
225
- if (iapRef) return iapRef;
224
+ function getRawIapInstance(): RnIap {
225
+ if (iapRef) return iapRef;
226
226
 
227
- if (isVegaOS()) {
228
- const vegaModule = getVegaIapModule();
229
- if (!vegaModule) {
230
- throw new Error(
231
- '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.',
232
- );
233
- }
234
- iapRef = vegaModule;
235
- return iapRef;
227
+ if (isVegaOS()) {
228
+ const vegaModule = getVegaIapModule();
229
+ if (!vegaModule) {
230
+ throw new Error(
231
+ '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.',
232
+ );
236
233
  }
234
+ iapRef = vegaModule;
235
+ return iapRef;
236
+ }
237
237
 
238
- // Attempt to create the HybridObject and map common Nitro/JSI readiness errors
239
- try {
240
- iapRef = NitroModules.createHybridObject<RnIap>('RnIap');
241
- } catch (e) {
242
- const msg = toErrorMessage(e);
243
- if (
244
- msg.includes('Nitro') ||
245
- msg.includes('JSI') ||
246
- msg.includes('dispatcher') ||
247
- msg.includes('HybridObject')
248
- ) {
249
- throw new Error(
250
- 'Nitro runtime not installed yet. Ensure react-native-nitro-modules is initialized before calling IAP.',
251
- );
238
+ // Attempt to create the HybridObject and map common Nitro/JSI readiness errors
239
+ try {
240
+ iapRef = NitroModules.createHybridObject<RnIap>('RnIap');
241
+ } catch (e) {
242
+ const msg = toErrorMessage(e);
243
+ if (
244
+ msg.includes('Nitro') ||
245
+ msg.includes('JSI') ||
246
+ msg.includes('dispatcher') ||
247
+ msg.includes('HybridObject')
248
+ ) {
249
+ throw new Error(
250
+ 'Nitro runtime not installed yet. Ensure react-native-nitro-modules is initialized before calling IAP.',
251
+ );
252
+ }
253
+ throw e;
254
+ }
255
+ return iapRef;
256
+ }
257
+
258
+ const IAP = {
259
+ get instance(): RnIap {
260
+ const instance = getRawIapInstance();
261
+ if (!attachingPendingNativeListeners) {
262
+ attachingPendingNativeListeners = true;
263
+ try {
264
+ tryAttachPendingNativeListeners();
265
+ } finally {
266
+ attachingPendingNativeListeners = false;
252
267
  }
253
- throw e;
254
268
  }
255
- return iapRef;
269
+ return instance;
256
270
  },
257
271
  };
258
272
 
@@ -264,6 +278,20 @@ const IAP = {
264
278
  // hyodotdev/openiap#382.
265
279
  // ============================================================================
266
280
 
281
+ function attachNativeListenerOrDefer(label: string, attach: () => void): void {
282
+ try {
283
+ attach();
284
+ } catch (error) {
285
+ if (toErrorMessage(error).includes('Nitro runtime not installed')) {
286
+ RnIapConsole.warn(
287
+ `[${label}] Nitro not ready yet; will retry on the next native operation`,
288
+ );
289
+ return;
290
+ }
291
+ throw error;
292
+ }
293
+ }
294
+
267
295
  const purchaseUpdateJsListeners = new Set<(purchase: Purchase) => void>();
268
296
  const purchaseUpdateDuplicateJsListeners = new Set<
269
297
  (purchase: Purchase) => void
@@ -304,6 +332,38 @@ const purchaseUpdateDuplicateNativeHandler: NitroPurchaseListener = (
304
332
  );
305
333
  };
306
334
 
335
+ function tryAttachPurchaseUpdateNative(
336
+ receiveDuplicateTransactionUpdatesIOS: boolean,
337
+ ): void {
338
+ const alreadyAttached = receiveDuplicateTransactionUpdatesIOS
339
+ ? purchaseUpdateDuplicateNativeAttached
340
+ : purchaseUpdateNativeAttached;
341
+ if (alreadyAttached) return;
342
+
343
+ attachNativeListenerOrDefer('purchaseUpdatedListener', () => {
344
+ const nativeOptions:
345
+ | (NitroPurchaseUpdatedListenerOptions &
346
+ NitroPurchaseUpdatedListenerOptionsParam)
347
+ | undefined = receiveDuplicateTransactionUpdatesIOS
348
+ ? {dedupeTransactionIOS: false}
349
+ : undefined;
350
+ const token = getRawIapInstance().addPurchaseUpdatedListener(
351
+ receiveDuplicateTransactionUpdatesIOS
352
+ ? purchaseUpdateDuplicateNativeHandler
353
+ : purchaseUpdateNativeHandler,
354
+ nativeOptions,
355
+ );
356
+ if (receiveDuplicateTransactionUpdatesIOS) {
357
+ purchaseUpdateDuplicateNativeToken =
358
+ typeof token === 'number' ? token : null;
359
+ purchaseUpdateDuplicateNativeAttached = true;
360
+ } else {
361
+ purchaseUpdateNativeToken = typeof token === 'number' ? token : null;
362
+ purchaseUpdateNativeAttached = true;
363
+ }
364
+ });
365
+ }
366
+
307
367
  const purchaseErrorJsListeners = new Set<(error: PurchaseError) => void>();
308
368
  let purchaseErrorNativeAttached = false;
309
369
  const purchaseErrorNativeHandler: NitroPurchaseErrorListener = (error) => {
@@ -334,6 +394,14 @@ const purchaseErrorNativeHandler: NitroPurchaseErrorListener = (error) => {
334
394
  }
335
395
  };
336
396
 
397
+ function tryAttachPurchaseErrorNative(): void {
398
+ if (purchaseErrorNativeAttached) return;
399
+ attachNativeListenerOrDefer('purchaseErrorListener', () => {
400
+ getRawIapInstance().addPurchaseErrorListener(purchaseErrorNativeHandler);
401
+ purchaseErrorNativeAttached = true;
402
+ });
403
+ }
404
+
337
405
  const promotedProductJsListeners = new Set<(product: Product) => void>();
338
406
  let promotedProductNativeAttached = false;
339
407
  const promotedProductNativeHandler: NitroPromotedProductListener = (
@@ -356,6 +424,16 @@ const promotedProductNativeHandler: NitroPromotedProductListener = (
356
424
  }
357
425
  };
358
426
 
427
+ function tryAttachPromotedProductNative(): void {
428
+ if (promotedProductNativeAttached) return;
429
+ attachNativeListenerOrDefer('promotedProductListenerIOS', () => {
430
+ getRawIapInstance().addPromotedProductListenerIOS(
431
+ promotedProductNativeHandler,
432
+ );
433
+ promotedProductNativeAttached = true;
434
+ });
435
+ }
436
+
359
437
  /**
360
438
  * Reset all JS-level listener tracking state.
361
439
  * Called during endConnection to ensure clean re-registration on next initConnection.
@@ -391,52 +469,11 @@ export const purchaseUpdatedListener = (
391
469
  : purchaseUpdateJsListeners;
392
470
 
393
471
  listeners.add(listener);
394
-
395
- if (!purchaseUpdateNativeAttached && !receiveDuplicateTransactionUpdatesIOS) {
396
- try {
397
- const token = IAP.instance.addPurchaseUpdatedListener(
398
- purchaseUpdateNativeHandler,
399
- );
400
- purchaseUpdateNativeToken = typeof token === 'number' ? token : null;
401
- purchaseUpdateNativeAttached = true;
402
- } catch (e) {
403
- const msg = toErrorMessage(e);
404
- if (msg.includes('Nitro runtime not installed')) {
405
- RnIapConsole.warn(
406
- '[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()',
407
- );
408
- } else {
409
- throw e;
410
- }
411
- }
412
- }
413
-
414
- if (
415
- !purchaseUpdateDuplicateNativeAttached &&
416
- receiveDuplicateTransactionUpdatesIOS
417
- ) {
418
- try {
419
- const nativeOptions: NitroPurchaseUpdatedListenerOptions &
420
- NitroPurchaseUpdatedListenerOptionsParam = {
421
- dedupeTransactionIOS: false,
422
- };
423
- const token = IAP.instance.addPurchaseUpdatedListener(
424
- purchaseUpdateDuplicateNativeHandler,
425
- nativeOptions,
426
- );
427
- purchaseUpdateDuplicateNativeToken =
428
- typeof token === 'number' ? token : null;
429
- purchaseUpdateDuplicateNativeAttached = true;
430
- } catch (e) {
431
- const msg = toErrorMessage(e);
432
- if (msg.includes('Nitro runtime not installed')) {
433
- RnIapConsole.warn(
434
- '[purchaseUpdatedListener] Nitro not ready yet; listener inert until initConnection()',
435
- );
436
- } else {
437
- throw e;
438
- }
439
- }
472
+ try {
473
+ tryAttachPurchaseUpdateNative(receiveDuplicateTransactionUpdatesIOS);
474
+ } catch (error) {
475
+ listeners.delete(listener);
476
+ throw error;
440
477
  }
441
478
 
442
479
  let removed = false;
@@ -466,7 +503,7 @@ export const purchaseUpdatedListener = (
466
503
  }
467
504
 
468
505
  try {
469
- IAP.instance.removePurchaseUpdatedListener(token);
506
+ getRawIapInstance().removePurchaseUpdatedListener(token);
470
507
  if (receiveDuplicateTransactionUpdatesIOS) {
471
508
  purchaseUpdateDuplicateNativeToken = null;
472
509
  purchaseUpdateDuplicateNativeAttached = false;
@@ -485,21 +522,11 @@ export const purchaseErrorListener = (
485
522
  listener: (error: PurchaseError) => void,
486
523
  ): EventSubscription => {
487
524
  purchaseErrorJsListeners.add(listener);
488
-
489
- if (!purchaseErrorNativeAttached) {
490
- try {
491
- IAP.instance.addPurchaseErrorListener(purchaseErrorNativeHandler);
492
- purchaseErrorNativeAttached = true;
493
- } catch (e) {
494
- const msg = toErrorMessage(e);
495
- if (msg.includes('Nitro runtime not installed')) {
496
- RnIapConsole.warn(
497
- '[purchaseErrorListener] Nitro not ready yet; listener inert until initConnection()',
498
- );
499
- } else {
500
- throw e;
501
- }
502
- }
525
+ try {
526
+ tryAttachPurchaseErrorNative();
527
+ } catch (error) {
528
+ purchaseErrorJsListeners.delete(listener);
529
+ throw error;
503
530
  }
504
531
 
505
532
  return {
@@ -519,7 +546,9 @@ export const purchaseErrorListener = (
519
546
 
520
547
  if (purchaseErrorNativeAttached) {
521
548
  try {
522
- IAP.instance.removePurchaseErrorListener(purchaseErrorNativeHandler);
549
+ getRawIapInstance().removePurchaseErrorListener(
550
+ purchaseErrorNativeHandler,
551
+ );
523
552
  purchaseErrorNativeAttached = false;
524
553
  } catch (e) {
525
554
  RnIapConsole.warn('[purchaseErrorListener] native remove failed:', e);
@@ -548,21 +577,11 @@ export const promotedProductListenerIOS = (
548
577
  }
549
578
 
550
579
  promotedProductJsListeners.add(listener);
551
-
552
- if (!promotedProductNativeAttached) {
553
- try {
554
- IAP.instance.addPromotedProductListenerIOS(promotedProductNativeHandler);
555
- promotedProductNativeAttached = true;
556
- } catch (e) {
557
- const msg = toErrorMessage(e);
558
- if (msg.includes('Nitro runtime not installed')) {
559
- RnIapConsole.warn(
560
- '[promotedProductListenerIOS] Nitro not ready yet; listener inert until initConnection()',
561
- );
562
- } else {
563
- throw e;
564
- }
565
- }
580
+ try {
581
+ tryAttachPromotedProductNative();
582
+ } catch (error) {
583
+ promotedProductJsListeners.delete(listener);
584
+ throw error;
566
585
  }
567
586
 
568
587
  return {
@@ -588,7 +607,9 @@ export const promotedProductListenerIOS = (
588
607
  * console.log('External transaction token received; send it to your backend without logging it.');
589
608
  *
590
609
  * // Send token to backend for Google Play reporting
591
- * await reportToGooglePlay(details.externalTransactionToken);
610
+ * void reportToGooglePlay(details.externalTransactionToken).catch((error) => {
611
+ * console.warn('Alternative billing report failed', error);
612
+ * });
592
613
  * });
593
614
  *
594
615
  * // Later, remove the listener
@@ -618,6 +639,16 @@ const userChoiceBillingNativeHandler: NitroUserChoiceBillingListener = (
618
639
  }
619
640
  };
620
641
 
642
+ function tryAttachUserChoiceBillingNative(): void {
643
+ if (userChoiceBillingNativeAttached) return;
644
+ attachNativeListenerOrDefer('userChoiceBillingListenerAndroid', () => {
645
+ getRawIapInstance().addUserChoiceBillingListenerAndroid(
646
+ userChoiceBillingNativeHandler,
647
+ );
648
+ userChoiceBillingNativeAttached = true;
649
+ });
650
+ }
651
+
621
652
  export const userChoiceBillingListenerAndroid = (
622
653
  listener: (details: UserChoiceBillingDetails) => void,
623
654
  ): EventSubscription => {
@@ -629,23 +660,11 @@ export const userChoiceBillingListenerAndroid = (
629
660
  }
630
661
 
631
662
  userChoiceBillingJsListeners.add(listener);
632
-
633
- if (!userChoiceBillingNativeAttached) {
634
- try {
635
- IAP.instance.addUserChoiceBillingListenerAndroid(
636
- userChoiceBillingNativeHandler,
637
- );
638
- userChoiceBillingNativeAttached = true;
639
- } catch (e) {
640
- const msg = toErrorMessage(e);
641
- if (msg.includes('Nitro runtime not installed')) {
642
- RnIapConsole.warn(
643
- '[userChoiceBillingListenerAndroid] Nitro not ready yet; listener inert until initConnection()',
644
- );
645
- } else {
646
- throw e;
647
- }
648
- }
663
+ try {
664
+ tryAttachUserChoiceBillingNative();
665
+ } catch (error) {
666
+ userChoiceBillingJsListeners.delete(listener);
667
+ throw error;
649
668
  }
650
669
 
651
670
  let removed = false;
@@ -661,7 +680,7 @@ export const userChoiceBillingListenerAndroid = (
661
680
  return;
662
681
  }
663
682
  try {
664
- IAP.instance.removeUserChoiceBillingListenerAndroid(
683
+ getRawIapInstance().removeUserChoiceBillingListenerAndroid(
665
684
  userChoiceBillingNativeHandler,
666
685
  );
667
686
  userChoiceBillingNativeAttached = false;
@@ -690,10 +709,11 @@ export const userChoiceBillingListenerAndroid = (
690
709
  * @example
691
710
  * ```typescript
692
711
  * const subscription = developerProvidedBillingListenerAndroid((details) => {
693
- * await processExternalPayment(details.products, details.linkUri);
694
- * if (details.externalTransactionToken) {
695
- * await reportToGooglePlay(details.externalTransactionToken);
696
- * }
712
+ * void processExternalPayment(details.products, details.linkUri)
713
+ * .then(() => details.externalTransactionToken
714
+ * ? reportToGooglePlay(details.externalTransactionToken)
715
+ * : undefined)
716
+ * .catch((error) => console.warn('Developer billing failed', error));
697
717
  * });
698
718
  *
699
719
  * // Later, remove the listener
@@ -722,6 +742,16 @@ const developerProvidedBillingNativeHandler: NitroDeveloperProvidedBillingListen
722
742
  }
723
743
  };
724
744
 
745
+ function tryAttachDeveloperProvidedBillingNative(): void {
746
+ if (developerProvidedBillingNativeAttached) return;
747
+ attachNativeListenerOrDefer('developerProvidedBillingListenerAndroid', () => {
748
+ getRawIapInstance().addDeveloperProvidedBillingListenerAndroid(
749
+ developerProvidedBillingNativeHandler,
750
+ );
751
+ developerProvidedBillingNativeAttached = true;
752
+ });
753
+ }
754
+
725
755
  export const developerProvidedBillingListenerAndroid = (
726
756
  listener: (details: DeveloperProvidedBillingDetailsAndroid) => void,
727
757
  ): EventSubscription => {
@@ -733,23 +763,11 @@ export const developerProvidedBillingListenerAndroid = (
733
763
  }
734
764
 
735
765
  developerProvidedBillingJsListeners.add(listener);
736
-
737
- if (!developerProvidedBillingNativeAttached) {
738
- try {
739
- IAP.instance.addDeveloperProvidedBillingListenerAndroid(
740
- developerProvidedBillingNativeHandler,
741
- );
742
- developerProvidedBillingNativeAttached = true;
743
- } catch (e) {
744
- const msg = toErrorMessage(e);
745
- if (msg.includes('Nitro runtime not installed')) {
746
- RnIapConsole.warn(
747
- '[developerProvidedBillingListenerAndroid] Nitro not ready yet; listener inert until initConnection()',
748
- );
749
- } else {
750
- throw e;
751
- }
752
- }
766
+ try {
767
+ tryAttachDeveloperProvidedBillingNative();
768
+ } catch (error) {
769
+ developerProvidedBillingJsListeners.delete(listener);
770
+ throw error;
753
771
  }
754
772
 
755
773
  return {
@@ -814,21 +832,12 @@ const subscriptionBillingIssueNativeHandler: NitroSubscriptionBillingIssueListen
814
832
 
815
833
  function tryAttachSubscriptionBillingIssueNative(): void {
816
834
  if (subscriptionBillingIssueNativeAttached) return;
817
- try {
818
- IAP.instance.addSubscriptionBillingIssueListener(
835
+ attachNativeListenerOrDefer('subscriptionBillingIssueListener', () => {
836
+ getRawIapInstance().addSubscriptionBillingIssueListener(
819
837
  subscriptionBillingIssueNativeHandler,
820
838
  );
821
839
  subscriptionBillingIssueNativeAttached = true;
822
- } catch (e) {
823
- const msg = toErrorMessage(e);
824
- if (msg.includes('Nitro runtime not installed')) {
825
- RnIapConsole.warn(
826
- '[subscriptionBillingIssueListener] Nitro not ready yet; will retry after initConnection()',
827
- );
828
- } else {
829
- throw e;
830
- }
831
- }
840
+ });
832
841
  }
833
842
 
834
843
  export const subscriptionBillingIssueListener = (
@@ -851,6 +860,30 @@ export const subscriptionBillingIssueListener = (
851
860
  };
852
861
  };
853
862
 
863
+ function tryAttachPendingNativeListeners(): void {
864
+ if (purchaseUpdateJsListeners.size > 0) {
865
+ tryAttachPurchaseUpdateNative(false);
866
+ }
867
+ if (purchaseUpdateDuplicateJsListeners.size > 0) {
868
+ tryAttachPurchaseUpdateNative(true);
869
+ }
870
+ if (purchaseErrorJsListeners.size > 0) {
871
+ tryAttachPurchaseErrorNative();
872
+ }
873
+ if (promotedProductJsListeners.size > 0) {
874
+ tryAttachPromotedProductNative();
875
+ }
876
+ if (userChoiceBillingJsListeners.size > 0) {
877
+ tryAttachUserChoiceBillingNative();
878
+ }
879
+ if (developerProvidedBillingJsListeners.size > 0) {
880
+ tryAttachDeveloperProvidedBillingNative();
881
+ }
882
+ if (subscriptionBillingIssueJsListeners.size > 0) {
883
+ tryAttachSubscriptionBillingIssueNative();
884
+ }
885
+ }
886
+
854
887
  // ------------------------------
855
888
  // Query API
856
889
  // ------------------------------
@@ -1170,8 +1203,16 @@ export const getAppTransactionIOS: QueryField<
1170
1203
 
1171
1204
  return null;
1172
1205
  } catch (error) {
1173
- RnIapConsole.error('Failed to get app transaction:', error);
1174
- throw error;
1206
+ const parsedError = parseErrorAndLogIfNeeded(
1207
+ 'Failed to get app transaction:',
1208
+ error,
1209
+ );
1210
+ throw createPurchaseError({
1211
+ code: parsedError.code,
1212
+ message: parsedError.message,
1213
+ responseCode: parsedError.responseCode,
1214
+ debugMessage: parsedError.debugMessage,
1215
+ });
1175
1216
  }
1176
1217
  };
1177
1218
 
@@ -1545,16 +1586,22 @@ export const getTransactionJwsIOS: QueryField<'getTransactionJwsIOS'> = async (
1545
1586
  *
1546
1587
  * @example
1547
1588
  * ```ts
1548
- * await initConnection();
1549
- * await initConnection({ enableBillingProgramAndroid: 'external-offer' });
1550
- * await initConnection({
1551
- * enableBillingProgramAndroid: 'billing-choice',
1552
- * billingChoiceScreenTypeAndroid: 'developer-rendered',
1553
- * });
1589
+ * // Choose exactly one connection call for the session.
1590
+ * const connected = await initConnection();
1591
+ * // Or replace the call above with one Android billing program:
1592
+ * // const connected = await initConnection({
1593
+ * // enableBillingProgramAndroid: 'external-offer',
1594
+ * // });
1595
+ * // const connected = await initConnection({
1596
+ * // enableBillingProgramAndroid: 'billing-choice',
1597
+ * // billingChoiceScreenTypeAndroid: 'developer-rendered',
1598
+ * // });
1599
+ * if (!connected) throw new Error('Store connection failed');
1554
1600
  * ```
1555
1601
  *
1556
- * @remarks When using `useIAP()`, connection is auto-managed on mount/unmount
1557
- * pass options to the hook instead of calling this directly.
1602
+ * @remarks When using `useIAP()`, the connection initializes on mount. On unmount,
1603
+ * React Native removes hook listeners but keeps the native connection open across
1604
+ * screens. Pass options to the hook instead of calling this directly.
1558
1605
  *
1559
1606
  * @see {@link https://openiap.dev/docs/apis/init-connection}
1560
1607
  */
@@ -1565,9 +1612,7 @@ export const initConnection: MutationField<'initConnection'> = async (
1565
1612
  const result = await IAP.instance.initConnection(
1566
1613
  config as Record<string, unknown> | undefined,
1567
1614
  );
1568
- if (subscriptionBillingIssueJsListeners.size > 0) {
1569
- tryAttachSubscriptionBillingIssueNative();
1570
- }
1615
+ tryAttachPendingNativeListeners();
1571
1616
  return result;
1572
1617
  } catch (error) {
1573
1618
  const parsedError = parseErrorAndLogIfNeeded(
@@ -1590,8 +1635,9 @@ export const initConnection: MutationField<'initConnection'> = async (
1590
1635
  */
1591
1636
  export const endConnection: MutationField<'endConnection'> = async () => {
1592
1637
  try {
1593
- if (!iapRef) return true;
1594
- return await IAP.instance.endConnection();
1638
+ const result = iapRef ? await IAP.instance.endConnection() : true;
1639
+ resetListenerState();
1640
+ return result;
1595
1641
  } catch (error) {
1596
1642
  const parsedError = parseErrorAndLogIfNeeded(
1597
1643
  'Failed to end IAP connection:',
@@ -1603,8 +1649,6 @@ export const endConnection: MutationField<'endConnection'> = async () => {
1603
1649
  responseCode: parsedError.responseCode,
1604
1650
  debugMessage: parsedError.debugMessage,
1605
1651
  });
1606
- } finally {
1607
- resetListenerState();
1608
1652
  }
1609
1653
  };
1610
1654
 
@@ -1652,10 +1696,14 @@ export const restorePurchases: MutationField<'restorePurchases'> = async () => {
1652
1696
  * - `type: 'in-app'` — pass `request.apple.sku` (iOS) and/or `request.google.skus` (Android).
1653
1697
  * - `type: 'subs'` — same shape, plus `request.google.subscriptionOffers: [{ sku, offerToken }]`.
1654
1698
  * @returns The dispatched purchase payload. **Do not rely on it** for the actual outcome.
1655
- * @throws Synchronous rejection from the store (e.g. `E_NOT_PREPARED`, validation failure).
1699
+ * @throws Invalid arguments or failures that prevent dispatch. Store outcomes are event-based;
1700
+ * on Android, `not-prepared` is delivered through `purchaseErrorListener`.
1656
1701
  *
1657
1702
  * @example
1658
1703
  * ```ts
1704
+ * const connected = await initConnection();
1705
+ * if (!connected) return;
1706
+ *
1659
1707
  * await requestPurchase({
1660
1708
  * request: {
1661
1709
  * apple: { sku: 'com.app.premium' },
@@ -1722,7 +1770,7 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
1722
1770
  throw unsupportedPlatformError();
1723
1771
  }
1724
1772
 
1725
- const unifiedRequest: NitroPurchaseRequest = {};
1773
+ const unifiedRequest: NitroPurchaseRequest = {type: normalizedType};
1726
1774
 
1727
1775
  if (Platform.OS === 'ios' && iosRequestSource) {
1728
1776
  const iosRequest = isSubs
@@ -1901,10 +1949,12 @@ export const requestPurchase: MutationField<'requestPurchase'> = async (
1901
1949
  *
1902
1950
  * @example
1903
1951
  * ```ts
1904
- * purchaseUpdatedListener(async (purchase) => {
1905
- * if (await verifyOnServer(purchase)) {
1906
- * await finishTransaction({ purchase, isConsumable: false });
1907
- * }
1952
+ * purchaseUpdatedListener((purchase) => {
1953
+ * void verifyOnServer(purchase)
1954
+ * .then((verified) => verified
1955
+ * ? finishTransaction({ purchase, isConsumable: false })
1956
+ * : undefined)
1957
+ * .catch((error) => console.warn('Transaction finalization failed:', error));
1908
1958
  * });
1909
1959
  * ```
1910
1960
  *
@@ -1952,11 +2002,11 @@ export const finishTransaction: MutationField<'finishTransaction'> = async (
1952
2002
  }
1953
2003
  return;
1954
2004
  } catch (error) {
2005
+ const parsedError = parseErrorStringToJsonObj(error);
1955
2006
  // If iOS transaction has already been auto-finished natively, treat as success
1956
2007
  if (Platform.OS === 'ios') {
1957
- const err = parseErrorStringToJsonObj(error);
1958
- const msg = (err?.message || '').toString();
1959
- const code = (err?.code || '').toString();
2008
+ const msg = (parsedError.message || '').toString();
2009
+ const code = (parsedError.code || '').toString();
1960
2010
  if (
1961
2011
  msg.includes('Transaction not found') ||
1962
2012
  code === 'E_ITEM_UNAVAILABLE'
@@ -1965,8 +2015,15 @@ export const finishTransaction: MutationField<'finishTransaction'> = async (
1965
2015
  return;
1966
2016
  }
1967
2017
  }
1968
- RnIapConsole.error('Failed to finish transaction:', error);
1969
- throw error;
2018
+ if (!isUserCancelledError(parsedError)) {
2019
+ RnIapConsole.error('Failed to finish transaction:', error);
2020
+ }
2021
+ throw createPurchaseError({
2022
+ code: parsedError.code,
2023
+ message: parsedError.message,
2024
+ responseCode: parsedError.responseCode,
2025
+ debugMessage: parsedError.debugMessage,
2026
+ });
1970
2027
  }
1971
2028
  };
1972
2029
 
@@ -2638,20 +2695,15 @@ export const getActiveSubscriptions: QueryField<
2638
2695
  *
2639
2696
  * @param subscriptionIds - Optional array of subscription IDs to check
2640
2697
  * @returns Promise<boolean> - True if there are active subscriptions
2698
+ * @throws When the store cannot determine subscription status
2641
2699
  *
2642
2700
  * @see {@link https://openiap.dev/docs/apis/has-active-subscriptions}
2643
2701
  */
2644
2702
  export const hasActiveSubscriptions: QueryField<
2645
2703
  'hasActiveSubscriptions'
2646
2704
  > = async (subscriptionIds) => {
2647
- try {
2648
- const activeSubscriptions = await getActiveSubscriptions(subscriptionIds);
2649
- return activeSubscriptions.length > 0;
2650
- } catch (error) {
2651
- // If there's an error getting subscriptions, return false
2652
- RnIapConsole.warn('Error checking active subscriptions:', error);
2653
- return false;
2654
- }
2705
+ const activeSubscriptions = await getActiveSubscriptions(subscriptionIds);
2706
+ return activeSubscriptions.length > 0;
2655
2707
  };
2656
2708
 
2657
2709
  // Type conversion utilities
@@ -2735,7 +2787,8 @@ const normalizeProductQueryType = (
2735
2787
  * ```typescript
2736
2788
  * // Enable external offers before connecting
2737
2789
  * enableBillingProgramAndroid('external-offer');
2738
- * await initConnection();
2790
+ * const connected = await initConnection();
2791
+ * if (!connected) throw new Error('Store connection failed');
2739
2792
  * ```
2740
2793
  */
2741
2794
  export const enableBillingProgramAndroid = (