Override defaults only when your app needs custom behavior. Normal setups need none of this page.
Full example
import AsyncStorage from '@react-native-async-storage/async-storage'
import { AppState, Platform } from 'react-native'
import { configureSubKit, createStoredPurchaseQueueStore } from '@piparotech/subkit-expo'
import { createExpoIapAdapter } from '@piparotech/subkit-expo/expo-iap'
configureSubKit({
sdkKey: 'sk_sdk_replace_me',
installationId: 'install_abc',
adapterBundle: createExpoIapAdapter(),
platform: Platform.OS === 'ios' ? 'ios' : 'android',
queue: createStoredPurchaseQueueStore({ storage: AsyncStorage }),
appStateSource: {
getCurrentState: () => (AppState.currentState === 'active' ? 'active' : 'background'),
subscribe: (listener) => {
const subscription = AppState.addEventListener('change', (state) => {
listener(state === 'active' ? 'active' : state === 'inactive' ? 'inactive' : 'background')
})
return { remove: () => subscription.remove() }
},
},
iap: {
autoSync: true,
customerInfoStaleAfterMs: 24 * 60 * 60 * 1000,
nonExpiringEntitlementMaxOfflineAgeMs: 30 * 24 * 60 * 60 * 1000,
},
})Why the durable queue exists
Apple and Google redeliver unfinished subscriptions and non-consumables, so the stores keep those durable for you. The stored queue covers what they don’t:
- iOS consumables — active-items-only reads mean a lost consumable event never comes back; the queue preserves it across restarts.
- Rejected/failed state — purchases rejected by SubKit are remembered and not retried forever.
- Retry caps — finish retries are counted per purchase instead of resetting every launch.
- User attribution — a queued purchase stays bound to the app user that first observed it.
- Early events — listener events arriving before identity/network are held until sync is possible.
A transaction is finished in the store only after SubKit’s runtime API says it is finishable.
Custom queue storage (MMKV)
import { createMMKV } from 'react-native-mmkv'
import { createMmkvJsonStorage, createStoredPurchaseQueueStore } from '@piparotech/subkit-expo'
const mmkv = createMMKV({ id: 'subkit' })
const queue = createStoredPurchaseQueueStore({
storage: createMmkvJsonStorage(mmkv),
})Any storage with the same getItem / setItem / removeItem shape works —
including encrypted implementations for stricter local-data requirements.
Fully custom queue
A custom queue must implement the complete PurchaseQueueStore interface:
enqueue, enqueueMany, listPending, markFailed, markFinished,
markRejected, and markVerified. The sync coordinator drains all available
store purchases through enqueueMany, so persist batches with a single
storage write.
Custom CustomerInfo cache
The default cache is scoped to SDK key + installation ID + hashed user
identity. Provide your own via customerInfoCache, or build one with the
exported helper:
import { createCustomerInfoCacheStore } from '@piparotech/subkit-expo'
const customerInfoCache = createCustomerInfoCacheStore({
keyPrefix: 'subkit.customerInfo',
storage: myJsonStorage, // getItem/setItem/removeItem
policy: { customerInfoStaleAfterMs: 6 * 60 * 60 * 1000 },
})A CustomerInfoCacheStore implements read(appUserId) and write(info).
Cached data is schema-validated on read; unparsable entries are treated as
missing.
Other injection points
| Option | Default | Override for |
|---|---|---|
adapterBundle |
lazy expo-iap adapter |
tests, custom native purchase bridge |
appStateSource |
React Native AppState |
tests, non-RN environments |
platform |
React Native Platform.OS |
forcing ios/android in tests |
logger |
silent | surfacing SDK warnings into your logging |
sessionId |
generated | correlating diagnostics across a session |
autoStart |
true |
manual start() control in tests |