appStoreReceiptURL == nil after restoreCompletedTransactions() - ios

SKPaymentQueue's restoreCompletedTransactions() does not update the appStoreReceiptURL receipt after completion.
I am working on a Mac app that will be supported via auto-renewing subscriptions. After restoring purchases finishes, I need the updated receipt because I will need to check the current subscription status (expiration date, etc.).
The only solution I could come up with was to refresh the receipt right after restore completed transactions finished, as shown below. This does appear to work. However, the user will be prompted to enter in their username and password twice, which is a less than a perfect experience. In addition, I am concerned that such behavior will lead to a rejection by app review.
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
if !fileExistsAtAppStoreReceiptURL() {
print("No receipt.")
}
let refreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
refreshRequest.delegate = self
refreshRequest.start()
}
private func fileExistsAtAppStoreReceiptURL() -> Bool {
guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else { return false }
return FileManager().fileExists(atPath: appStoreReceiptURL.path)
}
public func requestDidFinish(_ request: SKRequest) {
if request is SKReceiptRefreshRequest {
updateCachedReceiptFromAppStoreReceiptURL()
reverifyKnownPurchases()
notify { self.delegate?.inAppPurchaseController(self, restoreCompletedTransactionsDidFinishWith: .success) }
} else {
notify { self.delegate?.inAppPurchaseController(self, didFinishRequestingIndividualProductInformationWith: .success) }
}
}
Some other notes:
No receipt. does show up in the log.
Because this behavior occurs when using the sandbox environment, I don't know if the behavior is the same when the code executes in production.
The behavior did not change after an attempt with a newly-created test user.
Because I wanted to simulate a real-life situation, I restored purchases on a second Mac, and not the Mac that purchased the subscription.
The rest of the restore process appears to work correctly. paymentQueue:updatedTransactions: is called for each restored transaction with a transactionState of .restored.
SKPaymentQueue.default().finishTransaction(transaction) is called for each restored transaction.
SKPaymentQueue.default().add(self) is called in applicationDidFinishLaunching:.
Although I am making use of an auto-renewing subscription, it does not play a role. A receipt does not appear even if I make use of a non-consumable, for example.
I am not using a server.
The Restoring Purchased Products and the restoreCompletedTransactions() documentation neither explicitly confirm nor deny that the receipt is updated as a part of the restore process. Could this be the expected behavior?
Because I had a similar issue with an iOS test app earlier in the year, this may not be a Mac-only situation.
With all of that in mind, does anyone know how to properly handle such a situation? Has anyone had success with restore + refresh?

Related

StoreKit2 Transaction.updates does not emit transactions on subscription renew

I'm listening for transaction updates during the whole lifetime of the app as described in the Transaction.updates:
func startObservingTransactions() -> Task<Void, Error> {
return Task.detached {
for await result in Transaction.updates {
print("Transaction update")
if case .verified(let transaction) = result {
await transaction.finish()
...
}
}
}
}
The Transaction.updates never emits transactions when a subscription renews. Also, I don't receive unfinished transactions on the app launch. Refunding a transaction, however, produces an update as expected.
I experience the same behavior on Simulator and real devices using Xcode, Sandbox, and TestFlight environments.
Is it even supposed to notify about the subscription renewals or should I just observe the transaction queue as usual?
I'm using Xcode 13.2.1, iOS 15.2.

Storekit Appstore purchase promotion

this code is what is given on the apple developer website for when a user clicks on the promotional app-store product and it tells to check to see if can complete the transaction? how do I go about checking that? because then I have to cater if the transaction has failed or deferred and can't seem to figure out how to do that.
//MARK: - SKPaymentTransactionObserver
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment,
forProduct product: SKProduct) -> Bool {
// Check to see if you can complete the transaction.
// Return true if you can.
return true
}
There is also the next scenarios I have to cater for which I find to be the same scenario as checking if the transaction can be completed
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment,
forProduct product: SKProduct) -> Bool {
// ... Add code here to check if your app must defer the transaction.
let shouldDeferPayment = ...
// If you must defer until onboarding is completed, then save the payment and return false.
if shouldDeferPayment {
self.savedPayment = payment
return false
}
// ... Add code here to check if your app must cancel the transaction.
let shouldCancelPayment = ...
// If you must cancel the transaction, then return false:
if shouldCancelPayment {
return false
}
}
// (If you canceled the transaction, provide feedback to the user.)
// Continuing a previously deferred payment
SKPaymentQueue.default().add(savedPayment)
)
How do I check to see if the payment failed or needs to be deferred or can be completed as it says in both the code parts?
Whether the transaction needs to be deferred or not depends entirely on your app and any requirements your app may have.
As an example, say your app required the user to set up an account before they can purchase a subscription.
If the user begins the purchase in the App Store, the shouldAddStorePayment method will be called after your app is launched to complete the purchase.
At this point you may detect that the user has not set up an account in your app (indeed, the app could have been installed as a result of them tapping the promoted IAP). In this case you would return false fromshouldAddStorePayment because your app is not in a position to be able to complete the purchase.
Your app would then continue with its normal on-boarding process which gets the user to establish their account.
Once the account is established you want to complete the purchase; this is both a good user experience and ensures that you don't miss a sale.
This is where the other sample code in your question comes in; it shows how you can save the payment and initiate the purchase at a later stage.
In summary, when shouldAddStorePayment is called:
Determine if there is some reason that you cannot complete the purchase now
If there is, return false, otherwise return true
If you return false, save the purchase details so that you can initiate the purchase at a later stage when whatever was preventing the purchase has been resolved.

Don't receive auto-renewal subscription status notifications on production

