No receipt data for subscribers of auto-renewing subscription in UK? - ios

I'm getting sporadic reports of users who purchase my auto-renewing subscription and there is no receipt data on their device. After the purchase is made, I parse the receipt and save the expire date into UserDefaults. Then whenever you open the app I check the expire date against today and if its later, you can access my content.
I've created a gist with my relevant code that parses the receipt and saves the expireDate that is kicked off in my appDelegate's handle purchase:
extension AppDelegate: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue,
updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
handlePurchasingState(for: transaction, in: queue)
case .purchased:
handlePurchasedState(for: transaction, in: queue)
case .restored:
handleRestoredState(for: transaction, in: queue)
case .failed:
handleFailedState(for: transaction, in: queue)
case .deferred:
handleDeferredState(for: transaction, in: queue)
}
}
}
func handlePurchasingState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
print("User is attempting to purchase product id: \(transaction.payment.productIdentifier)")
}
func handlePurchasedState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
print("User purchased product id: \(transaction.payment.productIdentifier)")
queue.finishTransaction(transaction)
SubscriptionService.shared.uploadReceipt { (success) in
DispatchQueue.main.async {
//self.updateWatchContext()
NotificationCenter.default.post(name: SubscriptionService.purchaseSuccessfulNotification, object: nil)
}
}
}
func handleRestoredState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
print("Purchase restored for product id: \(transaction.payment.productIdentifier)")
queue.finishTransaction(transaction)
SubscriptionService.shared.uploadReceipt { (success) in
DispatchQueue.main.async {
NotificationCenter.default.post(name: SubscriptionService.restoreSuccessfulNotification, object: nil)
}
}
}
func handleFailedState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
queue.finishTransaction(transaction) //The RW tutorial did NOT finish the transaction, but iOS Slack said I should
print("Purchase failed for product id: \(transaction.payment.productIdentifier) on \(String(describing: transaction.transactionDate)) and this transaction is currently listed as \(transaction.transactionState) because of this error \(String(describing: transaction.error))")
}
func handleDeferredState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
print("Purchase deferred for product id: \(transaction.payment.productIdentifier)")
}
}
A few users have told me that they cannot unlock the content even though they subscribed (these users also said they had purchased the annual subscription, I also have a monthly option)...I had one user install a beta version with a Firebase integration so that I could push the receipt and expire date to the console to see what it says and it appears that there is no expire date or receipt on his device. Any thoughts on what the issue is and why this seems to only be the case in the UK? My active user base is roughly 600 and I haven't heard of this issue outside of the UK.

Related

Will Apple reject application if I call receipt validation after finishing all pending transaction inside removeTransaction delegate?

