revnix-capacitor
v0.2.0
Published
Revnix subscriptions and entitlements for Capacitor.
Readme
revnix-capacitor
Revnix subscriptions and entitlements for Capacitor — the same client contract as every Revnix SDK, one implementation per platform:
- iOS bridges
revnix-swift(StoreKit 2) - Android bridges
revnix-kotlin(Play Billing 8) - Web delegates to
revnix-react— the canonical TypeScript implementation of the resilience policy
Until the native SDKs ship to CocoaPods/SPM and Maven Central, their 0.2.0 sources are vendored inside this plugin (
ios/…/Revnix/,android/…/com/revnix/) so it builds standalone. Fix native bugs upstream, then re-copy.
What every platform guarantees: network-first entitlements with an offline
cache flagged stale, deliberate rejections (401/403/404/409) never masked by
that cache, one fetch per screen of gates (soft TTL + coalescing), a durable
purchase retry queue drained idempotently on every launch, and read-your-writes
unlock via waitForEntitlements(seq).
Quick start
import { Revnix } from 'revnix-capacitor';
await Revnix.configure({
apiKey: 'rvx_pk_live_…', // publishable key only
baseUrl: 'https://your-deployment.convex.site',
});
// Gate. Never rejects; unknown or unreachable means locked.
const { entitled } = await Revnix.isEntitled({ entitlementId: 'pro' });
// Paywall. The resolve identifies the customer so a running A/B experiment
// can serve a sticky variant — `placement.experiment` names it (or is null).
const placement = await Revnix.resolvePlacement({ placementKey: 'main_paywall' });
await Revnix.logPaywallShown({ placementKey: 'main_paywall' });
// After a store purchase completes (StoreKit 2 / Play Billing):
const result = await Revnix.registerPurchase({
source: 'apple',
token: originalTransactionId,
productId,
transactionId,
signedTransactionInfo: jws, // the proof path
});
await Revnix.waitForEntitlements({ seq: result.seq }); // read-your-writes unlockInstall
To use npm
npm install revnix-capacitorTo use yarn
yarn add revnix-capacitorSync native files
npx cap syncAPI
configure(...)getCustomerId()logout()getEntitlements()getCachedEntitlements()isEntitled(...)waitForEntitlements(...)registerPurchase(...)retryPendingPurchases()pendingPurchaseCount()resolvePlacement(...)registerInstall(...)logPaywallShown(...)setAttributes(...)addListener('diagnostic', ...)removeAllListeners()- Interfaces
- Type Aliases
Failure taxonomy: rejected calls carry code (network · timeout ·
rate_limited · server · bad_response · auth · not_found · purchase_blocked
· invalid) and data.isRetryable. Deliberate rejections (401/403/404/409)
are never masked by a cache — a kill-switch must stay a kill-switch.
configure(...)
configure(options: ConfigureOptions) => Promise<void>Configure once at startup. Native platforms drain the purchase retry queue and report the install as launch chores (both idempotent).
| Param | Type |
| ------------- | ------------------------------------------------------------- |
| options | ConfigureOptions |
getCustomerId()
getCustomerId() => Promise<{ customerId: string; }>Anonymous id, minted and persisted natively on first use.
Returns: Promise<{ customerId: string; }>
logout()
logout() => Promise<{ customerId: string; }>Mint a fresh anonymous identity. Call at sign-out, or the next user inherits the previous one's cached entitlements.
Returns: Promise<{ customerId: string; }>
getEntitlements()
getEntitlements() => Promise<CustomerEntitlements>Network-first entitlement read. Transient failures serve the cache
flagged stale; deliberate rejections (401/403/404/409) reject.
Returns: Promise<CustomerEntitlements>
getCachedEntitlements()
getCachedEntitlements() => Promise<{ snapshot: CustomerEntitlements | null; }>Last cached snapshot with the offline policy applied; snapshot: null
when this customer never had a live read. Never touches the network.
Returns: Promise<{ snapshot: CustomerEntitlements | null; }>
isEntitled(...)
isEntitled(options: { entitlementId: string; }) => Promise<{ entitled: boolean; }>Gate helper — never rejects. Unknown or unreachable resolves false, so gates fail closed.
| Param | Type |
| ------------- | --------------------------------------- |
| options | { entitlementId: string; } |
Returns: Promise<{ entitled: boolean; }>
waitForEntitlements(...)
waitForEntitlements(options: { seq: number; }) => Promise<CustomerEntitlements>Read-your-writes: poll until the ledger reflects seq. Returns the
last read (rather than rejecting) if the schedule runs out.
| Param | Type |
| ------------- | ----------------------------- |
| options | { seq: number; } |
Returns: Promise<CustomerEntitlements>
registerPurchase(...)
registerPurchase(options: RegisterPurchaseInput) => Promise<RegisterPurchaseResult>Register a store purchase. On a transient failure the claim is queued durably and the call rejects — it is never lost.
| Param | Type |
| ------------- | ----------------------------------------------------------------------- |
| options | RegisterPurchaseInput |
Returns: Promise<RegisterPurchaseResult>
retryPendingPurchases()
retryPendingPurchases() => Promise<{ delivered: number; }>Drain the persistent retry queue. Safe on every launch/foreground —
the server dedupes on the purchase key. (Web reports delivered: 0;
revnix-react drains internally without a count.)
Returns: Promise<{ delivered: number; }>
pendingPurchaseCount()
pendingPurchaseCount() => Promise<{ count: number; }>How many registrations are still queued. (Web always reports 0.)
Returns: Promise<{ count: number; }>
resolvePlacement(...)
resolvePlacement(options: { placementKey: string; }) => Promise<PlacementResolution>Resolve a placement to its offering + remote paywall config, with an offline fallback to the last successful resolution.
| Param | Type |
| ------------- | -------------------------------------- |
| options | { placementKey: string; } |
Returns: Promise<PlacementResolution>
registerInstall(...)
registerInstall(options?: { platform?: string | undefined; appVersion?: string | undefined; } | undefined) => Promise<void>Fire-and-forget install beacon; recorded once per customer id. (Web: handled automatically by configure.)
| Param | Type |
| ------------- | -------------------------------------------------------- |
| options | { platform?: string; appVersion?: string; } |
logPaywallShown(...)
logPaywallShown(options?: { placementKey?: string | undefined; paywallId?: string | undefined; } | undefined) => Promise<void>Fire-and-forget paywall impression. Call when the paywall becomes visible, not when you start loading it.
| Param | Type |
| ------------- | ----------------------------------------------------------- |
| options | { placementKey?: string; paywallId?: string; } |
setAttributes(...)
setAttributes(options: { attributes: Record<string, string | number | null>; }) => Promise<void>Set attributes on the current customer. Attributes are what A/B-test
audiences target — set country, app_version, locale, or any custom
key you want to segment on. null deletes a key.
Rejects, unlike the fire-and-forget beacons: the next placement resolve
may depend on these. email and username are reserved (secret key
only), and an attribute your backend already set cannot be changed from
a device.
| Param | Type |
| ------------- | -------------------------------------------------------------------------------------------------- |
| options | { attributes: Record<string, string | number | null>; } |
addListener('diagnostic', ...)
addListener(eventName: 'diagnostic', listenerFunc: (diagnostic: RevnixDiagnostic) => void) => Promise<PluginListenerHandle>Background failures the SDK swallowed (queue drains, telemetry).
| Param | Type |
| ------------------ | -------------------------------------------------------------------------------------- |
| eventName | 'diagnostic' |
| listenerFunc | (diagnostic: RevnixDiagnostic) => void |
Returns: Promise<PluginListenerHandle>
removeAllListeners()
removeAllListeners() => Promise<void>Interfaces
ConfigureOptions
| Prop | Type | Description |
| -------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| apiKey | string | Publishable key (rvx_pk_live_… / rvx_pk_test_…). The key fixes app + environment server-side. Secret keys must never ship in a binary — identify/alias are server-proxied by design and not part of this plugin. |
| baseUrl | string | e.g. https://your-deployment.convex.site |
| timeoutMs | number | Per-request timeout in ms. Default 10 000. |
| entitlementsTtlMs | number | Soft TTL on entitlement reads in ms. Default 30 000; 0 = always fetch. |
| offlineMaxCacheAgeMs | number | Snapshots older than this serve as all-inactive. Default 14 days. |
CustomerEntitlements
| Prop | Type | Description |
| ------------------ | -------------------------- | ------------------------------------------------------ |
| customerId | string | |
| cursor | number | Ledger position this read reflects (read-your-writes). |
| entitlements | Entitlement[] | |
| stale | boolean | True when served from the offline cache. |
| fetchedAt | number | Unix ms this snapshot was fetched. |
Entitlement
| Prop | Type | Description |
| ------------------- | -------------------------------- | --------------------------------------------------------------- |
| entitlementId | string | |
| isActive | boolean | |
| expiresAt | number | Unix ms; absent for a lifetime purchase or an open-ended grant. |
| sources | EntitlementSource[] | |
EntitlementSource
| Prop | Type |
| --------------- | -------------------- |
| kind | string |
| key | string |
| isActive | boolean |
| expiresAt | number |
RegisterPurchaseResult
| Prop | Type | Description |
| -------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------ |
| eventId | string | |
| seq | number | Ledger position — pass to waitForEntitlements to unlock. |
| duplicate | boolean | |
| customerId | string | The id to use going forward. The SDK adopts it automatically. |
| transferred | boolean | |
| ownedByOtherCustomer | boolean | |
| refused | string | |
| restored | boolean | |
| provisional | boolean | Recorded without store proof — live but time-boxed until the store confirms. Expect true for Google device claims. |
RegisterPurchaseInput
| Prop | Type | Description |
| --------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------- |
| source | RevnixStoreKind | |
| token | string | Apple: originalTransactionId · Google: purchaseToken. |
| productId | string | |
| transactionId | string | Google: this is the purchaseToken too (the established rule). |
| occurredAt | number | |
| expiresAt | number | |
| signedTransactionInfo | string | StoreKit 2 JWS — the proof path; claims without it are provisional. |
PlacementResolution
| Prop | Type | Description |
| ------------------ | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| status | string | |
| placementKey | string | |
| revision | number | Published catalog revision this resolution came from. |
| offering | PlacementOffering | |
| paywall | PlacementPaywall | Remote paywall render contract — your app renders it in v1. Absent when the placement has no paywall attached (the wire sends null; native bridges omit the key). |
| experiment | PlacementExperiment | null | Sticky experiment assignment for this customer; null when no running experiment covers the placement (older servers omit the key — every bridge normalizes that to null). |
PlacementOffering
| Prop | Type |
| ----------------- | ------------------------------- |
| offeringId | string |
| displayName | string |
| packages | PlacementPackage[] |
PlacementPackage
| Prop | Type |
| --------------- | ------------------- |
| packageId | string |
| productId | string |
PlacementPaywall
| Prop | Type |
| --------------- | ------------------------------------------------------- |
| paywallId | string |
| name | string |
| config | PaywallConfig |
PaywallConfig
Remote paywall design attached to a placement. Render contract — the app draws this with its own components; prices still come from the store (StoreKit / Play Billing) so the display never disagrees with the charge.
| Prop | Type | Description |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| template | 'focus' | 'feature-list' | 'minimal' | 'hero' | 'timeline' | 'plans' | 'feature-grid' | 'offer' | 'reveal' | Layout — the screen structure to render. The dashboard's template gallery is presets over these layouts; the first three are the original templates and render exactly as before. |
| mode | 'dark' | 'light' | Color scheme the paywall renders in. Absent (legacy config) = dark. |
| headline | string | |
| subheadline | string | |
| features | PaywallFeature[] | |
| ctaLabel | string | |
| highlightPackageId | string | packageId of the visually highlighted package. |
| badgeText | string | Badge on the highlighted package, e.g. "SAVE 17%". |
| accent | string | Accent hex like "#6478ff"; fall back to the app theme when absent. |
| heroImageUrl | string | Hero image URL rendered above the headline in place of the icon tile. |
| review | PaywallReview | |
| offer | PaywallOffer | |
| footer | PaywallFooter | |
PaywallFeature
| Prop | Type |
| ----------------- | ------------------- |
| icon | string |
| title | string |
| description | string |
PaywallReview
Social proof, dashboard-configured. Any layout renders the pieces that
are set: stars/quote card above the packages, count under the CTA.
| Prop | Type | Description |
| ------------ | ------------------- | ------------------------------------------------- |
| rating | number | 0–5; rendered as a star row. |
| quote | string | |
| author | string | |
| count | string | e.g. "Join 2M+ users" — small line under the CTA. |
PaywallOffer
Win-back/offer presentation: anchor price struck through on the highlighted package, urgency line above the CTA. Any layout.
| Prop | Type |
| ------------------------ | ------------------- |
| strikethroughPrice | string |
| urgencyText | string |
PaywallFooter
Footer links, dashboard-configured. Absent (legacy config) = show all three. When a URL is set open it directly; otherwise run your own terms/privacy handler.
| Prop | Type |
| ----------------- | -------------------- |
| showRestore | boolean |
| showTerms | boolean |
| showPrivacy | boolean |
| termsUrl | string |
| privacyUrl | string |
PlacementExperiment
A/B experiment assignment (REV-219). The served offering/paywall are
already the assigned variant's — this is attribution metadata, not
something the app needs to branch on.
| Prop | Type |
| --------------- | ------------------- |
| key | string |
| variantId | string |
PluginListenerHandle
| Prop | Type |
| ------------ | ----------------------------------------- |
| remove | () => Promise<void> |
RevnixDiagnostic
A background failure the SDK swallowed (queue drains, telemetry beacons).
| Prop | Type |
| ------------- | ------------------- |
| op | string |
| message | string |
Type Aliases
RevnixStoreKind
Wire-identical to the other Revnix SDKs (openapi.yaml is the source).
'apple' | 'google'
Record
Construct a type with a set of properties K of type T
{ [P in K]: T; }
