SwiftyStoreKit is a very difficult product for me since almost a week, I search everywhere in StackOverflow, YouTube, and Google for answers. I couldn't find any answer since I am working on Auto-Renewable and I tried to find out if user cancelled subscription or expired, app will ask for re-subscription. During the testing, the purchase worked great, but I will need to figure out how the app knows whether a subscription is expired or not. My verification receipts are always returned with the following error message each time I attempt to verify them. What did I do wrong?
Receipt verification failed: receiptInvalid(receipt: ["status": 21002], status: SwiftyStoreKit.ReceiptStatus.malformedOrMissingData)
Here my top code:
import SwiftyStoreKit
var sharedSecret = " * My shared Secret * "
let appleValidator = AppleReceiptValidator(service: .sandbox, sharedSecret: sharedSecret)
enum RegisteredPurchase: String {
case autoRenewable = " * My Auto-Renewable Bundle ID * "
}
My code with button while purchasing
@objc func startSub() {
StoreManager.shared.purchase(purchase: RegisteredPurchase.autoRenewable)
}
AppDelegate.swift
SwiftyStoreKit.completeTransactions(atomically: true) { purchaseStatus in
for products in purchaseStatus {
switch products.transaction.transactionState {
case .purchased,.restored:
if products.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(products.transaction)
}
default: break
}
}
}
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
switch result {
case .success(let receipt):
let productID = Set([RegisteredPurchase.autoRenewable.rawValue])
let purchaseResult = SwiftyStoreKit.verifySubscriptions(productIds: productID, inReceipt: receipt)
switch purchaseResult {
case .purchased(let expiryDate, let items):
print("\(productID) are valid until \(expiryDate), \(items).")
case .expired(let expiryDate, let items):
print("\(productID) are expired since \(expiryDate), \(items).")
case .notPurchased:
print("The product has never purchased...")
}
case .error(let error):
print("Receipt verification failed: \(error)")
}
}
StoreKitManager.swift
class StoreManager: NSObject {
@objc static let shared = StoreManager()
func getInfo(purchase: RegisteredPurchase) {
SwiftyStoreKit.retrieveProductsInfo([purchase.rawValue], completion: { result in
if result.error == nil {
for x in result.retrievedProducts {
print("Get Information: \(x.localizedTitle)")
}
} else {
print("Error getting information")
}
})
}
func purchase(purchase: RegisteredPurchase) {
SwiftyStoreKit.purchaseProduct(purchase.rawValue, completion: { result in
if case .success(let product) = result {
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
print("Purchasing: \(result)")
self.purchaseResult(result: result)
}
})
}
func verifyReceipt() {
SwiftyStoreKit.verifyReceipt(using: appleValidator, completion: { result in
self.verifyReceipt(result: result)
if case .error(let error) = result {
if case .noReceiptData = error {
self.refreshReceipt()
}
}
})
}
func verifyPurchase(product: RegisteredPurchase) {
SwiftyStoreKit.verifyReceipt(using: appleValidator, completion: { result in
switch result {
case .success(let receipt):
let productID = RegisteredPurchase.autoRenewable
if product == .autoRenewable {
let purchseResult = SwiftyStoreKit.verifySubscription(ofType: .autoRenewable, productId: productID.rawValue, inReceipt: receipt, validUntil: Date())
print("Verify Purchase Result: \(purchseResult)")
}
case .error(let error):
print("Error verify purchase: \(error)")
if case .noReceiptData = error {
self.refreshReceipt()
}
}
})
}
func refreshReceipt() {
SwiftyStoreKit.fetchReceipt(forceRefresh: true, completion: { result in
print("Start refreshing the receipt...")
})
}
func purchaseResult(result: PurchaseResult) {
switch result {
case .success(let product):
print("Purchase Successful: \(product.productId)")
case .error(let error):
print("Purchase Failed: \(error)")
}
}
func restoreResult(result: RestoreResults) {
if result.restoreFailedPurchases.count > 0 {
print("Restore failed by unknown error")
} else if result.restoredPurchases.count > 0 {
print("Restore Successful")
} else {
print("Nothing to Restore.")
}
}
func verifyReceipt(result: VerifyReceiptResult) {
switch result {
case .success(let receipt): return print("Verify Receipt: \(receipt)")
case .error(let error):
switch error {
case .noRemoteData: return print("No receipt founded. Try again")
default: return print("Error Verify Receipt: \(error)")
}
}
}
func verifySubscription(result: VerifySubscriptionResult) {
switch result {
case .purchased(let expiryDate):
print("Product is purchased, valid until \(expiryDate)")
case .notPurchased:
print("Product has never purchased.")
case .expired(let expiredDate):
print("The product is expired since \(expiredDate)!")
}
}
func verifyPurchase(result: VerifyPurchaseResult) {
switch result {
case .purchased: print("Product is purchased, will not expired.")
case .notPurchased: print("Product is not purchased, has never been purchased.")
}
}
func refreshReceipt(result: FetchReceiptResult) {
switch result {
case .success(let receiptData): print("Receipt Refreshed, \(receiptData)")
case .error(let receiptData): print("Receipt not Refresh, \(receiptData)")
}
}
}
I'm new to StoreKit, but here is my 2 cents. The error message indicates there might be a problem with your server not receiving valid data.
Here is what Apple says.
Do not call the App Store server verifyReceipt endpoint from your app. You can't build a trusted connection between a user’s device and the App Store directly, because you don’t control either end of that connection, which makes it susceptible to a machine-in-the-middle attack.
Validating Receipts with the App Store