InAppify

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

PolicyKotlinFlutter mobileBehavior
Cached or fetched; defaultCACHED_OR_FETCHEDcachedOrFetchedImmediate cache, stale background refresh, fetch on miss
Cache onlyCACHE_ONLYcacheOnlyCache or error, no network
Current server stateFETCH_CURRENTfetchCurrentWait for server, no successful stale fallback on error
Fresh cache or serverNOT_STALE_CACHED_OR_CURRENTnotStaleCachedOrCurrentFresh cache, otherwise wait for server

Flutter takes named fetchPolicy:; Kotlin passes the enum to getCustomerInfo(policy). Current-state examples:

import 'package:inappify_sdk/inappify_sdk.dart';

Future<bool?> fetchPremium() async {
  final result = await InappifySdk.getCustomerInfoResult(
    fetchPolicy: InappifyCacheFetchPolicy.fetchCurrent,
  );
  if (!result.isSuccess) return null; // Unknown, not inactive.
  return result.data?.entitlements.active.containsKey('premium') == true;
}
import com.inappify.sdk.*

suspend fun fetchPremium(client: InappifyV2Client): Boolean? =
    when (val result = client.getCustomerInfo(InappifyCacheFetchPolicy.FETCH_CURRENT)) {
        is InappifyResult.Success -> result.data.entitlements.active.containsKey("premium")
        is InappifyResult.Failure -> null
    }

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

PathWhen?
Internal Android recoveryConfigure/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.

Consumable delivery · Errors and testing