react-native-iap 15.1.0 → 15.2.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.
@@ -25,6 +25,7 @@ import {
25
25
  showAlternativeBillingDialogAndroid,
26
26
  createAlternativeBillingTokenAndroid,
27
27
  userChoiceBillingListenerAndroid,
28
+ subscriptionBillingIssueListener,
28
29
  isStandardIOS,
29
30
  } from '../';
30
31
 
@@ -112,6 +113,16 @@ export interface UseIapOptions {
112
113
  onError?: (error: Error) => void;
113
114
  onPromotedProductIOS?: (product: Product) => void;
114
115
  onUserChoiceBillingAndroid?: (details: UserChoiceBillingDetails) => void;
116
+ /**
117
+ * Fires when an active subscription enters a billing-issue state
118
+ * (StoreKit 2 Message.billingIssue on iOS 18+, Purchase.isSuspended on
119
+ * Play Billing 8.1+). Not invoked on Meta Horizon.
120
+ *
121
+ * Recommended: call deepLinkToSubscriptions on the returned purchase so
122
+ * the user can update their payment method in the platform subscription
123
+ * center.
124
+ */
125
+ onSubscriptionBillingIssue?: (purchase: Purchase) => void;
115
126
  /**
116
127
  * @deprecated Use enableBillingProgramAndroid instead.
117
128
  * - 'user-choice' → 'user-choice-billing'
@@ -178,6 +189,7 @@ export function useIAP(options?: UseIapOptions): UseIap {
178
189
  purchaseError?: EventSubscription;
179
190
  promotedProductIOS?: EventSubscription;
180
191
  userChoiceBillingAndroid?: EventSubscription;
192
+ subscriptionBillingIssue?: EventSubscription;
181
193
  }>({});
182
194
 
183
195
  // Track if component is mounted to prevent listener leaks on early unmount
@@ -463,6 +475,15 @@ export function useIAP(options?: UseIapOptions): UseIap {
463
475
  }
464
476
  });
465
477
  }
478
+
479
+ // Always attach so callers that supply `onSubscriptionBillingIssue` later
480
+ // (after the hook has already set up listeners) still receive events.
481
+ if (!subscriptionsRef.current.subscriptionBillingIssue) {
482
+ subscriptionsRef.current.subscriptionBillingIssue =
483
+ subscriptionBillingIssueListener((purchase: Purchase) => {
484
+ optionsRef.current?.onSubscriptionBillingIssue?.(purchase);
485
+ });
486
+ }
466
487
  }, [getActiveSubscriptionsInternal, getAvailablePurchasesInternal]);
467
488
 
468
489
  // Shared helper: clean up all listeners
@@ -471,10 +492,12 @@ export function useIAP(options?: UseIapOptions): UseIap {
471
492
  subscriptionsRef.current.purchaseError?.remove();
472
493
  subscriptionsRef.current.promotedProductIOS?.remove();
473
494
  subscriptionsRef.current.userChoiceBillingAndroid?.remove();
495
+ subscriptionsRef.current.subscriptionBillingIssue?.remove();
474
496
  subscriptionsRef.current.purchaseUpdate = undefined;
475
497
  subscriptionsRef.current.purchaseError = undefined;
476
498
  subscriptionsRef.current.promotedProductIOS = undefined;
477
499
  subscriptionsRef.current.userChoiceBillingAndroid = undefined;
500
+ subscriptionsRef.current.subscriptionBillingIssue = undefined;
478
501
  }, []);
479
502
 
480
503
  const initIapWithSubscriptions = useCallback(async (): Promise<void> => {
package/src/index.ts CHANGED
@@ -279,12 +279,14 @@ export const resetListenerState = (): void => {
279
279
  promotedProductNativeAttached = false;
280
280
  userChoiceBillingNativeAttached = false;
281
281
  developerProvidedBillingNativeAttached = false;
282
+ subscriptionBillingIssueNativeAttached = false;
282
283
  // Clear all JS listeners since native side clears them in endConnection
283
284
  purchaseUpdateJsListeners.clear();
284
285
  purchaseErrorJsListeners.clear();
285
286
  promotedProductJsListeners.clear();
286
287
  userChoiceBillingJsListeners.clear();
287
288
  developerProvidedBillingJsListeners.clear();
289
+ subscriptionBillingIssueJsListeners.clear();
288
290
  };
289
291
 
290
292
  export const purchaseUpdatedListener = (
@@ -565,6 +567,96 @@ export const developerProvidedBillingListenerAndroid = (
565
567
  };
566
568
  };
567
569
 
570
+ /**
571
+ * Listen for subscription billing-issue events (cross-platform).
572
+ *
573
+ * Fires when an active subscription enters a billing-issue state:
574
+ * - iOS 18+ / Mac Catalyst 18+: via StoreKit 2 `Message.Reason.billingIssue`.
575
+ * - Android (Play Billing 8.1+): when `isSuspendedAndroid === true` is observed.
576
+ * - Horizon / iOS 17 / older platforms: never fires.
577
+ *
578
+ * Recommended UX: on fire, call `deepLinkToSubscriptions()` so the user can
579
+ * update their payment method in the platform subscription center.
580
+ *
581
+ * @param listener - Function to call with the affected Purchase
582
+ * @returns EventSubscription with remove() method to unsubscribe
583
+ *
584
+ * @example
585
+ * ```typescript
586
+ * const subscription = subscriptionBillingIssueListener((purchase) => {
587
+ * console.warn('Subscription needs attention:', purchase.productId);
588
+ * deepLinkToSubscriptions({skuAndroid: purchase.productId, packageNameAndroid: 'com.example.app'});
589
+ * });
590
+ *
591
+ * subscription.remove();
592
+ * ```
593
+ */
594
+ type NitroSubscriptionBillingIssueListener = Parameters<
595
+ RnIap['addSubscriptionBillingIssueListener']
596
+ >[0];
597
+
598
+ const subscriptionBillingIssueJsListeners = new Set<(purchase: Purchase) => void>();
599
+ let subscriptionBillingIssueNativeAttached = false;
600
+ const subscriptionBillingIssueNativeHandler: NitroSubscriptionBillingIssueListener =
601
+ (nitroPurchase) => {
602
+ if (!validateNitroPurchase(nitroPurchase)) {
603
+ RnIapConsole.warn(
604
+ '[subscriptionBillingIssueListener] dropped malformed native payload',
605
+ );
606
+ return;
607
+ }
608
+ const purchase = convertNitroPurchaseToPurchase(nitroPurchase);
609
+ for (const listener of subscriptionBillingIssueJsListeners) {
610
+ try {
611
+ listener(purchase);
612
+ } catch (e) {
613
+ RnIapConsole.error(
614
+ '[subscriptionBillingIssueListener] callback threw:',
615
+ e,
616
+ );
617
+ }
618
+ }
619
+ };
620
+
621
+ function tryAttachSubscriptionBillingIssueNative(): void {
622
+ if (subscriptionBillingIssueNativeAttached) return;
623
+ try {
624
+ IAP.instance.addSubscriptionBillingIssueListener(
625
+ subscriptionBillingIssueNativeHandler,
626
+ );
627
+ subscriptionBillingIssueNativeAttached = true;
628
+ } catch (e) {
629
+ const msg = toErrorMessage(e);
630
+ if (msg.includes('Nitro runtime not installed')) {
631
+ RnIapConsole.warn(
632
+ '[subscriptionBillingIssueListener] Nitro not ready yet; will retry on next registration after initConnection()',
633
+ );
634
+ } else {
635
+ throw e;
636
+ }
637
+ }
638
+ }
639
+
640
+ export const subscriptionBillingIssueListener = (
641
+ listener: (purchase: Purchase) => void,
642
+ ): EventSubscription => {
643
+ subscriptionBillingIssueJsListeners.add(listener);
644
+ // Retry attachment every call so a listener registered before initConnection()
645
+ // doesn't stay permanently inert once Nitro is ready.
646
+ try {
647
+ tryAttachSubscriptionBillingIssueNative();
648
+ } catch (error) {
649
+ subscriptionBillingIssueJsListeners.delete(listener);
650
+ throw error;
651
+ }
652
+
653
+ return {
654
+ remove: () => {
655
+ subscriptionBillingIssueJsListeners.delete(listener);
656
+ },
657
+ };
658
+ };
659
+
568
660
  // ------------------------------
569
661
  // Query API
570
662
  // ------------------------------
@@ -1095,6 +1095,29 @@ export interface RnIap extends HybridObject<{ios: 'swift'; android: 'kotlin'}> {
1095
1095
  listener: (details: DeveloperProvidedBillingDetailsAndroid) => void,
1096
1096
  ): void;
1097
1097
 
1098
+ /**
1099
+ * Add a listener for subscription billing-issue events (cross-platform).
1100
+ *
1101
+ * Fires when a user's active subscription enters a state that needs attention
1102
+ * (payment method failed, card expired, etc.). Unifies:
1103
+ * - StoreKit 2 `Message.Reason.billingIssue` (iOS 18+ / Mac Catalyst 18+)
1104
+ * - Google Play Billing `Purchase.isSuspended` (Play Billing 8.1+)
1105
+ *
1106
+ * NOT fired on Meta Horizon (Billing 7.0 compat SDK lacks the suspended signal).
1107
+ *
1108
+ * @param listener - Called with the affected Purchase
1109
+ */
1110
+ addSubscriptionBillingIssueListener(
1111
+ listener: (purchase: NitroPurchase) => void,
1112
+ ): void;
1113
+
1114
+ /**
1115
+ * Remove a subscription billing-issue listener.
1116
+ */
1117
+ removeSubscriptionBillingIssueListener(
1118
+ listener: (purchase: NitroPurchase) => void,
1119
+ ): void;
1120
+
1098
1121
  // ╔════════════════════════════════════════════════════════════════════════╗
1099
1122
  // ║ BILLING PROGRAMS API (Android 8.2.0+) ║
1100
1123
  // ╚════════════════════════════════════════════════════════════════════════╝
package/src/types.ts CHANGED
@@ -457,7 +457,7 @@ export interface ExternalPurchaseNoticeResultIOS {
457
457
 
458
458
  export type FetchProductsResult = ProductOrSubscription[] | Product[] | ProductSubscription[] | null;
459
459
 
460
- export type IapEvent = 'purchase-updated' | 'purchase-error' | 'promoted-product-ios' | 'user-choice-billing-android' | 'developer-provided-billing-android';
460
+ export type IapEvent = 'purchase-updated' | 'purchase-error' | 'promoted-product-ios' | 'user-choice-billing-android' | 'developer-provided-billing-android' | 'subscription-billing-issue';
461
461
 
462
462
  export type IapPlatform = 'ios' | 'android';
463
463
 
@@ -1560,6 +1560,20 @@ export interface Subscription {
1560
1560
  purchaseError: PurchaseError;
1561
1561
  /** Fires when a purchase completes successfully or a pending purchase resolves */
1562
1562
  purchaseUpdated: Purchase;
1563
+ /**
1564
+ * Fires when an active subscription enters a billing-issue state that needs user action
1565
+ * (payment method failed, card expired, etc.). Cross-platform unification:
1566
+ *
1567
+ * - iOS 18+: delivered via StoreKit 2 `Message.Reason.billingIssue`.
1568
+ * - Android (Play flavor, Billing 8.1+): emitted when `isSuspended == true` is first detected
1569
+ * on a previously healthy subscription. Requires Google Play Billing Library 8.1.0 or newer.
1570
+ * - Android (Horizon flavor): NOT emitted. The Horizon Billing Compatibility SDK implements
1571
+ * the Play Billing 7.0 API surface which does not expose a suspended-subscription signal.
1572
+ *
1573
+ * Listeners should not assume the event will fire on every store. Direct users to the
1574
+ * platform subscription management UI (`deepLinkToSubscriptions`) to resolve the issue.
1575
+ */
1576
+ subscriptionBillingIssue: Purchase;
1563
1577
  /**
1564
1578
  * Fires when a user selects alternative billing in the User Choice Billing dialog (Android only)
1565
1579
  * Only triggered when the user selects alternative billing instead of Google Play billing
@@ -1965,6 +1979,7 @@ export type SubscriptionArgsMap = {
1965
1979
  promotedProductIOS: never;
1966
1980
  purchaseError: never;
1967
1981
  purchaseUpdated: never;
1982
+ subscriptionBillingIssue: never;
1968
1983
  userChoiceBillingAndroid: never;
1969
1984
  };
1970
1985