Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f5f9735
docs(web): add the Web SDK section
DreamingInBinary Aug 11, 2026
0665367
chore: add launch config for the docs preview server
DreamingInBinary Aug 11, 2026
479aa60
docs(web): correct claims after auditing against SDK source
DreamingInBinary Aug 12, 2026
2f5115b
docs(web): retarget at SDK 0.2.5
DreamingInBinary Aug 13, 2026
8d6a9ab
docs(web): document new surface added in 0.2.5
DreamingInBinary Aug 13, 2026
c51d787
docs(web): add a nav icon for the Web SDK section
DreamingInBinary Aug 13, 2026
1614321
docs(dashboard): document sharing a Superwall AI conversation
DreamingInBinary Aug 13, 2026
b140a42
docs(web-checkout): disambiguate Redeem Discount, link the Web SDK API
DreamingInBinary Aug 13, 2026
47c2330
docs(web-checkout): add placeholders for the two discount-code screen…
DreamingInBinary Aug 13, 2026
754a594
docs(web): remove em dashes and expositional prose
DreamingInBinary Aug 13, 2026
ac9ff09
docs(web): replace tree-shaking jargon with plain wording
DreamingInBinary Aug 13, 2026
10318a9
docs(web): explain that Web App applications are provisioned by Super…
DreamingInBinary Aug 13, 2026
4d3a749
Merge remote-tracking branch 'origin/main' into jordan/sw-5720-web-sd…
DreamingInBinary Aug 14, 2026
81fd6d5
docs: finish the Apply Discount rename in branch content
DreamingInBinary Aug 14, 2026
345aed3
docs(web): sentence-case the feature callback list
DreamingInBinary Aug 14, 2026
158f061
docs(web-checkout): rewrite the discount setup steps, add the real sc…
DreamingInBinary Aug 14, 2026
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
11 changes: 11 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "docs",
"runtimeExecutable": "bun",
"runtimeArgs": ["run", "dev"],
"port": 3000
}
]
}
4 changes: 4 additions & 0 deletions content/docs/dashboard/superwall-ai.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ The composer's model picker switches the underlying AI model: Sonnet is the defa

Conversations persist: reopen the panel and your last chat is right where you left it. The list icon in the top bar shows your chat history, and the pencil starts a fresh chat. Chats are private to you and stored per browser: teammates never see them, and a different browser or device starts with a fresh history.

You can also share conversations with colleagues. Click the share icon to generate a public link:

<Frame>![The Superwall AI panel with the share icon in the top bar, generating a public link to the conversation.](/images/ai_dash_share.jpg)</Frame>

## On the docs site and superwall.com

The same assistant is available outside the dashboard: the floating bubble in the bottom-right of the [docs](https://superwall.com/docs) and superwall.com, and the **Ask AI** button in the docs search dialog, which hands your search straight to it. Outside the dashboard it answers Superwall questions from the docs and SDK source; connect from the dashboard when you want it working with your account and analytics data.
Expand Down
Binary file added content/docs/images/ai_dash_share.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added content/docs/images/stripe_discount_apply.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions content/docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"expo",
"flutter",
"unity",
"web",
"react-native",
"community"
]
Expand Down
38 changes: 23 additions & 15 deletions content/docs/web-checkout/web-checkout-discount-codes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ description: "Let customers enter a Stripe promotion code on your web paywall, u

Discount codes let a customer type a promotion code into your web paywall, have it validated against Stripe, and see the price update before they check out. The code is then carried through to the Stripe checkout session, so the discount is applied to the purchase they complete.

This works entirely on your paywall — there is no separate discount widget to configure. You add a normal text **Input** for the customer to type into, then a button whose tap behavior runs the **Apply Discount** action. When the customer taps it, Superwall resolves the code against your Stripe promotion codes, re-prices the products on the paywall, and applies the code at checkout.
This works entirely on your paywall. There is no separate discount widget to configure. You add a normal text **Input** for the customer to type into, then a button whose tap behavior runs the **Apply Discount** action. When the customer taps it, Superwall resolves the code against your Stripe promotion codes, re-prices the products on the paywall, and applies the code at checkout.

<Note>
Discount codes are a Stripe web checkout feature. The customer redeems a promotion code you have
already created in your Stripe dashboard — Superwall does not create or manage the coupon itself.
already created in your Stripe dashboard. Superwall does not create or manage the coupon itself.

Don't confuse **Apply Discount** with **Redeem Purchase**, a separate tap action in the same
Purchases group that routes a customer after a completed purchase. This page covers Apply
Discount.
</Note>

## Before you start
Expand All @@ -27,22 +31,24 @@ Create the coupon and promotion code in Stripe first. Superwall resolves whateve

## Add discount code redemption to a paywall

You will add a text input for the customer to type into, then a button that redeems whatever they entered. Open the paywall you want to edit in the [paywall editor](/dashboard/dashboard-creating-paywalls/paywall-editor-overview) and follow along.
Open the paywall you want to edit in the [paywall editor](/dashboard/dashboard-creating-paywalls/paywall-editor-overview) and follow along.

<Steps>
<Step title="Add an input for the code">
Add an [Input element](/dashboard/dashboard-creating-paywalls/paywall-editor-input-component) where the customer will type their code, and give it a helpful placeholder such as "Promo code". Bind the input to a state variable — for example, `state.discountCode` — so the value the customer types is available to the Apply Discount action.
<Step title="Add a text input">
Add an [Input element](/dashboard/dashboard-creating-paywalls/paywall-editor-input-component) where the customer types their code, and give it a placeholder such as "Promo code". There is nothing to configure on the input itself. Its value is automatically available under **Element** in the variable picker.
</Step>
<Step title="Add a button to redeem">
Add a button (or reuse an existing one) next to the input. This is the element the customer taps to apply their code.
<Step title="Add a button">
Add a button (or reuse an existing one) next to the input. This is what the customer taps to apply the code.
</Step>
<Step title="Add the Apply Discount action">
<Step title="Give the button the Apply Discount action">
Select the button and, under [Tap Behavior](/dashboard/dashboard-creating-paywalls/paywall-editor-styling-elements#tap-behaviors), click **+ Add Action**. In the **Purchases** group, choose **Apply Discount**.
</Step>
<Step title="Point the action at the code">
Set the action's **code** to reference the input's state variable using Liquid, for example `{{ state.discountCode }}`. This redeems whatever the customer typed.
<Step title="Point the action at the input">
Click the **`<>`** icon at the right edge of the action's **Discount Code** field, then choose your input under **Element**. The editor inserts a reference to the input, so the action redeems whatever the customer typed.

<Frame>![The paywall editor with the Apply Discount action added and the variable picker open, selecting the promo code input under Element](/images/stripe_discount_apply.jpg)</Frame>

To offer a single fixed promotion instead of customer-entered codes, you can skip the input and set the code to a hardcoded literal such as `LAUNCH20`. Tapping the button then always applies that one code.
To offer a single fixed promotion instead of customer-entered codes, skip the input and type a literal code such as `LAUNCH20` into the field. Tapping the button then always applies that one code.
</Step>
<Step title="Publish and test">
[Publish](/dashboard/dashboard-creating-paywalls/paywall-editor-publishing) the paywall, then enter a real promotion code to confirm the price updates and the discount carries through to checkout. See [Testing purchases](/web-checkout/web-checkout-testing-purchases) for how to run test checkouts safely.
Expand All @@ -63,12 +69,14 @@ If the code is not valid, the products stay at their original price.

After a code applies, discount display variables become available so your paywall can react to the discount and show the new price. You can insert these from the variable picker in a text element or wherever you use [dynamic values](/dashboard/dashboard-creating-paywalls/paywall-editor-dynamic-values), for example:

- `hasDiscount` whether a discount is currently applied. Use it to show or hide a "discount applied" message, a strikethrough of the original price, or a badge.
- `discountCode` the code that was applied.
- `discountedPrice` the price after the discount.
- `hasDiscount`: whether a discount is currently applied. Use it to show or hide a "discount applied" message, a strikethrough of the original price, or a badge.
- `discountCode`: the code that was applied.
- `discountedPrice`: the price after the discount.

A common pattern is to show the original price with a strikethrough alongside `discountedPrice` once `hasDiscount` is true, so the savings are obvious before the customer checks out.

## Where discount codes apply

A redeemed code carries through Superwall's web checkout flows — redirect, embedded, and elements checkout all apply it at the Stripe checkout session. The one exception is deferred **Apple Pay** checkout, which does not apply a redeemed discount code.
A redeemed code carries through Superwall's web checkout flows: redirect, embedded, and elements checkout all apply it at the Stripe checkout session. The one exception is deferred **Apple Pay** checkout, which does not apply a redeemed discount code.

If you are building with the [Web SDK](/web), you can also redeem and clear codes from your own code via `activePaywall.redeemDiscount()`. See [Purchases](/web/guides/purchases#discount-codes).
111 changes: 111 additions & 0 deletions content/docs/web/guides/events.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
title: "Events"
description: "Listen to SDK lifecycle events and register a global delegate."
---

<Warning>

**Beta**

The Web SDK is in beta and its API may change between releases.

</Warning>

## Listening

`sw.events` is a native `EventTarget` with typed overloads, including `AbortSignal` for cleanup.

```ts
const ac = new AbortController();

sw.events.addEventListener(
"subscriptionStatus_didChange",
(event) => console.log(event.detail),
{ signal: ac.signal },
);

// Removes every listener registered with this signal.
ac.abort();
```

Handlers receive a typed `CustomEvent`. The payload is on `event.detail`.

<Tip>
In React, use [`useSuperwallEvent`](/web/react/hooks), which ties the subscription to the
component lifecycle.
</Tip>

## What is emitted

The event map covers the same lifecycle the mobile SDKs report:

| Area | Events |
| --- | --- |
| Session | `first_seen`, `app_open`, `app_close`, `app_launch`, `app_install`, `session_start`, `reset` |
| Configuration | `config_refresh`, `config_fail`, `config_attributes` |
| Identity and attributes | `identity_alias`, `user_attributes`, `device_attributes`, `integration_attributes` |
| Subscription | `subscriptionStatus_didChange`, `customerInfo_didChange` |
| Placements | `trigger_fire`, `paywallPresentationRequest`, `confirm_all_assignments` |
| Paywall lifecycle | `paywall_open`, `paywall_close`, `paywall_page_view`, `paywall_decline` |
| Transactions | `transaction_start`, `transaction_complete`, `transaction_fail`, `transaction_abandon`, `transaction_timeout`, `transaction_restore`, `subscription_start`, `freeTrial_start`, `nonRecurringProduct_purchase` |
| Restore | `restore_start`, `restore_complete`, `restore_fail` |
| Discounts | `discount_redeem_complete`, `discount_redeem_fail` |
| Paywall loading | `paywallPreload_start`, `paywallPreload_complete`, `paywallResponseLoad_start`, `paywallResponseLoad_complete`, `paywallResponseLoad_notFound`, `paywallResponseLoad_fail`, `paywallProductsLoad_start`, `paywallProductsLoad_complete`, `paywallProductsLoad_fail` |
| Webview | `paywallWebviewLoad_start`, `paywallWebviewLoad_complete`, `paywallWebviewLoad_fail`, `paywallWebviewLoad_timeout` |
| Surveys | `survey_response`, `survey_close` |
| Custom | `custom_placement`, `customPaywallAction` |
| Other | `deepLink_open`, `page_view`, `enrichment_start`, `enrichment_complete`, `enrichment_fail` |

Some events are local-only and never reach Superwall's servers. Those live in `LocalSuperwallEventMap`. `AllSuperwallEvents` is the union of both.

## Global delegate

The delegate receives global callbacks from the SDK. Register one at creation, or swap it later.

```ts
const sw = createSuperwall({
apiKey: "pk_…",
delegate: {
onSubscriptionStatusChange: (from, to) => {
console.log("subscription", from.status, "->", to.status);
},
},
});

sw.setDelegate(otherDelegate);
sw.setDelegate(null);
```

The delegate covers around a dozen callbacks: subscription and customer changes, the paywall present and dismiss pairs, URL and deep-link interception, link redemption, and `onLog`.

Per-call handlers passed to `register` fire alongside the delegate. The two are separate surfaces covering different callbacks. `onSkip` exists only on a per-call handler, and the delegate's methods cannot be supplied per call. Neither overrides the other, and there is no fallback between them.

<Note>
Custom actions reach you two ways. `onCustomPaywallAction(name)` on the delegate receives the
legacy `custom` action from the paywall. Actions defined as custom placements arrive as the
`custom_placement` event, whose detail carries `placementName`, `paywall_info`, and `params`.
</Note>

## Forwarding to your analytics

The delegate has a catch-all for this. It receives every wire-bound event, fully typed.

```ts
const sw = createSuperwall({
apiKey: "pk_…",
delegate: {
onEvent: (name, detail) => analytics.track(name, detail),
},
});
```

To subscribe to a specific set instead, type the names so `detail` narrows:

```ts
import type { AllSuperwallEvents } from "@superwall/paywalls-js";

const forward = (type: keyof AllSuperwallEvents) =>
sw.events.addEventListener(type, (e) => analytics.track(type, e.detail));

["paywall_open", "paywall_close", "subscriptionStatus_didChange"].forEach(forward);
```
98 changes: 98 additions & 0 deletions content/docs/web/guides/purchases.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
title: "Purchases"
description: "How checkout works, and how to take it over."
---

<Warning>

**Beta**

The Web SDK is in beta and its API may change between releases.

</Warning>

## The default: Superwall handles it

Create an instance without a `purchaseController` and the SDK runs the standard checkout itself: Stripe checkout from the paywall, `?code=` redemption on return, and polling for the resulting entitlements.

```ts
const sw = createSuperwall({ apiKey: "pk_…" });
```

Products come from the paywall you configured in the dashboard. Subscription status updates on its own once the purchase settles.

## Reading the catalog

```ts
const products = await sw.purchases.getProducts();
```

Products come from the parsed config. Expect an empty array before configuration lands, or when the config carries no products.

## Restoring

```ts
await sw.purchases.restore();
```

## Discount codes

When a paywall is on screen, `sw.activePaywall` exposes controls for Stripe promotion codes.

```ts
const active = sw.activePaywall.value;

if (active) {
const outcome = await active.redeemDiscount("LAUNCH20");
if (!outcome.valid) console.log("rejected:", outcome.reason);
}
```

Redeeming validates the code against the checkout backend, re-prices the paywall's Stripe products, and forwards the code to every Stripe web checkout session created afterwards, including rebuilding sessions that were already prefetched.

```ts
active.clearDiscount();
```

`clearDiscount` is fire-and-forget and returns `void`. The paywall sends no acknowledgement, so there is nothing to await.

Behavior to know:

- `redeemDiscount` resolves after roughly ten seconds with `{ valid: false, reason: "timeout" }` if the paywall never replies, which happens when it has no Stripe products loaded.
- A second `redeemDiscount` while one is in flight supersedes the first, which settles as `{ valid: false, reason: "superseded" }`. `clearDiscount()` settles an in-flight redemption the same way.
- It rejects with a `DiscountError` for an empty or whitespace-only code, when no paywall is presented (including a stale `activePaywall` handle from a paywall that has since been dismissed), and when the presenter has no message channel.
- A discount does not survive dismissal. Re-apply it after each presentation.
- `reason` is an open union. Known values include `code_not_found`, `code_invalid`, `no_valid_products`, `no_applicable_products`, `error`, `timeout`, `superseded`, and `paywall_dismissed`. Do not write an exhaustive `switch` over it.

<Note>
This is the programmatic equivalent of the **Apply Discount** tap action in the paywall editor.
See [Discount codes](/web-checkout/web-checkout-discount-codes) for setting the code up in Stripe.
</Note>

## Running your own checkout

To bill through your own system while keeping the SDK's paywalls and placements, push the outcome in yourself:

```ts
sw.purchases.setSubscriptionStatus({
status: "ACTIVE",
entitlements: [
{ id: "pro", type: "SERVICE_LEVEL", isActive: true, productIds: [] },
],
});
```

The SDK treats what you push as truth for client-side state. It is still not a security boundary. See [Server-side gating](/web/guides/server-side-gating).

## Taking over the controller

For full control of the transaction, pass a `purchaseController` at creation. It replaces the built-in automatic controller entirely, including `?code=` redemption and entitlements polling, so you own those too.

```ts
const sw = createSuperwall({
apiKey: "pk_…",
purchaseController: myPurchaseController,
});
```

There is no runtime setter. The controller is wired once, at `createSuperwall`.
Loading
Loading