Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions content/docs/framework/actions.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: "Actions"
description: "Close the paywall, restore purchases, open links, request OS permissions, and call back into your app — everything a paywall asks its host to do."
---

A paywall runs inside your app, and some things only the host can do: dismiss the paywall, open a link, prompt for a permission, run your app's code. All of it goes through `useActions()`:

```tsx
import { useActions } from "superwall/hooks";

const { close, restore, openUrl, requestPermission, requestCallback } = useActions();
```

## The actions

| Action | Use it for |
| --- | --- |
| `close()` | Closing the paywall — the X button. Closing is not navigation. |
| `restore()` | Restore purchases. Fire-and-forget: success arrives as a `transaction_complete` event or a dismissed paywall — there is no return value to await. See [Purchases](/framework/purchases). |
| `openUrl(url)` | Terms, privacy, any link. **Always this, never `<a href>`.** |
| `openExternalUrl(url)` | Open in the system browser instead of in-app. |
| `openDeepLink(link)` | Deep link into the app. |
| `customPlacement(name, params?)` | Fire a Superwall placement — which can present another paywall. |
| `requestPermission(type)` | OS permission prompt. Resolves `"granted" \| "denied" \| "unsupported"`. |
| `requestCallback(name, options?)` | Run **your app's** code and await its answer. Resolves `{ status: "success" \| "failure", data? }`. |
| `requestStoreReview("in-app" \| "external")` | Store review prompt. |

<Warning>
Links go through `openUrl`, never an `<a href>`. Inside a webview, an anchor either does nothing or navigates the paywall away from itself — `openUrl` hands the URL to the host so it opens the way the platform expects.
</Warning>

Closing works the same way: the paywall lives on a navigation stack of its own pages, but *leaving* the paywall isn't a navigation — it's `close()`. See [Pages & navigation](/framework/navigation).

## Permissions

```tsx
const status = await requestPermission("notification");
// "granted" | "denied" | "unsupported"
```

Permission types: `notification`, `camera`, `microphone`, `location`, `background_location`, `contacts`, `read_images`, `read_video` (Android only), `tracking`.

## Callbacks — ask your app a question

A callback runs code *in your app* and hands the answer back to the paywall — anything the paywall cannot know on its own: does this account exist, is this referral code valid, what did the user pick during signup.

```tsx
const result = await requestCallback<{ exists: boolean }>("checkAccount");

if (result.status === "success" && result.data?.exists) {
router.push("welcome-back");
}
```

Type the answer with a claim, as above — the generic is your statement of what the app returns.

### Permission vs callback

A **permission** asks the OS; a **callback** asks your app. Both resolve from code the paywall does not control, which shapes how you use them:

- **Show something while they run.** The OS prompt or your app's code takes as long as it takes.
- **Treat a denial as an ordinary outcome**, not an error. A user who declines notifications is still a user — design the path that continues without.

## In development

In `superwall dev`, actions don't reach a real host — they're logged in the studio's event log, and permission, callback, and purchase requests prompt **you** to pick the outcome. That makes both branches of every flow testable before a device ever sees it. See [The studio](/framework/studio).

<Tip>
The permissions example shows `requestPermission` and `requestCallback` side by side, with a denial treated as an outcome rather than an error. See [Examples](/framework/examples).
</Tip>
128 changes: 128 additions & 0 deletions content/docs/framework/assets.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
title: "Assets"
description: "Add images, video, audio, fonts, and animations to a paywall by importing files. The build handles optimization, hosting, and caching."
---

Add media to a paywall by importing files from an `assets/` directory. The build handles optimization, hosting, and caching; there is nothing to configure and no upload step.

## Where assets live

Assets follow the same two-level pattern as components and messages: shared at the project root, local inside a paywall.

```ts
superwall/
├── assets/ shared across every paywall
└── paywalls/pro/
└── assets/ this paywall's own
```

<Warning>
Every asset belongs in an `assets/` directory — `superwall/assets/` for shared files, `superwall/paywalls/<id>/assets/` for one paywall's own. If a large asset lives anywhere else, the build fails and names the file.
</Warning>

## Use an image

Import the file and use it like any URL:

```tsx
import hero from "@/assets/hero.jpg"; // shared: superwall/assets/
import badge from "../assets/badge.png"; // this paywall's own

<img src={hero} alt="" />
```

CSS `url()` works the same way. Imports typecheck because of the generated `superwall.d.ts` — one more reason to [commit it](/framework/project-structure).