I am developing Auto-Renewable In-App Purchase. Right now I am calling the Receipt Validation function inside of updatedTransactions delegate like this :
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach { (transaction) in
switch transaction.transactionState {
case .purchased:
self.IAPResponseCheck(iapReceiptValidationFrom: .purchaseButton)
KeychainWrapper.standard.set(false, forKey: receiptValidationAllow)
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
totalRestoredPurchases += 1
self.IAPResponseCheck(iapReceiptValidationFrom: .restoreButton)
KeychainWrapper.standard.set(false, forKey: receiptValidationAllow)
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
if let error = transaction.error as? SKError {
if error.code != .paymentCancelled {
onBuyProductHandler?(.failure(error))
} else {
onBuyProductHandler?(.failure(IAPManagerError.paymentWasCancelled))
}
PrintUtility.printLog(tag: String(describing: type(of: self)), text: "IAPError: \(error.localizedDescription)")
}
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred, .purchasing: break
#unknown default: break
}
}
}
First I am calling the Receipt Validation function where I am simply getting all the previous transactions list and calculating expiration dates and purchase dates to unlock my premium features from the lastest Info Receipt response array. In this function, I am checking the Purchase Status according to my logic and returning true or false. If it's true I take the user inside of my app and if it's false I take him to the purchase screen.
Then I am finishing the transaction immediately like this:
SKPaymentQueue.default().finishTransaction(transaction)
But what I have noticed is that If the user has a long transaction list (100+), It takes a long time to finish all the transactions. I print the finished transactions and remain transactions in the removedTransactions delegate like this:
func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
PrintUtility.printLog(tag: String(describing: type(of: self)), text: "Removed transactions: \(transactions.count)")
PrintUtility.printLog(tag: String(describing: type(of: self)), text: "Unfinished transaction: \(queue.transactions.count)")
}
The problem is If I try to restore or purchase a new product before finishing all pending transactions it triggers updatedTransactions weirdly. It works fine If I wait till it finishes all transactions one by one. So my question is, If I call receipt validation inside of removedTransactions delegate, and finish each transaction inside updateTransactions delegate will it be considered as a possible reason for app rejection on Apple?
Finally, It will look like this:
updatedTransactions delegate:
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach { (transaction) in
switch transaction.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction)
case .restored:
totalRestoredPurchases += 1
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
totalPurchaseOrRestoreFailed += 1
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred, .purchasing: break
#unknown default: break
}
}
}
removedTransactions delegate:
func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
print("Removed transactions: \(transactions.count)")
print("Unfinished transaction: \(queue.transactions.count)")
//This will be called after finishing all transactions
if queue.transactions.count == 0 {
if totalPurchaseOrRestoreFailed != 0 {
transactions.forEach { (transaction) in
switch transaction.transactionState {
case .purchased:break
case .restored: break
case .failed:
if let error = transaction.error as? SKError {
if error.code != .paymentCancelled {
onBuyProductHandler?(.failure(error))
} else {
onBuyProductHandler?(.failure(IAPManagerError.paymentWasCancelled))
}
print("IAP Error:", error.localizedDescription)
totalPurchaseOrRestoreFailed = 0
}
case .deferred, .purchasing: break
#unknown default: break
}
}
} else {
self.IAPResponseCheck(iapReceiptValidationFrom: .purchaseAndRestoreButton)
UserDefaults.standard.set(false, forKey: "receiptValidationAllow")
}
}
}
I believe the issue is exactly like in this SO question:
SKPaymentTransaction's stuck in queue after finishTransaction called
The problem isn't that Apple rejects your App because you finish each transaction inside updateTransactions, it's because Apple's framework introduces bugs if you do, and the payment simply doesn't work. It's important to realize that real users will never have this issue, because they won't have 100+ subscriptions. Because only in debugging/simulators a year-long-subscription will be considered as a 1 hour-subscription. So the solution is easy, just start the payment after the queue is empty. It's just a bug in Apple's framework, and real users won't have an issue. This is the code that I'm using, and Apple has not rejected my App since I used this:
while self.paymentQueue.transactions.count > 0 {
DLog("Still busy removing previous transactions: \(self.paymentQueue.transactions.count)")
delay(1) {
self.checkTransactions()
}
}
self.startPaymentRequest()

How to properly restore purchases using IAP in Swift

