---
title: "S15 Paywall · Paywall"
description: "Contract, a board per state (default, loading-products, purchasing, purchase-failed, restored, expired-return, loading, empty, error, offline), tokens and strings."
source: "design/hifi/S15/screen.md"
---

> Part of [Brand and design](/design/llms.txt). Every page: [llms.txt](/llms.txt).

# Hi-fi: S15 Paywall · Paywall

> Generated by `botgumi-design index` from `../../screens.json`. Edit that file, not this page.

Stage: **hifi** · Key: `paywall/paywall` · Data-binding map: [bindings.md](/design/hifi/s15/bindings/index.md)

Drawn in the gallery: [Paywall](/design/gallery/index.html#g-paywall). Each board is the committed HTML in `screens/`, drawn on the example data in `docs/api/examples/`.

## Contract

- **default**: Hard paywall (no close). Own diagnostic score + weakest dimension; 3/5/7 slider live-updates the projection from benchmarks and the 14-day calendar (practice days, streak milestones the schedule can actually reach, trial→paid date). Prices only from StoreKit JSON. 'Pilot data' label because benchmark_source = pilot_benchmark.
- **loading-products**: StoreKit products not loaded yet: skeleton cards, CTA disabled. Projection & calendar still work (they come from /v1/paywall).
- **purchasing**: Purchase in progress: CTA busy, cards locked; the system purchase sheet appears above this.
- **purchase-failed**: Purchase cancelled / failed: inline alert says nothing was charged; CTA becomes Retry.
- **restored**: Restore found an active entitlement (server-verified): toast + CTA to start training.
- **expired-return**: entitlement.status = expired on reopen: welcome back with the user's own progress (me.stats); projection uses current ability; no trial (not eligible).

Flows: F1 First launch → diagnostic → paywall → first real session, F7 Expired subscription.

Reads and calls:

- `GET /v1/paywall (Accept-Language: labels, badge, weakest_label)`
- `GET /v1/me (entitlement, stats — expired-return)`
- `StoreKit Product.products(for:)`
- `Product.purchase() → POST /v1/subscriptions/apple/transactions {signed_transaction}`
- `AppStore.sync() (restore)`

Local state:

- `local:paywall.selectedWeeklyDays`
- `local:paywall.selectedProductId`
- `local:storekit.products`

## Boards

One row per state. A state without boards of its own says what shows it.

| State | Board | Example data |
|---|---|---|
| default | [zh-Hans](/design/gallery/paywall/paywall--default.html) · [en](/design/gallery/paywall/paywall--default.en.html) | paywall.json ([zh-Hans](/design/examples/zh-Hans/paywall.json) · [en](/design/examples/en/paywall.json)), storekit-products.json ([zh-Hans](/design/examples/zh-Hans/storekit-products.json) · [en](/design/examples/en/storekit-products.json)) |
| loading-products | [zh-Hans](/design/gallery/paywall/paywall--loading-products.html) · [en](/design/gallery/paywall/paywall--loading-products.en.html) | paywall.json ([zh-Hans](/design/examples/zh-Hans/paywall.json) · [en](/design/examples/en/paywall.json)) |
| purchasing | [zh-Hans](/design/gallery/paywall/paywall--purchasing.html) · [en](/design/gallery/paywall/paywall--purchasing.en.html) | paywall.json ([zh-Hans](/design/examples/zh-Hans/paywall.json) · [en](/design/examples/en/paywall.json)), storekit-products.json ([zh-Hans](/design/examples/zh-Hans/storekit-products.json) · [en](/design/examples/en/storekit-products.json)) |
| purchase-failed | [zh-Hans](/design/gallery/paywall/paywall--purchase-failed.html) · [en](/design/gallery/paywall/paywall--purchase-failed.en.html) | paywall.json ([zh-Hans](/design/examples/zh-Hans/paywall.json) · [en](/design/examples/en/paywall.json)), storekit-products.json ([zh-Hans](/design/examples/zh-Hans/storekit-products.json) · [en](/design/examples/en/storekit-products.json)) |
| restored | [zh-Hans](/design/gallery/paywall/paywall--restored.html) · [en](/design/gallery/paywall/paywall--restored.en.html) | paywall.json ([zh-Hans](/design/examples/zh-Hans/paywall.json) · [en](/design/examples/en/paywall.json)), storekit-products.json ([zh-Hans](/design/examples/zh-Hans/storekit-products.json) · [en](/design/examples/en/storekit-products.json)) |
| expired-return | [zh-Hans](/design/gallery/paywall/paywall--expired-return.html) · [en](/design/gallery/paywall/paywall--expired-return.en.html) | me.json ([zh-Hans](/design/examples/zh-Hans/me.json) · [en](/design/examples/en/me.json)), paywall.json ([zh-Hans](/design/examples/zh-Hans/paywall.json) · [en](/design/examples/en/paywall.json)), storekit-products.json ([zh-Hans](/design/examples/zh-Hans/storekit-products.json) · [en](/design/examples/en/storekit-products.json)) |
| loading | [no board of its own](/design/hifi/s15/bindings/index.md#states): From [states.md](/design/states/index.md) (Paywall): [paywall--loading-products](/design/gallery/paywall/paywall--loading-products.html) (StoreKit) + skeleton projection (`/paywall`) |  |
| empty | [no board of its own](/design/hifi/s15/bindings/index.md#states): From [states.md](/design/states/index.md) (Paywall): — |  |
| error | [no board of its own](/design/hifi/s15/bindings/index.md#states): From [states.md](/design/states/index.md) (Paywall): `/paywall` failed → projection & calendar replaced by Retry card; products still purchasable · fatal: StoreKit unavailable → "App Store unavailable" + Restore |  |
| offline | [no board of its own](/design/hifi/s15/bindings/index.md#states): From [states.md](/design/states/index.md) (Paywall): cached `/paywall`; purchase disabled with "Connect to subscribe" |  |

### The boards

Each state as drawn (393 × 852), one board per locale. Open a board on its own for its spec panel.

**default**

[S15 default · zh-Hans](/design/gallery/paywall/paywall--default.html) [S15 default · en](/design/gallery/paywall/paywall--default.en.html)

**loading-products**

[S15 loading-products · zh-Hans](/design/gallery/paywall/paywall--loading-products.html) [S15 loading-products · en](/design/gallery/paywall/paywall--loading-products.en.html)

**purchasing**

[S15 purchasing · zh-Hans](/design/gallery/paywall/paywall--purchasing.html) [S15 purchasing · en](/design/gallery/paywall/paywall--purchasing.en.html)

**purchase-failed**

[S15 purchase-failed · zh-Hans](/design/gallery/paywall/paywall--purchase-failed.html) [S15 purchase-failed · en](/design/gallery/paywall/paywall--purchase-failed.en.html)

**restored**

[S15 restored · zh-Hans](/design/gallery/paywall/paywall--restored.html) [S15 restored · en](/design/gallery/paywall/paywall--restored.en.html)

**expired-return**

[S15 expired-return · zh-Hans](/design/gallery/paywall/paywall--expired-return.html) [S15 expired-return · en](/design/gallery/paywall/paywall--expired-return.en.html)

## Tokens used

By path in `../../system/tokens.source.json`.

| Token | Where |
|---|---|
| `color.accentPrimary` | `.btn--primary` |
| `color.accentPrimaryPressed` | `.btn--primary` |
| `color.bgCanvas` | `.device`, `.dock`, `.product`, `.radio` … |
| `color.bgInverse` | `.brand-mark`, `.toast`, `inline` |
| `color.bgInverseRaised` | `.skel` |
| `color.bgSunken` | `.btn`, `.skel`, `.stat`, `.tag` … |
| `color.bgSurface` | `.btn--secondary`, `.card`, `.product`, `.range` … |
| `color.focusRing` | `.range` |
| `color.lineHairline` | `.btn--secondary`, `.btn--sm` |
| `color.lineStrong` | `.btn--secondary`, `.link`, `.product`, `.radio` … |
| `color.reward` | `.xp-badge`, `inline` |
| `color.statusDanger` | `.banner--danger` |
| `color.statusDangerSoft` | `.banner--danger` |
| `color.statusWarning` | `.tag--amber` |
| `color.textDisabled` | `.btn` |
| `color.textOnAccent` | `.btn--primary` |
| `color.textOnInverse` | `.brand-mark`, `.toast` |
| `color.textOnReward` | `.xp-badge` |
| `color.textPrimary` | `.banner--danger`, `.btn--secondary`, `.device`, `.home-indicator` … |
| `color.textSecondary` | `.c-secondary`, `.legend`, `.link`, `.stat` … |
| `color.textTertiary` | `.c-tertiary`, `.cal`, `.range-ticks` |
| `color.topicAmber` | `inline` |
| `color.topicAmberSoft` | `.tag--amber` |
| `radius.button` | `.btn` |
| `radius.card` | `.card` |
| `radius.md` | `.card--md`, `.product`, `.stat` |
| `radius.pill` | `.btn--sm`, `.toast`, `.xp-badge` |
| `radius.sm` | `.banner` |
| `radius.xs` | `.tag` |
| `space.gutter` | `.dock`, `.pad` |
| `space.s5` | `.card` |
| `space.s6` | `.screen` |
| `space.safeBottom` | `.dock`, `.has-dock`, `.screen` |
| `space.safeTop` | `.device`, `.screen` |
| `space.sizeCtaHeight` | `.btn`, `.has-dock`, `.screen` |
| `space.sizeTabbarHeight` | `.screen` |
| `typography.caption1` | `.t-caption` |
| `typography.display` | `.t-display` |
| `typography.footnote` | `.t-footnote` |
| `typography.headline` | `.t-headline` |
| `typography.subheadline` | `.t-subhead` |
| `typography.title1` | `.t-title1` |
| `typography.title2` | `.t-title2` |

## Strings

`app.name`, `band.basic`, `common.cancel`, `common.date_range`, `common.retry`, `common.today`, `complete.stat.ability`, `diag.result.nav`, `dimension.structure`, `feedback.one_action.basis`, `home.greeting.format`, `home.quest.cta.start`, `library.row.status.best`, `micro.conclusion_first`, `notif.daily.title`, `onboarding.commitment.days.a11y`, `onboarding.commitment.title`, `paywall.calendar.legend.practice`, `paywall.calendar.legend.retest`, `paywall.calendar.legend.streak`, `paywall.calendar.legend.today`, `paywall.calendar.rest`, `paywall.calendar.retest`, `paywall.calendar.title`, `paywall.cta.loading`, `paywall.cta.loading.caption`, `paywall.cta.purchasing`, `paywall.cta.purchasing.caption`, `paywall.expired.eyebrow`, `paywall.expired.note`, `paywall.expired.question`, `paywall.expired.records`, `paywall.expired.stat.ability`, `paywall.expired.stat.focus`, `paywall.expired.stat.sessions`, `paywall.expired.title`, `paywall.eyebrow`, `paywall.failed.body`, `paywall.failed.caption`, `paywall.failed.title`, `paywall.fineprint`, `paywall.k.a11y_value`, `paywall.k.label`, `paywall.k.value`, `paywall.per.month`, `paywall.per.year`, `paywall.period.month`, `paywall.plan.annual`, `paywall.plan.annual.no_trial`, `paywall.plan.annual.trial`, `paywall.plan.monthly`, `paywall.plan.monthly.sub`, `paywall.plan.suffix.month`, `paywall.plan.suffix.year`, `paywall.plans.a11y`, `paywall.plans.title`, `paywall.privacy`, `paywall.projection.bucket.range`, `paywall.projection.day14`, `paywall.projection.disclaimer`, `paywall.projection.score`, `paywall.projection.you_now`, `paywall.restore`, `paywall.restored.caption`, `paywall.restored.cta`, `paywall.restored.toast`, `paywall.source.pilot`, `paywall.terms`, `paywall.title`, `paywall.weakest`, `plan.focus_label`, `plan.retest`, `plan.weekly_days`, `profile.account.title`, `profile.settings`, `profile.settings.a11y`, `profile.stat.ability`, `profile.stat.sessions`, `profile.sub.name`, `read.tag.daily`, `settings.nav`, `settings.retention.7`, `tab.library`

## Notes for the app tracks

Motion, haptics, Dynamic Type and VoiceOver: [engagement.md](/design/engagement/index.md), [tokens.md](/design/tokens/index.md) §6–7 and
[ios-handoff.md](/design/ios-handoff/index.md). Open a board with `?theme=dark&dt=1.35&binds=1` to see dark mode, the largest
tested type size and every bound element.

Tracking-plan events: no tracking plan page exists yet; ADR-001 D12 names the events.

Tokens with no group in `system/tokens.source.json` yet (they stay in [tokens.css](/design/tokens.css)): `--dt`, `--elevation-1`, `--elevation-3`, `--font-family-numeric`, `--font-family-text`, `--font-weight-bold`, `--font-weight-heavy`, `--font-weight-medium`, `--font-weight-regular`, `--font-weight-semibold`, `--motion-duration-base`, `--motion-duration-fast`, `--motion-duration-instant`, `--motion-duration-slow`, `--motion-ease-emphasized`, `--motion-ease-spring`, `--motion-ease-standard`, `--type-tracking-caps`, `--type-tracking-tight`.
