Skip to main content

@flokitai/subscriptions-sdk (shipping)

The package was renamed in v1.2.0. @flokit/subscriptions-sdk is no longer published anywhere — re-pin to @flokitai/subscriptions-sdk. Distribution moved to GitHub Packages, whose registry requires the package scope to match the repository owner. See Install.
@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 resolves baseUrl, 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 call identify(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
The package is private — authenticate to GitHub Packages with a token that has 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

RemotePaywall, Paywall, and getPaywall() are still exported by v2.0, but the endpoint they call — GET /api/paywall/config — now answers 410 with {"code": "PAYWALL_CONFIG_REMOVED"}. Remote paywall configuration no longer exists: paywalls are Flow-owned and served with the Flow page. Render your paywall in your own UI and drive purchases with purchase(), or send the user to the app’s published FloKit Flow page.

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:
Your native store adapter is one function — 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

  • Identityidentify(userId) links the persisted anonymous_ref to a known user (queues a user_alias event and posts the server-side alias edge); resetIdentity() severs it on logout.
  • Web→app handoffexchangeHandoffCode({ code }) redeems the single-use flokit_code (hc_…) from a FloKit web-funnel deep link (POST /api/paywall/handoff/exchange), adopts the web anonymous_ref, and enqueues an app_activation event; expired/used codes throw HandoffCodeError.
  • AttributionsubmitInstallReferrer() (Android Play Install Referrer) and submitAdServicesToken() (Apple Search Ads) feed install attribution.
  • Web checkoutstartWebCheckout() always throws CheckoutUnavailableError with code: '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-js on the Flow page.
  • Restorerestore() re-mints the token and fetches a fresh entitlement, bypassing the cache.
  • DiagnosticsgetDiagnostics() returns { sdkVersion, queueDepth, lastFlushAt, lastFlushError, droppedEvents, sessionId, tokenExpiresAt, entitlementCacheAgeMs, anonymousRef }; setDebugLogger(fn) streams queue/token/checkout debug lines. droppedEvents counts 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-retriable 400 that 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 (sessionTimeoutMs to override).
  • Lifecycle eventsfirst_open (once per install), app_open, session_start, session_end, and day_active (once per local calendar day, on that day’s first foreground — the DAU spine).
  • Entitlement transitionsentitlement_granted / entitlement_revoked fire when the cached entitlement flips. These are access events, not commercial ones: convert means they paid, entitlement_granted means they can use it, and a restore or a web checkout completing produces one without the other. The first observation carries first_observation: true. entitlement_revoked is 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.
  • Revenuepurchase({ priceUsdCents, currency }) stamps revenue and currency on the terminal event only, never on purchase_start, so abandoned attempts are not booked as income.

Migrating to v2.0

  1. Entitlement reads moved. GET /api/paywall/entitlement was deleted; the SDK now reads GET /api/entitlements/current. If you call the gateway directly, re-point it.
  2. Receipts moved. POST /api/paywall/receipt became POST /api/iap/receipts. The body schema is unchanged.
  3. product_identitlement_key on Entitlement, and it is now opaque — the store product id for native purchases, the Package key for web. Do not parse it. expires_at is now string | null, and state, source_event_id, and updated_at are new.
  4. startWebCheckout() always throws CheckoutUnavailableError (code: 'WEB_CHECKOUT_MIGRATED'). Delete the call site and send the user to the app’s published FloKit Flow page instead.
  5. previewOfferPrice() and PricePreview are gone. So is trackPaywallEvent({ offerId }).
  6. Remote paywall config is gone. getPaywall() / RemotePaywall hit an endpoint that now returns 410 — render your paywall yourself or use a Flow page.

Migrating to v1.2

  1. Re-pin to @flokitai/subscriptions-sdk with the .npmrc scope line above.
  2. Delete any host-side purchase_start / trial_start / convert / cancel emission around purchase() — the SDK owns that funnel now. Hand-rolling them is what produced double-counted purchase_start, trial_start recorded against failed purchases, and every error filed as a user cancellation.
  3. Pass appState: AppState (and ideally appVersion) to initSubscriptionsSdk().
  4. Expect roughly three extra events per launch (first_open once, then session_start and app_open).

Reliable events

Funnel events ride a persisted FIFO queue: each event is stamped with a client-minted event_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.
Server-side SDKs are planned as thin integration layers around the FloKit API: event ingestion, workspace and integration metadata, growth action approval workflows, and reporting reads for CAC, ROAS, payback, and LTV.
  • @flokit/node — Node.js / TypeScript (planned)
  • flokit-python — Python (planned)
  • flokit-go — Go (planned)