Summary
Purchase completion opens the process-global AI gate based on a session that was captured before the StoreKit await. If the signed-in account changes during that window, the gate can be opened while a different account is current. This is the purchase-path sibling of the restore-path ownership guard added in #50 (which was intentionally scoped to the restore path only).
Mechanism
PlanPickerView.purchaseProduct(_:):
- captures
session = TabMailAuthService.getSession() before await storeKit.purchase(...),
- after the await resolves, calls
AISubscriptionGate.shared.openGate().
AISubscriptionGate is process-global, not per-account. The transaction is correctly stamped with the purchaser's appAccountToken, but the local gate is opened for whichever account is current when the await resolves. If the app switched from account A → B during the purchase, B's gate opens. It self-heals on B's next authoritative whoami (AISubscriptionGate.apply() closes on has_subscription:false).
Proposed fix (common-path-safe)
At purchase completion, compare the current TabMailAuthService.getSession()?.userId to the captured purchaser id; do not open the gate if they differ.
Acceptance
Why deferred
Degenerate and self-healing; no user-visible harm beyond a transient wrong-open that the next revalidation corrects. Tracked for purchase-path parity with #50, to be picked up when convenient. Related: #50 (restore-path guard).
Summary
Purchase completion opens the process-global AI gate based on a session that was captured before the StoreKit
await. If the signed-in account changes during that window, the gate can be opened while a different account is current. This is the purchase-path sibling of the restore-path ownership guard added in #50 (which was intentionally scoped to the restore path only).main— not introduced by the signup-trial work (Support server-granted free trial in account and paywall UI #49 / Do not open the AI gate when restoring a different account's subscription #50).Mechanism
PlanPickerView.purchaseProduct(_:):session = TabMailAuthService.getSession()beforeawait storeKit.purchase(...),AISubscriptionGate.shared.openGate().AISubscriptionGateis process-global, not per-account. The transaction is correctly stamped with the purchaser'sappAccountToken, but the local gate is opened for whichever account is current when the await resolves. If the app switched from account A → B during the purchase, B's gate opens. It self-heals on B's next authoritativewhoami(AISubscriptionGate.apply()closes onhas_subscription:false).Proposed fix (common-path-safe)
At purchase completion, compare the current
TabMailAuthService.getSession()?.userIdto the captured purchaser id; do not open the gate if they differ.subscriptionOwnerUserIdownership check — the latter can be stale from a listener race on the purchase path and would risk false-closing a legitimate buyer, which is why Do not open the AI gate when restoring a different account's subscription #50 deliberately did not guard the purchase path with it.StoreKitManager Restore-Gate Ownership).Acceptance
Why deferred
Degenerate and self-healing; no user-visible harm beyond a transient wrong-open that the next revalidation corrects. Tracked for purchase-path parity with #50, to be picked up when convenient. Related: #50 (restore-path guard).