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 index from ../../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.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 · 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.