Skip to content

4.1.0: currentEntitlement(for:) returns the transaction behind an entitlement - #15

Merged
Ramiz69 merged 1 commit into
mainfrom
feature/current-entitlement
Nov 8, 2026
Merged

Ramiz69 merged 1 commit into
mainfrom
feature/current-entitlement

Conversation

@Ramiz69

@Ramiz69 Ramiz69 commented Nov 8, 2026

Copy link
Copy Markdown
Owner

What

hasEntitlement(for:) says whether the customer may use a product. currentEntitlement(for:) returns the
transaction behind that answer, with its dates, so an app can work out things like when a trial ends without
importing StoreKit:

if let trial = await purchases.currentEntitlement(for: "app.trial") {
    let trialEnds = trial.purchaseDate.addingTimeInterval(14 * 86_400)
}

The case that prompted it: App Review requires the free trial of a one-time purchase to be a free
non-consumable (guideline 3.1.1), and the app has to compute the trial's end from when it was "bought". That
date comes from the App Store, so it survives a reinstall and is the same on every device in a Family Sharing
group.

Behaviour

  • It reads Transaction.currentEntitlements(for:), the same source as hasEntitlement(for:), so it returns a
    transaction exactly when that returns true. It updates the cached flag and the purchasedProducts event
    the same way; the shared code moved into syncCachedFlag.

  • When several transactions qualify, the rule is fixed and documented:

    • revoked transactions are skipped;
    • the customer's own purchase wins over a family-shared grant;
    • then the earliest purchaseDate wins, then the lowest id.

    The answer therefore does not depend on StoreKit's order and does not change when a second grant appears
    later.

  • The new requirement has a default implementation that returns nil, so 4.0 conformers (including the mocks
    apps keep for tests and previews) still compile. That makes this a minor release, 4.1.0.

  • README asks for 4.1.0. GettingStarted, MigratingTo4 and the symbol pages cover the new method and say when a
    mock should implement it.

Tests

CurrentEntitlementTests checks the selection rule using transactions built by hand:

  • no entitlement gives nil;
  • a purchased non-consumable is returned with its purchase date;
  • a revoked transaction gives nil;
  • an own purchase beats an older family-shared one;
  • the earliest purchase wins whatever the input order, and a tie is broken by id;
  • a 4.0 conformer gets the nil default;
  • a stand-in's own implementation is called through any PurchasesProtocol.

The Transaction.currentEntitlements(for:) call itself is not tested, like every other live StoreKit path in
the kit, because it needs a host app.

Verified locally (Xcode 27.2 / Swift 6.4.2)

  • swift test: 106 tests in 12 suites pass, with 0 warnings.
  • xcodebuild build succeeds for iOS, Mac Catalyst, watchOS, tvOS and visionOS with 0 warnings, and the tests
    build for visionOS.
  • Builds with all six upcoming Swift 7 features and -warnings-as-errors.
  • DocC from a fresh derived-data path: 0 warnings.
  • Examples/PaywallDemo builds unchanged.

hasEntitlement(for:) answers whether the customer may use a product, but some
answers need a date. A free trial that App Review requires to be a free
non-consumable is the case that prompted this: the app computes the end of
the trial from when it was "bought". That date has to come from the App Store,
so that it survives a reinstall and is the same on every device of a Family
Sharing group. Until now the only way to get it was to import StoreKit next to
the kit.

currentEntitlement(for:) returns that transaction as a StoreTransaction. It
reads Transaction.currentEntitlements(for:), the same source as
hasEntitlement(for:), so a non-nil result is exactly the case where that
returns true, and it brings the cached flag and the purchasedProducts event
into line the same way. The shared bookkeeping moved into syncCachedFlag so
the two cannot drift apart.

When several transactions entitle the customer, the rule is fixed and
documented: revoked ones are skipped, the customer's own purchase wins over a
family-shared grant (nobody else can withdraw it, and its date is theirs),
then the earliest purchase date, then the lowest id. The answer therefore does
not move when a second grant appears later, and it never depends on StoreKit's
unspecified order. The rule lives in StoreTransaction.preferredEntitlement(among:)
and is tested with constructed values; reading StoreKit itself still needs a
host app and stays untested, like every other live StoreKit path.

The new requirement has a default that returns nil, so conformers written for
4.0, including the stand-ins apps keep for tests and previews, keep compiling.
That makes this 4.1.0 rather than a major. The docs say when a mock should
implement it: as soon as it answers hasEntitlement(for:) with true.

The purchase date, original purchase date, ownership and revocation date that
callers need were already public on StoreTransaction.
@Ramiz69
Ramiz69 merged commit a509944 into main Nov 8, 2026
1 check passed
@Ramiz69
Ramiz69 deleted the feature/current-entitlement branch November 8, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant