Subscription Billing Issue Event
A single cross-platform event that fires when a user's subscription enters a state that needs attention due to a payment problem (card declined, expired payment method, billing retry, grace period, etc.).
Platform behavior#
| Platform | Signal Source | Delivery |
|---|---|---|
| iOS / iPadOS | StoreKit.Message.Reason.billingIssueiOS/iPadOS 16.4+ | Push, while app is active |
| Mac Catalyst | StoreKit.Message.Reason.billingIssueMac Catalyst 16.4+ | Push, while app is active |
| Android (Play) | Purchase.isSuspendedPlay Billing Library 8.1+ | Poll via getAvailablePurchases or on onPurchasesUpdated |
| Android (Meta Horizon) | Not available Billing 7.0 compat SDK | Listener is a no-op; resolver fails as unsupported |
| Android (Amazon) | Not available Amazon Appstore IAP | Listener is a no-op; resolver fails as unsupported |
| visionOS | StoreKit.Message.Reason.billingIssuevisionOS 1.0+ | Push, while app is active |
| macOS / tvOS / watchOS | StoreKit.Message not available | Never fires |
On Apple platforms, OpenIAP still presents StoreKit's system billing message through Message.display(in:). The billing-issue event is an additional app notification for custom UI or analytics; it does not replace or suppress Apple's default message.
Apple references: StoreKit.Message · Reason.billingIssue. Google reference: Suspended subscriptions.
Recommended UX#
When this event fires, route the user to the platform subscription center via deepLinkToSubscriptions() so they can update their payment method. Do not re-grant entitlements on the assumption the subscription is still active — Play suspends entitlement for these purchases, and iOS will re-emit the message until the billing issue is resolved.
Usage#
// react-native-iap
import {
subscriptionBillingIssueListener,
deepLinkToSubscriptions,
} from 'react-native-iap';
// OR expo-iap
// import {
// subscriptionBillingIssueListener,
// deepLinkToSubscriptions,
// } from 'expo-iap';
const subscription = subscriptionBillingIssueListener((purchase) => {
console.warn('Subscription needs attention:', purchase.productId);
void deepLinkToSubscriptions({
skuAndroid: purchase.productId,
packageNameAndroid: 'com.example.app',
}).catch((error) => {
console.error('Failed to open subscription settings', error);
});
});
// Cleanup
subscription.remove();Deduping#
On Android, the native SDK tracks emitted purchase tokens per session so the event fires once per affected purchase even if the app polls getAvailablePurchases repeatedly. The dedupe set is only cleared on endConnection() or app restart — a purchase that exits suspension and re-enters within the same session willnot re-emit until the next reconnect or process restart.
On iOS the StoreKit Message may be re-delivered by the system until the user resolves the underlying issue; for a given message the SDK discovers subscription groups from transaction history, then reads StoreKit's authoritative status and fires one event per transaction in .inBillingRetryPeriod or .inGracePeriod. This includes retrying subscriptions that are no longer current entitlements.
Native References#
- Apple · StoreKit.Message
- Apple · Message.Reason.billingIssue
- Google · Purchase.isSuspended()
- Google · Suspended subscriptions