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?
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. – SensualityCoreData
share the same risks asUserDefaults
? It seems excessive to validate the receipt every time the user opens. – FranklinfrankliniteUserDefaults
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