Slopes Diaries is my ongoing journey to turn my indie app into a more sustainable part of my business. First time reading? Catch up on the journey so far.
What is Slopes? Think Nike+, Runkeeper, Strava, MapMyRun, etc for skiers and snowboarders.
Midnight Tuesday I did the iOS app dev trick hit release for Slopes 2 in iTunes Connect, planning on the usual 4 - 8 hours for it to propagate so it'd be ready for a Tues 11am public launch.
The final StarCraft 2 expansion also had a midnight release, so I played a bit of the campaign while refresh-monkeying my phone for the update. I'm glad I had StarCraft to keep me distracted as there was no way I could have gone to sleep; launch-day anxiety was hitting me hard.
2am-ish (much faster than expected) the Slopes 2 update hit my phone and I launched the app. It all looked good, except for one thing. The thing I was most worried about.
The anxiety I had felt leading up to launch largely related to the grandfathering of features I was trying to do for users who bought v1. Parts of Slopes 1 were now pay-walled in Slopes 2 (although, Slopes 1 was in and of itself a pay-wall, but you get the idea). I didn't want to give my existing users free subscriptions for life, but I did want to take care of them and make sure their purchase of Slopes was appreciated. I settled on unlocking some subscription-only features for life for them, at least the ones that existing in Slopes 1 in some form.
Easy enough to pull off, conceptually. Since iOS 7 you can check out the local receipt and it includes which version of the app the user originally purchased. From there you can include in-app logic to do whatever you want.
(Aside: I'm using Receigen from the MAS to generate my receipt validation and parsing code, awesome product that you can integrate as a build phase.)
Receipt validation was new territory for me, and I was worried it might not work. I had asked a ton of people during development about IAP/receipt stuff, and didn't get much concrete info, so I want to document that here so people can avoid my mistake.
Apple doesn't require a refresh IAP / receipt button for non-renewable subscriptions. They ask that you as the developer persist all that server-side with a user account, or use iCloud. As such I didn't include a button for that refresh capability. I made the (faulty) assumption that at least basic info like "what version did they buy?" would always be in the receipt, even if IAP data was missing.
Depending on how the app was installed (from backup, vs iCloud restore vs fresh download) that data can be missing and isn't restored until you perform a receipt refresh. Even downloading an update from the App Store doesn't refresh anything, it has to be app-initiaed as best I can tell.
Some of my confusion / assumptions came from the fact that testing this stuff is hard, especially if you've never done IAP before. So, basically: if you install the app through Xcode over top of the App Store version, it seems you keep your App Store receipt (might have to refresh, but if you do you get an App Store receipt). If you install via TestFlight / Internal Testers instead then you have a sandbox receipt, which is missing info like which version was purchased, even if the internal tester isn't a sandbox user. This mis-match is what threw me off, and I wrote my issue in testing off as caused by the sandbox when it wasn't.
In hindsight I figured that pattern out, but during testing it just left me confused. Live and learn.
Fortunately all this was an easy fix: just add a "restore v1 unlocks" button and perform a receipt refresh if they tapped it. The vast majority of users that were hit by this were of the "ok, cool, no problem seen this before in other apps!" camp, which was refreshing. Only a half-dozen emailed me, and no 1-stars, so I think the scope was a bit limited too.
Besides that bug Fabric is reporting that I'm humming along at 99.6% crash-free users.
Feels nice to have 2.0 out. Once I have a week's worth of data I'll be talking about my first week as a free app. Some interesting numbers on this side of the fence.