MyRecipes 1.0 purchase, restore, and synchronization contract
Issue: #267
Baseline: 2026-08-02
Source of truth
StoreKit reports transaction events, but it does not directly grant MyRecipes
cloud access. The app sends the store verification payload to the App Check and
Firebase Authentication-protected record_purchase_call Function. Only the
canonical schema-v1 entitlement subsequently written under
users/{uid}.subscription determines access.
The app listens to that user document after sign-in. A backend purchase, restore, notification, or scheduled reconciliation therefore updates the subscription screen without a reinstall or locally manufactured boolean. Signing out cancels both the StoreKit and entitlement listeners, clears the in-memory entitlement, and clears persistent private Firestore cache before a different account initializes.
Transaction states
| StoreKit state | Client behavior | Complete transaction? | Access decision |
|---|---|---|---|
| Purchased | Send to trusted verifier | Only after backend returns verified=true |
Canonical entitlement |
| Restored | Send to the same trusted verifier | Only after backend returns verified=true |
Canonical entitlement and original-transaction binding |
| Pending / Ask to Buy | Display pending; wait for a later StoreKit event | No | No new access |
| Cancelled | Display cancellation and clear pending UI | No | No new access |
| Error | Display bounded retry guidance | No | No new access |
| Verification/network/App Check failure | Retain transaction for StoreKit replay; offer restore/retry | No | Existing bounded canonical entitlement only |
Duplicate purchase-stream deliveries are coalesced while verification is in flight. The backend remains idempotent by verified transaction identity, so repeated restore and interrupted verification cannot duplicate entitlement or move one original transaction to another UID.
Startup and outage behavior
The app binds the purchase stream before product lookup. This permits an unfinished verified transaction to replay after termination or restart. It also starts a Firestore entitlement listener for the current UID. Product loading, store availability, and restore feedback are bounded and expose retry messages.
When backend refresh is temporarily unavailable, the client uses only the
server-written state and deadline already present in Firestore. It does not
extend expiry. The backend may issue the approved bounded outage state under
269; otherwise writes fail closed under #303.
Products and disclosures
Only the explicitly configured monthly and annual product identifiers are classified as subscriptions. The purchase screen shows the store-provided full price, monthly/annual duration, included cloud service, eligible 14-day introductory trial, automatic-renewal disclosure, restore action, Apple standard Terms of Use, Privacy Policy, and platform-appropriate subscription management.
App Store Connect pricing, localization, introductory-offer eligibility, URLs, and the privacy text remain subject to the final compliance review in #271.
Evidence still required
Repository and emulator tests cannot generate authoritative StoreKit sandbox evidence. Before #267 closes, record monthly and annual sandbox purchase, cancellation, pending/deferred, failed verification and retry, interrupted verification, repeated restore, reinstall/Device B restore, account switching, and live paywall refresh against the designated TestFlight build. Final deployed authorization is also part of #305.