# SubscriptionLanding

> The three public pages of an email list, subscribe with explicit consent, confirm for double opt-in, and unsubscribe with an optional reason and a way back.

Source: https://docs.nasaqui.com/components/subscription-landing

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/subscription-landing.json
```

The pages a recipient lands on from a signup form or from an email link. One component, three modes.

## When to use

- The public subscribe, confirm and unsubscribe addresses of a newsletter or broadcast list.

## When not to use

- Account sign in: use an auth card.
- Managing consent inside the app: use [`ContactIdentities`](https://docs.nasaqui.com/components/contact-identities).

## Import

```tsx
import { SubscriptionLanding } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
<SubscriptionLanding mode="subscribe" brand="Nasaq" onSubscribe={async ({ email }) => subscribe(email)} />
<SubscriptionLanding mode="confirm" email="sara@example.com" onConfirm={confirm} />
<SubscriptionLanding mode="unsubscribe" email="sara@example.com" onUnsubscribe={unsubscribe} onResubscribe={resubscribe} />
```

## Anatomy

- Subscribe: optional name, email, an unticked consent box, then "Check your inbox".
- Confirm: a button (never automatic, so mail scanners do not subscribe anyone), then "You are subscribed".
- Unsubscribe: an optional reason, then "You are unsubscribed" with an undo.

## API

| Prop | Type | Description |
| --- | --- | --- |
| `mode` | `"subscribe"`, `"confirm"` or `"unsubscribe"` | Which page. |
| `brand` | `ReactNode` | The sender's name as text. |
| `email` | `string` | For confirm and unsubscribe. Shown masked. |
| `onSubscribe` | `({ email, name? }) => Promise<...>` | |
| `onConfirm` | `() => Promise<...>` | |
| `onUnsubscribe` | `({ reason?, note? }) => Promise<...>` | |
| `onResubscribe` | `() => Promise<...>` | Shows the undo. |
| `reasons` | `string[]` | Reason keys. |
| `askName` | `boolean` | Adds a name field. |
| `labels` | `Partial<SubscriptionLandingLabels>` | Override any string. |

Pure helpers: `isSubscriberEmail`, `maskSubscriberEmail`, `validateSubscription`.

## Examples

Pass your own `reasons` and matching `labels.reasons` to change the survey.

## Accessibility

Errors are announced. Outcomes are `role="status"`. The consent box has a real label.

## RTL & i18n

English and Arabic ship. The email field is left-to-right.

## Styling & tokens

Semantic tokens only.

## Do / Don't

- Do require an unticked consent box.
- Don't confirm or unsubscribe on page load.

## Related

- [`CampaignComposer`](https://docs.nasaqui.com/components/campaign-composer)

## Lab

https://docs.nasaqui.com/?path=/docs/components-marketing-subscription-landing--docs

## Code

### React

```tsx
<SubscriptionLanding mode="subscribe" brand="Nasaq" onSubscribe={async ({ email }) => subscribe(email)} />
<SubscriptionLanding mode="confirm" email="sara@example.com" onConfirm={confirm} />
<SubscriptionLanding mode="unsubscribe" email="sara@example.com" onUnsubscribe={unsubscribe} onResubscribe={resubscribe} />
```

### shadcn

```tsx
<SubscriptionLanding mode="subscribe" brand="Nasaq" onSubscribe={async ({ email }) => subscribe(email)} />
<SubscriptionLanding mode="confirm" email="sara@example.com" onConfirm={confirm} />
<SubscriptionLanding mode="unsubscribe" email="sara@example.com" onUnsubscribe={unsubscribe} onResubscribe={resubscribe} />
```

### Vue

```vue
<script setup lang="ts">
import { NqSubscriptionLanding } from "@fadymondy/nasaq/vue";

async function subscribe({ email }: { email: string }) {
  console.log("subscribe", email);
}
</script>

<template>
  <NqSubscriptionLanding mode="subscribe" brand="Nasaq" :on-subscribe="subscribe" />
</template>
```

### Blade

```blade
<x-nq::subscription-landing mode="subscribe" brand="Nasaq" x-on:nq-subscribe="$event.detail.waitUntil(new Promise((resolve) => setTimeout(resolve, 600)))" />
```

### HTML + Alpine

```html
<div data-slot="subscription-landing" data-mode="subscribe" x-data="nqSubscriptionLanding(JSON.parse('{\u0022mode\u0022:\u0022subscribe\u0022,\u0022email\u0022:null,\u0022labels\u0022:[]}'))"
    x-on:nq-subscribe="$event.detail.waitUntil(new Promise((resolve) =&gt; setTimeout(resolve, 600)))" class="flex min-h-full w-full flex-col items-center justify-center gap-6 p-4 sm:p-8">
            <div class="text-h4 text-foreground">Nasaq</div>
        <div data-slot="card" class="flex flex-col gap-4 rounded-card border border-border bg-card text-card-foreground w-full max-w-md p-6"><div x-show="stage === 'pending'" style="display: none" class="flex flex-col gap-4" role="status">
                <div class="flex flex-col items-center gap-3 text-center">
    <span aria-hidden="true" class="flex size-12 items-center justify-center rounded-full bg-secondary text-foreground [&_svg]:size-6"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"/>
  <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
  <path d="m16 19 2 2 4-4"/>
</svg></span>
    <h1 class="text-h3 text-foreground">Check your inbox</h1>
            <p dir="auto" class="text-body-sm text-muted-foreground" x-text="pendingText"></p>
    </div>
                <button data-slot="button"
     type="button"                         x-on:click="startOver()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Wrong address? Start over</button>
            </div>
            <form x-show="stage === 'form'" novalidate class="flex flex-col gap-4" x-on:submit.prevent="submitSubscribe()">
                <div class="flex flex-col items-center gap-3 text-center">
    <span aria-hidden="true" class="flex size-12 items-center justify-center rounded-full bg-secondary text-foreground [&_svg]:size-6"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"/>
  <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
  <path d="m16 19 2 2 4-4"/>
</svg></span>
    <h1 class="text-h3 text-foreground">Stay in the loop</h1>
            <p dir="auto" class="text-body-sm text-muted-foreground">Product news and new articles, once in a while. No spam.</p>
    </div>
                <div data-slot="alert" data-tone="danger" role="alert"
        x-show="error" style="display: none" class="relative grid grid-cols-[auto_1fr_auto] items-start gap-x-3 rounded-card border p-3 text-start border-nq-danger/30 bg-nq-danger-soft">
    <svg aria-hidden="true" data-slot="alert-icon" class="mt-0.5 size-4 text-nq-danger-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
  <path d="m15 9-6 6"/>
  <path d="m9 9 6 6"/>
</svg>    <div data-slot="alert-body" class="flex min-w-0 flex-col gap-0.5">
                            <div data-slot="alert-description" class="text-body-sm text-foreground"><span x-text="error"></span></div>
            </div>
    </div>
                                <div data-slot="field" x-data="nqField(false)" x-modelable="invalid" x-id="['nq-field']"
         data-valid     x-model="emailBad" x-effect="emailBad = emailIssue !== &#039;&#039;" class="flex flex-col gap-1.5">
    <label data-slot="field-label"  class="text-label text-foreground data-disabled:opacity-50">Email address</label>
                    <input data-slot="input" type="email"
     dir="ltr"                 autocomplete="email" x-model="email" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control text-start">
                    <p x-show="emailIssue" style="display: none" role="alert" class="text-caption text-destructive" x-text="emailIssue"></p>
</div>
                <div class="flex flex-col gap-1">
                    <label for="nq-sub-9fe94a-consent" class="flex items-start gap-2 text-body-sm">
                        <button type="button" role="checkbox" data-slot="checkbox" x-data="nqCheckbox(false, false)" x-modelable="checked" x-bind="root"
    aria-checked="false"
     data-unchecked                 id="nq-sub-9fe94a-consent" x-model="consent" class="relative inline-flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-nq-line-strong bg-card text-primary-foreground outline-none transition-colors duration-150 ease-nq data-checked:border-primary data-checked:bg-primary data-indeterminate:border-primary data-indeterminate:bg-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:cursor-not-allowed data-disabled:opacity-50 after:absolute after:-inset-1 mt-0.5">
    <span data-slot="checkbox-indicator" class="flex items-center justify-center [&_svg]:size-3 [&_svg]:stroke-3" x-show="checked || indeterminate"  style="display: none" >
        <svg aria-hidden="true" x-show="indeterminate" style="display: none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
</svg>        <svg aria-hidden="true" x-show="! indeterminate" style="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg>    </span>
    </button>
                        <span>Yes, send me emails. I can unsubscribe at any time.</span>
                    </label>
                    <p x-show="consentIssue" style="display: none" role="alert" class="text-caption text-destructive" x-text="consentIssue"></p>
                </div>
                <button data-slot="button"
     type="submit"                         x-bind:disabled="busy" x-bind:aria-busy="busy ? &#039;true&#039; : null" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)] w-full">
        <template x-if="busy"><svg data-slot="spinner" aria-hidden="true" class="size-4 animate-spin motion-reduce:animate-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg></template>
                    Subscribe</button>
                <p class="text-center text-caption text-muted-foreground">We keep your address private and never sell it.</p>
            </form></div>
</div>
```
