Mac Appstore StoreKit 2 - validate purchase & where is purchase receipt cached?

This is re-posted from this Stack Overflow post.

I am looking at validating the purchase of a paid app from Mac AppStore. Based on this WWDC video about StoreKit 2, I am attempting to this with AppTransaction. I have not found meaningful high-level documentation about this specific use case beyond that.

My approach is to first get the "cached" AppTransaction by calling AppTransaction.shared. If that is not there I proceed to getting it from Apple, via AppTransaction.refresh(). If they don't have it, or when the network is down, the user automagically gets the familiar "log in to your store account" UI that has been around as long as the Mac AppStore.

Once I have the AppTransaction I use it to verify we are on the right device, using code like this, where the returned Bool represents validation success:

guard let deviceVID = AppStore.deviceVerificationID?.uuidString.lowercased() else { return false }
let nonce = appTransaction.deviceVerificationNonce.uuidString.lowercased()
let combo = nonce + deviceVID
let digest = SHA384.hash(data: Data(combo.utf8))
return (digest == appTransaction.deviceVerification)

My first question is: Does that look like the right approach? Is there something else I should do, or check?

My second question is around testing this approach. Refreshing the AppTransaction in the sandbox invariably yields a valid item, even if the app version does not yet exist in AppStoreConnect. This is also the case when I log out in the App Store app on the Mac. This makes me think it is using my AppleID which I am logged into in System Settings. Does that sound right?

I would like to be able to remove / delete the cached AppTransactions - where might I find those on the system?

Thanks for everyone's help!

Replies

Does that look like the right approach?

Yes, that's the correct way to verify the app transaction is valid for the device you access AppStore.deviceVerificationID from. For context, the purpose of device verification is to detect cases where someone may have copied a valid app transaction from one Mac, and then tried to install it on another Mac. This transaction may have a valid signature because it was generated & signed by Apple, so if you only checked the signature and ignored the deviceVerification property it would seem valid.

Is there something else I should do, or check?

The best way to validate your app is owned legitimately is to send the signed app transaction to a server you control to perform this validation, instead of only relying on on-device validation. Make sure you're also validating the app transaction's signature, including the certificate chain you get the public key from, in addition to verifying the transaction is valid for the device.

If you're implementing this for the first time, check out the App Store Server Library which works with Swift and other server programming languages and will streamline your integration.

This makes me think it is using my AppleID which I am logged into in System Settings. Does that sound right?

When you're testing an app you installed with Xcode against the App Store Sandbox environment, you sign in with a sandbox tester you've configured in App Store Connect. If you're testing an app you've downloaded from TestFlight, you sign in with an Apple ID.

I would like to be able to remove / delete the cached AppTransactions - where might I find those on the system?

The persistent storage for cached app transactions isn't directly accessible from your app, you can only interface with it using the Swift APIs on AppTransaction. Deleting your app will also delete the associated app transaction. Generally, you should always check the AppTransaction.shared property, and rely on StoreKit to always provide the most up to date version available. This includes providing a cached value if there is no network access or the value hasn't changed.

If you've used StoreKit for in-app purchase before, you may be familiar with the AppStore.sync() API. AppTransaction.refresh() is very similar to AppStore.sync() in that you should only call the method in response to input when someone believes there is a discrepancy.

For example, some developers choose to crash their app if they suspect an app transaction is invalid. Instead of this, you might choose to offer a control to repair the app, blocking access to other content in the meanwhile. When someone triggers this control, you can call AppTransaction.refresh(). Like you've observed in the App Store Sandbox environment, if someone legitimately owns the app you will always receive a valid app transaction after calling AppTransaction.refresh() (assuming suitable network conditions).