Skip to content
Open
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. There is no restore-result event and no value to await; success surfaces as a dismissed paywall. See [Purchases](/framework/purchases#restore). |
| `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>
122 changes: 122 additions & 0 deletions content/docs/framework/assets.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
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; }
```

Google Fonts go in a CSS `@import` at the top of the stylesheet, never a React-rendered `<link>` tag. 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.

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/react-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 `fetch`es its WebAssembly engine from a CDN by default, and a published paywall's CSP allows `connect-src` only to Superwall's own origins, so that fetch never lands. 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. (Stylesheets, fonts, images and media from `https:` are fine, which is why the Google Fonts `@import` above works.)
</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. Every hosted asset a version references is also stamped into the paywall's pre-cache manifest on promote, so the SDK caches it on device before the paywall opens.

## Keep it light

- Big imagery is fine. It's served from the CDN and cached, not carried by the paywall itself.
- 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 these paywalls or funnels; repeatable or comma-separated. |
| `--rename <old>=<new>` | Declare a directory rename so CI can resolve it. |
| `-m <note>` | Recorded on the source commit, it only lands when the source actually changed. |

A push refuses, before anything is written, when:

- a selected paywall has diagnostics (publishing is immutable; fix first), but a directory missing `app/index.tsx` or `config.ts` isn't a paywall yet, so it's skipped silently rather than blocking; `superwall dev` is where you see it,
- 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 code each version was built from is recoverable. `.env`, `node_modules/`, `.git/`, and anything `superwall/.gitignore` lists 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 or funnel. |
| `--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>`, `--id`, and `--rename`. 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