This document defines the contract required to rebuild the underlying Superwall-backed SDK exposed by this repository. It focuses on the native iOS and Android behavior, the shared Expo/native-module bridge, and the backend APIs the SDK depends on. It does not describe product strategy or app-level integration guidance.
This is a target specification, not a pure snapshot of the current wrapper. Where the current implementation is inconsistent, incomplete, or explicitly flagged by TODOs, this document defines the behavior the rebuilt SDK must implement and calls out current-state differences separately.
This specification is derived from the current repository and the backend
repository at Documents/superwall/paywall-next. The most important source
files are:
ios/SuperwallExpoModule.swiftios/Bridges/PurchaseControllerBridge.swiftios/Bridges/SuperwallDelegateBridge.swiftandroid/src/main/java/expo/modules/superwallexpo/SuperwallExpoModule.ktandroid/src/main/java/expo/modules/superwallexpo/bridges/PurchaseControllerBridge.ktandroid/src/main/java/expo/modules/superwallexpo/bridges/SuperwallDelegateBridge.ktsrc/SuperwallExpoModule.tssrc/SuperwallExpoModule.types.tssrc/useSuperwall.tssrc/useSuperwallEvents.tssrc/usePlacement.tssrc/compat/index.tsui_test_app/app/*paywall-next/packages/subscriptions/src/api/public-schema/groups/*paywall-next/packages/config/src/config-api-schema/groups/*paywall-next/packages/config/src/config-api-impl/groups/*paywall-next/apps/web/fapi/*
This specification covers the underlying SDK contract. That includes methods, events, result types, state transitions, purchase-controller behavior, and the backend calls needed to configure, resolve entitlements, and support checkout and redemption flows.
This specification does not define dashboard authoring, paywall rendering internals, or the full web-paywall runtime. It also does not treat the hooks layer as primary. The hooks layer is documented later as a thin wrapper over the core contract.
The rebuilt SDK must preserve the current split of responsibilities.
- The native layer owns direct interaction with
SuperwallKiton iOS andcom.superwall.sdkon Android. - The shared JS layer owns argument normalization, event subscription, handler scoping, and compatibility wrappers.
- The purchase controller bridge owns the manual purchase round-trip from native to JS and back to native.
- The delegate bridge owns native lifecycle and analytics events emitted into JS.
- The backend contract provides config data, entitlements, products, checkout, and redemption data required by native and web-backed purchase flows.
The rebuilt SDK must expose one coherent cross-platform contract with clearly marked platform-specific behavior where symmetry is not possible.
The SDK must implement these lifecycle states:
unconfigured: no native configuration has been completed.configuring:configure()has started and has not yet resolved.configured: configuration completed successfully and delegate bridging is active.configFailed: configuration failed or the SDK emitted a config failure event.identityKnown: an app user has been identified.identityReset: the SDK has cleared user-linked identity state.
The rebuilt SDK must enforce these lifecycle rules:
configure()must be the first meaningful method call.configure()must install the delegate bridge before resolving success.identify(),reset(),registerPlacement(),getAssignments(),confirmAllAssignments(),getEntitlements(),getSubscriptionStatus(),setSubscriptionStatus(),getUserAttributes(),setUserAttributes(),preloadPaywalls(),preloadAllPaywalls(),setIntegrationAttributes(), andgetIntegrationAttributes()must be treated as post-configuration methods.dismiss(),setLogLevel(), andsetInterfaceStyle()may be treated as safe no-op or permissive calls if configuration is already in progress, but they must not crash.dismiss()must be idempotent when no paywall is active.
This section defines the normative method contract.
This method must initialize the native SDK using the platform-specific public API key and the supplied options.
Inputs:
apiKey: stringoptions?: SuperwallOptionsusingPurchaseController?: booleansdkVersion?: string
Required behavior:
- Parse and normalize options before passing them to native.
- If
usingPurchaseControlleristrue, attach the purchase controller bridge. If it isfalse, native must manage purchases itself. - Install the delegate bridge before resolving.
- Set the platform wrapper to
Expoand pass the wrapper version string if provided. - On Android, wire
onBackPressedrouting through the paywall options.
Failure behavior:
- Reject on native configuration failure.
- Emit or preserve downstream config-failure signaling through
handleSuperwallEvent.
This method must return the native configuration status as a stable string representation. The current code exposes at least configured, pending, and failed states through wrapper types.
This method must identify the current app user.
Inputs:
userId: stringoptions?: { restorePaywallAssignments?: boolean }
Required behavior:
- Forward
restorePaywallAssignmentsif provided. - Preserve configuration state.
- Update downstream user and subscription views after completion.
This method must clear the current identity and user-linked SDK state. It must reset the current user, identity aliasing state, and local assignment context.
This method must register a placement and allow native to decide whether a paywall presents, a paywall is skipped, or the feature path continues.
Inputs:
placement: stringparams?: Record<string, any>handlerId?: string | null
Required behavior:
- Forward
paramsas a JSON-compatible object. - If
handlerIdexists, create a presentation handler and scope the paywall-related events to that handler. - Emit
onPaywallPresent,onPaywallDismiss,onPaywallError,onPaywallSkip, andonCustomCallbackwith the originatinghandlerId.
Target behavior:
- Feature execution must be decided by native paywall outcome, not by blindly resolving the wrapper promise. The current compat layer has a TODO because it executes the feature block too early.
This method must evaluate the placement without presenting a paywall. It must return a presentation result object that tells the caller what native would do for that placement and parameter set.
This method must dismiss an active paywall if one is visible and must resolve without error if none is visible.
This method must confirm all pending assignments and return the confirmed assignment list.
This method must return the user’s known experiment assignments.
This method must return canonical entitlement state using one schema across platforms.
Canonical shape:
type EntitlementsInfo = {
active: Entitlement[]
inactive: Entitlement[]
all?: Entitlement[]
}Target rule:
activeandinactiveare required.allmay be returned for backend parity, but consumers must not rely on it unless the public contract explicitly exposes it.
This method must return one of:
{ status: "UNKNOWN" }{ status: "INACTIVE" }{ status: "ACTIVE", entitlements: Entitlement[] }
This method must let the app update subscription state when manual purchase management is in use.
Required behavior:
- Accept the same status union returned by
getSubscriptionStatus(). - Unknown values must coerce safely to
UNKNOWN. ACTIVEwithout a valid entitlement list must not crash.
This method must override the native interface style if a supported value is
provided. undefined or null must revert to system default behavior.
These methods must expose the SDK’s custom user attributes.
Required behavior:
- Values must be JSON-compatible.
- Keys reserved by Superwall may be dropped by native or backend.
undefinedvalues must be filtered before passing to native.
This method must return the device attribute map resolved by native.
This method must attempt native deep-link handling and return boolean.
Required behavior:
- Invalid URL input must return
false. - Native deep-link errors must not crash the caller.
These methods must start paywall preloading. They do not need to await the entire preload lifecycle, but they must not silently drop the request.
This method must set native log level for supported values:
debuginfowarnerrornone
Unknown values must be ignored, not rejected.
These methods must round-trip supported integration IDs. Unknown keys must be ignored rather than causing the whole call to fail.
Supported keys currently include:
adjustIdamplitudeDeviceIdamplitudeUserIdappsflyerIdbrazeAliasNamebrazeAliasLabelonesignalIdfbAnonIdfirebaseAppInstanceIditerableUserIditerableCampaignIditerableTemplateIdmixpanelDistinctIdmparticleIdclevertapIdairshipChannelIdkochavaDeviceIdtenjinIdposthogUserIdcustomerioIdappstackId
These methods complete native work started by onPurchase,
onPurchaseRestore, onCustomCallback, and onBackPressed.
didPurchase(result)didRestore(result)didHandleCustomCallback(callbackId, status, data?)didHandleBackPressed(shouldConsume)on Android only
Required behavior:
- Unknown
callbackIdmust resolve as a safe no-op. - Purchase and restore completion must resolve the currently pending native continuation or future.
- Android back-press completion must resolve the waiting promise.
This section defines all events emitted by the native module.
These events must include handlerId when they come from a scoped placement
handler.
| Event | Payload | Notes |
|---|---|---|
onPaywallPresent |
{ paywallInfoJson, handlerId } |
Fired when a paywall presents |
onPaywallDismiss |
{ paywallInfoJson, result, handlerId } |
Fired on dismissal |
onPaywallError |
{ errorString, handlerId } |
Fired on handler-level error |
onPaywallSkip |
{ skippedReason, handlerId } |
Fired when placement does not present |
onCustomCallback |
{ callbackId, name, variables?, handlerId } |
Fired for paywall callback round-trip |
Rules:
- Scoped listeners must ignore events whose
handlerIddoes not match. - Unscoped listeners must not complete custom callbacks intended for scoped handlers.
| Event | Payload | Notes |
|---|---|---|
onPurchase |
iOS: { productId, platform: "ios" } |
Manual purchase flow |
onPurchase |
Android: { productId, platform: "android", basePlanId, offerId? } |
Manual purchase flow |
onPurchaseRestore |
no payload | Manual restore flow |
Rules:
- iOS purchase payload does not include base plan or offer IDs.
- Android purchase payload must include
basePlanIdand may includeofferId.
onBackPressed must only exist on Android and only when back-button rerouting
is enabled in paywall options.
Payload:
{ paywallInfo: PaywallInfo }Rules:
- Native must wait for
didHandleBackPressed. - Default JS behavior is
falseif no handler responds.
These events must remain globally observable:
subscriptionStatusDidChangehandleSuperwallEventhandleCustomPaywallActionwillDismissPaywallwillPresentPaywalldidDismissPaywalldidPresentPaywallpaywallWillOpenURLpaywallWillOpenDeepLinkhandleLogwillRedeemLinkdidRedeemLink
Payload rules:
subscriptionStatusDidChangemust provide{ from, to }.handleSuperwallEventmust provide{ eventInfo }.- paywall lifecycle events must provide
{ info: PaywallInfo }. handleLogmust providelevel,scope,message,info, anderror.didRedeemLinkmust provide a singleRedemptionResult.
This section captures the minimum model contract the rebuilt SDK must preserve.
type VariantType = "TREATMENT" | "HOLDOUT"
type Variant = {
id: string
type: VariantType
paywallId: string
}
type Experiment = {
id: string
groupId: string
variant: Variant
}
type Entitlement = {
id: string
type: "SERVICE_LEVEL"
}type PaywallSkippedReason =
| { type: "Holdout"; experiment: Experiment }
| { type: "NoAudienceMatch" }
| { type: "PlacementNotFound" }
type PaywallResult =
| { type: "purchased"; productId: string }
| { type: "declined" }
| { type: "restored" }PaywallInfo must include at least:
- identity fields:
identifier,name,url - experiment context:
experiment - product context:
products,productIds - presentation source:
presentedByEventWithName,presentedByEventWithId,presentedByEventAt,presentedBy,presentationSourceType - timing fields for response, web view, and product loading
isFreeTrialAvailablefeatureGatingBehaviorcloseReasoncomputedPropertyRequestssurveyslocalNotificationsstate
type CustomCallback = {
name: string
variables?: Record<string, any>
}
type CustomCallbackResult = {
status: "success" | "failure"
data?: Record<string, any>
}The rebuilt SDK must preserve the current union:
SUCCESSERRORCODE_EXPIREDINVALID_CODEEXPIRED_SUBSCRIPTION
Each successful or expired-subscription result must include redemption info with ownership, purchaser info, optional paywall info, and entitlements.
type TriggerResult =
| { result: "placementNotFound" }
| { result: "noAudienceMatch" }
| { result: "paywall"; experiment: Experiment }
| { result: "holdout"; experiment: Experiment }
| { result: "error"; error: string }The rebuilt SDK must preserve the reason model currently encoded by
PaywallPresentationRequestStatus and
PaywallPresentationRequestStatusReason.
Known status values:
presentationnoPresentationtimeout
Known no-presentation reasons:
debuggerPresentedpaywallAlreadyPresentedholdoutnoRuleMatcheventNotFoundnoPaywallViewControllernoViewControlleruserIsSubscribederrorpaywallIsGated
handleSuperwallEvent delivers a SuperwallEventInfo payload that wraps a
large discriminated union. The rebuilt SDK must preserve the event names below
even if some payload details are thinly typed in JS today.
Core lifecycle and identity:
firstSeenappOpenappLaunchidentityAliasappInstallsessionStartresetconfigRefreshconfigFailconfigAttributesconfirmAllAssignmentsdeviceAttributessubscriptionStatusDidChangeappClosedeepLinkuserAttributesintegrationAttributes
Restoration, redemption, and enrichment:
restoreStartrestoreCompleterestoreFailredemptionStartredemptionCompleteredemptionFailenrichmentStartenrichmentCompleteenrichmentFailwillRedeemLinkdidRedeemLink
Trigger and paywall events:
triggerFirepaywallOpenpaywallClosepaywallDeclinepaywallPresentationRequestcustomPlacementpaywallResponseLoadStartpaywallResponseLoadNotFoundpaywallResponseLoadFailpaywallResponseLoadCompletepaywallWebviewLoadStartpaywallWebviewLoadFailpaywallWebviewLoadCompletepaywallWebviewLoadTimeoutpaywallWebviewLoadFallbackpaywallWebviewProcessTerminatedpaywallProductsLoadStartpaywallProductsLoadFailpaywallProductsLoadCompletepaywallProductsLoadRetrypaywallProductsLoadMissingProductspaywallPreloadStartpaywallPreloadComplete
Transactions and purchases:
transactionStarttransactionFailtransactionAbandontransactionCompletesubscriptionStartfreeTrialStarttransactionRestoretransactionTimeoutnonRecurringProductPurchasestripeCheckoutStartstripeCheckoutSubmitstripeCheckoutCompletestripeCheckoutFail
Other current events:
touchesBegansurveyClosesurveyResponseadServicesTokenRequestStartadServicesTokenRequestFailadServicesTokenRequestCompleteshimmerViewStartshimmerViewCompletenetworkDecodingFailcustomerInfoDidChangereviewRequestedpermissionRequestedpermissionGrantedpermissionDeniedtestModeModalOpentestModeModalCloseunknown
This section defines the backend calls the rebuilt SDK must understand. Paths are marked as verified or inferred.
The verified public subscriptions API uses bearer auth with the public API key.
Header:
Authorization: Bearer <public_api_key>This is verified by
paywall-next/packages/subscriptions/src/api/public-schema/authn/PublicApiKeyAuthn.ts.
Verified prefix:
/subscriptions-api/public
All verified endpoint paths below sit under that prefix. For example, the full checkout session path is:
/subscriptions-api/public/v1/checkout/session
This API is verified by
paywall-next/packages/subscriptions/src/api/public-schema/groups/checkout-public.ts.
This endpoint initiates a checkout session and returns a checkout directive and a sealed checkout-context identifier.
Request body:
{
"currencyCode": "USD",
"productIdentifier": "com.example.premium.monthly",
"store": "stripe",
"allowedCheckoutDirectives": ["redirect", "drawer", "embedded"],
"context": {
"paywall": {
"paywallId": "456",
"paywallIdentifier": "test-paywall",
"paywallName": "Test Paywall",
"paywallUrl": "https://example.com/paywall",
"paywallProductIds": "com.example.premium.monthly"
},
"experiment": {
"experimentId": "123",
"variantId": "456"
},
"presentment": {
"isFreeTrialAvailable": true,
"presentationSourceType": "register",
"presentedBy": "placement",
"presentedByEventId": "evt_123",
"presentedByEventName": "campaign_trigger"
},
"placementParams": {
"placementParams": {}
},
"identity": {
"appUserId": "user_123",
"aliasId": null,
"deviceId": "$SuperwallDevice:uuid",
"email": "user@example.com"
},
"device": {
"publicApiKey": "pk_test_123",
"platform": "ios",
"appVersion": "1.0.0",
"osVersion": "17.4",
"deviceModel": "iPhone 15",
"deviceLocale": "en_US",
"deviceLanguageCode": "en",
"deviceCurrencyCode": "USD",
"deviceCurrencySymbol": "$",
"timezoneOffset": "0"
},
"product": {
"identifier": "com.example.premium.monthly",
"currencyCode": "USD",
"currencySymbol": "$",
"dailyPrice": "0.33",
"weeklyPrice": "2.31",
"monthlyPrice": "9.99",
"yearlyPrice": "119.88",
"languageCode": "en",
"locale": "en_US",
"localizedPeriod": "per month",
"period": "month",
"periodAlt": "mo",
"periodDays": 30,
"periodMonths": 1,
"periodWeeks": 4,
"periodYears": 0.0833,
"periodly": "monthly",
"price": "9.99",
"rawPrice": 999,
"rawTrialPeriodPrice": 0,
"trialPeriodDailyPrice": "0.00",
"trialPeriodDays": 7,
"trialPeriodWeeks": 1,
"trialPeriodMonths": 0,
"trialPeriodEndDate": "2026-03-13T00:00:00.000Z",
"trialPeriodMonthlyPrice": "0.00",
"trialPeriodPrice": "0.00",
"trialPeriodText": "7-day free trial",
"trialPeriodWeeklyPrice": "0.00",
"trialPeriodYearlyPrice": "0.00",
"trialPeriodYears": 0
}
},
"stripeCustomerId": null
}Success response:
{
"directive": {
"directive": "redirect",
"checkoutUrl": "https://checkout.example.com/...",
"checkoutSessionId": "cs_123"
},
"checkoutContextId": "v1:keyId:encryptedPayload"
}Directive variants:
redirectdrawerembedded
The drawer variant may additionally include:
publishableKeycheckoutSessionIdsubscriptionDetailspaymentMethodTypesintentType- optional application details like
iconUrl
The embedded variant includes:
publishableKeycheckoutSessionId
Errors:
- invalid product format
- missing store application id
- missing web paywall domain
- checkout-context encoding, storage, or sealing failures
- unexpected checkout directive
- internal server error
This endpoint polls the session state.
Request body:
{
"checkoutId": "v1:keyId:encryptedPayload"
}Success response:
{
"status": "pending",
"paymentStatus": "requires_action",
"redemptionUrl": "https://..."
}Allowed status values:
pendingcompletedabandoned
This endpoint confirms a checkout session after payment setup.
Request body:
{
"setupIntentId": "seti_123",
"checkoutContextId": "v1:keyId:encryptedPayload",
"email": "user@example.com"
}Success response:
{
"customerId": "cus_123",
"subscriptionId": "sub_123",
"redemptionUrl": "https://..."
}This endpoint marks checkout completion.
Request body:
{
"checkoutContextId": "v1:keyId:encryptedPayload"
}Success response:
{
"redemptionUrl": "https://..."
}This endpoint polls the redemption state after checkout completion.
Request body:
{
"checkoutContextId": "v1:keyId:encryptedPayload",
"deviceId": "$SuperwallDevice:uuid",
"appUserId": "user_123"
}Success response:
{
"status": "complete",
"codes": [],
"entitlements": [],
"customerInfo": {
"subscriptions": [],
"nonSubscriptions": [],
"entitlements": []
}
}Allowed poll status values:
pendingfailedcomplete
This API is verified by
paywall-next/packages/subscriptions/src/api/public-schema/groups/redemption-public.ts.
This endpoint redeems one or more codes and returns entitlement state.
Request body:
{
"deviceId": "$SuperwallDevice:uuid",
"aliasId": "$SuperwallAlias:uuid",
"appUserId": "user_123",
"appTransactionId": "txn_123",
"externalAccountId": "external_123",
"codes": [
{
"code": "PROMO-CODE",
"firstRedemption": true
}
],
"receipts": {},
"metadata": {}
}Success response:
{
"codes": [
{
"status": "SUCCESS",
"code": "PROMO-CODE",
"redemptionInfo": {
"ownership": {
"type": "APP_USER",
"appUserId": "user_123"
},
"purchaserInfo": {
"appUserId": "user_123",
"email": "user@example.com",
"storeIdentifiers": {
"store": "STRIPE",
"stripeCustomerId": "cus_123",
"stripeSubscriptionIds": ["sub_123"]
}
},
"paywallInfo": null,
"entitlements": []
}
}
],
"entitlements": [],
"customerInfo": {
"subscriptions": [],
"nonSubscriptions": [],
"entitlements": []
}
}This API is verified by
paywall-next/packages/subscriptions/src/api/public-schema/groups/entitlements-public.ts.
This endpoint returns entitlements and customer info for a user or device.
Path parameter:
appUserIdOrDeviceId
Optional URL parameter:
deviceId
Success response:
{
"entitlements": [
{
"identifier": "pro",
"type": "SERVICE_LEVEL"
}
],
"customerInfo": {
"subscriptions": [],
"nonSubscriptions": [],
"entitlements": []
}
}This API is verified by
paywall-next/packages/subscriptions/src/api/public-schema/groups/products-public.ts.
This endpoint returns products known to the backend.
Success response:
{
"object": "list",
"data": [
{
"object": "product",
"identifier": "com.example.premium.monthly",
"platform": "ios",
"price": {
"amount": 9.99,
"currency": "USD"
},
"subscription": {
"period": "month",
"period_count": 1,
"trial_period_days": 7
},
"entitlements": [
{
"identifier": "pro",
"type": "SERVICE_LEVEL"
}
],
"storefront": "USA"
}
]
}The config API uses artifact groups rather than ordinary REST resources. Two path styles are visible in the backend repo:
- verified direct read paths used by internal artifact consumers
- verified public manifest paths used by at least the retention worker
The rebuilt SDK must treat the artifact namespace as the source of config truth.
config-api
Resource:
application
Verified data shape:
{
"applicationId": 1,
"organizationId": 1,
"projectId": 1,
"stripeApplicationId": 9,
"storeApplicationIds": {
"stripe": 9,
"paddle": null,
"ios": 3,
"android": 4,
"promotional": null
},
"webPaywallDomain": "paywall.example.com",
"applicationIcon": "https://..."
}Verified direct-read path pattern from tests:
/config-api/read/v1-application-public-api-key/{publicApiKey}/application/{responseVersion}
Public manifest path:
- exact runtime usage is not shown for this group
- the manifest form is therefore inferred as:
/config-api/public/manifest/v1-application-public-api-key/{publicApiKey}/application
Resources:
static-configapplication
Verified direct-read path patterns from tests:
/config-api/read/v1-web-paywall-domain/{domain}/static-config/{responseVersion}
/config-api/read/v1-web-paywall-domain/{domain}/application/{responseVersion}
The application resource returns:
{
"applicationId": 9,
"storeApplicationIds": {
"stripe": 9,
"paddle": null,
"ios": 3,
"android": 4
}
}Resource relevant to SDK/backend parity:
entitlements
Verified direct-read path pattern from tests:
/config-api/read/v1-application-id/{applicationId}/entitlements/{responseVersion}
Verified response shape:
{
"products": [
{
"productId": "live:price_123:no-trial",
"store": "stripe",
"entitlements": [
{
"identifier": "pro_example",
"type": "SERVICE_LEVEL"
}
],
"rawPrice": "9.99",
"period": "month",
"trialPeriodDays": 7
}
]
}Resource:
config
Verified public manifest path from the retention worker:
/config-api/public/manifest/v1-retention-messaging-config/{publicApiKey}/config
Verified response shape:
{
"production": {
"realtime": {},
"defaultMessages": {}
},
"sandbox": {
"realtime": {},
"defaultMessages": {}
},
"signingKey": {
"keyId": "key-id",
"issuerId": "issuer-id",
"privateKey": "pem-private-key",
"bundleId": "com.example.app"
}
}These endpoints are visible in paywall-next/apps/web/fapi and align with the
assignment methods exposed by the current SDK wrappers. They must be documented
as compatibility endpoints unless the backend is upgraded to a newer public
contract.
Success response:
{
"assignments": [
{
"experiment_id": "123",
"variant_id": "456"
}
]
}Behavior:
- returns an empty list if alias resolution fails
- triggers a
userAlias_potentiallyMergeevent as a side effect
Request body:
{
"assignments": [
{
"experiment_id": "123",
"variant_id": "456"
}
]
}Success response:
{
"status": "ok",
"assignments": [
{
"experiment_id": "123",
"variant_id": "456"
}
]
}Rules:
- reject more than 100 assignments
- validate each experiment and variant against the current application
- support idempotent duplicate confirmation
- emit
assignment_confirmside effects
This section defines how the main flows must operate.
- JS calls
configure(). - Native configures the SDK with the public API key and options.
- Native installs the purchase controller bridge if enabled.
- Native installs the delegate bridge.
- Native sets the platform wrapper metadata.
- The call resolves.
- Downstream code may fetch user and subscription state.
- JS calls
registerPlacement(). - Native evaluates the placement.
- Native either:
- presents a paywall and emits present/dismiss events,
- skips and emits
onPaywallSkip, or - errors and emits
onPaywallError.
- If the paywall invokes a custom callback, native emits
onCustomCallbackand waits for completion.
- Native requests purchase through the purchase controller bridge.
- Native emits
onPurchase. - JS performs the store-specific purchase.
- JS calls
didPurchase(result). - Native resumes the pending purchase continuation.
- Native emits
onPurchaseRestore. - JS performs restore.
- JS calls
didRestore(result). - Native resumes the pending restore continuation.
- Native paywall intercepts back press.
- Native emits
onBackPressed. - JS decides whether to consume.
- JS calls
didHandleBackPressed(shouldConsume). - Native either consumes the event or lets dismissal continue.
- SDK gathers paywall, experiment, identity, device, placement, and product context.
- SDK calls
POST /subscriptions-api/public/v1/checkout/session. - Backend returns a checkout directive and sealed checkout-context ID.
- SDK executes redirect, drawer, or embedded checkout.
- SDK polls status or confirms completion using the sealed context ID.
- SDK may poll redemption result to retrieve entitlements and customer info.
- SDK calls
POST /subscriptions-api/public/v1/redeem. - Backend returns per-code redemption results, entitlements, and customer info.
- Native delegate and JS listeners propagate
willRedeemLinkanddidRedeemLink.
The rebuilt SDK must preserve these platform differences explicitly.
- Android supports
onBackPressed. iOS does not. - iOS purchase payload is
{ productId, platform: "ios" }. - Android purchase payload includes
basePlanIdand optionalofferId. - iOS and Android may serialize some internal native models differently, but the JS-facing contract must remain canonical.
The rebuilt SDK must normalize these current issues:
- Compat
register(..., feature)currently runs the feature block too early. - Hooks-side identity refresh relies on a
setTimeout(0)workaround. - Entitlements serialization differs by platform.
- Event payload naming is inconsistent between
paywallInfoJsonandpaywallInfo. - Assignment APIs live in an older
/api/v1/*surface while newer backend functionality is schema-driven under/subscriptions-api/public.
The rebuilt SDK must handle these cases deterministically.
- Invalid deep-link URL: return
false. - Unknown custom-callback completion ID: resolve with no side effect.
- Unsupported log level: ignore.
- Unknown integration attribute key: ignore that key only.
ACTIVEsubscription status with malformed entitlements: coerce safely or reject without crashing.- Missing paywall handler for scoped events: drop the event.
- Missing custom callback handler: return callback failure to native.
- Duplicate assignment confirmation: remain idempotent.
- Empty or missing checkout session state: surface a typed backend error.
- Missing config artifact: surface a typed not-found or config-read error.
The hooks layer is a convenience wrapper over the core contract.
useSuperwallwraps the shared store and exposes methods likeidentify,registerPlacement,dismiss,getEntitlements, andsetUserAttributes.useSuperwallEventsbinds module events to React callbacks and owns the default JS response path for custom callbacks and Android back presses.usePlacementcreates a scopedhandlerId, subscribes to scoped events, and exposes a local paywall state machine.
The hooks contract must not redefine native behavior. It must map onto the core method and event rules defined above.
An implementation satisfies this specification only if all of the following are true:
- Every method in the public SDK contract exists and follows the rules in this document.
- Every event listed in the event contract is emitted with the required payload shape.
- Manual purchase and restore round-trips work on both platforms.
- Android back-button rerouting works exactly as defined.
- Checkout, redemption, entitlements, and product APIs are implemented against the verified backend contract.
- Config retrieval uses the artifact-group model and distinguishes verified paths from inferred public-manifest paths.
- Legacy assignment endpoints are either supported for compatibility or explicitly replaced by an intentional migration plan.
- Current wrapper inconsistencies are resolved in favor of the target behavior defined in this document.
If you use this document as the implementation source, build the SDK in this order:
- Native method and event parity.
- Purchase-controller and callback round-trips.
- Backend client for config, checkout, redemption, entitlements, and products.
- Hooks wrapper on top of the stabilized core contract.