Skip to content

superwall/Superwall-Web

Repository files navigation

Superwall Web SDK

TypeScript SDK for Superwall in the browser, on the server, and inside React.


Packages

Package What Runs in
@superwall/paywalls-js Headless core: No DOM refs at module load. Node, Bun, edge, workers, SSR, browser
@superwall/paywalls-js/browser createBrowserPresenter createBrowserStorage Browser only
@superwall/paywalls-react <SuperwallProvider>, useSuperwall, useSignal, useUser, usePlacement, useSuperwallEvent, useDelegate. React 19. Browser (SSR-safe imports)
@superwall/example-browser Runnable Bun + vanilla TS demo. Browser

ESM-only.


Install

# In your app:
bun add @superwall/paywalls-js                 # headless + browser subpath
bun add @superwall/paywalls-react react        # if you're using React

Workspace dev install:

bun install        # from the workspace root

Quick start — vanilla TS

import { createSuperwall } from "@superwall/paywalls-js";

const sw = createSuperwall({ apiKey: "pk_web_…" });

// Optional — block until config + identity hydration land. Most methods
// internally await sw.ready so you don't need to.
await sw.ready;

await sw.user.identify("user_42");

// Listen on lifecycle events (typed CustomEvent)
const ac = new AbortController();
sw.events.addEventListener("subscriptionStatus_didChange", () => {
  console.log("status changed →", sw.subscriptionStatus.value);
}, { signal: ac.signal });

// Show a paywall (or skip if entitled)
const result = await sw.register({
  placement: "checkout",
  feature: () => unlock(),                 // runs on entitled / purchased / non-gated skip
});

if (result.type === "presented" && result.result.type === "purchased") {
  console.log("Bought:", result.result.productId);
}

Tree-shakeable singleton (Expo-style)

import { createSuperwall, user, register, events } from "@superwall/paywalls-js";

createSuperwall({ apiKey: "pk_web_…" });   // first call registers the default

await user.identify("user_42");
const r = await register({ placement: "checkout" });
events.addEventListener("paywall_close", (e) => console.log(e.detail));

If you only import { user }, the rest is dead code → ESM bundlers drop it.


Quick start — React 19

import { SuperwallProvider, useUser, usePlacement } from "@superwall/paywalls-react";

function App() {
  return (
    <SuperwallProvider apiKey="pk_web_…">
      <Home />
    </SuperwallProvider>
  );
}

function Home() {
  const { id, isLoggedIn, identify } = useUser();
  const { register, state } = usePlacement({
    onPresent: (info) => console.log("opened:", info.identifier),
    onDismiss: (_info, result) => console.log("dismissed:", result),
  });

  return (
    <>
      <button onClick={() => identify("user_42")}>Sign in</button>
      <button onClick={() => register({ placement: "checkout" })}>Upgrade</button>
      <p>userId: {id || "anonymous"} • paywall: {state.type}</p>
    </>
  );
}

Gating render on configuration (optional)

By default every method internally awaits sw.ready, so you don't need a Suspense boundary. Use it only if you want a fallback while initial config + enrichment land:

import { use, Suspense } from "react";
import { useSuperwall } from "@superwall/paywalls-react";

function ConfigGate({ children }: { children: React.ReactNode }) {
  use(useSuperwall().ready);                 // suspends until ready
  return <>{children}</>;
}

<SuperwallProvider apiKey="pk_web_…">
  <Suspense fallback={<Loading />}>
    <ConfigGate><Home /></ConfigGate>
  </Suspense>
</SuperwallProvider>

SSR caveat. use(sw.ready) will suspend during server render — render eagerly server-side. The Provider hydrates with seeded identity from cookies; client-side re-render is automatic if the resolved identity differs.


Common recipes

Checkout

Default — built-in Stripe checkout. A standard Superwall web paywall runs Stripe checkout inside the paywall iframe; the SDK + backend complete it and flip sw.subscriptionStatus to ACTIVE automatically (via the default purchase controller). You don't run checkout or set status yourself — just read sw.subscriptionStatus / gate on entitlements.

Custom user / placement types

Module augmentation closes the shape — no generics on call sites:

// types.d.ts
declare module "@superwall/paywalls-js" {
  interface UserAttributes {
    email?: string;
    plan?: "free" | "pro" | "enterprise";
  }
  interface PlacementParams {
    screen?: string;
    referrer?: string;
  }
}

sw.user.setAttributes({ email: "a@b.co", plan: "pro" });    // ✅ typed
sw.user.setAttributes({ email: "a@b.co", plan: "premium" }); // ❌ TS error

Custom presenter (BE / RN Web / test fixture)

Implement the PaywallPresenter interface:

import type { PaywallPresenter } from "@superwall/paywalls-js";

const myPresenter: PaywallPresenter = {
  async present(info, ctx) {
    // ... show your UI; return when the user dismisses
    return { type: "purchased", productId: "pro_yearly" };
  },
  dismiss() { /* tear down */ },
};

// Pass per-call via register(), not at createSuperwall time:
await sw.register({ placement: "checkout", presenter: myPresenter });

Global delegate (analytics / logging firehose)

sw.setDelegate({
  onEvent(name, detail) { analytics.track(name, detail); },
  onSubscriptionStatusChange(from, to) { /* … */ },
  onPaywallDidPresent(info) { /* … */ },
});

In React, prefer the hook (auto-cleanup):

useDelegate({ onEvent: (name) => analytics.track(name) });

Backend / SSR

import { createSuperwall } from "@superwall/paywalls-js";

// No /browser import; no presenter; pre-seed identity from cookies.
const sw = createSuperwall({
  apiKey,
  identity: {
    aliasId: req.cookies["_sw_alias_id"],
    appUserId: req.cookies["_sw_user_id"],
  },
});

// `register` requires a presenter — use getPresentationResult instead for
// pure server-side decisions:
const decision = await sw.placements.getPresentationResult("checkout");
// → { type: "paywallNotAvailable" } in v0 alpha (config processing pending — see MISSING.md)

Development

bun install                                # workspace install (Bun workspaces)
bun run typecheck                          # turbo run typecheck (all packages)
bun run test                               # turbo run test (all packages)
bun run build                              # turbo run build (no-op in v0; ESM source ships as-is)

# Run the example (vanilla TS)
bun --filter @superwall/example-browser dev
# → http://localhost:3000

Layout

Superwall-Web/
  package.json                            # workspace root, Bun + Turbo
  turbo.json
  tsconfig.base.json                      # strict TS + Effect language service plugin
  packages/
    paywalls-js/                          # headless core + /browser subpath
      src/                                # public modules
      src/internal/                       # Effect-only (not exported from barrel)
      src/browser/                        # browser presenter + storage
    paywalls-react/                       # React 19 bindings
  example/
    example-browser/                      # runnable Bun + TS demo

Tests

paywalls-js uses vitest (bun run test calls vitest run). Browser-package tests use @happy-dom/global-registrator registered via bunfig.toml preload. React tests use @testing-library/react.

326 tests across the workspace.

Effect language service

The effect-language-service TS plugin is wired into tsconfig.base.json. In VS Code: F1 → "TypeScript: Select TypeScript Version" → "Use Workspace Version" to get Effect-specific diagnostics.


About

Superwall SDK for Web applications - alpha

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors