feat(mobile): define storefront presentation contract and paywall design

Merged
GI
2d ago
+7695-19

Superseded: the schema-1 contract below is replaced by schema_version 2 (backend #21397, client #21363). This PR lands via #21363.

What changed and why

Add the mobile presentation-only storefront model and hermetic fixtures for Ladder v1 (W6, spec §5–§7). The backend owns tier order, names, recommendation, outcome copy, prices, savings, usage context and entitlement decisions. Add a typed checkout seam that rejects a presentation price if it differs from the existing catalog-selected ID; existing callers are unchanged. No payment rail or entitlement behavior changes.

Paywall design contract

Omi is a wearable memory companion: sell continuity and useful follow-through, without making captured memories feel hostage. One contextual sheet explains the exact budget/MCP limit or Pro feature the person reached for, then offers a calm comparison. No unsolicited acquisition popups. North star: ARPMAU; guardrails: activated 30-day paid conversion and payer net MRR retention. Client funnel events measure the journey; backend billing events measure revenue.

Render backend-ordered Free / Plus / Pro / Architect as stacked, readable cards. The backend recommended tier gets a neutral border and label and is never preselected. Recommendation is contextual and backend-owned: Pro at proactive-agent or screen-context gates, and for Plus users or legacy plans priced at or above Plus; Plus for Free users at usage-budget or MCP daily caps. Without context, Plus for Free users and Pro otherwise. The client passes the opening source and only renders the returned recommended flag. Each card leads with a concrete outcome, then server-computed ≈ recording hours, followed by honest billed amount and monthly equivalent. Never say “unlimited”, infer hours from old minute caps, invent icons, or disturb filter icons/chat composer.

Monthly is the initial billing choice. Annual is an explicit opt-in. Display savings only when the backend computes them from live monthly/annual prices; show full billed amount, monthly equivalent and renewal terms together. Checkout forwards the exact price ID through the existing purchase calls. No new iOS external links or purchase copy; showSubscriptionUI remains authoritative.

Show the current plan separately, including legacy plans absent from the new-buyer ladder. Never preselect a replacement, alter legacy entitlements or auto-migrate a subscriber. Restore/refresh and existing payment management remain reachable. Close stays visible outside the scrolling content during every state and payment attempt.

Use the existing Omi sheet, button, spinner and page-state primitives, neutral light/dark tokens, scalable type and ≥44 pt controls. Stack cards and wrap billing controls on small screens; scroll rather than truncate prices or renewal terms. Screen readers announce card headings, recommendation, current-plan state and the billing selection. No decorative hero competes with the decision.

Loading shows a bounded spinner; empty means no offers, with refresh and manage still reachable; errors offer Try Again without substituting stale or invented prices. A malformed/unsupported payload cannot enable checkout. At 80–<100% show a nonblocking budget banner; W4 owns the 100% choice. Pro gate decisions come from the backend, with legacy accounts exempt. MCP context uses the same sheet.

Integration boundary

W5's endpoint is not on this base. This is a provisional client contract and test fixture, not live catalog data. The JSON fixture includes synthetic price IDs and illustrative hours/prices exclusively under app/test/. A later adapter owns endpoint/field mapping in one file. W5 must implement the coordinator’s contextual recommendation ruling; W6 passes source and renders the returned flag without client-side inference. No fixture can be used as a checkout catalog.

Product invariants affected

none

How it was verified

  • Unit test: cd app && flutter test test/models/plan_storefront_test.dart — 4 tests pass.
  • Local preflight: make preflight — result recorded in final report.
  • Full mobile shard: bash app/test.sh launched with empty API base; result recorded in final report.
  • Live API/payment/production: not run; no production access or writes.

Tests

Tests preserve exact IDs and ordering, computed server savings, future plan identity, hidden UI default, malformed schema/price rejection, duplicate tiers and the legacy/100% nudge boundary.

UI evidence

UI-Evidence: none -- typed model, hermetic fixture and nonvisual checkout validation seam only; no rendered surface changes.

Failure class (fixes)

Failure-Class: none

Reviewer risks

  1. W5 must confirm endpoint and field mapping; the current client shape is provisional.
  2. Illustrative fixture equivalents are not ratified budgets or marketing copy.
  3. Contextual recommendations and the source vocabulary must be reconciled with W5 before rollout.
  4. Legacy identity/current price stays separate from purchasable ladder tiers; no migration is implied.