I'm trying to display an alert to the user when purchases are restored. But when i debug and print out the number of restored purchases i get 0 transactions were restored. I don't understand why it would bring back 0. I thought i only needed to call to restoreCompletedTransactions() method. I post a notification to notify me if restoration has completed and i doesn't even reach this point. I'm using
paymentQueueRestoreCompletedTransactionsFinished(_ pQueue: SKPaymentQueue) to notify me when restoration is done. How can i properly restore the purchases.
let paymentQueue = SKPaymentQueue.default()
func restorePurchases() {
if !self.canMakePurchases {
return
}
self.paymentQueue.add(self)
self.paymentQueue.restoreCompletedTransactions()
}
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
print("Restore failed")
}
func paymentQueue(_ pQueue: SKPaymentQueue, updatedTransactions pTransactions: [SKPaymentTransaction]) {
for scanTransaction in pTransactions {
switch scanTransaction.transactionState {
case .purchasing:
break
case .purchased:
NotificationCenter.default.post(name: .AppDelegateUserHasPurchasedProductNotification, object: self)
pQueue.finishTransaction(scanTransaction)
default:
pQueue.finishTransaction(scanTransaction)
}
}
}
func paymentQueueRestoreCompletedTransactionsFinished(_ pQueue: SKPaymentQueue) {
print("Received restored transactions: \(pQueue.transactions.count)")
for scanTansaction in pQueue.transactions {
switch scanTansaction.transactionState {
case .restored:
NotificationCenter.default.post(name: .AppDelegateUserHasRestoredPurchasesNotification, object: self)
pQueue.finishTransaction(scanTansaction)
default:
break
}
}
}
The Logs
Received restored transactions: 0
Your implementation of
func paymentQueue(_ pQueue: SKPaymentQueue, updatedTransactions pTransactions: [SKPaymentTransaction]) {
is wrong. You have left out the transaction state for when a purchase is restored! You have case .purchased but you forgot case .restored. Put it in. That is where you are notified and can respond.

IAP Autorenenewable subscription purchase returns status as failed in observer but says successful in the pop up

extension IAPService:SKPaymentTransactionObserver{
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions{
print(transaction.transactionState.status(),transaction.payment.productIdentifier)
switch transaction.transactionState {
case .purchasing :break
case .purchased : NotificationCenter.default.post(name: Notification.Name(rawValue: "purchased"), object: self)
receiptValidation()
default :queue.finishTransaction(transaction)
}
}
}
}
When I try purchasing a subscription it works fine sometimes but then after a period it says failed in status log. But still the pop up from apple says purchase successful you are all set. Is there some kind of limit we can use a sandbox account buy and test the app?

Restore Purchase : Non-Consumable

I have completed a small app where I have a non-consumable purchase option. It is on the App Store.
The purchase of the product runs OK. It's my Restore Purchase function that seems to do nothing.
I have added this code for the Restore Purchase #IBAction:
#IBAction func restorePurchases(sender: AnyObject) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
But nothing happens when I hit the restore purchase button.
I think I have to add a function that checks if the restore was successful or not. Am planning to amend code to the following:
#IBAction func restorePurchases(sender: AnyObject) {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
for transaction:AnyObject in transactions {
if let trans:SKPaymentTransaction = transaction as? SKPaymentTransaction{
switch trans.transactionState {
case .Restored:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as SKPaymentTransaction)
var alert = UIAlertView(title: "Thank You", message: "Your purchase(s) were restored.", delegate: nil, cancelButtonTitle: "OK")
alert.show()
break;
case .Failed:
SKPaymentQueue.defaultQueue().finishTransaction(transaction as SKPaymentTransaction)
var alert = UIAlertView(title: "Sorry", message: "Your purchase(s) could not be restored.", delegate: nil, cancelButtonTitle: "OK")
alert.show()
break;
default:
break;
}
}
}
Will this do the trick?
I have been through every thread in relation to effecting Restore Purchase transactions, and my research has led me to the above. So I don't think this is a duplicate of a question, but perhaps may clarify how to successfully restore purchases for others facing my similar situation.
Your codes looks pretty fine for the most part, although some parts seem to be from older tutorials . There is some changes you should make, one of them is that you need to call your unlockProduct function again.
This is the code I use (Swift 3).
/// Updated transactions
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
// Transaction is being added to the server queue.
case .purchased:
// Transaction is in queue, user has been charged. Client should complete the transaction.
defer {
queue.finishTransaction(transaction)
}
let productIdentifier = transaction.payment.productIdentifier
unlockProduct(withIdentifier: productIdentifier)
case .failed:
// Transaction was cancelled or failed before being added to the server queue.
defer {
queue.finishTransaction(transaction)
}
let errorCode = (transaction.error as? SKError)?.code
if errorCode == .paymentCancelled {
print("Transaction failed - user cancelled payment")
} else if errorCode == .paymentNotAllowed { // Will show alert automatically
print("Transaction failed - payments are not allowed")
} else {
print("Transaction failed - other error")
// Show alert with localised error description
}
case .restored:
// Transaction was restored from user's purchase history. Client should complete the transaction.
defer {
queue.finishTransaction(transaction)
}
if let productIdentifier = transaction.original?.payment.productIdentifier {
unlockProduct(withIdentifier: productIdentifier)
}
case .deferred:
// The transaction is in the queue, but its final status is pending external action
// e.g family member approval (FamilySharing).
// DO NOT freeze up app. Treate as if transaction has not started yet.
}
}
}
Than use the delegate methods to show the restore alert
/// Restore finished
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
guard queue.transactions.count != 0 else {
// showAlert that nothing restored
return
}
// show restore successful alert
}
/// Restore failed
func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: NSError) {
/// handle the restore error if you need to.
}
Unlock product is just a method I am sure you already have too.
func unlockProduct(withIdentifier productIdentifier: String) {
switch productIdentifier {
/// unlock product for correct ID
}
}
As a side note, you should move this line
SKPaymentQueue.default().add(self)
out of your restore and buy function and put it in viewDidLoad.
Apple recommends you add the transaction observer as soon as your app launches and only remove it when your app is closed. A lot of tutorials unfortunately dont teach you this correctly. This way you unsure that any incomplete transactions e.g due to network error, will always resume correctly.
https://developer.apple.com/library/content/technotes/tn2387/_index.html
In my real projects my code for IAPs is in a Singleton class so I would actually using delegation to forward the unlockProduct method to my class that handles gameData. I can than also make sure the observer is added at app launch.
Hope this helps
Took me a while to suss out, but the reason my StoreKit was not updatingTransactions and restoring the purchase was because of a broken Configuration setting in my app's Scheme. When I set that to None, it worked!
In Xcode I went into Edit>Scheme (image1) clicked on the Run>Options tab and selected None for StoreKit Configuration (image2). I also went on my physical device, and logged out of my personal Apple purchase account (Settings >Your Name/Pic at the Top > Media & Purchases > Sign Out) (image3). And finally, this step might not be critical, but I logged into my test sandbox account on my device at the bottom of the Settings>App Store menu(image4 and image5). That is an account that I setup in developer.apple.com under test users.