Supported out of the box:

| Kind | Formats |
| --- | --- |
| Images | `png` `jpg` `jpeg` `webp` `avif` `gif` `svg` `ico` `apng` |
| Video | `mp4` `webm` `mov` `m4v` |
| Audio | `mp3` `m4a` `aac` `wav` `ogg` |
| Fonts | `woff2` `woff` `ttf` `otf` |
| Animation & 3D | `lottie` `riv` `glb` |

`?url`, `?raw`, and `?inline` import suffixes work too, as do CSS modules.

## How hosting works

You never choose where an asset is served from — the build decides, and nothing about your code changes either way:

- **Video, audio, and fonts** are always served from Superwall's CDN, whatever their size. Video streams properly instead of being carried by the paywall, and one upload is reused across every version of every paywall.
- **Images** embed in the paywall when small and move to the CDN when large.

```tsx
import promo from "../assets/promo.mp4";

<video src={promo} autoPlay muted loop playsInline />
```

## Custom fonts

A relative-path `@font-face` is the whole setup:

```css
@font-face {
font-family: "Manrope Custom";
font-display: swap;
font-weight: 200 800;
src: url("../assets/manrope-latin.woff2") format("woff2-variations");
}

:root { --sans: "Manrope Custom", ui-sans-serif, system-ui, sans-serif; }
```

A few habits keep fonts cheap:

- **Subset before you ship.** A full variable font carries alphabets the paywall will never render — latin-only Manrope is around 24 kB against roughly 90 kB for the whole family.
- **Ship woff2.** Anything older is bytes for nothing you support.
- **Google Fonts go in a CSS `@import`**, at the top of the stylesheet — never React-rendered `<link>` tags. The stylesheet ships in the page itself, so the browser finds the `@import` immediately; a rendered `<link>` waits for JavaScript to run first, and the text flashes.
- One family plus one mono is a good budget.

The custom-fonts [example](/framework/examples) shows a local file and a Google Fonts import side by side.

## Lottie

Two ways to ship a Lottie animation, with different trade-offs:

```tsx
// 1. Animation JSON — embedded in the paywall. Offline-proof, zero requests.
// Best for small animations.
import spinner from "@/assets/spinner.json";

// 2. A .lottie file — served from the CDN and pre-cached on device by the
// SDK before the paywall opens. Best for bigger animations.
import intro from "@/assets/intro.lottie";
```

## Rive

`.riv` files load like any asset, plus one required setup step:

```tsx
import { useRive, RuntimeLoader } from "@rive-app/canvas";
import riveWasm from "@rive-app/canvas/rive.wasm?url";
import smiley from "../assets/smiley.riv";

RuntimeLoader.setWasmUrl(riveWasm);
RuntimeLoader.setWasmFallbackUrl(null);

const { RiveComponent } = useRive({ src: smiley, stateMachines: "State Machine 1", autoplay: true });
```

<Warning>
Rive fetches its WebAssembly engine from a CDN by default, and published paywalls cannot reach external CDNs. Bundle the wasm with the `?url` import as above, and null the fallback so a failure stays loud rather than silently retrying a CDN that will never answer.
</Warning>

Also pass the file's **real state-machine name** — naming one that doesn't exist leaves a blank canvas and no error. The with-rive [example](/framework/examples) is the reference.

## Multi-page flows

Nothing to do — while the user is on the current page, the next pages' images, video, and fonts warm automatically. `.lottie` and `.riv` files go further: the SDK pre-caches them on device before the paywall even opens.

## Keep it light

- Big imagery is fine — it's served from the CDN and cached, not carried by the paywall itself.
- Compress and size media for a phone screen; every open pays for what the paywall loads.
- Pushing files over 50 MB warns — every future clone of the source pays for them — but nothing is capped.
112 changes: 112 additions & 0 deletions content/docs/framework/cli.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
title: "CLI Reference"
description: "Every superwall command — create, dev, push, promote, publish — with flags, auth, and the checks that run before anything ships."
---

The CLI has git semantics on purpose: **push saves, promote ships.** Every push mints a sealed version; nothing users see changes until promote points production at it. This page is the command reference — [Push, promote & publish](/framework/push-and-promote) explains the model.

```sh
superwall create # scaffold superwall/ inside your app
superwall dev # studio on http://localhost:6100
superwall push # build + version. Production untouched.
superwall promote # point production at the latest push
superwall publish # push + promote in one step
superwall publish -m "Q3 test" # record why
```

