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.
Related
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.
I am trying to enable In-App Purchases in Swift 4 for iOS and I can not get the Display Name and Description to properly show up (or load) in the app when I test. I do get the error handing message that I put in if (products.count == 0) {. I can't get the Product Title and Product Description that I set up in AppStoreConnect to properly load when I test my app. Instead the code thinks that there are no products because it runs the code in the if (products.count == 0) { closure. I quadruple checked everything else (i.e. Bundle ID, Product ID, StoreKit's imported, the delegates are set up, everything on the AppStoreConnect side is set up, etc.). There are no Xcode Warnings and the In-App Purchase status is "Ready To Submit". Any suggestions?
var product: SKProduct?
var productID = "myProductID"
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
var products = response.products
if (products.count == 0) {
productTitle.text = "Warning" // GETS DISPLAYED
productDescripton.text = "Unable to connect to In-App Purchase." // GETS DISPLAYED
} else {
product = products[0]
productTitle.text = product?.localizedTitle
productDescripton.text = product?.localizedDescription
buyButton.isEnabled = true
buyButton.alpha = 1.0
}
let invalid = response.invalidProductIdentifiers
for product in invalid {
print("\(product)")
}
}
If you are a rookie at integrating In-App Purchases, then you should read this. If you have clean code and you think that you set everything up properly in App Store Connect, but still can't test properly. Specifically, if your products array is showing empty when it shouldn't be. Then, make sure that you have done the following:
1) Set up a Sandbox Tester via Users and Access in App Store Connect. This needs to be set up with an email that is not already used with iTunes, App Store, or Apple.
2) Filled out all of the necessary forms in Agreements, Tax, and Banking via App Store Connect. You'll know that everything is filled out when Paid Applications shows an Active Status.
3) Are testing on a real device that has been signed out of iTunes and App Store.
Common things that will cause this without warning are:
Your products aren't in the 'Ready to Submit' stage. This is most often from a missing screenshot, which is required even for Sandbox (you can upload a blank image for sandbox testing).
You haven't signed the 'Paid Applications' agreement in App Store Connect
Here's a good blog post that covers everything in a little more detail: Configuring In-app Products is Hard
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?
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")
}
}}
If I place a payment into the queue...
public func buyProduct(_ product: SKProduct) {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
... normally, the SKPaymentTransactionObserver will receive a call back regarding this payment. Can it also receive a call back for a transaction from, say, a previous usage of the app?
In other words, does my SKPaymentTransactionObserver need to be able to handle a transaction that I'm expecting and any other transactions, which may still need to complete, even though the user may not be directly concerned with them at that moment?