The fastest way to get misled in purchase infrastructure is to treat every recovery path as the same and every error as fatal. AppActor gives you enough state to be more precise than that.

Read-time fallbacks

Across the SDKs, getCustomerInfo() aims for authoritative server state first. On transient failures, the SDKs can still surface offline-derived entitlement state when they have enough local evidence. That means your UI should pay attention to:
  • whether the snapshot is offline-derived
  • whether verification is server-backed or device-backed
  • whether you are reading a cached surface or forcing a fresh fetch

Cached reads are intentional

Use cached reads when you need fast UI decisions:
  • cached offerings for paywall paint
  • cached remote configs for local behavior
  • cached customer info when you want local continuity during app startup
Use authoritative network reads when you need the latest entitlement truth.

Common recovery mistakes

  • using restore as a silent background sync
  • using queue drain as a normal restore button
  • treating wrapper parity names like quietSyncPurchases() as if they were a different business flow from native sync
  • overloading custom attributes with reserved profile or attribution fields

Debug signals to watch

The most useful signals are:
  • receipt pipeline events
  • deferred purchase resolution events
  • SDK log events where wrappers expose them
  • explicit error objects such as AppActorError or wrapper equivalents with code, message, detail, request ID, and retry metadata

Purchase error taxonomy

Empty offering decision tree

  1. Is the app configured with the expected public app SDK key?
  2. Does the project have a current offering?
  3. Does the offering contain packages for the running platform?
  4. Do the packages map to store products available to this tester?
  5. Are the products attached to the entitlement your app checks?
  6. Are you rendering offering.packages, not hardcoded product IDs?

Purchase succeeded but access did not unlock

Check:
  • product is attached to the premium entitlement
  • customer info was refreshed after purchase
  • CustomerInfo.hasActiveEntitlement("premium") is true
  • status is active or valid grace, not billing_retry, expired, or revoked
  • verification is not failed
  • backend resources are checking AppActor server-side state

Practical rule of thumb

  • user taps restore: run restore
  • app wants quiet reconciliation: run sync
  • you know receipts are stuck locally: run queue drain
  • app is temporarily offline: use offline entitlement helpers as a temporary access decision, then resync when possible
For a complete manual purchase flow, read Displaying Products and Making Purchases.