Every IAP submission is exercised by an Apple reviewer who will attempt to cancel the OS payment sheet. CWE-755 (Improper Handling of Exceptional Conditions) applies directly: an unhandled cancellation error that surfaces a crash or an error dialog is an automatic rejection under Apple guideline 3.1.1. Beyond review, a purchase flow that crashes or shows a confusing error when a user declines to buy drives uninstalls and negative reviews — user cancellation is a normal event, not an error state. A broken purchase error flow also risks leaving entitlement state inconsistent: the user may be charged by the platform but denied access by the app if the success path is never reached.
High because a crash or error dialog on user cancellation causes immediate App Store rejection and is exercised by every reviewer examining an IAP app.
Wrap every purchase SDK call in a try/catch and explicitly distinguish user cancellation (silent, no UI) from billing failures (show a toast or alert). Verify entitlement state after a successful purchase rather than assuming the success callback equals granted access.
// React Native IAP
try {
await requestSubscription({ sku: PRODUCT_IDS.MONTHLY });
} catch (error) {
if (error instanceof PurchaseError && error.code === 'E_USER_CANCELLED') {
return; // Silent — user chose not to subscribe
}
showErrorToast('Purchase failed. Please try again.');
}
// RevenueCat
try {
const { customerInfo } = await Purchases.purchasePackage(pkg);
if (customerInfo.entitlements.active['premium']) unlockPremium();
} catch (e) {
if (!e.userCancelled) showErrorToast('Something went wrong. Please try again.');
}
Also handle network-level failures — users on flaky connections will trigger purchase timeouts that must recover gracefully without leaving the purchase screen in a broken state.
app-store-iap-subscriptions.iap-integration.purchase-errorshighreact-native-iap: look for try/catch around requestSubscription() or requestPurchase(), handling of PurchaseError with code E_USER_CANCELLED (must be silent, not shown as an error). For RevenueCat: look for catch on Purchases.purchasePackage(), distinguishing PurchasesErrorCode.purchaseCancelledError (silent) from real errors. For Flutter in_app_purchase: look for purchaseStream listener handling PurchaseStatus.error, PurchaseStatus.canceled. For StoreKit 2 (Swift): look for try await product.purchase() with switch result { case .success, .userCancelled, .pending }. For Play Billing (Kotlin): look for BillingResult with responseCode == BillingClient.BillingResponseCode.USER_CANCELED. Critical failure patterns: (1) showing an error dialog when user taps "Cancel" on the OS payment sheet — Apple explicitly prohibits this; (2) no error handling at all (bare await sdk.purchase() with no catch); (3) app crashes or hangs after a failed purchase; (4) purchase state is left inconsistent after an error (user charged but entitlement not granted, or vice versa — check for post-purchase verification)."requestSubscription() in src/screens/PaywallScreen.tsx has no try/catch — any purchase error crashes the screen" or "User cancellation triggers an error dialog in PurchaseButton.tsx — Apple Guideline 3.1.1 violation"try {
await requestSubscription({ sku: PRODUCT_IDS.MONTHLY });
} catch (error) {
if (error instanceof PurchaseError && error.code === 'E_USER_CANCELLED') {
return; // Silent — user chose not to subscribe
}
showErrorToast('Purchase failed. Please try again.');
}
try {
const { customerInfo } = await Purchases.purchasePackage(pkg);
if (customerInfo.entitlements.active['premium']) {
unlockPremium();
}
} catch (e) {
if (!e.userCancelled) {
showErrorToast('Something went wrong. Please try again.');
}
}