iOS Auto Renewing Subscriptions

3 min read

Auto-renewing subscriptions in iOS are confusing. These are my notes on how the process works and other tips.

NOTE: This doc is only for iOS7 and up.

Overview

All current subscription information is stored in one big receipt – found at NSBundle.mainBundle().appStoreReceiptURL.

You need to “verify” this receipt to see whether their subscription is active.

When the app is first installed, that receipt probably doesnt exist at that URL yet – so you have to load it using SKReceiptRefreshRequest.

The receipt will always be created once you refresh it.

Even if the user purchased a subscription on another device, refreshing it would load that subscription onto the current device.

When you refresh the receipt, you are likely prompting the user to sign in, so dont refresh without a good reason.

If you refresh and its still invalid, then that means they either dont have an internet connection or they dont have an active subscription.

NOTE: To test in-app purchases, setup a test sandbox account in iTunes Connect and test on a real device (simulator wont work). Dont sign in via the settings app. Sign out via the settings app. Then just login when your app prompts you.

The Receipt

The appStoreReceiptURL receipt is super encrypted so you can decrypt it locally (hard) or send it to Apples server for decryption.

If you do it locally, youll probably want to use RMStore as a reference. The decryption process is intentionally very difficult and its bad to use an open source library because that would make it easy for someone to noop your validation process (thus getting a free subscription).

The other bad thing about local verification is that any time it goes out of date, youd need to refresh it (which triggers the user to enter their password).

It appears that the local receipt will be updated automatically without a refresh in some situations, but not always.

Apple recommends you verify the receipt by sending it to their server. You send the receipt and your iap secret key and Apple will send back the decrypted receipt in a nice JSON format.

The big bonuses are (1) you dont have to mess with the difficult decryption process and (2) the Apple server will return a latest_receipt_info field which includes the updated receipt. So with the server you are guaranteed to be working with the latest version of the receipt – and the user doesnt have to enter their password.

Ive also noticed that even after a refresh, the local receipt has incorrect fields. For example, the is_trial_period field is false on the local receipt after a refresh but is correctly “true” on the latest receipt from the server.

Initial Receipt

So what does the actual receipt look like? How do subscriptions, trial periods, and cancellations look in the actual receipt?

Apple lists the Receipt Fields in the docs but its difficult to understand what the full server response will look. So Ive included example receipts below for reference:

Heres what my server-verified receipt looks like before I purchased anything:

Receipt Before Purchase

Post Purchase

If the users begins the payment process (via SKPaymentQueue.addPayment) but cancels before confirmation then there will be no record in the receipt.

Regardless of whether the payment cancels/confirms, the paymentQueue updatedTransactions callback will be called. UpdatedTransactions will contain an errored transaction if cancelled. I ignore the transactions in the callback, and just use this event to tell me I need to check the receipt again.

Receipt After Purchase

Receipt After Trial Period Ends, During First Subscription Period

Receipt During Second Active Subscription Period

Distant Future Receipt

After a lot of time passes (multiple re-subscription periods), the receipt file (within the app) didnt have any of the new receipts for the auto purchases. Its possible that this file will be automatically updated, but not guaranteed. As always, you can refresh the receipt to get the latest info or you can verify your old file via Apples server and get the new info from latest_receipt_info.

So it appears that once you have the original receipt, you never need to refresh it (unless the user signs in with a different account, i guess).

Heres the result of validating that local receipt file with Apples service:

Distant Future Receipt, Pre Refresh

Distant Future Receipt, Post Refresh

App Version Changes

Part of the validation process is making sure the receipts application_version matches the current apps version.

When the user installs an update, the receipt file is not automatically updated with the new application version. You have to refresh the receipt locally to get the version number to match (server verification wont change the version number).

My feeling is that you should just ignore the application_version validation. Otherwise youll have to prompt the user for their password on every version update.

Receipt With New App Version After Refresh

Algorithm

Say you have an app that requires an active subscription:

App Loads
If CACHED_EXPIRATION is in the future
    Grant Access!
If LOCAL_RECEIPT is missing
    Refresh LOCAL_RECEIPT
If LOCAL_RECEIPT fails to load
    Show error "unable to connect"
Use LOCAL_RECEIPT to load SERVER_RECEIPT
If SERVER_RECEIPT fails to load
    Show error "unable to connect"
Find EXPIRATION_DATE in server receipt
Update CACHED_EXPIRATION to EXPIRATION_DATE
If CACHED_EXPIRATION is in the future
    Grant Access!
Otherwise
    Show "Buy Subscription" Button

References

I referenced the following articles when learning this myself:

Leave a Reply

Your email address will not be published. Required fields are marked *