iOS Subscriptions Primer
The allure of paid up front was easy: you set price via dropdown and that was it. The money would (hopefully) start rolling in as customers found your product. As the App Store has grown up, we've seen the rise of free-up-front with some kind of monetization in-app. The latest of which is a new push from Apple to support subscriptions.
Unfortunately subscriptions require a lot more work on our part than paid up front. StoreKit and receipt validation can be new territory, even for a lot of senior developers as many just haven't had to touch a lot of this before.
I'd like to unpack what I've learned by adding non-auto-renwable subscriptions to Slopes last year, and now auto-renew subs for this year, hopefully helping developers new to subscriptions in iOS with some tips along the way (feel free to skip down to Queue Recovery if you know StoreKit).
StoreKit, if you've never used it before, is the home to all of the IAP transactions running through your app. Fortunately using subscriptions in your app is not too much different than any other IAP. I don't want to go too in depth on StoreKit here (check out WWDC 2016's Using Store Kit for In-App Purchases with Swift 3), but lets go over the basics.
Looking up Products
There are three main things to understand about StoreKit. The first: it's what you'll use to get information about your IAP (including localized pricing) from Apple. You can adjust your price at any time in the App Store using iTunes Connect and you don't want to hard-code any of that. At a minimum, though, you need the product identifiers so you can look them up with Apple. I'll usually just ship those in-app as part of the code.
This lookup is managed by making a
SKProductsRequest with the identifiers and providing a
SKProductsRequestDelegate. Assuming the user can access the App Store, you'll get a callback with detailed information on each IAP you requested via
SKProducts. You'll need that
SKProduct to initiate a purchase of an IAP.
To make a new IAP available to your app you head to your app in iTC and click the "Features" section. Anything you create there is available in the sandbox right away, only submit for review alongside the new binary that actually uses them.
(Aside: fair warning, if you delete an IAP in iTC, that product identifier is never freed up. Use junk product codes like
com.consumedbycode.slopes.test1 when you think you might need to.)
Once you've requested your product identifiers through Apple, you're ready to make a purchase. This is where
SKPaymentQueue, the backbone of IAPs in iOS, comes into play. The queue is what tracks the IAP purchase as it transitions between various states. You'll be made aware of these state changes by creating a class that conforms to
SKPaymentTransactionObserver and adding it to the queue's observers via
When you're ready to tell Apple that your user wants to buy something, you create a
SKPayment from the
SKProduct from above. Then you add the payment to the queue via
addPayment and the transaction process begins:
- Your observer gets a call with the transaction while it's state is
.Purchasing. Apple is going to attempt to charge the customer. Usually not much to do here.
- You'll get another call with the state set to
.Purchased. This is where Apple is telling you that the charge has gone through, and now it's your turn to unlock anything in-app associated with the purchase.
- Once you're done unlocking anything (or tracking any server-side flags for the purchase, or completing the download of unlocked content) you're required to call the queue's
finishTransactionfor the transaction. It isn't until you've done this that Apple will actually finalize the transaction charge. But don't cheat - only call this when you've truly finished unlocking (we'll get to why in a second).
That's a high-level overview of the flow. There are other states like
.Restored. Please watch that WWDC video if you're unfamiliar with these, it's really important you have a solid understanding of how StoreKit works.
One thing to stress here: don't get fancy trying to track payments as you process them with your own in-app logic or custom queue. Let Apple do the work. Always rely
SKPaymentQueue and it's observers as the source of knowledge for what's going on with your IAPs. And there's a good reason...
Remember that observer you registered for the
SKPaymentQueue? You'll want to make sure you're doing that on every app launch, somewhere in the
Why? The queue is smart. Apple is here to guarantee you get paid, and the customer gets what they paid for.
Lets say the user bought an IAP and the app crashed as you tried to unlock (so you never called
finishTransaction). Could be a tricky situation, but Apple is tracking this state for you, just incase of situations like this.
If your app ever launches and there are unfinished transactions, Apple will add them back onto the queue, calling your observer with the
.Purchased state. You're expected to try to unlock the features again and eventually call
This app launch situation isn't only for crashes. There are cases in which, for example, customers may redeem an IAP via the iTunes store as opposed to in-app. If they do, your next app launch will have a transaction waiting for you. At that point you should follow the same logic as if that transaction originated from the user tapping your "Buy" button.
It's important you have this observer ready at app launch for subscriptions, too.
StoreKit and Subscriptions
Subscriptions in StoreKit follow the same lifecycle as other IAPs. They move from
.Purchased, then you call
When a renewal of a subscription is coming up Apple will, ~1 week out, start to verify that the customer has a valid credit card on file and start to bug them if not. They'll also let the user know of the upcoming charge so it isn't a surprise to them. Then, 24 hours prior to the renewal date, they'll start to attempt to charge the customer.
If that process is successful and they charge the customer, a new transaction is automatically created by Apple (as opposed to you via
addPayment) and passed into your observers via the normal queue logic. You don't need to poll Apple asking if the user renewed - they'll tell you when a renewal happens via the queue.
One annoyance, here, is that this only seems to happen on app launch. If the user is inside your app when the renewal actually goes through, you won't get the callback until next time.
SKPaymentQueue is the source of record for any in-process transactions, how do we view past ones? We could store that ourselves locally, but like the queue we want to rely on Apple as much as possible for gathering information on when the user had active subscriptions.
Apple automatically stores and updates receipts on the user's phones on a per app basis. This receipt contains information on when they purchased your app, which version they first installed, and many other things. Most notably of which is certain types of in-app purchases - subscriptions being one of them.
You can grab this receipt data in your app using
NSData(contentOfURL: NSBundle.mainBundle().appStoreReceiptURL), but it's a complex format and you don't want to try to write a parser yourself. When running on the phone, I like using Kvitto for local validation of the receipt. When I get the receipt back from Kvitto I'll do some quick sanity checking to make sure things like the bundle identifier of the receipt matches what I expect (people can try to tamper with receipts).
(Speaking of tampering Apple recommends any verification you do is based on hard-coded values, not your Info.plist. That's way too easy to tamper with, too.)
One note with that parser choice: it doesn't verify that the receipt itself is signed by Apple. To do that you'd need to embed OpenSSL, Apple's root certificate, and do that yourself. Some apps like Receigen promise to make that easier for you, but I never had any luck with them.
So I'm not verifying that Apple is the one that issues the receipt?? Seems like a pretty big security hole I'm letting in, right?
I'm a bit cautious of doing purely phone-side processing of the receipt. It, like your app, can be tampered with. While I'm not obsessed with preventing piracy, I do want to at least make it a little harder for people to crack Slopes. Plus in my case, I want to be able to offer web-based purchases of my IAPs (and who knows, maybe even Android one day). So I needed one central place to track in-app and other payments, which in this case is my server.
Trust, but Verify
Apple provides endpoints (one for production, one for the testing sandbox) to which you can provide raw receipt data and it'll send back JSON for the decoded information (same as what you'd get on the phone - purchase date, IAPs, etc), plus status on if the receipt itself is issued by Apple (more on their endpoints here).
Note: Apple stresses that you do not call these from inside your app. Remember, your app can be tampered with. It might be tempting to do this in-app to avoid the server, but if you are that keen to avoid a server, just go OpenSSL + Kvitto.
My backend is a custom PHP application. If anyone else is in that boat I'd take a look at store-receipt-validator. It's a great library to parse and validate App Store / Play Store / and Amazon receipts.
So now my server is able to not only parse out receipts to check out which IAPs a user has purchased, but also verify that the receipt can be trusted. I record the transactions I find in these receipts to my DB and associate them with the logged in user, giving them credit towards a Slopes Pass for the time purchased.
This server-side approach is also nice because I can easily credit any user some extra time for any customer support hickups I run into. No in-app logic or promo codes required.
Putting Server + Client Together
I don't want to spam my server with receipt decode requests every time the user boots the app. So how does this all fit together?
- Whenever I get a callback on my queue observer with a
.Purchased, I'll send the receipt up to my server where it will record any new transactions. Assuming I track them successfully server-side, the phone will just finalize those transactions, having received a new expiration date calculated by the server.
- When I start up I check the receipt locally to see if any purchases have been canceled (App Store customer support refunded a purchase. Cancel != auto-renew turned off). If so, I'll pass the receipt up to the server to make sure I terminate access originally granted by that IAP.
- If the user triggers a "Restore Purchases", I'll send the reloaded receipt up. Since I associate transactions with users, I can use this method to remind them of the account email they originally used and auto-fill that on a new device.
Sever-side Renewals with Subscriptions
Remember that earlier annoyance, in which you might not know about a renewal until the user actually launches the app? Turns out when you do things server-side that doesn't have to be the case.
When you have an auto-renewing subscription in the receipt, those receipt verification endpoints from Apple will append a field called
latest_receipt_info (their docs on it here). This array of transactions will include subscription renewal transactions that were not in the original receipt you sent. You just need a copy of the app's receipt, not necessarily a most recent one from when the app was launched post-renewal.
That means, that even if your app hasn't been launched, you can see if Apple charged the customer. Here's how I'm using this:
- When the customer buys a subscription I store the raw receipt data on my server for later (not in the DB. That data can grow to be large).
- I know when subscriptions will expire, server-side, since I track all that myself. So in the 24 hours leading up to a subscription I start polling that endpoint every few hours with the latest receipt data I have on hand for the user.
- If I get a new transaction back in
latest_receipt_info, I'll record that and credit the user for the renewal, extending the expiration and stopping checks against the endpoint until that new expiration.
- When the user launches the app (could a month after actual renewal -- my app usage depends on the weather and beginning of winter) they'll already see the renewal in place. Much smoother experience.
- Even though they see the renewal already, I still have to go through the new transaction on queue -> sever sync -> finalize dance. You still have to follow normal StoreKit rules, even if you used the endpoints to get a sneak peek at the renewal.
But this extra server-side dance lets me better understand my cancellation churn, vs users not getting around to opening my app this season yet.
- There is no insight into when they turn off auto-renew. I can't adjust my wording in-app to acknowledge that they disabled auto-renew. I'd love to say "Your pass will expire on XXXX" vs "Your pass is set to renew on XXXX".
- Subscriptions were born from newstand, and it still shows. One of the final steps of the purchase phase is to prompt the user to share email, name, and zip code. It is billed as "the publisher wants this information", but I don't! A friend on Twitter pointed me to this dev forum post which explains that there is no automated way to opt out of asking this, but if you email App Review they'll manually flag your app to remove the prompt. I hope to see this improved as we move away from only content providers offering subscriptions.
- You can have your app launch the manage subscriptions page in iTunes by doing an openURL with
https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/manageSubscriptions. If I know the user is within their subscription range I throw up a nice UI stating their subscription is managed through Apple and to tap a button to manage it. Don't make them hunt for that screen in Settings.app, give them a link.
- Unlike some other IAP types, auto-renewing subscriptions will always be in the receipt. This can make things easier to sync between devices without the need for a server (when I used non-auto-renew subscriptions before this I was responsible for making sure it appeared on all devices). As such, Apple will require a "Restore Purchases" button.
If you choose to purchase a Season Pass, payment will be charged to your iTunes account, and your account will be charged for renewal 24-hours prior to the end of the current period. Auto-renewal may be turned off at any time by going to your settings in the iTunes Store after purchase. Current price for the Season Pass is $19.99/yr USD, and may vary from country to country.
I think that's a pretty holistic 101 on subscriptions in the App Store. After finding a lot of success with non-auto-renewing subscriptions last year, I'm looking forward to seeing how these work out over the winter season.