Wrap StoreKit.Transaction so a purchase can be mocked - #6
Merged
Conversation
purchase(productID:) returned a StoreKit.Transaction. That type has no public initializer and cannot be constructed at all, so a stand-in implementation of PurchasesProtocol could only ever throw from purchase — the successful path was impossible to express and therefore impossible to test against. It was the last thing keeping the protocol from being mockable, which is the reason the protocol exists. StoreTransaction follows StoreProduct: the kit builds it from the StoreKit value and keeps that value on `transaction`, and a public initializer takes the fields directly, leaving `transaction` nil. It surfaces what entitlement code actually reads — the two identifiers and purchase dates, expiration, revocation, upgrade state, quantity, account token and subscription group. The raw transaction stays reachable for anything it does not carry. The identity fields default to the transaction itself: a one-off purchase has no original distinct from itself, and repeating both identifiers at every call site would be noise. This changes the return type of purchase(productID:) on both the protocol and the manager, so it is source-breaking for callers that name the type and for conformers. Verified from outside the module: a mock now returns a successful purchase, reports nil for the backing transaction, and still throws for an unknown identifier.
Wrapping StoreKit.Transaction changes the return type of purchase(productID:), which is source-breaking, so it lands as 3.0.0 rather than on top of the 2.0.0 tag that is already published. The migration section now separates the single change that affects someone on 2.0 from the full list that affects someone still on 1.x, so neither audience has to work out which entries apply to them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The last thing keeping
PurchasesProtocolfrom being mockable, which is the reason the protocol exists.The problem
purchase(productID:)returned aStoreKit.Transaction. That type has no public initializer and cannot be constructed at all, so a stand-in implementation could only ever throw frompurchase— the successful path was impossible to express, and therefore impossible to test against.2.0 fixed the same problem for
StoreProduct, butpurchasestill handed back a raw transaction, so the one method that matters most for a paywall stayed untestable.The change
StoreTransactionfollows the shapeStoreProductalready uses: the kit builds it from the StoreKit value and keeps that value on.transaction, and a public initializer takes the fields directly, leaving.transactionnil.It surfaces what entitlement code actually reads — both identifiers, both purchase dates,
expirationDate,revocationDate,isUpgraded,purchasedQuantity,appAccountTokenandsubscriptionGroupID. Anything it does not carry is still reachable through the raw transaction.The identity fields default to the transaction itself: a one-off purchase has no original distinct from itself, and repeating both identifiers at every call site would be noise.
Verified from outside the module
A consumer package building a mock against the public API only:
Tests go from 24 in 5 suites to 30 in 6, including the case that could not be written before: a stand-in returning a successful purchase.
Breaking
The return type of
purchase(productID:)changes on both the protocol and the manager, so this is source-breaking for callers that name the type and for conformers. It lands as 3.0.0 rather than being folded into the already-published 2.0.0 tag. README asks for3.0.0and splits the migration notes so someone on 2.0 sees only the one entry that affects them.