How can you get Original Application Version in Production?
Asked Answered
F

1

6

We recently transitioned a purchasable app to the "freemium" model. We are using Bundle.main.appStoreReceiptURL to pull the receipt and then check the "original_application_version" to see if a user downloaded an older paid version from the App Store, or if they downloaded a newer free version with a non-consumable in app purchase to upgrade to the full version.

This works perfectly when in testing Sandbox, but in Production, older versions of the app are not properly verifying that they were downloaded prior to the freemium version.

The following code is called with productionStoreURL and the receipt obtained from Bundle.main.appStoreReceiptURL:

private let productionStoreURL = URL(string: "https://buy.itunes.apple.com/verifyReceipt")
private let sandboxStoreURL = URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")

private func verifyIfPurchasedBeforeFreemium(_ storeURL: URL, _ receipt: Data) {
    do {
        let requestContents:Dictionary = ["receipt-data": receipt.base64EncodedString()]
        let requestData = try JSONSerialization.data(withJSONObject: requestContents, options: [])

        var storeRequest = URLRequest(url: storeURL)
        storeRequest.httpMethod = "POST"
        storeRequest.httpBody = requestData

        URLSession.shared.dataTask(with: storeRequest) { (data, response, error) in
            DispatchQueue.main.async {
                if data != nil {
                    do {
                        let jsonResponse = try JSONSerialization.jsonObject(with: data!, options: []) as! [String: Any?]

                        if let statusCode = jsonResponse["status"] as? Int {
                            if statusCode == 21007 {
                                print("Switching to test against sandbox")
                                self.verifyIfPurchasedBeforeFreemium(self.sandboxStoreURL!, receipt)
                            }
                        }

                        if let receiptResponse = jsonResponse["receipt"] as? [String: Any?], let originalVersion = receiptResponse["original_application_version"] as? String {
                            if self.isPaidVersionNumber(originalVersion) {
                                // Update to full paid version of app
                                UserDefaults.standard.set(true, forKey: upgradeKeys.isUpgraded)
                                NotificationCenter.default.post(name: .UpgradedVersionNotification, object: nil)
                            }
                        }
                    } catch {
                        print("Error: " + error.localizedDescription)
                    }
                }
            }
            }.resume()
    } catch {
        print("Error: " + error.localizedDescription)
    }
}

private func isPaidVersionNumber(_ originalVersion: String) -> Bool {
    let pattern:String = "^\\d+\\.\\d+"
    do {
        let regex = try NSRegularExpression(pattern: pattern, options: [])
        let results = regex.matches(in: originalVersion, options: [], range: NSMakeRange(0, originalVersion.count))

        let original = results.map {
            Double(originalVersion[Range($0.range, in: originalVersion)!])
        }

        if original.count > 0, original[0]! < firstFreemiumVersion {
            print("App purchased prior to Freemium model")
            return true
        }
    } catch {
        print("Paid Version RegEx Error.")
    }
    return false
}

The first freemium version is 3.2, which is our current build. All previous builds were 3.1.6 or earlier.

The Production URL shouldn't be the issue, or else it wouldn't kick back the 21007 status code to trigger the Sandbox validation for us. However, troubleshooting this is particularly tricky since we can't test against the Apple's Production URL itself.

Does anyone have any insight as to why this would work in Sandbox but not Production?

Franklinfranklinite answered 31/12, 2018 at 18:56 Comment(3)
Not an answer to the specific problem that you're asking about, but I'd strongly recommend reconsidering the use of UserDefaults to track if paid features should be made available. Tools exist that made it easy to modify user defaults values, even on non-jailbroken devices. This article details how user defaults can be exploited.Sensuality
@JamieEdge Thanks for the tip! I will definitely change this. Does CoreData share the same risks as UserDefaults? It seems excessive to validate the receipt every time the user opens.Franklinfranklinite
Yeah, storing data on the filesystem (such as the underlying SQLite database which would be used with Core Data) will almost always carry this sort of risk, as values are stored within the application's sandbox directory which can be viewed and modified (UserDefaults uses a plist file within the sandbox). The keychain is more difficult to modify on a non-jailbroken device - values might be extractable from the a backup, however I'm not aware of any simple way to modify the values (however it very much may be possible).Sensuality
F
5

It looks like it was not a problem with obtaining the receipt at all.

Some of the older values for original_application_version were not formatted correctly, preventing us from obtaining the app version to compare against.

Franklinfranklinite answered 5/1, 2019 at 19:19 Comment(6)
Can you tell something about how they were formatted? I am now checking original_application_version in a production app by detecting if the string contains dots (full stops) but not a single user is getting the correct result.Elisha
There is no guarantee about how it will be formatted since original_application_version uses the Build field, which developers can set however they want. However, you can check what these values have been in the past by logging into App Store Connect, opening your app, and then opening the Activity tab. Expand each version and look at the values listed in the Build column. Those are the values that are being returned for the original_application_version for that given build.Franklinfranklinite
thank you, unfortunately my app is 10 years old and I cannot see all of the older versions in AppStoreConnect. So I guess I should have done a better job at documenting my build numbers :)Elisha
@Mr.Zystem If you are using Git or another version control system, you can look at the history of the plist.info file. The CFBundleVersion value is the Build number in XCode and is the value returned as the original_application_version.Franklinfranklinite
@Makalele The issue ended up being how our team had versioned the previously released builds and our incorrect assumptions on what was being returned by original_application_version. The changes to fix the problem were resolved by changing the regular expression, but this would be specific to each individual project based on their previous formatting of version used at the time of those old releases. As a result, there isn't really anything to share. The code above should technically work, assuming that the previous versioning convention falls in line with the regular expression.Franklinfranklinite
@Makalele However, make note of the security risks raised in the comments of the original question about using Core Data vs User Defaults.Franklinfranklinite

© 2022 - 2024 — McMap. All rights reserved.