StoreKit2: .purchase() not working after expiry of subscription in app, but renewing in AppStore sandbox does...

Hi, thanks for reading my question. I need help with some odd behaviour with product.purchase() not triggering a confirmation dialog after a subscription has expired and trying to purchase it again. Seeing this in iOS 16.2 and 15.7.2 (haven't tried any other versions) on actual devices, not in simulator.

I'm using a sandbox user on the sandbox environment (not using the local store kit config file testing option).

Using a newly created sandbox user, first subscription purchase goes through just fine, dialog box pops up, login with sandbox user, get confirmation of purchase and then Transaction.currentEntitlements has one item as expected. It auto renews for 12 times (each time Transaction.currentEntitlements contains the correct results) and then expires, as expected for sandbox. Transaction.currentEntitlements is then also empty, as expected. All good so far.

Now I want to test purchasing it again...Call product.purchase() again to renew/start a new subscription and nothing happens, no confirm purchase dialog box pops up at all. The purchase function simply exits BUT returns success (as in the following gets called) but in self.updatePurchasedProducts(), Transaction.currentEntitlements is empty.

case let .success(.verified(transaction)):
     // Successful purchase
      await transaction.finish()
     await self.updatePurchasedProducts()

if I instead go to Settings->App Store->Sandbox User-> Manage Subscriptions and renew the subscription there, instead of in my app, then Transaction.currentEntitlements has a new entry and all is good again.

Alternatively, if I create yet another new sandbox user and logout of the old one I was using, I am once again able to purchase from within the app, so .purchase() once again works as normal.

Is there something I am missing about expired subscriptions and trying to purchase them again in the app? Is this a sandbox issue and in production I'll have no problem?

The sandbox user has purchasing enabled in Settings->App Store.

I've also tried calling AppStore.sync() (which is in my "Restore Purchase" button) before calling product.purchase() after the subscription stops renewing, expires and this issue comes up, doesn't resolve it.

Also have a less important question, the initial call to product.purchase(), the one that works as expected, has a bit of a delay before the confirmation dialog pops up, a few seconds, which will probably result in the user clicking the buy button again thinking it didn't work. Is a bit of a delay normal for sandbox? Will it be ok in production? When it fails, and I have to renew in Settings->AppStore->Sandbox user, there's also a bit of a delay after I return to my app, 5-15 or so seconds, before the transaction observer fires and currentEntitlements is checked again, is there a way to reduce this delay?

Thank you! Colin



@MainActor

class IAPManager: NSObject, ObservableObject {

 // removed other functions.....

  func purchase(_ product: Product) async throws {
   let result = try await product.purchase()

    switch result {

   case let .success(.verified(transaction)):
     // Successful purchase
      await transaction.finish()
     await self.updatePurchasedProducts()

   case let .success(.unverified(_, error)):
      break

    case .pending:
      break

    case .userCancelled:
      break

    @unknown default:
      break
  }
}

 func updatePurchasedProducts() async {

    for await result in Transaction.currentEntitlements {
      guard case .verified(let transaction) = result else {
        continue
      }

      if transaction.revocationDate == nil {
        self.purchasedProductIDs.insert(transaction.productID)
      } else {
        self.purchasedProductIDs.remove(transaction.productID)
      }
    }
  }
}
Post not yet marked as solved Up vote post of Colin_newbie Down vote post of Colin_newbie
5.8k views

Replies

I am having this exact issue as well. Switching products also works (monthly to annual or vice versa). Has any verified if this behavior happens in production or is it TestFlight specific?

I'll be sending it to TestFlight soon to try if it works there, was hoping to sort it out before submitting!

I forgot to add in my original post, the call to purchase() (which is NOT popping a confirmation to purchase dialog) is returning a successful and verified result....but, the expiry date on the transaction is in the past....

Note: I've XXXX'd out some fields that shouldn't be relevant. Ran this at 6:12 p.m (GMT/UTC) on January 13, 2023, so should be expired....which would explain why later on, in self.updatePurchasedProducts(), Transactions.currentEntitlements is empty. But still no idea why there's a successful transaction and no confirmation dialog in the first place!

{
  "bundleId" : "app.XXXXXXXXXXXXXXXXXX",
  "deviceVerification" : "Y+Jlapp.XXXXXXXXXXXXXXXXXXJnwgU0qV7YMP6x\/I",
  "deviceVerificationNonce" : "42e5c641-2a67-4815-b162-3c15515028ea",
  "environment" : "Sandbox",
  "expiresDate" : 1673556592000,
  "inAppOwnershipType" : "PURCHASED",
  "originalPurchaseDate" : 1673369053000,
  "originalTransactionId" : "2000000246509209",
  "productId" : "XXXXXXXXXXXXXXX",
  "purchaseDate" : 1673554432000,
  "quantity" : 1,
  "signedDate" : 1673559083181,
  "subscriptionGroupIdentifier" : "21XXXXXXXXXXXXXXXXXX3",
  "transactionId" : "2000000248833618",
  "type" : "Auto-Renewable Subscription",
  "webOrderLineItemId" : "2000XXXXXXXXXXXXXXXXXX7"
}

Original Purchase Date: 2023-01-10 16:44:13 +0000  

Purchase Date: 2023-01-12 20:13:52 +0000  

Expiration Date: 2023-01-12 20:49:52 +0000  

Signed Date: 2023-01-12 21:31:23 +0000  

let result = try await product.purchase()
        switch result {
            case .success(let verificationResult):
                switch verificationResult {
                    case .verified(let verifiedSuccessfulTransaction):
                         print(verifiedSuccessfulTransaction)
                         print("Original Purchase Date: \(verifiedSuccessfulTransaction.originalPurchaseDate ?? Date(timeIntervalSince1970: 0) )  ")
                         print("Purchase Date: \(verifiedSuccessfulTransaction.purchaseDate ?? Date(timeIntervalSince1970: 0) )  ")
                         print("Expiration Date: \(verifiedSuccessfulTransaction.expirationDate ?? Date(timeIntervalSince1970: 0) )  ")
                         print("Signed Date: \(verifiedSuccessfulTransaction.signedDate ?? Date(timeIntervalSince1970: 0) )  ")

                         await verifiedSuccessfulTransaction.finish()
                         await self.updatePurchasedProducts()

                        

Switching products that I am attempting to purchase doesn't solve it either for me, still returns success & verified showing older times I bought when testing, which have expired as well.

If I go in to AppStore -> Sandbox users and successfully buy a new subscription (which results in currentEntitlements now having valid entries as one would expect), the call to purchase() for that product (as if trying to buy it again during a valid subscription) returns (again without a dialog box popping up) showing a new expiry date in the future, as one would expect from a successful transaction.

I'm guessing this is some sandbox weirdness.

Having the same issue here. Has anyone released to production to see if this issue is in the sandbox only? Any meaningful workarounds? Disappointing to face such challenges in core areas like subscriptions.

I have the same problem, and it happens in the production environment. If I change to storekit v1 to initiate the purchase, I can successfully call the payment pop-up window,This problem only occurs in storekit v2

  • Thanks for verifying this in production, saves me from sending to test flight before resolving!

    I'm going to file a bug report and probably use one of my code level supports for the year. If I learn more or how to resolve it, will post here.

  • Colin - have you had any resolution? I am facing same issue.

  • my resolution: when storekit v2 return purchase succeed, but verify the order is not valid, then restart purchase use storekit v1

Having the exact same issue in the sandbox

I'm seeing this too, is there any resolution?

Originally I used an example implementation at a Swift tutorial web site, replacing the store classes from that with the ones in Apple’s StoreKit 2 tutorial ( https://developer.apple.com/documentation/storekit/in-app_purchase/implementing_a_store_in_your_app_using_the_storekit_api ) and now the call to purchase() not popping a dialog is resolved. I’ve gone through the original code I used and this new code and cannot see anything that would make a difference but it works so I’m sticking with it. Perhaps it coincided with an OS update or something else and it’s just a coincidence it works now since I left it unresolved for awhile before coming back to work on it and didn’t bother retesting that the old code still failed.

Only issue now is it’s working except when the subscription expires, the listener isn’t always fired right away (including in Test Flight) so I am now polling the current entitlements list every hour to check if an expiry occurred. Note if you do polling like this you need to account for your app being backgrounded and make sure you’re checking every X minutes based on wall clock time, not app run time, otherwise you might not catch an expiry until way afterwards.

  • Thanks for the response, I can reproduce the issue here although I had seen it before but it did resolve (the subscribe dialog took a long time to display). Thanks I'll probably need to add the polling also.

Add a Comment

So now my above polling solution is no longer working on at least iOS 15.7.6 and I am back to where I started. Zero code changes, simply testing the same code base and boom, out of nowhere, buy button no longer initiates the pop up confirmation dialog. It was working on 15.7.6 up until a day or two ago, so the problem was not introduced by the new version. Tried rebooting, app delete and reinstall, etc.

iOS 16.5 (same app code base, no different code paths between iOS versions) continues to work just fine, guess I'm just dropping support for iOS 15 and keeping my fingers crossed that 16 continues working. Or maybe this is an AppStore sandbox issue and has nothing to do with my app.

  • I still experience the Issue in iOS 16.5 in Sandbox environment. Restarting the App solved the Issue on my side.

Add a Comment

Hi guys,

In our case this behaviour was due to the fact that we still had some pending transactions in the queue. Remember to always finish all subscription transactions even if they are expired.

We could reproduce it 100% with the following steps:

  1. Purchase a subscription, call finish() on the verified transaction that results from the purchase process.
  2. Wait for renewals.
  3. Don't call .finish() on any renewal transactions.
  4. Let the subscription expire.
  5. Try to purchase another subscription with the same id.
  6. Purchase flow will always return an expired transaction as a result for the purchase flow.

Our fix was:

  1. Improve our transaction updates task to always finish subscription transactions, even if transactions are already expired (let's say you have monthly subscription and a user will open your app after 4 months, you need to call .finish on 3 expired transactions and 1 valid transaction).
  2. Make sure you finish all pending transactions belonging to a subscription before making a new purchase (this is for safety in case you missed finishing a pending transaction or the transaction update comes with a delay).

Note: We only had 2 days of testing and we couldn't reproduce the issue anymore in the Sandbox environment. We'll keep you updated if anything changes.

Hope it helps!

Cheers!

Add a Comment

@Matheo0003 is right. I had the same issue. I was calling transaction.finish() only on product.purchase(), but I also have Transaction.updates method where I didn't call .finish(). When I fixed it, the problem was gone!