Brand and design · Hi-fi screens · Hi-fi screens
Hi-fi: S15 Paywall · Paywall
Contract, a board per state (default, loading-products, purchasing, purchase-failed, restored, expired-return, loading, empty, error, offline), tokens and strings.
Generated by
botgumi-design indexfrom../../screens.json. Edit that file, not this page.
Stage: hifi · Key: paywall/paywall · Data-binding map: bindings.md
Drawn in the gallery: 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.selectedWeeklyDayslocal:paywall.selectedProductIdlocal: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 · en | paywall.json (zh-Hans · en), storekit-products.json (zh-Hans · en) |
| loading-products | zh-Hans · en | paywall.json (zh-Hans · en) |
| purchasing | zh-Hans · en | paywall.json (zh-Hans · en), storekit-products.json (zh-Hans · en) |
| purchase-failed | zh-Hans · en | paywall.json (zh-Hans · en), storekit-products.json (zh-Hans · en) |
| restored | zh-Hans · en | paywall.json (zh-Hans · en), storekit-products.json (zh-Hans · en) |
| expired-return | zh-Hans · en | me.json (zh-Hans · en), paywall.json (zh-Hans · en), storekit-products.json (zh-Hans · en) |
| loading | no board of its own: From states.md (Paywall): paywall--loading-products (StoreKit) + skeleton projection (/paywall) |
|
| empty | no board of its own: From states.md (Paywall): — | |
| error | no board of its own: From states.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: From states.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
loading-products
purchasing
purchase-failed
restored
expired-return
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, tokens.md §6–7 and
ios-handoff.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): --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.