@flokitai/subscriptions-sdk (shipping)
@flokitai/subscriptions-sdk v2.0 is FloKit’s React Native SDK for purchases, entitlements, and app measurement. You initialize it once; the SDK then runs the native purchase → receipt-verification → entitlement flow, tracks sessions and app lifecycle, and reports funnel events through a persisted, offline-safe queue. Funnel events carry an optional placement — a stable name for a moment in your app (onboarding_complete, settings_upgrade) — so reporting can be sliced by where in the app something happened.
- Platform: React Native (TypeScript). Peer dependencies:
react >= 18,react-native >= 0.70. - No native dependency: the native store call (StoreKit 2 / Play Billing / expo-iap) is injected by your app, so the SDK stays pure TypeScript.
- Context-first: call
initSubscriptionsSdk()once. Every other function resolvesbaseUrl,appId, the user reference, and credentials from that context — you never thread them through call sites. - Tenant-safe by design: the SDK sends only the user reference (
x-user-id) and your app’s publishable key (x-app-key); your workspace/tenant is always derived server-side. The context mints and refreshes a short-lived app-session token (x-app-token) automatically. - Anonymous-first identity: a persisted
anonymous_ref(a UUID) is the user reference until you callidentify(userId)after login.
Install
The package is published to GitHub Packages under the@flokitai scope. Point the scope at the GitHub registry in your .npmrc, then install:
.npmrc
read:packages on the flokitai org. Contact your FloKit team for access.
Migrating from
@flokit/subscriptions-sdk v1.0.x? Re-pin to the new scope, delete any host-side purchase_start / trial_start / convert / cancel emission around purchase() (the SDK emits them now), and pass appState at init. See Migrating to v1.2.Initialize once
appState is optional but strongly recommended: without it the SDK cannot observe
foreground/background transitions, so sessions never roll over, the lifecycle events never
fire, and the queue is not flushed when the app backgrounds. All device/app context fields
are host-supplied, which is what keeps the SDK dependency-free.
userId is optional at init — identity is anonymous-first. Call identify(userId) when the user
authenticates and resetIdentity() on logout.
Remote paywalls are retired
Purchase and entitlement
purchase() runs the store adapter you injected at init, verifies the receipt through the gateway
(POST /api/iap/receipts), updates the entitlement cache, and auto-tracks purchase_start and
(on an active entitlement) convert:
PurchaseNative — that runs the store purchase and
returns the receipt the gateway verifies. See the README for a complete expo-iap example. Keeping
it injected is what lets the SDK stay free of any native dependency.
Identity, handoff, and web checkout
- Identity —
identify(userId)links the persistedanonymous_refto a known user (queues auser_aliasevent and posts the server-side alias edge);resetIdentity()severs it on logout. - Web→app handoff —
exchangeHandoffCode({ code })redeems the single-useflokit_code(hc_…) from a FloKit web-funnel deep link (POST /api/paywall/handoff/exchange), adopts the webanonymous_ref, and enqueues anapp_activationevent; expired/used codes throwHandoffCodeError. - Attribution —
submitInstallReferrer()(Android Play Install Referrer) andsubmitAdServicesToken()(Apple Search Ads) feed install attribution. - Web checkout —
startWebCheckout()always throwsCheckoutUnavailableErrorwithcode: 'WEB_CHECKOUT_MIGRATED'. It is retained only so old call sites fail loudly. Web checkout is created from a published Flow’s paywall and checkout configuration and driven same-origin by@flokitai/checkout-json the Flow page. - Restore —
restore()re-mints the token and fetches a fresh entitlement, bypassing the cache. - Diagnostics —
getDiagnostics()returns{ sdkVersion, queueDepth, lastFlushAt, lastFlushError, droppedEvents, sessionId, tokenExpiresAt, entitlementCacheAgeMs, anonymousRef };setDebugLogger(fn)streams queue/token/checkout debug lines.droppedEventscounts events this process gave up on (queue overflow or a non-retriable reject) — both are silent by design, so a non-zero value is the only signal that telemetry is being lost.
Sessions, lifecycle, and general events
track(eventName, properties?, opts?)— general-purpose funnel events beyond the paywall. Names come from a canonical vocabulary shared verbatim with the hosted FloKit web funnel (quiz_start,quiz_answered,offer_view,lead_captured, …), so a web session and an app session describe one journey in one language. Unknown names are refused on-device — the gateway answers an unknown name with a non-retriable400that discards the whole batch, so failing fast keeps one typo from costing up to 49 unrelated events.- Sessions — every event carries a
session_id. A session survives a background trip shorter than 30 minutes (sessionTimeoutMsto override). - Lifecycle events —
first_open(once per install),app_open,session_start,session_end, andday_active(once per local calendar day, on that day’s first foreground — the DAU spine). - Entitlement transitions —
entitlement_granted/entitlement_revokedfire when the cached entitlement flips. These are access events, not commercial ones:convertmeans they paid,entitlement_grantedmeans they can use it, and a restore or a web checkout completing produces one without the other. The first observation carriesfirst_observation: true.entitlement_revokedis the only on-device signal that a trial lapsed or a cancellation took effect — the entitlement is revalidated on foreground (60s TTL) to catch it. - Revenue —
purchase({ priceUsdCents, currency })stampsrevenueandcurrencyon the terminal event only, never onpurchase_start, so abandoned attempts are not booked as income.
Migrating to v2.0
- Entitlement reads moved.
GET /api/paywall/entitlementwas deleted; the SDK now readsGET /api/entitlements/current. If you call the gateway directly, re-point it. - Receipts moved.
POST /api/paywall/receiptbecamePOST /api/iap/receipts. The body schema is unchanged. product_id→entitlement_keyonEntitlement, and it is now opaque — the store product id for native purchases, the Package key for web. Do not parse it.expires_atis nowstring | null, andstate,source_event_id, andupdated_atare new.startWebCheckout()always throwsCheckoutUnavailableError(code: 'WEB_CHECKOUT_MIGRATED'). Delete the call site and send the user to the app’s published FloKit Flow page instead.previewOfferPrice()andPricePrevieware gone. So istrackPaywallEvent({ offerId }).- Remote paywall config is gone.
getPaywall()/RemotePaywallhit an endpoint that now returns410— render your paywall yourself or use a Flow page.
Migrating to v1.2
- Re-pin to
@flokitai/subscriptions-sdkwith the.npmrcscope line above. - Delete any host-side
purchase_start/trial_start/convert/cancelemission aroundpurchase()— the SDK owns that funnel now. Hand-rolling them is what produced double-countedpurchase_start,trial_startrecorded against failed purchases, and every error filed as a user cancellation. - Pass
appState: AppState(and ideallyappVersion) toinitSubscriptionsSdk(). - Expect roughly three extra events per launch (
first_openonce, thensession_startandapp_open).
Reliable events
Funnel events ride a persisted FIFO queue: each event is stamped with a client-mintedevent_id,
occurred_at, session_id, sdk_version, and its placement_id; the queue survives app kills via
the injected StorageAdapter and is batch-POSTed to POST /api/paywall/events (≤50 per batch) with
capped exponential backoff. With appState wired, the queue also flushes when the app backgrounds.
Tracking never throws.
purchase() owns its whole funnel — it emits purchase_start on entry, then exactly one
terminal event: convert, trial_start (when trialDays > 0), cancel (user backed out), or
purchase_failed (with a reason). Do not emit these yourself. impression, offer_tap,
checkout_*, user_alias, app_activation, and the lifecycle events are auto-tracked too. For
anything else, use track(eventName, properties?) for vocabulary events or
trackPaywallEvent({ eventType, placement, paywallId, variantId }) for paywall-scoped ones.
Export surface
Exported types include
Offer, PaywallVariant, PaywallConfigResponse, Entitlement, Provider,
EntitlementProvider, StoreReceipt, PaywallEventType, PurchaseNative, AppToken,
StorageAdapter, InitSubscriptionsSdkOptions, SubscriptionsSdkContext, IdentityState,
GetEntitlementOptions, RestoreResult, EntitlementListener, HandoffSession,
SdkDiagnostics, EventName, PaywallEventName, TrackOptions, JourneyRefs,
AppStateSource, SessionTrackerOptions, plus the option/prop types for each export.
See the Paywall API and Entitlements API for the underlying endpoints.
Server SDKs (roadmap)
The packages below are planned, not shipping. Use the v1 REST API preview docs to shape requirements — package names are placeholders until publishing is ready.
@flokit/node— Node.js / TypeScript (planned)flokit-python— Python (planned)flokit-go— Go (planned)