Reputation: 276
I have tried a number of methods to validate a storeKit receipt using simulator with storeKit and in testFlight but can't seem to get anything other than status 21002. The code I have last used originated (Implementing Receipt Validation in Swift 3). Its seems many have had this issue, some saying that validation can't be done using the sandbox. Apple documentation indicates it can be done? The issue seems to be with func receiptValidation10() at the bottom of code below. Everything else seems to work fine. Any help is appreciated. Thanks!
import StoreKit
import UIKit
let SUBSCRIPTION_SECRET = "xxx"
#if DEBUG
let certificate = "StoreKitTestCertificate"
#else
let certificate = "AppleIncRootCertificate"
#endif
class purchaseViewController: UIViewController, SKProductsRequestDelegate, SKPaymentTransactionObserver {
private var models = [SKProduct]()
var pChoice = "Trial"
var pDate = today
var eDate = today
var myProducts: SKProduct?
@IBAction func back(_ sender: Any) { dismiss(animated: true, completion: nil)
}
@IBAction func home(_ sender: Any) { nav(to: "home")
}
@IBAction func basic(_ sender: Any) {
pChoice = "Basic"
eDate=dateAdd(date1: today, inc: 1, metric: "yr")
if SKPaymentQueue.canMakePayments() { // needed? https://www.youtube.com/watch?v=qyKmpr9EjwU
let payment = SKPayment(product: models[whichProduct(title: "com.markv.REX3.basic")])
SKPaymentQueue.default().add(self) // needed? https://www.youtube.com/watch?v=qyKmpr9EjwU
SKPaymentQueue.default().add(payment)
}
}
@IBAction func premium(_ sender: Any) {
pChoice = "Premium"
eDate=dateAdd(date1: today, inc: 1, metric: "yr")
if SKPaymentQueue.canMakePayments() {
let payment = SKPayment(product: models[whichProduct(title: "com.markv.REX3.premium")])
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
}
}
@IBAction func unlimited(_ sender: Any) {
pChoice = "Unlimited"
eDate="None"
if SKPaymentQueue.canMakePayments() {
let payment = SKPayment(product: models[whichProduct(title: "com.markv.REX3.unlimited")])
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().add(payment)
}
}
@IBAction func cancel(_ sender: Any) {
UIApplication.shared.open(URL(string: "Https://apps.apple.com/account/subscriptions")!)
}
func fetchProduct(upgrade: String) {
let request = SKProductsRequest(productIdentifiers: [upgrade])
request.delegate = self
request.start()
}
func whichProduct(title: String) -> Int {
for i in 0 ..< models.count {
let product = models[i]
if title == product.productIdentifier {
return i
}
}
print("No product match")
return models.count
}
enum Product: String, CaseIterable {
case unlimited = "com.markv.ddd.unlimited"
case basic = "com.markv.ddd.basic"
case premium = "com.markv.ddd.premium"
}
private func fetchProducts() {
let request = SKProductsRequest(productIdentifiers: Set(Product.allCases.compactMap({ $0.rawValue })))
request.delegate = self
request.start()
}
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
DispatchQueue.main.async {
print("Count:\(response.products.count)")
self.models = response.products
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
print("purchasing")
// no op
break
case .purchased, .restored:
SKPaymentQueue.default().finishTransaction(transaction)
SKPaymentQueue.default().remove(self) // needed? https://www.youtube.com/watch?v=qyKmpr9EjwU
print("Purchase or Restore, version=",pChoice)
print("Method1:")
receiptValidation10()
break
case .failed, .deferred:
SKPaymentQueue.default().finishTransaction(transaction)
SKPaymentQueue.default().remove(self)
print("no payment")
//dismiss(animated: true)
break
default:
SKPaymentQueue.default().finishTransaction(transaction)
SKPaymentQueue.default().remove(self)
print("no payment")
//dismiss(animated: true)
break
}
}
}
func receiptValidation10() {
// method by Yasin Aktimur, updated by Not Batman, says working for swift 4, 11/17/2017, updated 1/17/18 https://stackoverflow.com/questions/39711350/implementing-receipt-validation-in-swift-3
//
#if DEBUG
let urlString = "https://sandbox.itunes.apple.com/verifyReceipt"
#else
let urlString = "https://buy.itunes.apple.com/verifyReceipt"
#endif
let receiptPath = Bundle.main.appStoreReceiptURL?.path
if FileManager.default.fileExists(atPath: receiptPath!){
var receiptData:NSData?
do{
receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped)
}
catch{
print("ERROR: " + error.localizedDescription)
}
let base64encodedReceipt = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions.endLineWithCarriageReturn)
let requestDictionary = ["receipt-data":base64encodedReceipt!,"password":SUBSCRIPTION_SECRET]
guard JSONSerialization.isValidJSONObject(requestDictionary) else { print("requestDictionary is not valid JSON"); return }
do {
let requestData = try JSONSerialization.data(withJSONObject: requestDictionary)
let validationURLString = urlString // this works but as noted above it's best to use your own trusted server
guard let validationURL = URL(string: validationURLString) else { print("the validation url could not be created, unlikely error"); return }
let session = URLSession(configuration: URLSessionConfiguration.default)
var request = URLRequest(url: validationURL)
request.httpMethod = "POST"
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringCacheData
let queue = DispatchQueue(label: "itunesConnect")
queue.async {
let task = session.uploadTask(with: request, from: requestData) { (data, response, error) in
if let data = data , error == nil {
do {
let appReceiptJSON = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? NSDictionary
print("success. here is the json representation of the app receipt: \(String(describing: appReceiptJSON))")
//self.getExpireInfo(appReceiptJSON!)
} catch let error as NSError {
print("json serialization failed with error: \(error)")
}
} else {
print("the upload task returned an error: \(error ?? "couldn't upload" as! Error)")
}
}
task.resume()
}
} catch let error as NSError {
print("json serialization failed with error: \(error)")
}
}
}
Upvotes: 1
Views: 747
Reputation: 276
I was not able to find what the issue was with the func receiptValidation10() method above but I found a very similar one that works and is provided below. Along the way I discovered that receipt validation cannot be done in xcode, but has to be done in the sandbox through testflight. The code below works in sandbox but not in xcode simulation. Lots of waisted time there. The one remaining problem with the method below is that it seems to grab a random expire date from the receipt instead of the most recent one. If anyone has a suggestion here that would be great.
// Source below dated 24 Dec 19 by Debabrata Roy //https://mobikul.com/in-app-subscription-receipt-validation-in-ios-using-swift/?unapproved=93334&moderation-hash=e45d82e3ff1795281893824bb6f8c908#comment-93334
func receiptValidation() { // This method works!
let verifyReceiptURL = "https://sandbox.itunes.apple.com/verifyReceipt"
let receiptFileURL = Bundle.main.appStoreReceiptURL
let receiptData = try? Data(contentsOf: receiptFileURL!)
let recieptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
let jsonDict: [String: AnyObject] = ["receipt-data" : recieptString! as AnyObject, "password" : SUBSCRIPTION_SECRET as AnyObject]
do {
let requestData = try JSONSerialization.data(withJSONObject: jsonDict, options: JSONSerialization.WritingOptions.prettyPrinted)
let storeURL = URL(string: verifyReceiptURL)!
var storeRequest = URLRequest(url: storeURL)
storeRequest.httpMethod = "POST"
storeRequest.httpBody = requestData
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: storeRequest, completionHandler: { (data, response, error) in //removed [weak self]
do {
if let jsonResponse = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary{
actLog(newPrint:"Response :"+String(describing: jsonResponse))
if let date = getExpirationDateFromResponse(jsonResponse) {
actLog(newPrint:"EXPIRATON DATE="+String(describing:date))
}
if let lProd = getProductFromResponse(jsonResponse) {
actLog(newPrint:"LATEST PRODUCT ID="+String(describing:lProd))
}
}
} catch let parseError {
actLog(newPrint:String(describing:parseError))
}
})
task.resume()
} catch let parseError {
actLog(newPrint:String(describing:parseError))
}
}
func getExpirationDateFromResponse(_ jsonResponse: NSDictionary) -> Date? {
actLog(newPrint:"This date came into getExire func:\n="+String(describing: jsonResponse))
if let receiptInfo: NSArray = jsonResponse["latest_receipt_info"] as? NSArray {
actLog(newPrint:"last receipt exists")
let lastReceipt = receiptInfo.lastObject as! NSDictionary
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"
// Unfortunately this provides an expire date but not the most recent one :((
if let expiresDate = lastReceipt["expires_date"] as? String {
actLog(newPrint:"expire date available")
actLog(newPrint:String(describing: expiresDate))
return formatter.date(from: expiresDate)
}
actLog(newPrint:"expire data not available")
return nil
}
else {
actLog(newPrint:"could not get latest_receipt_info")
return nil
}
}
Upvotes: 1