CustomerInfo, cache and purchase recovery
Four CustomerInfo policies, subscription expiry, entitlement checks and automatic recovery versus manual store sync/restore.
Use verified CustomerInfo and active entitlements for access, not an isPaid flag, payment URL or attempt history. Keep failed fetch separate from authoritative inactive state.
Reads and defaults
| Policy | Kotlin | Flutter mobile | Behavior |
|---|---|---|---|
| Cached or fetched; default | CACHED_OR_FETCHED | cachedOrFetched | Immediate cache, stale background refresh, fetch on miss |
| Cache only | CACHE_ONLY | cacheOnly | Cache or error, no network |
| Current server state | FETCH_CURRENT | fetchCurrent | Wait for server, no successful stale fallback on error |
| Fresh cache or server | NOT_STALE_CACHED_OR_CURRENT | notStaleCachedOrCurrent | Fresh cache, otherwise wait for server |
Flutter takes named fetchPolicy:; Kotlin passes the enum to getCustomerInfo(policy). Current-state examples:
Omit the parameter for the default; substitute table values for other modes. entitlements.active['premium'] reads active access, entitlements.all['premium'] known history. Synchronous hasEntitlement, isActiveEntitlement, getEntitlement read the latest snapshot without HTTP. Mobile has no public checkEntitlement.
Offline access and expiration
Android considers CustomerInfo stale after five minutes foreground/25 hours background; current iOS uses five minutes. Staleness means refresh eligibility, not entitlement extension/expiration. Explicit network requests can fail; verified cache remains for supported policies. No permanent polling timer is installed.
A verified 30-day subscription can remain accessible from a valid cache when the app reopens offline on day ten, under a cache-using policy. Its expiration is still checked. An active verified non-consumable may be lifetime, but null expiry alone is not proof of access. Consumable inventory belongs to the host delivery ledger.
Offline cache cannot guarantee observing new server revocations. Do not replace trusted-time behavior with raw device-clock checks or promise new anti-tampering guarantees.
Automatic and manual recovery
| Path | When? |
|---|---|
| Internal Android recovery | Configure/logIn, foreground or restored internet; pending work or five minutes, at least thirty-second error backoff |
syncStorePurchases() | Manual reconciliation of store work/uncertain attempts; no payment UI |
restoreStorePurchases() | Explicit user restoration of eligible subscriptions/non-consumables |
syncPendingConsumables() | Verified deliveries/consumption checkpoints; a registered handler may perform fulfillment |
confirmDelivery(id) | Only after a durable exactly-once host grant |
Kotlin and Flutter mobile use these names without parameters except confirm's ID. There is no public recover. Do not add a competing Android lifecycle/sync loop. Restore reports restored/alreadyProcessed/failed counts; aggregate success doesn't mean every item succeeded and never re-grants consumed inventory.
Separate iOS and Web
iOS has internal startup recovery but does not install Android foreground/connectivity observers. On relevant payment return/resume, refresh CustomerInfo and reconcile Direct deliveries as needed. This backend's syncStorePurchases/restoreStorePurchases only reconcile Direct deliveries, without StoreKit and with zero restored store receipts.
Direct JS getCustomerInfo() defaults to FETCH_CURRENT and supports four uppercase enum modes. Customer snapshots are memory-only. Flutter Web 0.2.1 does not forward fetchPolicy and calls that provider default; don't promise offline cacheOnly through its facade. Read Web.
Payment return and observations
Android recovery recognizes only marked Inappify return links; initial/new intents are forwarded automatically. Custom routers can forward handleDeepLink(url). Arbitrary links must not force recovery, and markers don't prove payment.
Observe Flutter events/snapshot or Kotlin addEventListener/stateUpdates(), and release observers with their owner. Android attempt history covers missed events; CustomerInfo authorizes access.