Restore InApp purchase using swift, iOS

I am implementing restore in app purchase. I have a button whose action is
#IBAction func restorePurchases(send : AnyObject){
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
// if (success) {
// I want to do something here
// }
}
My question is.
Is this the right way to restore?
How can we verify success action for restoring purchases?
don't forget to check if you can make a payment:
if (SKPaymentQueue.canMakePayments()) {
SKPaymentQueue.default().restoreCompletedTransactions()
}
for check if the restore was good you have to follow the protocol:
SKPaymentTransactionObserver
and then implement the method:
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!)
and subscribe to the event by doing:
SKPaymentQueue.default().add(self)
Finally here is an exemple of how I check the result:
func paymentQueue(_ queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) {
print("Received Payment Transaction Response from Apple");
for transaction in transactions {
switch transaction.transactionState {
case .purchased, .restored:
print("Purchased purchase/restored")
SKPaymentQueue.default().finishTransaction(transaction as SKPaymentTransaction)
break
case .failed:
print("Purchased Failed")
SKPaymentQueue.default().finishTransaction(transaction as SKPaymentTransaction)
break
default:
print("default")
break
}
}
}
For some reason, even after trying SKPaymentQueue.default().restoreCompletedTransactions() the following function was never called for me during restoration.
func paymentQueue(queue: SKPaymentQueue!,
updatedTransactions transactions: [AnyObject]!)
This line of code SKPaymentQueue.default().add(self) was added to make my class the observer and still no updates.
It seems that this function gets called with the restored scenario if you try to make a payment all over again and the StoreKit API automatically decides if this should be purchased or restored and shows the message accordingly.
Even thought from a user's point of view, they are not charged again, Apple rejected our app because the restore scenario was not added.
So it seems if you call SKPaymentQueue.default().restoreCompletedTransactions() in your code, handle these additional StoreKit delegates to manage your restoration:
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
handleSuccess()
}
func paymentQueue(_ queue: SKPaymentQueue,
restoreCompletedTransactionsFailedWithError error: Error) {
handleError()
}

Resources