How to verify plain, non-inApp purchase with AppTransaction

Hi all,

I'm trying to switch from the 'exit(173)' method to using AppTransaction for a plain, paid app. My current attempt in swift looks like this:

Task {
        let shared = try await AppTransaction.shared
        switch (shared) {
        case .verified(let transaction):
            print("verified <3")
        case .unverified(let transaction, let error):
            print("unverified. :'(")
            exit(0)
        }
    }

However running this on my dev machine always ends up in the "verified <3" branch.

I am ruinning on macOS 15 and am pretty sure that on older systems with the exit(173) method, I would see a window asking me to log into my app store account...

I already created a new sandbox account and tried using an empty ".storekit" file... Am I doing something wrong in my code, or is the purchase coming from somewhere else? I already set the bundle-identifier to a non-existent one, but it still seems to think that there was a purchase.

Is there any documentation on how to do normal purchase / receipt validation for paid apps using AppTransaction? I only found in-app related docs :'(

I did another test with the app, running it in another user account on the same machine, and there it indeed opened the login window.

So, now I just have to figure out why the app in the dev account thinks it was purchased, but I don't really know how to do that. The purchases used to be stored in a _Receipt file in the app bundle, but I couldn't find that anymore. Did something about this change?

Maybe I should add, that I was originally using the exit(173) method and classic, manual receipt validation. However on macOS 15 I started seeing a popup, telling me that "The exit(173) API is no longer available." and asking me to switch to "Transaction.all or AppTransaction.shared to verify in-app purchases"

Upon further reflection, I am wondering if this is just a bug in Xcode / macOS 15?! Because I am actually not using any in-app purchases at the moment. In my last test macOS even got stuck in an infinite loop, redisplaying the same alert every time I pressed "OK". I had to log out of the user account to get rid of the alert...

I also searched for documentation and googled a lot more in the meantime, but could only find other posts and messages from people equally confused about this.

An old WWDC21 video "Meet StoreKit 2" explicitly states:

One final thing to note is that these new JWS objects are for in-app purchase only. So if you need to validate the app receipt, you should use the existing API and process for that.

Which sounds to me like the manual receipt parsing is still the way to go for paid apps. Or is the "existing API and process" something else? Or is that video from 2021 is just too old and not up to date anymore?

Is there anything documenting this in writing? How can I make sure to be informed about such changes in the future?

Or is that video from 2021 is just too old and not up to date anymore?

Correct. At that time, there was no AppTransaction.

I believe that what you are doing is correct. (Maybe you should also look at some properties of the verified receipt object?) I’m unsure what exactly your problem is. You seem to think that verification should fail in development, or that it should succeed only after an App Store login dialog. I’m not sure why you think that. If you think that only because that’s what happened in the old system - well, that was the old system.

What I want to do is verify that the app was actually purchased through the app store and not just copied over from another computer.

Disclaimer: I know more about iOS than macOS.

Do you believe that your developer build should fail to run if copied to another computer? I'm not sure about that.

I would hope what you are doing would correctly identify app store builds that have been copied. I don't know how you would actually test that though, at least not until it has been published on the store.

Thank you for your replies and sorry if my posts are a bit all over the place...

When I copy the app over to another user account on the same machine, it already behaves differently. In the other user account, it does show a log in window, but if you dismiss it, the app just keeps running. So it seems like the .unverified branch is never reached and instead, "try await AppTransaction.shared" just never returns.

That the app behaves differently on a developer account and on a normal user account on my computer is surprising to me and together with the fact that .unverified is never reached it looks like I don't properly understand how this is supposed to work.

I am feeling really uncomfortable releasing an app if I don't have a solid understanding of how exactly the appstore verification works and that's why it is so frustrating that I cannot find any detailed documentation for my usecase. If all new users pay for an app and then cannot use it, because the verification that is always true on a dev account doesn't actually work, that'd be a bit of an issue :(

I did some more tests, releasing my app as a beta through TestFlight, and it turns out that my code above doesn't provide any copy protection at all. Even with added code to verify the "transaction.deviceVerification" manually.

What I did:

  • Released app as beta via TestFlight
  • Downloaded and ran Beta app
  • Copied the app bundle over to another computer
  • Initially, it didn't run on the other computer. macOS said that the beta had expired or wasn't valid anymore.
  • Removed the receipt and signing from the app bundle
  • Re-signed with ad-hoc signing
  • Now the app runs and shows the AppStore login window on startup. However, pressing cancel in that window simply dismisses it and the app keeps running as if it was a valid purchase.

My Interpretation:

"AppTransaction.shared" will only return a result if the app has a valid receipt. It will not inform the app if there is no valid receipt. Maybe it also returns if there is a receipt and there was just a crypto issue, but that seems like an edge case.

Conclusion

This is where documentation or guidance by apple would be helpful. Previously, without a correct receipt, the user couldn't start the app at all, as it would "exit(173)". Now, there is no reliable way for the app to tell that the receipt is invalid. All I can think of is to have a global state signalling "receipt was verified" and unless that is true, bock all UI actions the user might want to perform in a purchased app. So, kinda treat the purchase as an in-app purchase required to do anything.

Now the question is:

In which scenarios might my app end up with an invalid/missing receipt? Should it be prepared to recover from that and offer the user to refresh the receipt like it would for in-app purchases?

Sprinkling my code with a bunch of "if !g_hasReceipt { complainToUser() }" in every IBAction also seems like an odd pattern. Is that really the right thing to do?

Also, having to do experiments to find out how the StoreKit is supposed to be used feels like a rather flawed approach. Did I overlook a major part of the documentation or am I just not getting how to find out about system changes like this?!

have a global state signalling "receipt was verified" and unless that is true, bock all UI actions the user might want to perform in a purchased app.

Right, consider the app "unverified" until the await has returned.

How to verify plain, non-inApp purchase with AppTransaction
 
 
Q