We're working on the app with auto-renewal subscription. We have just one option - monthly auto-renewal subscription with one week free trial. We also use "Subscription Status URL" to receive subscription notifications.
The app itself is similar to "TO DO LIST" applications, that can share tasks between multiple users. Thus, we keep data on the server.
Each time user loads the app or creates a task, data comes from the server with current_subscription_status parameter, e.g. we do subscription status validation on the server by simply checking receipt expiration date against current date on the server.
Currently we have only iOS version, but working on Android version as well. And user should be able to sing in to his/her account on different devices with different apple id.
The problem, we've met, is we don't receive actual purchase (subscription) notifications. E.g. when user taps "Start your free 1-week trial" button and subscribes we receive a notification (type INITIAL_BUY). After this one week trial period, we supposed to get another notification with type of something like "RENEWAL", but we receive nothing.
We contacted Apple developer support, but didn't get a real help. They just send links to Apple documentation. Where we found the following note (here is the link):
"To obtain up-to-date information as you process events, your app should verify the latest receipt with the App Store."
So, based on this, I have a question about a possible use case scenario:
User subscribes on iOS device for monthly auto-renewal subscription, e.g. he/she would like to be charged of his/her apple iTunes account. But, after initial purchase he/she closes (kills) the app and doesn't even open it on iOS device anymore. User downloads the app to Android device and use the app only on Android. So, App Store supposed to charge this user each month and send subscription status notifications to the server even if user never opened the app on his iOS device again. So, "...latest receipt verification..." never happens with the iOS app. How can this be implemented?
Tech details:
We use SwiftyStoreKit. These are two parts of in-app purchase implementation:
In AppDelegate we have:
// Auto-renewal complete transactions
SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
for purchase in purchases {
if purchase.transaction.transactionState == .purchased || purchase.transaction.transactionState == .restored {
if purchase.needsFinishTransaction {
// Deliver content from server, then:
SwiftyStoreKit.finishTransaction(purchase.transaction)
}
}
}
}
Here is subscribe function called when user taps "Start free trial button":
func subscribe(completion: #escaping (_ response:Bool, _ message: String?) -> Void) {
SwiftyStoreKit.purchaseProduct(self.productId, atomically: true) { result in
if case .success(let purchase) = result {
if purchase.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(purchase.transaction)
}
let appleValidator = AppleReceiptValidator(service: self.env, sharedSecret: self.sharedSecret)
SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
if case .success(let receipt) = result {
let purchaseResult = SwiftyStoreKit.verifySubscription(
type: .autoRenewable,
productId: self.productId,
inReceipt: receipt)
switch purchaseResult {
case .purchased(let expiryDate, let receiptItems):
if let receiptItem = receiptItems.first {
// Send receipt to the server functionality
.............................
}
completion(true, nil)
case .expired(let expiryDate, let receiptItems):
completion(false, "Receipt has been expired")
case .notPurchased:
completion(false, "Purchase has not been processed")
}
} else {
// receipt verification error
completion(false, "ERROR OCCURED")
}
}
} else {
// purchase error
completion(false, "Canceled")
}
}}

Why does the latest_receipt object changing every time I request it from Apple?

There is one thing I don't quite understand when it comes to In-App Subscription purchase.
I obtain the receipt on iOS client like this:
private func loadReceipt() -> Data? {
guard let url = Bundle.main.appStoreReceiptURL else {
return nil
}
do {
let receipt = try Data(contentsOf: url)
return receipt
} catch {
print("Error loading receipt data: \(error.localizedDescription)")
return nil
}
}
And send it for verification to my server (written n Python).
def verify_receipt(self, receipt):
r = requests.post(config.APPLE_STORE_URL, json=receipt)
request_date_ms = DateUtils.generate_ms_from_current_time()
for item in r.json()['latest_receipt_info']:
expires_date_ms = int(item['expires_date_ms'])
if expires_date_ms > request_date_ms:
return True
return False
I'm not sure if this is the correct way of verifying if a subscription is still valid.
I get the expires_date_ms from latest_receipt_info, and if it's greater than the current time in milliseconds, then the subscription counts as still valid.
However what I noticed is that the separate latest_receipt, which is supposed to be equal to the one I have just sent in, is actually changing every time I call the API. But why? I haven't subscribed to anything new, why is the latest receipt changing?
According to the docs:
latest_receipt
Only returned for receipts containing auto-renewable subscriptions.
For iOS 6 style transaction receipts, this is the base-64 encoded
receipt for the most recent renewal. For iOS 7 style app receipts,
this is the latest base-64 encoded app receipt.
If this is against the sandbox then the subscription is auto-renewing on a predefined period. See:
https://help.apple.com/app-store-connect/#/dev7e89e149d

Requests password on every open while transaction is on SKPaymentQueue

I have an iOS app with non-consumable IAP Apple hosted content. The downloads are very large (~500MB).
While downloading the content, I have to keep the transaction "open", and only finish the transaction once the download is complete.
The problem with this is, Store Kit will automatically request to re-authenticate every time I move the app from background to foreground (or open the app) while a transaction is on the queue.
So, if I start a download, press the home button, return to app, Store Kit will request me to login.
It doesn't even matter if I re-login or just press "Cancel", the download will continue.
Is there anyway to get rid of this re-authentication request?
You can use SKPaymentTransactionStateDeferred to avoid that. On start up try to restore all transactions except those with SKPaymentTransactionStateDeferred state.
Objective-C:
NSArray *transactionsInQ = [[SKPaymentQueue defaultQueue] transactions];
for(SKPaymentTransaction *transaction in transactionsInQ) {
if(transaction.transactionState != SKPaymentTransactionStateDeferred) {
// your code
}
}
Swift:
let transactionsInQ = SKPaymentQueue.defaultQueue().transactions
for transaction in transactionsInQ {
if (transaction.transactionState != .Deferred) {
// your code
}
}

Resources