The SDK keeps SubKit and the native store reconciled through silent automatic sync plus an explicit restore action for user-initiated recovery.
Automatic sync
With default configuration, the SDK syncs purchases:
- on app start,
- after
identify(), - when the app returns to foreground after a configured background duration
(
sessionResumeThresholdMs, throttled byforegroundMinIntervalMs), - after purchase listener events,
- during manual restore.
Silent sync uses the store’s getAvailablePurchases() and never calls
prompt-prone restore APIs — users see no login sheets from background syncs.
All triggers are configurable via the iap options — see
Configuration.
Manual restore
Provide a restore action for reinstalls, device changes, and stuck pending purchases. Restore may prompt the user (store login), so it belongs behind an explicit button:
import { client } from '@piparotech/subkit-expo'
const PRO = 'pro'
async function restoreAndCheckAccess() {
const result = await client.restorePurchases()
if (result == null) {
showRestorePending()
return
}
if (result.conflicts.length > 0 || result.rejectedPurchases.length > 0) {
showRestoreRecovery(result)
return
}
const access = await client.getAccess(PRO)
if (access.state === 'granted') unlockPaidAccess()
else if (access.state === 'device_blocked') showDeviceRecovery(access.reason)
else showNothingToRestore()
}restorePurchases() calls the native restore API, then forces a
manual_restore sync and returns the PurchaseSyncResult. null means no
terminal result is available, including when a durable reconcile job is still
running. It does not mean there was nothing to restore. Keep the pending UI
and resume with syncPurchases({ force: true, reason: 'queue_retry' }).
Fetching CustomerInfo alone does not drain the purchase queue.
Manual sync
syncPurchases({ reason, force? }) triggers a sync directly. Every sync
carries one of these reasons, which SubKit receives as diagnostic context:
| Reason | Used for |
|---|---|
app_start |
Startup sync |
foreground |
Return to foreground |
identity_changed |
After identify() |
purchase_event |
Purchase listener events |
manual_restore |
Explicit restore |
paywall_preflight |
Fresh state before showing a paywall |
queue_retry |
Retrying queued purchases |
await client.syncPurchases({ reason: 'paywall_preflight' })Pass force: true to bypass throttling.
The auto-sync hook
For apps that disable syncOnAppStart or want the sync tied to a specific
screen mount:
import { useSubKitIapAutoSync } from '@piparotech/subkit-expo'
export function AppRoot() {
useSubKitIapAutoSync() // one app_start sync on first mount
return <AppNavigation />
}Options: enabled, syncOnMount, and a logger with warn for sync
failures. The hook guards against duplicate syncs across re-renders.
What a sync returns
PurchaseSyncResult contains acceptedPurchases, rejectedPurchases,
conflicts, finishableTransactions, verificationStatus, checkedAt, and
fresh customerInfo. Do not interpret it into access yourself; read
getAccess(key) or the access hook after restore. Transactions are finished in
the store only after SubKit returns them as finishable — see
Ownership & unclaimed for the conflict cases.