I'm porting our iOS app to MacOs and have experienced a strange problem. When I purchase an auto-renewable subscription, the transaction state of the given transaction isn't .purchased, but .restored. That makes no sense.
#objc 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)
#unknown default:
SwiftyBeaver.debug("Payment: unknown error")
}
}
}
This leads wrongly to the following function:
func handleRestoredState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
SKPaymentQueue.default().finishTransaction(transaction)
}
But because there is nothing to be restored the final function isn't called either:
#objc func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
// once all items are restored, this gets called here:
verifyWithTegantAPI()
}
Hence our Api isn't called. The user has now to go and click on Restore Purchases again to get the purchase activated, which is anything but ideal.
If the transaction had the correct state, it would have led to here:
func handlePurchasedState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {
SKPaymentQueue.default().finishTransaction(transaction)
verifyWithServerAPI()
}
The purchase itself happens like this:
func purchase(subscription: Subscription) {
SwiftyBeaver.debug(subscription.product.productIdentifier)
let payment = SKPayment(product: subscription.product)
SKPaymentQueue.default().add(payment)
}
What am I missing please?
Related
I am implementing the In-App purchases function today, and I just followed the tutorial step by step, created sandbox testers, wrote the code, and it says
<SKPaymentQueue: 0x282e50860>: Payment completed with error: Error Domain=ASDServerErrorDomain Code=3502 "This item is not available." UserInfo={NSLocalizedDescription=This item is not available.
Why is "This item is not available."? I searched the relevant information online, but there is no answer for it.
Here is my code
#IBAction func purchaseButtonPressed(_ sender: UIButton) {
print("PRESSED")
purchaseApp()
}
func purchaseApp() {
let productID = "com.crazycat.Reborn.FullFuctionalities"
if SKPaymentQueue.canMakePayments() {
let paymentRequest = SKMutablePayment()
paymentRequest.productIdentifier = productID
SKPaymentQueue.default().add(paymentRequest)
} else {
print("Can't make payments")
}
}
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
if transaction.transactionState == .purchased {
print("Thanks for shopping")
} else if transaction.transactionState == .failed {
print("purchase Failed")
}
}
}
I had the same issue.
Please make sure your paid application agreement in Appstore connect is Active and not Expired. Check if there are any warnings in the App Store connect. Complete all of the bank, tax, and contact information on your App Store Connect Paid Apps Agreements.
Then relaunch the app from Xcode on your physical device.
The transaction should be successful then.
Check below points
Use the same test account you specified in developer console.
Make sure the In-App product shows a status of Ready to Submit on the developer console.
Make sure the In-App product id matches what your using in your app.
I'm facing a very strange behaviour, As the code was not changed, It seems to me like a version specific issue, as it comes from nowhere
I'm testing on sandbox environment
the scenario is when I tries to buy product using
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
the apple default purchase or any other popup didn't show
and the control goes directly into restored transactionState
public func paymentQueue(_: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
DispatchQueue.main.async {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
self.complete(transaction: transaction)
break
case .failed:
hideLoader()
self.fail(transaction: transaction)
break
case .restored:
hideLoader()
self.restore(transaction: transaction)
break
case .deferred:
hideLoader()
break
case .purchasing:
showLoader()
break
#unknown default:
break
}
}
}
}
I'm not sure why it is going there, as apple need to show any popup or any information related to the process
After this function we are validating reciept and reciept returns
"pending_renewal_info": [
{
"expiration_intent": "1",
"auto_renew_product_id": "BUNDLE_ID",
"original_transaction_id": "transaction_id",
"is_in_billing_retry_period": "0",
"product_id": "PRODUCT ID",
"auto_renew_status": "0"
}
]
I'm not sure why the expiration_intent is coming 1 in this case
I had the same issue. Problem was that iTunes/Store account is separated from sandbox testing account.
Settings > iTunes & App Stores > at the bottom there should be SANDBOX ACCOUNT section. Change that account to some other that doesn't have the purchase on it.
I'm kind of new to programming in general, so I have this maybe simple question. Actually, writing helps me to identify the problem faster.
Anyway, I have an app with multiple asynchronous calls, they are nested like this:
InstagramUnoficialAPI.shared.getUserId(from: username, success: { (userId) in
InstagramUnoficialAPI.shared.fetchRecentMedia(from: userId, success: { (data) in
InstagramUnoficialAPI.shared.parseMediaJSON(from: data, success: { (media) in
guard let items = media.items else { return }
self.sortMediaToCategories(media: items, success: {
print("success")
// Error Handlers
Looks horrible, but that's not the point. I will investigate the Promise Kit once I get this working.
I need the sortMediaToCategories to wait for completion and then reload my collection view. However, in the sortMediaToCategories I have another nested function, which is async too and has a for in loop.
func sortMediaToCategories(media items: [StoryData.Items],
success: #escaping (() -> Swift.Void),
failure: #escaping (() -> Swift.Void)) {
let group = DispatchGroup()
group.enter()
for item in items {
if item.media_type == 1 {
guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else {return}
mediaToStorageDistribution(withImageUrl: url,
videoUrl: nil,
mediaType: .jpg,
takenAt: item.taken_at,
success: { group.notify(queue: .global(), execute: {
self.collectionView.reloadData()
group.leave()
}) },
failure: { print("error") })
//....
I can't afford the collection view to reload every time obviously, so I need to wait for loop to finish and then reload.
I'm trying to use Dispatch Groups, but struggling with it. Could you please help me with this? Any simple examples and any advice will be very appreciated.
The problem you face is a common one: having multiple asynchronous tasks and wait until all are completed.
There are a few solutions. The most simple one is utilising DispatchGroup:
func loadUrls(urls: [URL], completion: #escaping ()->()) {
let grp = DispatchGroup()
urls.forEach { (url) in
grp.enter()
URLSession.shared.dataTask(with: url) { data, response, error in
// handle error
// handle response
grp.leave()
}.resume()
}
grp.notify(queue: DispatchQueue.main) {
completion()
}
}
The function loadUrls is asynchronous and expects an array of URLs as input and a completion handler that will be called when all tasks have been completed. This will be accomplished with the DispatchGroup as demonstrated.
The key is, to ensure that grp.enter() will be called before invoking a task and grp.leave is called when the task has been completed. enter and leave shall be balanced.
grp.notify finally registers a closure which will be called on the specified dispatch queue (here: main) when the DispatchGroup grp balances out (that is, its internal counter reaches zero).
There are a few caveats with this solution, though:
All tasks will be started nearly at the same time and run concurrently
Reporting the final result of all tasks via the completion handler is not shown here. Its implementation will require proper synchronisation.
For all of these caveats there are nice solutions which should be implemented utilising suitable third party libraries. For example, you can submit the tasks to some sort of "executer" which controls how many tasks run concurrently (match like OperationQueue and async Operations).
Many of the "Promise" or "Future" libraries simplify error handling and also help you to solve such problems with just one function call.
You can reloadData when the last item calls the success block in this way.
let lastItemIndex = items.count - 1
for(index, item) in items.enumerated() {
if item.media_type == 1 {
guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else {return}
mediaToStorageDistribution(withImageUrl: url,
videoUrl: nil,
mediaType: .jpg,
takenAt: item.taken_at,
success: {
if index == lastItemIndex {
DispatchQueue.global().async {
self.collectionView.reloadData()
}
}
},
failure: { print("error") })
}
You have to move the group.enter() call inside your loop. Calls to enter and leave have to be balanced. If your callbacks of the mediaToStorageDistribution function for success and failure are exclusive you also need to leave the group on failure. When all blocks that called enter leave the group notify will be called. And you probably want to replace the return in your guard statement with a break, to just skip items with missing URLs. Right now you are returning from the whole sortMediaToCatgories function.
func sortMediaToCategories(media items: [StoryData.Items], success: #escaping (() -> Void), failure: #escaping (() -> Void)) {
let group = DispatchGroup()
for item in items {
if item.media_type == 1 {
guard let url = URL(string: (item.image_versions2?.candidates?.first!.url)!) else { break }
group.enter()
mediaToStorageDistribution(withImageUrl: url,
videoUrl: nil,
mediaType: .jpg,
takenAt: item.taken_at,
success: { group.leave() },
failure: {
print("error")
group.leave()
})
}
}
group.notify(queue: .main) {
self.collectionView.reloadData()
}
}
I'm a bit confused on how to use closures or in my case a completion block effectively. In my case, I want to call a block of code when some set of asynchronous calls have completed, to let my caller know if there was an error or success, etc.
So an example of what I'm trying to accomplish might look like the following:
// Caller
updatePost(forUser: user) { (error, user) in
if let error = error {
print(error.description)
}
if let user = user {
print("User was successfully updated")
// Do something with the user...
}
}
public func updatePost(forUser user: User, completion: #escaping (Error?, User?) -> () {
// Not sure at what point, and where to call completion(error, user)
// so that my caller knows the user has finished updating
// Maybe this updates the user in the database
someAsyncCallA { (error)
}
// Maybe this deletes some old records in the database
someAsyncCallB { (error)
}
}
So ideally, I want my completion block to be called when async block B finishes (assuming async block A finished already, I know this is a BAD assumption). But what happens in the case that async block B finishes first and async block A takes a lot longer? If I call my completion after async block B, then my caller thinks that the method has finished.
In a case like this, say I want to tell the user when updating has finished, but I only really know it has finished when both async blocks have finished. How do I tackle this or am I just using closures wrong?
I don't know if your question has been answered. What I think you are looking for is a DispatchGroup.
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
someAsyncCallA(completion: {
dispatchGroup.leave()
})
dispatchGroup.enter()
someAsyncCallB(completion: {
dispatchGroup.leave()
})
dispatchGroup.notify(queue: .main, execute: {
// When you get here, both calls are done and you can do what you want.
})
Really important note: enter() and leave() calls must balance, otherwise you crash with an exception.
try below code:
public func updatePost(forUser user: User, completion: #escaping (Error?, User?) -> () {
// Not sure at what point, and where to call completion(error, user)
// so that my caller knows the user has finished updating
// Maybe this updates the user in the database
someAsyncCallA { (error)
// Maybe this deletes some old records in the database
someAsyncCallB { (error)
completion()
}
}
}
please try updated answer below:
public func updatePost(forUser user: User, completion: #escaping (Error?, User?) -> () {
var isTaskAFinished = false
var isTaskBFinished = false
// Not sure at what point, and where to call completion(error, user)
// so that my caller knows the user has finished updating
// Maybe this updates the user in the database
someAsyncCallA { (error)
// Maybe this deletes some old records in the database
isTaskAFinished = true
if isTaskBFinished{
completion()
}
}
someAsyncCallB { (error)
isTaskBFinished = true
if isTaskAFinished{
completion()
}
}
}
I'm trying to implement Auto-renewable subscription product. The problem is that SKPaymentQueue cannot finish SKPaymentTransaction by calling SKPaymentQueue.defaultQueue().finishTransaction(transaction).
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
if let transactions = transactions as? [SKPaymentTransaction] {
for transaction in transactions {
switch transaction.transactionState {
case .Purchasing:
break
case .Purchased:
queue.finishTransaction(transaction) // finish transaction
self.purchasedWithTranasction(transaction)
case .Failed:
queue.finishTransaction(transaction) // finish transaction
self.failedWithTransaction(transaction)
case .Restored:
queue.finishTransaction(transaction) // finish transaction
self.restoredWithTransaction(transaction)
case .Deferred:
queue.finishTransaction(transaction) // finish transaction
}
}
}
}
As the above, In paymentQueue:updatedTransactions: method, queue.finishTransaction(transaction) are called. Normally, the transactions are finished and will not stay anymore.
But when I run the app again, that transactions still remain not finished. By the way, I set the transaction observer in the AppDelegate like the below. So, when the app launched, remaining transactions start being processed by calling paymentQueue:updatedTransactions:. It's not like my expectation.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// add transaction observer
SKPaymentQueue.defaultQueue().addTransactionObserver(CargoBay.sharedManager())
....
return true
}
Do you guys have any idea about this problem? Is there any case that SKPaymentQueue.defaultQueue().finishTransaction(transaction) does not finish transaction?
Is it possible that you're not seeing the same transaction again, but instead a new auto-renewal transaction from the iTunes sandbox?
When testing auto-renewing subscriptions in the iTunes sandbox, they artificially speed up the rate of renewals. This causes a new transaction to appear every few minutes.
From Testing Your App and In-App Purchase Products:
When testing auto-renewable subscriptions in the test environment, keep in mind that the duration times are compressed. Additionally, test subscriptions only auto-renew a maximum of six times.
1 week : 3 minutes
1 month : 5 minutes
2 months : 10 minutes
3 months : 15 minutes
6 months : 30 minutes
1 year : 1 hour