Implementing iOS Subscription Grace Periods

Extend a subscriber’s access to your app while they are in a billing issue state.

Supporting iOS Billing Grace Period
Jacob Eiting

Jacob Eiting

PublishedLast updated

iOS Grace Periods

In September 2019, Apple surprisingly announced they’d added support for grace periods to iOS and macOS in-app subscriptions. Grace Periods allow you to extend a subscriber’s access to your app while they are in a billing issue state. Billing issues happen, usually, when the users credit card on file with the App Store is declined for some reason (expired, etc.)

Grace periods extend the subscription of a user for the beginning part of the billing retry state.

If you enable Grace Periods in App Store Connect, a new field will appear in the pending renewal info section of the StoreKit receipt whenever a user enters the billing retry period.

Supporting grace periods on iOS requires two things from the developer:

  1. Enabling them in App Store Connect
  2. Adding support for them in your receipt verification server

Enabling Billing Grace Period

Grace periods are enabled on a per app basis and your app needs to have at least one subscription product to be eligible.

To turn on Billing Grace Period, navigate to your app in App Store Connect. In the toolbar, click Features, and in the left column, click In-App Purchases. You’ll see a new ‘Billing Grace Period’ section with a button to Turn On.

You’ll get a popup window to confirm, and agree that your purchase code has no bugs and you’ve read the entire developer agreement.

Note: Some developers experienced issues enabling Billing Grace Period that were resolved by switching to Safari.

Parsing the Pending Renewal Info

To provide the new grace period expiration date, Apple has added a new field to the pending renewal info section of the /verifyReceipt response. The pending renewal info on the receipt response is an array of dictionaries that contains per-subscription information, like renewal intents, original transaction versions, and billing issue states.

Example Receipt with Grace Period

1{
2    "in_app": [
3    {
4        "quantity": "1",
5        "product_id": "com.products.monthly",
6        "transaction_id": "580000296563423",
7        "original_transaction_id": "580000296512323",
8        "purchase_date": "2018-11-24 15:03:03 Etc/GMT",
9        "purchase_date_ms": "1543071783000",
10        "purchase_date_pst": "2018-11-24 07:03:03 America/Los_Angeles",
11        "original_purchase_date": "2018-11-24 15:03:04 Etc/GMT",
12        "original_purchase_date_ms": "1543071784000",
13        "original_purchase_date_pst": "2018-11-24 07:03:04 America/Los_Angeles",
14        "expires_date": "2019-02-24 15:03:03 Etc/GMT",
15        "expires_date_ms": "1551020583000",
16        "expires_date_pst": "2019-02-24 07:03:03 America/Los_Angeles",
17        "web_order_line_item_id": "580000080123351",
18        "is_trial_period": "false",
19        "is_in_intro_offer_period": "false"
20      },
21    ],
22    "pending_renewal_info": [
23        {
24          "expiration_intent": "2",
25          "grace_period_expires_date": "2019-06-11 13:43:59 Etc/GMT",
26          "auto_renew_product_id": "com.products.monthly",
27          "original_transaction_id": "580000296512323",
28          "is_in_billing_retry_period": "0",
29          "grace_period_expires_date_pst": "2019-06-11 06:43:59 America/Los_Angeles",
30          "product_id": "com.products.monthly",
31          "grace_period_expires_date_ms": "1560260639000",
32          "auto_renew_status": "0"
33        }
34    ]
35}

‍The /verifyReceipt response will contain two interesting keys: the in_app array of transactions, and the pending_renewal_info.

Without grace periods, the normal mechanism for determining an expiration date would be to loop through the in_app array and find the latest expiration date. With grace periods, it becomes slightly more complicated: you also need to loop through the pending_renewal_infos and map any infos to their respective transactions and take the maximum between the grace period expiration and the transaction expiration.

1 response = get_verify_receipt_response(shared_secret, b64_encoded_receipt)
2
3  # Find the max expires date
4  expires_date_by_subscription = {}
5  for tx in response['in_app']:
6    old_date = expires_date_by_subscription[tx.original_transaction_id]
7    if old_date is None:
8      old_date = 0	
9    expires_date_by_subscription[tx.original_transaction_id] = max(old_date, int(tx.expires_date_ms))
10
11  # Find the grace period expiration dates
12  grace_periods_by_subscription = {}
13  for info in response['pending_renewal_infos']:
14    old_date = grace_periods_by_subscription[info.original_transaction_id]
15    if old_date is None:
16      old_date = 0	
17    grace_periods_by_subscription[info.original_transaction_id] = max(old_date, int(info.grace_period_expires_date_ms))	
18
19  # Find the max of the two 
20  expiration_dates = {}
21  for subscription in grace_periods_by_subscription:
22    if subscription in grace_periods_by_subscription:
23      expiration_dates[subscription] = max(grace_periods_by_subscription[subscription], expires_date_by_subscription[subscription])
24    else:
25      expiration_dates[subscription] = expires_date_by_subscription[subscription]

It’s not a terribly complicated problem to solve, but it does add one more thing you need to think about when supporting in-app subscriptions.

A Better Way

This is an instance where the support for grace periods coming out of Mountain View is actually better implemented. Google’s implementation allows you to skip a step by just modifying the expires date of the affected transaction, essentially giving you grace period support “for free.”

Also, if you are user of RevenueCat, grace periods for Apple and Google are automatically detected and handled by our receipt server and SDK.

References

  1. https://developer.apple.com/app-store-connect/whats-new/?id=billinggraceperiod
  2. https://help.apple.com/app-store-connect/#/dev58bda3212
  3. https://developer.apple.com/documentation/storekit/in-app_purchase/reducing_involuntary_subscriber_churn?language=objc
  4. https://developer.apple.com/documentation/storekit/in-app_purchase/reducing_involuntary_subscriber_churn?language=objc

In-App Subscriptions Made Easy

See why thousands of the world's tops apps use RevenueCat to power in-app purchases, analyze subscription data, and grow revenue on iOS, Android, and the web.

Related posts

How we solved RevenueCat’s biggest challenges on data ingestion into Snowflake
How we solved RevenueCat’s biggest challenges on data ingestion into Snowflake
Engineering

How we solved RevenueCat’s biggest challenges on data ingestion into Snowflake

Challenges, solutions, and insights from optimizing our data ingestion pipeline.

Jesús Sánchez

Jesús Sánchez

April 15, 2024

How RevenueCat handles errors in Google Play’s Billing Library
How RevenueCat handles errors in Google Play’s Billing Library  
Engineering

How RevenueCat handles errors in Google Play’s Billing Library  

Lessons on Billing Library error handling from RevenueCat's engineering team

Cesar de la Vega

Cesar de la Vega

April 5, 2024

Use cases for RevenueCat Billing
Engineering

Use cases for RevenueCat Billing

3 ways you can use the new RevenueCat Billing beta today.

Charlie Chapman

Charlie Chapman

March 21, 2024

Want to see how RevenueCat can help?

RevenueCat enables us to have one single source of truth for subscriptions and revenue data.

Olivier Lemarié, PhotoroomOlivier Lemarié, Photoroom
Read Case Study