Purchase flow handles cancellation, failure, and network errors gracefully
Why it matters
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.
Severity rationale
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.
Remediation
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.
Detection
- ID:
purchase-errors - Severity:
high - What to look for: Count all relevant instances and enumerate each. Trace the purchase initiation code path — the function or handler called when a user taps "Subscribe" or "Buy". Look for the SDK call and examine ALL branches it can return. For
react-native-iap: look fortry/catcharoundrequestSubscription()orrequestPurchase(), handling ofPurchaseErrorwith codeE_USER_CANCELLED(must be silent, not shown as an error). For RevenueCat: look forcatchonPurchases.purchasePackage(), distinguishingPurchasesErrorCode.purchaseCancelledError(silent) from real errors. For Flutterin_app_purchase: look forpurchaseStreamlistener handlingPurchaseStatus.error,PurchaseStatus.canceled. For StoreKit 2 (Swift): look fortry await product.purchase()withswitch result { case .success, .userCancelled, .pending }. For Play Billing (Kotlin): look forBillingResultwithresponseCode == 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 (bareawait 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). - Pass criteria: Purchase flow has: a try/catch or equivalent error handler; explicit handling for user cancellation (silent, no error shown); user-facing error message for billing failures (network error, card declined); and state recovery (purchase state restored after error). At least 1 implementation must be verified. A partial or placeholder implementation does not count as pass.
- Fail criteria: No error handling around purchase calls; user cancellation displays an error dialog; app crashes or freezes after failed purchase; no network error handling.
- Skip (N/A) when: No IAP detected in the app.
- Detail on fail:
"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" - Remediation: Purchase flows are exercised by every reviewer who examines an IAP app. A crash on cancellation is an immediate rejection.
- For 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.'); } - For 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.'); } } - Always verify entitlement state after a successful purchase rather than assuming the purchase = entitlement
- For React Native IAP:
External references
- cwe · CWE-755 — Improper Handling of Exceptional Conditions
- external · apple-guideline-3.1.1 — Apple App Store Review Guidelines § 3.1.1 — In-App Purchase (purchase error handling requirements)
Taxons
History
- 2026-04-18·v1.0.0·Initial import from app-store-iap-subscriptions·automated