# Billing grace period supported for subscription renewals

- **Pattern:** `ab-000409` (`app-store-iap-subscriptions.restore-entitlements.grace-period`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000409
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000409)

## Why it matters

Apple's billing grace period allows subscribers to retain premium access for up to 16 days while a renewal payment fails — the platform retries the charge during this window. Apps that immediately revoke access on the first `DID_FAIL_TO_RENEW` notification lock out users who have a temporary billing issue (expired card, insufficient funds) and would have renewed successfully within the grace period. This creates unnecessary churn from users who intended to remain subscribers and generates chargebacks and support volume. Google Play has a parallel mechanism under the `SUBSCRIPTION_ON_HOLD` state in Real-Time Developer Notifications.

## Severity rationale

Low because immediate access revocation on billing failure causes avoidable churn but does not constitute a rejection reason — it is a product-quality and revenue-retention deficiency.

## Remediation

Enable billing grace period in App Store Connect under Subscriptions → Billing Grace Period (up to 16 days). In code, retain access during the grace period window rather than revoking on the first failed renewal signal.

```tsx
// RevenueCat — user retains active entitlement during grace period
const entitlement = customerInfo.entitlements.active['premium'];
const hasBillingIssue = entitlement?.billingIssueDetectedAt !== null;
const isPremium = entitlement !== undefined; // true during grace period
// Show a soft banner if hasBillingIssue, but do not revoke access
```

For custom webhook handlers, check for `isInBillingRetryPeriod: true` in the Apple App Store Server Notification v2 payload before revoking — only revoke after the grace period expires, not on the first failure event.

## Detection

- **ID:** `grace-period`
- **Severity:** `low`
- **What to look for:** Count all relevant instances and enumerate each. Billing grace periods allow users to retain premium access for a short period (up to 16 days on iOS) while a subscription renewal fails due to a billing issue. Look for: (1) Apple's `isInBillingRetryPeriod` or `DID_FAIL_TO_RENEW` App Store Server Notification v2 handling in webhook code. (2) Google Play's `SUBSCRIPTION_ON_HOLD` or grace period state in RTDN handler. (3) RevenueCat's `PeriodType.trial` and billing issues in `customerInfo` — RevenueCat surfaces `customerInfo.entitlements['premium'].billingIssueDetectedAt`. (4) Entitlement grant logic that checks grace period status: users in billing retry should retain access, not be immediately downgraded. Fail pattern: entitlement check uses only `isActive` without considering grace period, causing premium users with a billing issue to be immediately locked out while Apple/Google are still retrying payment.
- **Pass criteria:** Grace period handling is present (RevenueCat `billingIssueDetectedAt` checked, or Apple/Google webhook handles billing retry with continued access grant), OR app is iOS-only and grace period is configured in App Store Connect (requires code to honor it). At least 1 implementation must be verified. OR this cannot be determined (skip).
- **Fail criteria:** Entitlement code immediately revokes access when renewal fails, without a grace period — confirmed by webhook handler that sends revoke on first `DID_FAIL_TO_RENEW` event.
- **Skip (N/A) when:** No subscription IAP (consumables only); or grace period status cannot be determined from the codebase.
- **Detail on fail:** `"Apple App Store Server Notifications handler revokes premium access on DID_FAIL_TO_RENEW with no grace period — Apple recommends maintaining access during billing retry period"`
- **Remediation:** Enable billing grace period in App Store Connect → Subscriptions → Billing Grace Period (up to 16 days). In code, retain access during this window:
  ```tsx
  // RevenueCat
  const hasBillingIssue = customerInfo.entitlements.active['premium']?.billingIssueDetectedAt !== null;
  const isPremium = customerInfo.entitlements.active['premium'] !== undefined; // true during grace period
  ```

## External references

- external apple-billing-grace-period — https://developer.apple.com/documentation/storekit/in-app_purchase/subscriptions_and_offers/reducing_involuntary_subscriber_churn
- external google-play-grace-period — https://developer.android.com/google/play/billing/subscriptions#grace-period

Taxons: error-resilience

HTML version: https://auditbuffet.com/patterns/ab-000409
