FB19914338

I submitted the following feedback today. If you ever plan to change your business model from a paid up-front to freemium model, read this report and avoid a day of headache and stress.

Title: The sample code for a business model change was written by someone who’s never submitted an app to the App Store

Please describe the issue and what steps we can take to reproduce it:

The source code example using in Supporting business model changes by using the app transaction does not work if you’re using current Xcode and App Store conventions. Additionally, the sandbox environment uses the same outdated conventions.

And when you use that sample code, that you cannot test in the Xcode transaction simulator or in the TestFlight sandbox environment, it will fail spectacularly on launch day. You will be inundated with support requests from people who are expecting to see a payment for the previous version AND you’ll be in a state of panic because YOU HAVE NO IDEA WHAT THE HELL IS GOING ON. And did I mention that you can’t test this in production?

The sample code implies that the originalAppVersion is a string that’s separated by periods (“.”). The sandbox environment returns a value of “1.0” which reinforces this notion that it’s a value that separated by periods.

It is not.

If you’d read the Xcode documentation, you’d know that it automatically generates an app’s Info.plist. This has been the default setting for quite while – most developers have no idea this is a configurable option: they fill in the “Version” and “Build” number in the target’s General settings and are done with it.

For most, the build number will just be a single number that increments each time you submit to TestFlight (and eventually to the App Store).

When GENERATE_INFOPLIST_FILE is enabled, it sets the value of the CFBundleVersion key in the Info.plist file to the value of the build number (CURRENT_PROJECT_VERSION, or the “Build” in General settings). And that means your Info.plist is getting a CFBundleVersion without periods.

So what happens when you use this code?

let versionComponents = appTransaction.originalAppVersion.split(separator: ".")
let originalMajorVersion = versionComponents[0]

Well, if you’re an inexperienced Swift developer, your app is going to crash with an array index that’s out of bounds. Those of us who are more careful in our receipt processing code will skip over the originalMajorVersion because versionComponents is empty.

And that’s when the emails from customers start arriving.

Luckily, there is this nugget of information describing originalAppVersion:

The originalAppVersion remains constant and doesn’t change when the customer upgrades the app. The string value contains the original value of the CFBundleShortVersionString for apps running in macOS, and the original value of the CFBundleVersion for apps running on all other platforms.

So even though CFBundleVersion was originally intended as a major/minor/patch format, its current use is as a single integer that increments when you submit to TestFlight. So the code above is expecting “1.0” and is actually getting “83”.

(And why the hell is it different on macOS? You do realize that cross platform apps are a thing, right?)

Again, you have no way to test this theory other than going through App Review (with an expedited review if you’re lucky). And if you’re even luckier, you’ll have folks on Mastodon that will confirm that this sample code is a piece of shit. A few hours later you’ll breathe a sigh of relief when folks start telling you that things are working fine.

And then the next day, you’ll write this bug report and post it publicly because no one else should have endure the stress caused by this sloppy code.