I have the following code in my SKPaymentTransactionObserver and I've confirmed that it runs the .purchased case when I complete a sandbox purchase.
func paymentQueue(_ queue: SKPaymentQueue,
updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing, .deferred: break
case .purchased, .restored:
Settings.didPurchase = true
DispatchQueue.main.async {
tableView.reloadData()
}
SKPaymentQueue.default().finishTransaction(transaction)
case .failed: // other code here
}
}
}
The data source for my UITableView includes this function:
func numberOfSections(in tableView: UITableView) -> Int {
return Settings.didPurchase ? 3 : 4
}
While debugging, I confirmed that this function is called after I call tableView.reloadData() and that it returns 3 after the purchase. But, the view doesn't update to include the extra section. It only updates if I close the entire view and reload it after completing the purchase, in which case it triggers another table view reload and looks like it's supposed to.
My theory is that because the UITableView isn't the top-level view when I call reloadData() (it's covered by an alert from Apple saying that the purchase was successful), it's not updating the view, thinking it's not visible. But, is this reasonable? And either way, how can I fix my code so that the view updates as expected when the user completes a purchase?
Related
I am following the tutorial for IAP from the following site:
https://betterprogramming.pub/set-up-your-swiftui-app-to-support-in-app-purchases-ef2e0a11d10c
Here is the portion of the code that handles the transaction and the states:
extension IAPManager: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach { (transaction) in
switch transaction.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction)
purchasePublisher.send(("Purchased ",true))
case .restored:
totalRestoredPurchases += 1
SKPaymentQueue.default().finishTransaction(transaction)
purchasePublisher.send(("Restored ",true))
case .failed:
if let error = transaction.error as? SKError {
purchasePublisher.send(("Payment Error \(error.code) ",false))
print("Payment Failed \(error.code)")
}
SKPaymentQueue.default().finishTransaction(transaction)
case .deferred:
print("Ask Mom ...")
purchasePublisher.send(("Payment Diferred ",false))
case .purchasing:
print("working on it...")
purchasePublisher.send(("Payment in Process ",false))
default:
break
}
}
}
}
In short, it checks each case on the queue for each buy request you placed with the server and sends the status back to the SwiftUI interface through the purchasePublisher PassThruSubject that looks like this:
let purchasePublisher = PassthroughSubject<(String, Bool), Never>()
That is the part I am confused about!? How do I access the purchasePublisher so that I can check on the SwiftUI view (SwiftUI interface) that the purchase was in fact completed successfully so that then I can take action accordingly?
purchasePublisher is an instance of PassthroughSubject, which is a publisher that broadcasts information to subscribers. You must go through this instance to access those subscriptions and you can do that with the sink method:
purchasePublisher.sink(receiveValue: { (value) in
print(value)
})
And there are, as always, caveats. You may find something like this useful for what you're doing: .send() and .sink() do not seem to work anymore for PassthroughSubject in Xcode 11 Beta 5
When a button is pressed I use storekit to purchase it. But the issue is the createItems() function and segue are performed before the purchase is confirmed. Is there any way to do this?
func addItems() {
IAPService.shared.purchase(product: .nonConsumable)
createItems()
performSegue(withIdentifier: "returnItems", sender: self)
}
I need the function + segue to be run after the in app purchase has been completed.
IAP function:
func purchase(product: IAPProduct) {
guard let productToPurchase = products.filter({ $0.productIdentifier == product.rawValue}).first else {return}
let payment = SKPayment(product: productToPurchase)
paymentQueue.add(payment)
}
payment Queue function
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
print(transaction.transactionState.status(), transaction.payment.productIdentifier)
switch transaction.transactionState {
case .purchasing:
break
default:
queue.finishTransaction(transaction)
}
}
}
One simple solution is for the paymentQueue method, when it is called because the purchase has taken place, to post a Notification thru the NotificationCenter. Any other view controller that needs to be informed instantly when the purchase takes place will have registered for this Notification, and thus will hear about it at that moment and can take any desired action.
Keep in mind, too, that you are going to record somewhere, typically in UserDefaults, the fact that the purchase has occurred. Thus any subsequent view controller can always check in its viewDidLoad or viewWillAppear to see whether the purchase has happened, and can modify its behavior accordingly.
I have a problem with my code. The function updatedTransactions is only called once while the transaction is .Purchasing and is not called after the transaction has ben completed.
func buyProduct(product: SKProduct) {
let payment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().addPayment(payment)
}
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
print(transaction)
switch (transaction.transactionState) {
case .Purchased, .Restored:
print("Completed")
complete(transaction)
break
case .Failed:
fail(transaction)
break
default:
break
}
}
}
Sorry I might answer a bit late, but it might be related to this question. Are you testing your IAP from Xcode when it doesn't reach the .purchased state?
If yes, then you need to set your device's App Store ID to one you have created on iTunes Connect (User menu, Sandbox testers).
If no, then maybe it could be because your SKPaymentQueue already contains too many transactions waiting to be finished. You can check it with SKPaymentQueue.default().transactions.
In my case I had 28 transactions waiting to be finished because I had a bad switch case statement during the purchase phase.
If this is the case, you can add those lines in your viewDidLoad to finish each one of them (don't forget to remove them after you find why they didn't finish in your code):
for transaction: AnyObject in SKPaymentQueue.default().transactions {
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
}
So i can't seem to get the updatedTransactions protocol to fire when trying to restore purchases.
I have a button in one view controller which calls the following method in my IAPViewController file restoreIAP() which is set up like so.
func restoreIAP(){
SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}
This method is called when the user presses the button so this is the class which handles this.
class SettingsViewController: IAPViewController {
#IBAction func restoreDidTouch(sender: AnyObject) {
restoreIAP()
activityTitle = "Restoring"
}
}
In my IAPViewController nothing seems to be triggering this method so that i can do something.
// Check the transaction
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
// Check the tranactions
for transaction in transactions {
switch transaction.transactionState {
case .Purchasing:
// TODO: Start Activity Indicator
showPurchaseIndicator(activityTitle)
break
case .Purchased:
// TODO: End the purchasing activity indicator
dismissPurchaseIndicator()
print("Transaction completed successfully.")
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
transactionInProgress = false
// TODO: Put method here to unlock all news sources
storiesMethods.unlockAllStories()
break
case .Restored:
// TODO: Start Activity Indicator
// showPurchaseIndicator(activityTitle)
break
case .Failed:
dismissPurchaseIndicator()
notificationMethods.showAlertErrorMessage(self, title: "Purchase", actionMessage: "Dismiss", message: "Unable to complete transaction please try again later.")
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
transactionInProgress = false
break
default:
print(transaction.transactionState.rawValue)
break
}
}
}
Did your controller added as observer using SKPaymentQueue.defaultQueue().addTransactionObserver(..)?
PS: Have a look at SwiftyStoreKit ( InAppProductPurchaseRequest.swift )
From your description and the code snippets it looks like everything is in the right order.
If the paymentQueue function is never called, your IAPViewController probably doesn't conform to the SKPaymentTransactionObserver protocol, just make it conform:
class IAPViewController: UIViewController, SKPaymentTransactionObserver
and you're good to go.
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()
}