The scaffolded package scripts mirror these (`dev`, `push`, `promote`, `ship`).

## Auth

Run `superwall login` once interactively. In CI, set `SUPERWALL_API_KEY` (an `sk_…` key) — the project's `.env` is the usual home for it. `dev` needs no login.

## `superwall create`

Scaffolds a complete project: the directory skeleton, a starter paywall, dependencies installed, your Superwall app connected, and a git init if needed.

| Flag | What it does |
| --- | --- |
| `--example <name>` | Start from an [example](/framework/examples) instead of the default starter. |

## `superwall dev`

Hosts [the studio](/framework/studio) for the project — or several at once with a glob (`superwall dev examples/*`). Regenerates `superwall.d.ts` first, so route and product types are always current.

| Flag | What it does |
| --- | --- |
| `--port`, `-p` | Port, default `6100` — moves to the next free port if taken. |
| `--host` | Bind address, for previewing from another device. |

Project problems (stray files in `app/`, duplicate routes) print as warnings here — the same ones that block a push, so fix them as they appear.

## `superwall push`

Builds every paywall, versions the changed ones, and leaves production alone. Re-running with nothing changed is a no-op.

| Flag | What it does |
| --- | --- |
| `--id <id>` | Limit to one paywall; repeatable. |
| `--rename <old>=<new>` | Declare a directory rename so CI can resolve it. |
| `-m <note>` | Record why, shown with the version in the dashboard. |

A push refuses — before anything is written — when:

- a selected paywall has diagnostics (publishing is immutable; fix first),
- a product in `config.ts` doesn't exist on the dashboard (every variable on it would be undefined on device),
- a directory rename is unresolved (below).

The first push binds each paywall — creating it on Superwall if needed — and records the binding in `superwall.lock`; commit that file. After that, push always updates the same paywall; no IDs ever appear in your code.

Every push also snapshots your `superwall/` source, so the dashboard can show and diff the code each version was built from. `.env`, `node_modules/`, and gitignored files never leave the machine.

### Renames

Renaming a paywall directory is detected, never guessed. Interactively, push asks whether the unfamiliar directory is a rename (keeping the live paywall attached) or a new paywall. In CI, declare it — anything unresolved stops the push rather than creating a duplicate:

```sh
superwall push --rename plus-upgrade=pro-upgrade
```

Deleting a paywall directory never blocks a push — the dashboard paywall keeps serving, and restoring the directory re-binds it.

## `superwall promote`

Points production at a pushed version. Promote never rebuilds — it only moves the live pointer.

| Flag | What it does |
| --- | --- |
| `--id <id>` | Limit to one paywall. |
| `--version`, `-v <n>` | Pick a specific version (with a single `--id`) — which is also the **rollback**. |

```sh
superwall promote --id plus-upgrade --version 5
# → Rolled back version 7 → 5
```

## `superwall publish`

Push + promote in one step. Takes `-m <note>`. Also warns about other paywalls that are pushed-but-not-live, so nothing ships half-forgotten.

## Before pushing: create the products

A push refuses if a `config.ts` names a product the dashboard doesn't have. The fix is a command away — the same CLI writes products directly:

```sh
superwall entitlements list --json # grab the NUMERIC entitlement id
superwall products create pro_3999_year \
--project <id> --app <id> \
--name "Annual" --price 39.99 --period year \
--trial-days 7 --entitlement <numeric-id> --json
```

- `--entitlement` takes the **numeric id** (`55688`), not the identifier (`pro`) — the identifier fails with a decode error.
- Pass `--project` explicitly when your account has several, or the command errors with "Multiple projects found".
- `--price` is major units (`39.99`); `--period` is `day|week|month|year`; `--trial-days` sets the intro offer.
- `--dry-run` confirms the target before writing anything.

## Two gates worth checking early

- **Headless paywalls must be enabled on the application** — otherwise every push fails with "Headless paywalls are not enabled for this application". It's a server-side feature flag; check with `superwall apps list --json` and look for `headless_paywalls` in `features_enabled`. Contact us to have it turned on.
- **One broken surface blocks the whole push.** A leftover scaffold aimed at a nonexistent product stops everything — push what you built with repeated `--id` flags instead of touching unrelated directories.

For the full error-message-to-fix table, see [Troubleshooting](/framework/troubleshooting).
Loading
Loading