From 38265af73478216a59a7e1db3b5921c96ad65dbe Mon Sep 17 00:00:00 2001 From: Belli Date: Mon, 17 Aug 2026 20:08:47 -0700 Subject: [PATCH 1/2] docs: App Store screenshot direction and a screenshot tour UI test --- Savely.xcodeproj/project.pbxproj | 4 + .../StoreScreenshotTourUITests.swift | 67 ++++++ docs/plans/app-store-screenshots.md | 210 ++++++++++++++++++ 3 files changed, 281 insertions(+) create mode 100644 SavelyUITests/StoreScreenshotTourUITests.swift create mode 100644 docs/plans/app-store-screenshots.md diff --git a/Savely.xcodeproj/project.pbxproj b/Savely.xcodeproj/project.pbxproj index c98d9f2..1561a15 100644 --- a/Savely.xcodeproj/project.pbxproj +++ b/Savely.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + F25F23C6EEAA47853BFA137D /* StoreScreenshotTourUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D0412010EDA0CEB30538B1 /* StoreScreenshotTourUITests.swift */; }; C5DDD46B40453371BD90C01A /* ReceiptScanUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C696EE6BA0906CAD429D0881 /* ReceiptScanUITests.swift */; }; 7479BDFD4C96ED1C9E05A119 /* ReceiptOCRIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A7AF8AF4BCB197FCA023DC6 /* ReceiptOCRIntegrationTests.swift */; }; 31A69355DA4D561A928D09D7 /* DocumentCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2DA345E04E3ACA1DEDFCDD2 /* DocumentCameraView.swift */; }; @@ -120,6 +121,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 46D0412010EDA0CEB30538B1 /* StoreScreenshotTourUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreScreenshotTourUITests.swift; sourceTree = ""; }; C696EE6BA0906CAD429D0881 /* ReceiptScanUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptScanUITests.swift; sourceTree = ""; }; 4A7AF8AF4BCB197FCA023DC6 /* ReceiptOCRIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptOCRIntegrationTests.swift; sourceTree = ""; }; D2DA345E04E3ACA1DEDFCDD2 /* DocumentCameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentCameraView.swift; sourceTree = ""; }; @@ -530,6 +532,7 @@ 1DC258212CC07DCF0002DDE3 /* SavelyUITests */ = { isa = PBXGroup; children = ( + 46D0412010EDA0CEB30538B1 /* StoreScreenshotTourUITests.swift */, C696EE6BA0906CAD429D0881 /* ReceiptScanUITests.swift */, 1DC258222CC07DCF0002DDE3 /* SavelyUITests.swift */, 1DC258242CC07DCF0002DDE3 /* SavelyUITestsLaunchTests.swift */, @@ -834,6 +837,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F25F23C6EEAA47853BFA137D /* StoreScreenshotTourUITests.swift in Sources */, C5DDD46B40453371BD90C01A /* ReceiptScanUITests.swift in Sources */, 1DC258252CC07DCF0002DDE3 /* SavelyUITestsLaunchTests.swift in Sources */, 1DC258232CC07DCF0002DDE3 /* SavelyUITests.swift in Sources */, diff --git a/SavelyUITests/StoreScreenshotTourUITests.swift b/SavelyUITests/StoreScreenshotTourUITests.swift new file mode 100644 index 0000000..56b8aab --- /dev/null +++ b/SavelyUITests/StoreScreenshotTourUITests.swift @@ -0,0 +1,67 @@ +// +// StoreScreenshotTourUITests.swift +// SavelyUITests +// +// Not a test of behaviour — a screenshot tour. Walks the main tabs and the +// "+" sheet and attaches a full-resolution screenshot of each, so App +// Store artwork can be built from real screens: +// xcodebuild test -scheme Savely -only-testing:SavelyUITests/StoreScreenshotTourUITests \ +// -destination 'platform=iOS Simulator,name=iPhone 17 Pro' -resultBundlePath build/Tour.xcresult +// xcrun xcresulttool export attachments --path build/Tour.xcresult --output-path build/tour +// Seed the Simulator with real-looking data first (goals, a few expenses/ +// incomes) — an empty app makes empty screenshots. Skipped on CI like the +// rest of SavelyUITests. +// + +import XCTest + +final class StoreScreenshotTourUITests: XCTestCase { + + @MainActor + func testTourAttachesEveryMainScreen() throws { + let app = XCUIApplication() + app.launchArguments += ["-isOnboardingComplete", "YES"] + app.launch() + + let addButton = app.buttons["Add"] + XCTAssertTrue(addButton.waitForExistence(timeout: 20), "the main tab bar did not appear") + snap(app, "01-home") + + for (tab, name) in [("Goals", "02-goals"), ("Money", "03-money"), ("Me", "04-me")] { + app.buttons[tab].firstMatch.tap() + _ = app.buttons[tab].firstMatch.waitForExistence(timeout: 2) + snap(app, name) + } + + app.buttons["Home"].firstMatch.tap() + addButton.tap() + XCTAssertTrue(app.staticTexts["What's the move?"].waitForExistence(timeout: 5)) + snap(app, "05-plus-sheet") + + // Log expense keypad + let logExpense = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Log expense'")).firstMatch + if logExpense.waitForExistence(timeout: 2) { + logExpense.tap() + _ = app.staticTexts["Log expense"].waitForExistence(timeout: 3) + for key in ["2", "4", "5"] { app.buttons[key].firstMatch.tap() } + snap(app, "06-log-expense") + app.buttons["Back"].firstMatch.tap() + } + + // Scanner (Simulator: no camera → unavailable card, still shows the surface) + let scan = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Scan a receipt'")).firstMatch + if scan.waitForExistence(timeout: 2) { + scan.tap() + _ = app.buttons["Choose from Photos"].waitForExistence(timeout: 5) + snap(app, "07-scan-camera") + app.buttons["Close"].firstMatch.tap() + } + } + + private func snap(_ app: XCUIApplication, _ name: String) { + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/docs/plans/app-store-screenshots.md b/docs/plans/app-store-screenshots.md new file mode 100644 index 0000000..e46ff09 --- /dev/null +++ b/docs/plans/app-store-screenshots.md @@ -0,0 +1,210 @@ +# App Store screenshots — direction & production plan + +**Date:** 2026-08-17 +**Status:** direction chosen (this doc); production is the next job (Opus, ultracode). +**Owner decision needed:** none blocking — the choices below are recommendations +with the alternatives kept; flip anything before production starts. + +Research method: three parallel researchers (Apple specs & review rules verified on +developer.apple.com; a survey of ~20 finance/indie/editorial App Store pages opened +live; copy craft for en-US + es-MX), three independent art-direction proposals against +DESIGN.md/PRODUCT.md, then a judge scoring fit / distinctness / store effectiveness / +honesty / producibility. Full research dump: session artifact "Savely storefront". + +--- + +## 0. TL;DR + +- **Direction: "Seed to Sprout" on the notebook page.** Every screenshot is one page of + the same warm notebook (ground `#f6f4ee`, one full-bleed hairline rule under a small + green tracked kicker, a two-line *regular* New York headline, one SF Pro sentence) with + the real app as a flat, untilted photo in a thin warm-ink bezel tucked under the page + edge. The only thing that changes from page to page besides the copy is the + **SproutMark growing one stage per page** — seed on 1, stem on 2–3, first leaf 4–5, + second leaf 6–7, full sprout under a lamp (dark mode) on 8. It is the splash + animation's own geometry laid across the gallery: "a well-made notebook that + something is growing in", literally. +- Why it is *ours*: not Alisio (no colored bands, no bold sans, no dark UI), not Fingo + (no gradients, no tilt, no shouting), not Thirds (Thirds floats UI cards on cream with a + heavy serif and no device; Savely tucks a framed photo, keeps the serif at weight 400, + puts a green kicker on a rule, and grows a plant), and not the 2025-26 "cream + + serif + orange kicker" default that Monarch/Matter now own (green kicker, sprout, + ink bezel, no italics, warm `#f6f4ee` not ivory). +- **Ledger rule** (from PRODUCT.md, applied to the store): every state a caption names — + "on track", "behind", "a drop", "never uploaded", a figure — must be visible in the UI + on that same page. This decides which screen each frame shows. +- **8 frames, en-US + Spanish (Mexico)**, master at 1320×2868 (6.9"), Apple scales the + rest. Light 1–7, dark 8. Order 1 goal → 2 privacy → 3 payday; PPO-test payday-first + and a full-dark set later. +- **Blocking prerequisites before shooting** (§5): es-419 gaps (~139 strings + hard-coded + English literals on the exact screens we show), the truth gate for "No account. No + cloud." (Config.plist with the OpenAI key is in the Resources phase; ATS exception for + openai.com; stray GoogleService-Info.plist), `TARGETED_DEVICE_FAMILY = 1,2` (iPad set + or go iPhone-only), a seeded fictional dataset, and dark mode via `-darkModeEnabled YES`. + +--- + +## 1. Facts that shape the plan (verified 2026-08-17) + +| Fact | Consequence | +|---|---| +| iPhone: only the **6.9"** set is effectively required (1320×2868 / 1290×2796 / 1260×2736, portrait); every smaller size scales down from it. 1–10 images, PNG/JPG, **no alpha**. | One master set per language. Skip 6.5"/6.3"/6.1"/5.5". Optional 4.7" (750×1334) only if SE users matter. | +| iPad 13" (2064×2752) is required **if the app runs on iPad**; the project has `TARGETED_DEVICE_FAMILY = "1,2"`. | Set it to `1` (iPhone-only) or budget an iPad adaptation. | +| Search results show the **first 3 portrait screenshots** (1 if landscape); Apple: "make sure these highlight the essence of your app"; one Dark Mode screenshot recommended. | Portrait; frames 1–3 carry the whole pitch; frame 8 dark. | +| Review 2.3.3: screenshots must show the app in use — not merely title art / splash. Overlays and text allowed. 2.3.7: no prices/"free"/"best". 2.3.9: fictional data, not a real person's. | Every frame shows real UI; the privacy frame shows a settings screen, not a slogan card; no laurels/user counts; seeded fictional data. | +| App Store Connect languages: **Spanish (Mexico)** (default for MX + 16 LatAm storefronts, and an additional language on the US storefront) — there is no es-419 slot. | Upload en-US + es-MX; the es-MX set serves LatAm and US Spanish speakers. | +| Product Page Optimization: ≤3 treatments, 90 days, per locale, submittable without a new build (if no icon change). | Full-dark set and "payday-first" order are PPO treatments, not part of v1. | +| iOS 27 "Creative Assets" (product-page header + search-results image) announced at WWDC26, specs pending. | Keep the same grammar (kicker + rule + sprout) ready for a header asset later; iOS 26 users still see frames 1–3. | +| Legibility math (derived, not Apple's): a 1320px canvas is ≈118pt wide in a 3-up search result → a 136–144px headline reads ≈12–13pt. | Headline ≥136px, ≤5 words, exactly 2 lines, in the top 25% of the canvas. | +| Dark mode in Savely is an **in-app override** (`.preferredColorScheme` from `@AppStorage("darkModeEnabled")`); `simctl ui appearance dark` does nothing. | Launch with `-darkModeEnabled YES` for frame 8. | + +--- + +## 2. The system ("Seed to Sprout on the notebook page") + +All coordinates on the 1320×2868 master. Everything is left-aligned at x=96 on every +frame; nothing alternates, nothing pans across frames — patience means the same +position on every page. + +**Page** +- Ground: `bg #f6f4ee` (dark frame: `bg-dark #161512`). No texture, grain, vignette or gradient. +- Header rule: 3px `line` rgba(30,25,15,.08) full-bleed at **y=192** (dark: `line-dark` rgba(242,239,231,.10)). The only "ruled paper" cue. +- Kicker: x=96, baseline ≈ y 156, **SF Pro Semibold 36px, uppercase, tracking +0.10em, Meadow Green `#2f6b4a`** (dark `#78b58f`). ≤3 words. It is the app's own 11pt tracked label scaled up — the one and only kicker style. No tinted tile, no glyph. +- Headline: y 256–540, **New York Regular 136–144px**, line-height 1.04, tracking −0.005em, ink `#1a1a17` (dark `#f2efe7`), max-width 1128, ≤5 words, **hand-broken to exactly two lines** on every frame so the horizon never moves. Never bold, never italic. +- Sub: 28px below, **SF Pro Regular 44–46px** / 1.3, `ink-soft #55524c` (dark `#c6c1b5`), max-width 980, ≤12 words, ≤2 lines. Sentence case, periods, no exclamation marks. +- Growth mark: SproutMark geometry (SproutMark.swift, 120-unit space) in a **~180px box right-aligned to x=1224, seed tangent to the header rule** (box top ≈ 192 − 180·101/120 ≈ 40) — the plant grows out of the notebook line. Stages: 1 seed only · 2 stem 35% · 3 stem 100% · 4 + left leaf 45% · 5 left leaf 100% · 6 + right leaf 45% · 7 full · 8 full in dark tokens (leaves `#78b58f`/`#5d9c7c`, seed `#dea64a`). Flat fills, no outline, no gradient, never bigger, never a mascot. +- Device ("the photo"): flat, untilted, left-flush. Bezel x=96→1098 (1002 wide), y=1024→off canvas, 26px warm ink `#1a1a17`, outer radius 160; screen 950 wide = the source capture ×0.72, radius 134, `overflow:hidden`; Dynamic Island 270×80 in the same ink as part of the bezel silhouette. The canvas bottom crops the tab bar/home indicator under the page edge. Dark frame: bezel `surface-dark #262420` + a `line-dark` hairline outside it (an ink bezel vanishes on the dark ground). No shadow, no reflection, no Apple photographic titanium. +- Right of the device the page keeps its empty margin (x 1098–1224) — the notebook margin. + +**Color discipline (One-Green Rule at poster scale):** green appears only *as ink* — kicker + sprout — plus whatever the real UI carries. Never a green ground/band/frame. Amber/clay/sky/lilac appear only *inside* the screenshot (tiles, chips, the clay "Behind"). Ink-not-gray for every muted text. + +**Banned:** colored bands (Alisio), gradients/blobs/tilt (Fingo/YNAB/Copilot), silver photographic bezels, drop shadows, floating Dynamic-Island pill without a bezel, callouts/arrows/pop-out cards (v1), laurels/press/user counts, illustration or mascot beyond the four SproutMark shapes, confetti, emoji, italic or bold serif, a second kicker style, prices/"free"/"best"/"#1", a brand-only slide, page folios. + +**Contrast:** ink on ground ≈16:1, ink-soft ≈7:1, green kicker ≈5.6:1 (light); `#f2efe7`/`#78b58f` on `#161512` ≈15:1 / 7:1 (dark) — all AA+. + +--- + +## 3. Sequence & copy (en-US · es-MX) + +Kickers are page headers, headlines are ≤5 words / 2 lines, subs ≤12 words. Spanish is +written as its own sentence (tú, no exclamation marks, "recibo" not "ticket", +"quincena" is deliberately Mexican). Every sub obeys the ledger rule. + +| # | Screen (must show…) | Sprout | Kicker | Headline | Sub | +|---|---|---|---|---|---| +| 1 | **GoalDetailView** of the favorite goal — ring, status line "On track · by Dec 12 · $125/wk", pace pills. (The Dashboard hero card only says "PACE · On track", so the detail view is the honest choice.) | seed | SAVINGS · AHORRO | Your goal.
A real pace. · Tu meta.
Ritmo real. | On track or behind — from what you actually put in. · A tiempo o atrasada, según lo que de verdad depositas. | +| 2 | **Profile → Data & privacy** ("Your data never leaves this iPhone." — es-419 exists). PPO alternate: onboarding "Yours, and only yours" (needs es-419 first). | stem 35% | YOUR DATA · TUS DATOS | No account.
No cloud. · Sin cuenta.
Sin nube. | Your money data lives on your iPhone and never leaves it. · Tus datos viven en tu iPhone y nunca salen de ahí. | +| 3 | **Log income sheet** with the green-soft auto-move banner, un-armed ("Auto-move $150 to Trip to Oaxaca from this paycheck?" + YES pill), keypad cut at its last row. | stem 100% | PAYDAY · DÍA DE PAGO | Save before
you spend. · Ahorra antes
de gastar. | Log your pay, tap Yes, and part of it goes to your goal. · Registra tu quincena, toca Sí y una parte se va a tu meta. | +| 4 | **Log expense keypad**, Display amount, Coffee chip selected (amber tile visible). | left leaf 45% | EVERYDAY MONEY · GASTO DIARIO | Log it in
seconds. · Anótalo en
segundos. | Amount, a category, done. The rest is optional. · Monto, una categoría y listo. Lo demás es opcional. | +| 5 | **Receipt review sheet** ("We read this — is it right?"), fictional merchant "Corner Market" / "Abarrotes La Esquina", total, date. Reached via Photos import of a synthetic receipt (Simulator has no camera). Never the live viewfinder. | left leaf 100% | RECEIPTS · RECIBOS | Scan it.
It stays here. · Escanéalo.
Se queda aquí. | Total, merchant, date — read on your iPhone, never uploaded. · Total, comercio y fecha. Se leen en tu iPhone y nunca se suben. | +| 6 | **Money tab**, 7-month trend with a genuinely lower month (July > August), month total. | right leaf 45% | THE MONTH · EL MES | Bad months
count too. · Los meses malos
también cuentan. | Real trends, month over month. Drops included. · Tendencia real, mes a mes. Bajadas incluidas. | +| 7 | **GoalDetailView of a Behind goal** — clay status line "Behind · by Mar 1, 2027 · $25/wk", ETA pill "1+ year". (Goal rows only say "Behind"; the detail view proves the caption.) | full | THE PACE · EL RITMO | The date
is honest. · La fecha
no miente. | The ETA comes from real deposits. Behind is shown, not hidden. · La fecha sale de depósitos reales. Si vas atrasado, se ve. | +| 8 | **Home, dark mode** (`-darkModeEnabled YES`), same seeded state as frame 1. | full, dark | AT NIGHT · DE NOCHE | The notebook,
under a lamp. · La libreta,
de noche. | Dark mode keeps the warmth. Same calm. · El modo oscuro conserva la calidez. La misma calma. | + +Order rationale: all three researchers and the judge agree privacy is the stated +differentiator and belongs in the search trio; PPO treatment B tests 1 → 3 → 2 +(payday-first) since casual savers may respond more to the active benefit. + +**Fictional dataset (2.3.9-safe, one state for all frames; ×10 in MXN for es-MX):** +Goal A "Trip to Oaxaca / Viaje a Oaxaca" target $2,400, saved $1,010 (42%), $500 deposited +inside the last 4 weeks (GoalPace window) → $125/wk, deadline Dec 12 2026 → On track. +Goal B "Laptop" target $1,800, saved $320, $100 in the last 4 weeks → $25/wk, deadline +Mar 1 2027 → Behind, ETA 59 wk → "1+ year". Payday +$1,240, auto-move $150 to Goal A +(keep the ES auto-move under $1,000 until the banner formats thousands). Expense $7.50 +Coffee. Receipt "Corner Market" $23.80 dated the capture day. Income July > August so +the trend badge is genuinely negative. Fictional merchants only; no real names. + +--- + +## 4. Alternatives considered (kept for the record) + +- **Marginalia (the ruled notebook page).** Same page as above but with a static sprout + colophon and a "marginal clipping" (a 1.15× pixel crop of one UI row pinned over the + device corner) on task frames. Best token discipline; lost on distinctness — kicker + above serif on warm ground with a static mark is the Thirds/Monarch grammar, and the + clipping reads as a template callout at thumbnail size. Its page composition is what + we kept. +- **Honest Ledger.** Typography-led: headline + a 216px pull-figure lifted from the + screen ("$125 /wk", "0 accounts", "−12%") + a frameless full-bleed native crop of one + hero object; two dark frames (2 and 8). Its "ledger check" rule is what we kept; the + giant figures pull toward fintech "big number" energy (not Discreet), frameless UI on + cream is structurally Thirds, and date-sensitive figures make re-shoots brittle. +- Judge scores (fit / distinct / store / voice / producible): Marginalia 5·3·4·4·4 = 20, + Seed to Sprout 4·4·4·4·4 = 20, Honest Ledger 4·3·3·4·3 = 17 → tie-break on "its own + style" → Seed to Sprout on Marginalia's page. + +--- + +## 5. Prerequisites (do these before any capture) + +1. **Localization of the frames we show.** ~139 catalog entries are en-only, several on + these exact screens (Log expense/income, Recent, Expenses, Achievements, 7-month + trend, Scan a receipt), and these are hard-coded English literals: `On track` / + `Behind` / `1+ year` / `/wk` (GoalPace, GoalDetailView), `Morning/Afternoon/Evening` + (Dashboard greeting), the auto-move banner text, and date patterns + (`EEEE · MMMM d`, `MMM d, yyyy` render "mar 12, 2027" in es_MX). Also normalize the + ~23 old usted/exclamation strings to tú, and "Escanear Recibo" → "Escanear recibo". + Without this the es-MX set leaks English on frames 1, 3, 6, 7, 8. +2. **Truth gate for "No account. No cloud."** — `Savely/Config.plist` (OPENAI_API_KEY) is + in the app's Resources build phase even though tips are flagged off: remove it from + the target (CI already generates a placeholder — adjust) and rotate the key; drop the + `openai.com` ATS exception from Info.plist; delete the leftover + `GoogleService-Info.plist`; App Privacy label = "Data Not Collected". Do not write + "no network" in copy while OpenAIClient is compiled — "No account. No cloud." is as + far as the claim goes. +3. **iPad:** set `TARGETED_DEVICE_FAMILY = 1` (iPhone-only) or plan a 13" (2064×2752) + adaptation of the same page. +4. **Seed dataset behind a launch argument** (e.g. `-SavelyScreenshotSeed en|es`): wipes + and seeds SwiftData with §3's dataset, pins "today" for stable greeting/month/ETA, + marks Goal A favorite. Never Iván's real data (house rule + 2.3.9). Wipe after. +5. **Capture rig:** iPhone 17 Pro Max simulator (1320×2868 native); + `xcrun simctl status_bar booted override --time 9:41 --batteryState charged + --batteryLevel 100 --wifiBars 3 --cellularBars 4`; `-AppleLanguages (es-MX) + -AppleLocale es_MX` for the ES run; `-darkModeEnabled YES` for frame 8; + `SavelyUITests/StoreScreenshotTourUITests` (already in the repo) as the driver — + extend it to the exact frames and read `XCTAttachment`s out with + `xcrun xcresulttool export attachments`. Capture in the submission week so month + headers are true; never edit pixels inside a capture. + +--- + +## 6. Production pipeline (for the Opus session) + +1. **Composer:** one HTML template (`Design/ScreenshotKit/page.html`, 1320×2868) with + the tokens as CSS variables, `data-theme="light|dark"`, `copy.json` keyed by locale + and frame, `stage` 1–8 selecting the SproutMark SVG (traced 1:1 from + SproutMark.swift: stem `M60 87 C52 76 68 68 60 56`, seed r9 at (60,92), the two leaf + curves; stem stages via `stroke-dasharray`, leaf stages via scale about (60,56)). + Device = ink bezel div with `overflow:hidden` and the capture at ×0.72. +2. **Fonts:** render on macOS WebKit so `font-family: ui-serif` → New York and + `-apple-system` → SF Pro (nothing embedded, nothing licensed for web). Playwright: + `npx playwright install webkit`; `page.setViewportSize({width:1320,height:2868})`, + `deviceScaleFactor: 1`, `page.screenshot({type:'png'})`. Check New York's optical + size at 136px against a SwiftUI reference (`.font(.system(size: 45, design: .serif))` + at 3×); if WebKit picks a spindlier cut, use the alternative: a tiny SwiftUI + `ScreenshotComposer` target with `.warmFont`, `Color+Warm` and `SproutMark`, exported + with `ImageRenderer` at 1320×2868 — same result, zero web fonts. Stand-ins for + off-Mac previews only (Fraunces opsz 144 / Newsreader Regular + Inter) — never + sign off line breaks on them. +3. **Export:** 8 PNGs per locale, 1320×2868, sRGB, **no alpha** (`sips -g hasAlpha`; + flatten with `-background '#f6f4ee' -alpha remove` if needed), named + `savely-en-US-01-goal.png` … `savely-es-MX-08-night.png`. Upload only the 6.9" slot + for English (U.S.) and Spanish (Mexico). +4. **Checks before upload:** 25% zoom legibility in both languages; ES two-liners + ("Los meses malos / también cuentan.", "La libreta, / de noche.") hold at ≥128px; + hairlines visible in the ASC preview (go 4px, never darker); ledger check on every + frame; no real merchant/person data; no text cut by the bottom crop. +5. **Later (PPO):** treatment B = full-dark set (all 8 under the lamp; template flips + with `data-theme`); treatment C = payday-first order (1 → 3 → 2). Keep the header + grammar ready for the iOS 27 Creative Assets header. + +--- + +## 7. Open questions for Iván (non-blocking) + +- Keep iPad support (then an iPad set) or go iPhone-only for the App Store release? +- Frame 2: Profile → Data & privacy (real settings, safest under 2.3.3) vs the + onboarding "Yours, and only yours" page (prettier, has the sprout) — recommend + Profile in v1, onboarding as the PPO alternate. +- Any objection to the drawn warm-ink bezel (vs Apple's silver product bezel)? The ink + bezel keeps "warmth by type and tint, not chrome" and is the same on both themes. From aecaeec09816cb75dbfcb7d543386873bb1dc581 Mon Sep 17 00:00:00 2001 From: Belli Date: Tue, 18 Aug 2026 08:27:36 -0700 Subject: [PATCH 2/2] =?UTF-8?q?chore(store):=20screenshot=20kit=20?= =?UTF-8?q?=E2=80=94=20seeded=20tour,=20composer=20and=20the=206.9"=20set?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The App Store set from docs/plans/app-store-screenshots.md, produced end to end from real screens: - ScreenshotSeed (DEBUG, -SavelyScreenshotSeed) wipes the Simulator store and seeds the fictional dataset the captions have to prove: a goal on track with a real $125/wk pace, a second goal honestly behind with a "1+ year" ETA, a paycheck that offers an auto-move, and a month-over-month drop in the trend. - StoreScreenshotTourUITests walks the eight screens and attaches the raw 1320×2868 captures; Design/ScreenshotKit/collect.sh renames them. - Design/ScreenshotKit/compose.swift renders each page (warm notebook ground, green tracked kicker on a hairline rule, regular New York headline, the app as a flat photo in a warm-ink bezel, SproutMark growing one stage per page) with SwiftUI ImageRenderer, so the type is the real New York / SF Pro. - TARGETED_DEVICE_FAMILY = 1: iPhone-only for this release, so no iPad set is required. --- Design/ScreenshotKit/README.md | 50 ++++ Design/ScreenshotKit/collect.sh | 20 ++ Design/ScreenshotKit/compose.swift | 235 ++++++++++++++++++ .../fixtures/receipt-corner-market.png | Bin 0 -> 70001 bytes Savely.xcodeproj/project.pbxproj | 16 +- Savely/Utilities/ScreenshotSeed.swift | 106 ++++++++ Savely/Views/SplashScreenView.swift | 15 ++ .../StoreScreenshotTourUITests.swift | 146 ++++++++--- docs/plans/app-store-screenshots.md | 24 +- 9 files changed, 567 insertions(+), 45 deletions(-) create mode 100644 Design/ScreenshotKit/README.md create mode 100755 Design/ScreenshotKit/collect.sh create mode 100644 Design/ScreenshotKit/compose.swift create mode 100644 Design/ScreenshotKit/fixtures/receipt-corner-market.png create mode 100644 Savely/Utilities/ScreenshotSeed.swift diff --git a/Design/ScreenshotKit/README.md b/Design/ScreenshotKit/README.md new file mode 100644 index 0000000..ed373db --- /dev/null +++ b/Design/ScreenshotKit/README.md @@ -0,0 +1,50 @@ +# ScreenshotKit — App Store screenshots + +Produces the eight 6.9" (1320×2868) App Store pages described in +[docs/plans/app-store-screenshots.md](../../docs/plans/app-store-screenshots.md): +warm notebook page, green tracked kicker on a hairline rule, regular New York +headline, the real app as a flat photo in a warm-ink bezel, and the SproutMark +growing one stage per page. + +## 1. Capture the raw screens + +```bash +UD=$(xcrun simctl list devices available | grep -m1 "iPhone 17 Pro Max" | grep -oE "[0-9A-F-]{36}") +xcrun simctl boot "$UD" +xcrun simctl status_bar "$UD" override --time 9:41 --batteryState charged \ + --batteryLevel 100 --wifiBars 3 --cellularBars 4 +# frame 5 needs a fictional receipt in the photo library: +xcrun simctl addmedia "$UD" Design/ScreenshotKit/fixtures/receipt-corner-market.png + +xcodebuild test -project Savely.xcodeproj -scheme Savely \ + -destination "platform=iOS Simulator,id=$UD" \ + -only-testing:SavelyUITests/StoreScreenshotTourUITests \ + -parallel-testing-enabled NO -resultBundlePath build/Tour.xcresult +xcrun xcresulttool export attachments --path build/Tour.xcresult --output-path build/tour +Design/ScreenshotKit/collect.sh build/tour build/captures +``` + +The tour launches the app with `-SavelyScreenshotSeed` (DEBUG only, +`Savely/Utilities/ScreenshotSeed.swift`), which **wipes** the Simulator store and +seeds the fictional dataset — never run it against a device with real data. + +## 2. Compose the pages + +```bash +swift Design/ScreenshotKit/compose.swift build/captures build/store +``` + +Renders with SwiftUI's `ImageRenderer` on macOS, so the type is the real +New York / SF Pro (nothing embedded, nothing licensed for the web). Copy lives +in `compose.swift` — one file, one source of truth. + +## 3. Before upload + +```bash +sips -g hasAlpha build/store/*.png # must be "no"; flatten if not: +magick in.png -background '#f6f4ee' -alpha remove -alpha off out.png +``` + +Upload only the **6.9" slot** (Apple scales every smaller iPhone from it); +the project is iPhone-only (`TARGETED_DEVICE_FAMILY = 1`), so no iPad set is +required. Run the 25% zoom legibility check before submitting. diff --git a/Design/ScreenshotKit/collect.sh b/Design/ScreenshotKit/collect.sh new file mode 100755 index 0000000..bc31893 --- /dev/null +++ b/Design/ScreenshotKit/collect.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Renames the XCTest attachments exported by `xcresulttool export attachments` +# into the slugs compose.swift expects. +# collect.sh +set -euo pipefail +IN="${1:?usage: collect.sh }" +OUT="${2:?usage: collect.sh }" +mkdir -p "$OUT" +python3 - "$IN" "$OUT" <<'PY' +import json, os, shutil, sys +src, out = sys.argv[1], sys.argv[2] +manifest = json.load(open(os.path.join(src, "manifest.json"))) +for test in manifest: + for att in test.get("attachments", []): + name = att.get("suggestedHumanReadableName", "") + if name[:2].isdigit(): + slug = name.split("_")[0] + shutil.copy(os.path.join(src, att["exportedFileName"]), os.path.join(out, slug + ".png")) + print(slug) +PY diff --git a/Design/ScreenshotKit/compose.swift b/Design/ScreenshotKit/compose.swift new file mode 100644 index 0000000..f3b045f --- /dev/null +++ b/Design/ScreenshotKit/compose.swift @@ -0,0 +1,235 @@ +#!/usr/bin/env swift +// +// compose.swift — Savely App Store screenshot composer +// +// Renders the eight 6.9" pages (1320×2868) of docs/plans/app-store-screenshots.md +// from raw Simulator captures, using the real system fonts (New York / SF Pro) +// through SwiftUI's ImageRenderer on macOS. Nothing is embedded, nothing is +// redrawn inside the captures — the screenshot is placed, scaled and clipped. +// +// swift Design/ScreenshotKit/compose.swift +// +// Captures expected in (from StoreScreenshotTourUITests): +// 01-goal-on-track.png 02-privacy.png 03-payday.png 04-expense.png +// 05-receipt-review.png 06-month-trend.png 07-goal-behind.png 08-home-dark.png +// Output: savely-en-US-01-goal.png … savely-en-US-08-night.png (RGBA; flatten to +// RGB before upload — see Design/ScreenshotKit/README.md). +// + +import AppKit +import SwiftUI + +// MARK: - Tokens (DESIGN.md) + +struct Theme { + let bg, ink, inkSoft, green, line, bezel, bezelLine: Color + let leafPrimary, leafSecondary, seed: Color + static let light = Theme( + bg: hex(0xF6F4EE), ink: hex(0x1A1A17), inkSoft: hex(0x55524C), green: hex(0x2F6B4A), + line: Color(red: 30 / 255, green: 25 / 255, blue: 15 / 255).opacity(0.08), + bezel: hex(0x1A1A17), bezelLine: .clear, + leafPrimary: hex(0x2F6B4A), leafSecondary: hex(0x37795A), seed: hex(0xA8741C) + ) + static let dark = Theme( + bg: hex(0x161512), ink: hex(0xF2EFE7), inkSoft: hex(0xC6C1B5), green: hex(0x78B58F), + line: Color(red: 242 / 255, green: 239 / 255, blue: 231 / 255).opacity(0.10), + bezel: hex(0x262420), bezelLine: Color(red: 242 / 255, green: 239 / 255, blue: 231 / 255).opacity(0.10), + leafPrimary: hex(0x78B58F), leafSecondary: hex(0x5D9C7C), seed: hex(0xDEA64A) + ) + static func hex(_ v: UInt32) -> Color { + Color(red: Double((v >> 16) & 0xFF) / 255, green: Double((v >> 8) & 0xFF) / 255, blue: Double(v & 0xFF) / 255) + } +} + +// MARK: - Frames (copy lives here on purpose: one file, one source of truth) + +struct Frame { + let n: Int, slug: String, capture: String, kicker: String, headline: String, sub: String + let stage: Int // sprout growth stage 1…8 + let dark: Bool +} + +let frames: [Frame] = [ + Frame(n: 1, slug: "goal", capture: "01-goal-on-track", kicker: "Savings", + headline: "Your goal.\nA real pace.", + sub: "On track or behind — from what you actually put in.", stage: 1, dark: false), + Frame(n: 2, slug: "privacy", capture: "02-privacy", kicker: "Your data", + headline: "No account.\nNo cloud.", + sub: "Your money data lives on your iPhone and never leaves it.", stage: 2, dark: false), + Frame(n: 3, slug: "payday", capture: "03-payday", kicker: "Payday", + headline: "Save before\nyou spend.", + sub: "Log your pay, tap Yes, and part of it goes to your goal.", stage: 3, dark: false), + Frame(n: 4, slug: "expense", capture: "04-expense", kicker: "Everyday money", + headline: "Log it in\nseconds.", + sub: "Amount, a category, done. The rest is optional.", stage: 4, dark: false), + Frame(n: 5, slug: "receipt", capture: "05-receipt-review", kicker: "Receipts", + headline: "Scan it.\nIt stays here.", + sub: "Total, merchant, date — read on your iPhone, never uploaded.", stage: 5, dark: false), + Frame(n: 6, slug: "month", capture: "06-month-trend", kicker: "The month", + headline: "Bad months\ncount too.", + sub: "Real trends, month over month. Drops included.", stage: 6, dark: false), + Frame(n: 7, slug: "pace", capture: "07-goal-behind", kicker: "The pace", + headline: "The date\nis honest.", + sub: "The ETA comes from real deposits. Behind is shown, not hidden.", stage: 7, dark: false), + Frame(n: 8, slug: "night", capture: "08-home-dark", kicker: "At night", + headline: "The notebook,\nunder a lamp.", + sub: "Dark mode keeps the warmth. Same calm.", stage: 8, dark: true), +] + +// MARK: - Sprout (SproutMark.swift geometry, 120-unit space) + +func pt(_ x: CGFloat, _ y: CGFloat, _ r: CGRect) -> CGPoint { + CGPoint(x: r.minX + x / 120 * r.width, y: r.minY + y / 120 * r.height) +} +struct StemShape: Shape { + func path(in r: CGRect) -> Path { + var p = Path(); p.move(to: pt(60, 87, r)) + p.addCurve(to: pt(60, 56, r), control1: pt(52, 76, r), control2: pt(68, 68, r)); return p + } +} +struct SeedShape: Shape { + func path(in r: CGRect) -> Path { + let c = pt(60, 92, r); let rad = 9 / 120 * r.width + return Path(ellipseIn: CGRect(x: c.x - rad, y: c.y - rad, width: rad * 2, height: rad * 2)) + } +} +struct LeafShape: Shape { + let left: Bool + func path(in r: CGRect) -> Path { + var p = Path(); p.move(to: pt(60, 56, r)) + if left { + p.addCurve(to: pt(36, 26, r), control1: pt(42, 54, r), control2: pt(34, 40, r)) + p.addCurve(to: pt(60, 56, r), control1: pt(52, 28, r), control2: pt(60, 42, r)) + } else { + p.addCurve(to: pt(82, 28, r), control1: pt(74, 52, r), control2: pt(82, 40, r)) + p.addCurve(to: pt(60, 56, r), control1: pt(68, 30, r), control2: pt(60, 42, r)) + } + p.closeSubpath(); return p + } +} + +/// Growth stages: 1 seed · 2 stem 35 % · 3 stem 100 % · 4 + left leaf 45 % · +/// 5 left leaf 100 % · 6 + right leaf 45 % · 7 full · 8 full (dark tokens). +struct Sprout: View { + let stage: Int, theme: Theme, size: CGFloat + var stem: CGFloat { stage <= 1 ? 0 : (stage == 2 ? 0.35 : 1) } + var leftLeaf: CGFloat { stage <= 3 ? 0 : (stage == 4 ? 0.45 : 1) } + var rightLeaf: CGFloat { stage <= 5 ? 0 : (stage == 6 ? 0.45 : 1) } + var body: some View { + let tip = UnitPoint(x: 60 / 120, y: 56 / 120) + ZStack { + StemShape().trim(from: 0, to: stem) + .stroke(theme.leafPrimary, style: StrokeStyle(lineWidth: size * 5.5 / 120, lineCap: .round)) + SeedShape().fill(theme.seed) + LeafShape(left: true).fill(theme.leafPrimary).scaleEffect(leftLeaf, anchor: tip) + LeafShape(left: false).fill(theme.leafSecondary).scaleEffect(rightLeaf, anchor: tip) + } + .frame(width: size, height: size) + } +} + +// MARK: - Page + +let W: CGFloat = 1320, H: CGFloat = 2868 +let margin: CGFloat = 96 +let ruleY: CGFloat = 192 +let sproutSize: CGFloat = 240 +let bezelWidth: CGFloat = 1002, bezelInset: CGFloat = 26, bezelTop: CGFloat = 980 +let screenWidth: CGFloat = bezelWidth - 2 * bezelInset // 950 +let screenHeight: CGFloat = screenWidth * H / W // 2064 +let bezelRadius: CGFloat = 160, screenRadius: CGFloat = 134 + +struct Page: View { + let frame: Frame + let shot: NSImage + var theme: Theme { frame.dark ? .dark : .light } + + var body: some View { + ZStack(alignment: .topLeading) { + theme.bg + + // Header rule (full bleed) — the one "ruled paper" cue. + Rectangle().fill(theme.line).frame(width: W, height: 3).offset(y: ruleY) + + // Kicker — the app's own tracked label, in green as ink. + Text(frame.kicker.uppercased()) + .font(.system(size: 36, weight: .semibold)) + .tracking(3.6) + .foregroundStyle(theme.green) + .offset(x: margin, y: 118) + + // Growth mark: seed tangent to the rule, right-aligned to the margin. + Sprout(stage: frame.stage, theme: theme, size: sproutSize) + .offset(x: W - margin - sproutSize, y: ruleY - sproutSize * 101 / 120 + 1.5) + + // Headline + sub + VStack(alignment: .leading, spacing: 28) { + Text(frame.headline) + .font(.system(size: 136, weight: .regular, design: .serif)) + .foregroundStyle(theme.ink) + .lineSpacing(-4) + .fixedSize(horizontal: false, vertical: true) + .frame(width: 1128, alignment: .leading) + Text(frame.sub) + .font(.system(size: 46, weight: .regular)) + .foregroundStyle(theme.inkSoft) + .lineSpacing(10) + .fixedSize(horizontal: false, vertical: true) + .frame(width: 980, alignment: .leading) + } + .offset(x: margin, y: 236) + + // The photo: flat, untilted, tucked under the page edge. + device + .offset(x: margin, y: bezelTop) + } + .frame(width: W, height: H) + .clipped() + } + + var device: some View { + ZStack(alignment: .top) { + RoundedRectangle(cornerRadius: bezelRadius, style: .continuous) + .fill(theme.bezel) + .overlay(RoundedRectangle(cornerRadius: bezelRadius, style: .continuous).stroke(theme.bezelLine, lineWidth: 3)) + Image(nsImage: shot) + .resizable() + .interpolation(.high) + .frame(width: screenWidth, height: screenHeight) + .clipShape(RoundedRectangle(cornerRadius: screenRadius, style: .continuous)) + .offset(y: bezelInset) + // Dynamic Island as part of the bezel silhouette. + Capsule().fill(theme.bezel).frame(width: 270, height: 80).offset(y: bezelInset + 18) + } + .frame(width: bezelWidth, height: screenHeight + 2 * bezelInset + 200, alignment: .top) + } +} + +// MARK: - Main + +let args = CommandLine.arguments +guard args.count >= 3 else { + FileHandle.standardError.write(Data("usage: compose.swift \n".utf8)) + exit(2) +} +let capturesDir = URL(fileURLWithPath: args[1]) +let outDir = URL(fileURLWithPath: args[2]) +try? FileManager.default.createDirectory(at: outDir, withIntermediateDirectories: true) + +MainActor.assumeIsolated { + for frame in frames { + let shotURL = capturesDir.appendingPathComponent(frame.capture + ".png") + guard let shot = NSImage(contentsOf: shotURL) else { + print("!! missing capture \(shotURL.path)"); continue + } + let renderer = ImageRenderer(content: Page(frame: frame, shot: shot)) + renderer.scale = 1 + renderer.proposedSize = ProposedViewSize(width: W, height: H) + guard let cg = renderer.cgImage else { print("!! render failed \(frame.n)"); continue } + let rep = NSBitmapImageRep(cgImage: cg) + guard let png = rep.representation(using: .png, properties: [:]) else { continue } + let name = String(format: "savely-en-US-%02d-%@.png", frame.n, frame.slug) + let out = outDir.appendingPathComponent(name) + do { try png.write(to: out); print("wrote \(name) \(cg.width)x\(cg.height)") } catch { print("!! \(error)") } + } +} diff --git a/Design/ScreenshotKit/fixtures/receipt-corner-market.png b/Design/ScreenshotKit/fixtures/receipt-corner-market.png new file mode 100644 index 0000000000000000000000000000000000000000..d792b62ed23cbb921da0fe0e87c8259f0acb6505 GIT binary patch literal 70001 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�QayUaUBY(V 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 H`tsiZcP_m? literal 0 HcmV?d00001 diff --git a/Savely.xcodeproj/project.pbxproj b/Savely.xcodeproj/project.pbxproj index 1561a15..ea1fb0c 100644 --- a/Savely.xcodeproj/project.pbxproj +++ b/Savely.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 120AE56C41CBD54B070A8436 /* ScreenshotSeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11E18C2383669F09FCC4748 /* ScreenshotSeed.swift */; }; F25F23C6EEAA47853BFA137D /* StoreScreenshotTourUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D0412010EDA0CEB30538B1 /* StoreScreenshotTourUITests.swift */; }; C5DDD46B40453371BD90C01A /* ReceiptScanUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C696EE6BA0906CAD429D0881 /* ReceiptScanUITests.swift */; }; 7479BDFD4C96ED1C9E05A119 /* ReceiptOCRIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A7AF8AF4BCB197FCA023DC6 /* ReceiptOCRIntegrationTests.swift */; }; @@ -121,6 +122,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + F11E18C2383669F09FCC4748 /* ScreenshotSeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotSeed.swift; sourceTree = ""; }; 46D0412010EDA0CEB30538B1 /* StoreScreenshotTourUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreScreenshotTourUITests.swift; sourceTree = ""; }; C696EE6BA0906CAD429D0881 /* ReceiptScanUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptScanUITests.swift; sourceTree = ""; }; 4A7AF8AF4BCB197FCA023DC6 /* ReceiptOCRIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptOCRIntegrationTests.swift; sourceTree = ""; }; @@ -306,6 +308,7 @@ 1D88B7D62CC9FA28009B6538 /* Utilities */ = { isa = PBXGroup; children = ( + F11E18C2383669F09FCC4748 /* ScreenshotSeed.swift */, D2C90599A224DF05C1C7BC95 /* ReceiptOCR.swift */, 5A440101E954A364F752379E /* Receipt */, F774124079B5BEC3DD37C743 /* AutoMoveSuggestion.swift */, @@ -739,6 +742,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 120AE56C41CBD54B070A8436 /* ScreenshotSeed.swift in Sources */, 31A69355DA4D561A928D09D7 /* DocumentCameraView.swift in Sources */, 15C9F87752F242860A062A9C /* ReceiptScanFlowView.swift in Sources */, 6BEBD87A257D196FC2B5F572 /* ReceiptReviewView.swift in Sources */, @@ -1028,7 +1032,7 @@ SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -1064,7 +1068,7 @@ SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; @@ -1086,7 +1090,7 @@ SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Savely.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Savely"; }; name = Debug; @@ -1109,7 +1113,7 @@ SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Savely.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Savely"; }; name = Release; @@ -1130,7 +1134,7 @@ SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; TEST_TARGET_NAME = Savely; }; name = Debug; @@ -1151,7 +1155,7 @@ SUPPORTS_MACCATALYST = YES; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; TEST_TARGET_NAME = Savely; }; name = Release; diff --git a/Savely/Utilities/ScreenshotSeed.swift b/Savely/Utilities/ScreenshotSeed.swift new file mode 100644 index 0000000..1100831 --- /dev/null +++ b/Savely/Utilities/ScreenshotSeed.swift @@ -0,0 +1,106 @@ +// +// ScreenshotSeed.swift +// Savely +// +// DEBUG-only fictional dataset for App Store screenshots. Applied once at +// launch when the app is started with `-SavelyScreenshotSeed` (see +// docs/plans/app-store-screenshots.md §3 for why these exact figures: every +// caption in the store must be provable on the screen it sits over). +// +// It WIPES the local store first — it exists for the Simulator only and is +// compiled out of Release builds. Never run it against real data. +// + +#if DEBUG +import Foundation +import SwiftData + +enum ScreenshotSeed { + static let launchArgument = "-SavelyScreenshotSeed" + + static var isRequested: Bool { + CommandLine.arguments.contains(launchArgument) + } + + /// Replaces everything in `context` with the screenshot dataset. + @MainActor + static func apply(to context: ModelContext, now: Date = Date(), calendar: Calendar = .current) throws { + try context.delete(model: DepositModel.self) + try context.delete(model: GoalModel.self) + try context.delete(model: IncomeModel.self) + try context.delete(model: ExpenseModel.self) + + func daysAgo(_ days: Int, hour: Int = 12) -> Date { + let day = calendar.date(byAdding: .day, value: -days, to: now) ?? now + return calendar.date(bySettingHour: hour, minute: 15, second: 0, of: day) ?? day + } + func date(_ year: Int, _ month: Int, _ day: Int) -> Date { + calendar.date(from: DateComponents(year: year, month: month, day: day, hour: 12)) ?? now + } + + // Goal A — the favorite. $500 deposited inside GoalPace's 4-week + // window → actual pace $125/wk; deadline Dec 12 2026 → required + // ≈ $82/wk → "On track · by Dec 12, 2026 · $125/wk". + let oaxaca = GoalModel( + name: "Trip to Oaxaca", current: 1010, target: 2400, color: .sage, + isFavorite: true, autoMoveEnabled: true, autoMoveAmount: 150, + deadline: date(2026, 12, 12) + ) + context.insert(oaxaca) + for (offset, amount) in [(3, 125.0), (10, 125.0), (17, 125.0), (24, 125.0)] { + context.insert(DepositModel(goalID: oaxaca.id, amount: amount, date: daysAgo(offset), source: .autoMove)) + } + context.insert(DepositModel(goalID: oaxaca.id, amount: 510, date: daysAgo(60), source: .manual)) + + // Goal B — honestly behind. $100 in the window → $25/wk; deadline + // Mar 1 2027 → required ≈ $53/wk → "Behind"; ETA 59 wk → "1+ year". + let laptop = GoalModel( + name: "Laptop", current: 320, target: 1800, color: .blue, + isFavorite: false, autoMoveEnabled: false, autoMoveAmount: 0, + deadline: date(2027, 3, 1) + ) + context.insert(laptop) + for (offset, amount) in [(6, 50.0), (20, 50.0)] { + context.insert(DepositModel(goalID: laptop.id, amount: amount, date: daysAgo(offset), source: .manual)) + } + context.insert(DepositModel(goalID: laptop.id, amount: 220, date: daysAgo(75), source: .manual)) + + // Goal C — small, on track, no deadline. + let bike = GoalModel(name: "New bike", current: 180, target: 600, color: .yellow) + context.insert(bike) + context.insert(DepositModel(goalID: bike.id, amount: 60, date: daysAgo(9), source: .manual)) + context.insert(DepositModel(goalID: bike.id, amount: 120, date: daysAgo(40), source: .manual)) + + // Income — one paycheck per month for seven months. Last month is + // higher than this month so the 7-month trend shows a real drop. + let paychecks: [Double] = [2200, 2400, 2350, 2500, 2450, 2600, 2280] + for (i, amount) in paychecks.enumerated() { + let monthsAgo = paychecks.count - 1 - i + let base = calendar.date(byAdding: .month, value: -monthsAgo, to: now) ?? now + var comps = calendar.dateComponents([.year, .month], from: base) + comps.day = min(12, calendar.component(.day, from: now)) // never in the future + comps.hour = 9 + let payday = calendar.date(from: comps) ?? base + context.insert(IncomeModel(incomeDescription: "Paycheck", amount: amount, date: payday, source: "Paycheck")) + } + context.insert(IncomeModel(incomeDescription: "Freelance", amount: 340, date: daysAgo(19), source: "Freelance")) + + // Expenses — this month, small and ordinary. Merchants are fictional. + let expenses: [(String, Double, Int, ExpenseCategory)] = [ + ("Café Norte", 7.50, 0, .coffee), + ("Corner Market", 23.80, 1, .food), + ("Bus pass", 14.00, 2, .transit), + ("Pharmacy", 32.10, 4, .shopping), + ("Café Norte", 6.75, 5, .coffee), + ("Taquería del Sol", 18.40, 6, .food), + ("Corner Market", 41.25, 9, .food), + ("Movie night", 24.00, 12, .other), + ] + for (name, amount, offset, category) in expenses { + context.insert(ExpenseModel(expenseDescription: name, amount: amount, date: daysAgo(offset, hour: 18), category: category.rawValue)) + } + + try context.save() + } +} +#endif diff --git a/Savely/Views/SplashScreenView.swift b/Savely/Views/SplashScreenView.swift index 1380e14..43fd133 100755 --- a/Savely/Views/SplashScreenView.swift +++ b/Savely/Views/SplashScreenView.swift @@ -6,6 +6,7 @@ import SwiftUI /// sequence is skipped for Reduce Motion users (single crossfade instead). struct SplashScreenView: View { @Environment(\.accessibilityReduceMotion) private var reduceMotion + @Environment(\.modelContext) private var modelContext @State private var isSplashEnded = false // Sprout @@ -104,9 +105,23 @@ struct SplashScreenView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } .onAppear(perform: runAnimation) + .onAppear(perform: seedForScreenshotsIfRequested) } } + /// DEBUG + `-SavelyScreenshotSeed` only: replace the store with the + /// fictional App Store dataset before the main screens appear. + private func seedForScreenshotsIfRequested() { + #if DEBUG + guard ScreenshotSeed.isRequested else { return } + do { + try ScreenshotSeed.apply(to: modelContext) + } catch { + print("Screenshot seed failed: \(error)") + } + #endif + } + /// The sprout mark, drawn natively so each part can animate: /// stem below seed (matches the icon's layer order), leaves anchored /// at the stem tip (60,56 in the 120-unit design space). diff --git a/SavelyUITests/StoreScreenshotTourUITests.swift b/SavelyUITests/StoreScreenshotTourUITests.swift index 56b8aab..4a9696b 100644 --- a/SavelyUITests/StoreScreenshotTourUITests.swift +++ b/SavelyUITests/StoreScreenshotTourUITests.swift @@ -2,14 +2,19 @@ // StoreScreenshotTourUITests.swift // SavelyUITests // -// Not a test of behaviour — a screenshot tour. Walks the main tabs and the -// "+" sheet and attaches a full-resolution screenshot of each, so App -// Store artwork can be built from real screens: +// Not a test of behaviour — the App Store screenshot tour. Launches the app +// with the fictional `-SavelyScreenshotSeed` dataset, walks the eight +// frames of docs/plans/app-store-screenshots.md and attaches a full- +// resolution capture of each. Run on the iPhone 17 Pro Max simulator +// (1320×2868 native) after `xcrun simctl status_bar … override --time 9:41`: +// // xcodebuild test -scheme Savely -only-testing:SavelyUITests/StoreScreenshotTourUITests \ -// -destination 'platform=iOS Simulator,name=iPhone 17 Pro' -resultBundlePath build/Tour.xcresult +// -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \ +// -parallel-testing-enabled NO -resultBundlePath build/Tour.xcresult // xcrun xcresulttool export attachments --path build/Tour.xcresult --output-path build/tour -// Seed the Simulator with real-looking data first (goals, a few expenses/ -// incomes) — an empty app makes empty screenshots. Skipped on CI like the +// +// Frame 5 (receipt review) needs a fictional receipt image in the +// Simulator's photo library (`simctl addmedia`). Skipped on CI like the // rest of SavelyUITests. // @@ -17,48 +22,113 @@ import XCTest final class StoreScreenshotTourUITests: XCTestCase { - @MainActor - func testTourAttachesEveryMainScreen() throws { - let app = XCUIApplication() - app.launchArguments += ["-isOnboardingComplete", "YES"] + private var app: XCUIApplication = XCUIApplication() + + private func launch(dark: Bool = false) { + app = XCUIApplication() + app.launchArguments += ["-isOnboardingComplete", "YES", "-SavelyScreenshotSeed", "-darkModeEnabled", dark ? "YES" : "NO"] app.launch() + XCTAssertTrue(app.buttons["Add"].waitForExistence(timeout: 25), "the main tab bar did not appear") + } - let addButton = app.buttons["Add"] - XCTAssertTrue(addButton.waitForExistence(timeout: 20), "the main tab bar did not appear") - snap(app, "01-home") + @MainActor + func testFrame1And7GoalDetails() throws { + launch() + app.buttons["Goals"].firstMatch.tap() + // The goal card is an accessibility container, so tap its name. + let oaxaca = app.staticTexts["Trip to Oaxaca"] + XCTAssertTrue(oaxaca.waitForExistence(timeout: 8), "seeded goal missing — was -SavelyScreenshotSeed applied?") + snap("02-goals-list") + oaxaca.tap() + XCTAssertTrue(app.navigationBars["Goal"].waitForExistence(timeout: 8), "goal detail did not open") + snap("01-goal-on-track") + app.navigationBars.buttons.firstMatch.tap() - for (tab, name) in [("Goals", "02-goals"), ("Money", "03-money"), ("Me", "04-me")] { - app.buttons[tab].firstMatch.tap() - _ = app.buttons[tab].firstMatch.waitForExistence(timeout: 2) - snap(app, name) - } + let laptop = app.staticTexts["Laptop"] + XCTAssertTrue(laptop.waitForExistence(timeout: 8)) + laptop.tap() + XCTAssertTrue(app.navigationBars["Goal"].waitForExistence(timeout: 8)) + snap("07-goal-behind") + } - app.buttons["Home"].firstMatch.tap() - addButton.tap() - XCTAssertTrue(app.staticTexts["What's the move?"].waitForExistence(timeout: 5)) - snap(app, "05-plus-sheet") + @MainActor + func testFrame2Privacy() throws { + launch() + app.buttons["Me"].firstMatch.tap() + _ = app.buttons["Me"].firstMatch.waitForExistence(timeout: 2) + // Scroll to the bottom so the Data & privacy block sits high enough to + // survive the device crop in the composed page. + for _ in 0..<9 { app.swipeUp() } + XCTAssertTrue(app.staticTexts["Your data never leaves this iPhone."].waitForExistence(timeout: 5)) + snap("02-privacy") + } - // Log expense keypad - let logExpense = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Log expense'")).firstMatch - if logExpense.waitForExistence(timeout: 2) { - logExpense.tap() - _ = app.staticTexts["Log expense"].waitForExistence(timeout: 3) - for key in ["2", "4", "5"] { app.buttons[key].firstMatch.tap() } - snap(app, "06-log-expense") - app.buttons["Back"].firstMatch.tap() - } + @MainActor + func testFrame3PaydayAndFrame4Expense() throws { + launch() + app.buttons["Add"].tap() + let income = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Log income'")).firstMatch + XCTAssertTrue(income.waitForExistence(timeout: 5)) + income.tap() + XCTAssertTrue(app.staticTexts["Log income"].waitForExistence(timeout: 5)) + for key in ["1", "2", "4", "0"] { app.buttons[key].firstMatch.tap() } + _ = app.buttons["YES"].waitForExistence(timeout: 3) + snap("03-payday") + app.buttons["Back"].firstMatch.tap() + + let expense = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Log expense'")).firstMatch + XCTAssertTrue(expense.waitForExistence(timeout: 5)) + expense.tap() + XCTAssertTrue(app.staticTexts["Log expense"].waitForExistence(timeout: 5)) + for key in ["7", "Decimal point", "5", "0"] { app.buttons[key].firstMatch.tap() } + snap("04-expense") + } - // Scanner (Simulator: no camera → unavailable card, still shows the surface) + @MainActor + func testFrame5ReceiptReview() throws { + launch() + app.buttons["Add"].tap() let scan = app.buttons.matching(NSPredicate(format: "label BEGINSWITH 'Scan a receipt'")).firstMatch - if scan.waitForExistence(timeout: 2) { - scan.tap() - _ = app.buttons["Choose from Photos"].waitForExistence(timeout: 5) - snap(app, "07-scan-camera") - app.buttons["Close"].firstMatch.tap() + XCTAssertTrue(scan.waitForExistence(timeout: 5)) + scan.tap() + let picker = app.buttons["Choose from Photos"] + XCTAssertTrue(picker.waitForExistence(timeout: 8)) + picker.tap() + let photo = app.images.matching(NSPredicate(format: "label BEGINSWITH 'Photo'")).firstMatch + guard photo.waitForExistence(timeout: 60) else { + throw XCTSkip("no receipt photo in the Simulator library — add one with simctl addmedia") } + photo.tap() + XCTAssertTrue(app.staticTexts["Check the receipt"].waitForExistence(timeout: 30)) + snap("05-receipt-review") + } + + @MainActor + func testFrame6MonthTrend() throws { + launch() + app.buttons["Money"].firstMatch.tap() + let income = app.buttons["Income"].firstMatch + XCTAssertTrue(income.waitForExistence(timeout: 8)) + income.tap() + let trend = app.staticTexts.matching(NSPredicate(format: "label CONTAINS[c] '7-month trend'")).firstMatch + XCTAssertTrue(trend.waitForExistence(timeout: 8), "income trend not found") + snap("06-month-trend") + app.buttons["Expenses"].firstMatch.tap() + _ = app.staticTexts["Expenses"].waitForExistence(timeout: 3) + snap("06b-expenses") + } + + @MainActor + func testFrame8HomeDarkAndHomeLight() throws { + launch(dark: true) + _ = app.staticTexts["Recent"].waitForExistence(timeout: 5) + snap("08-home-dark") + launch(dark: false) + _ = app.staticTexts["Recent"].waitForExistence(timeout: 5) + snap("00-home-light") } - private func snap(_ app: XCUIApplication, _ name: String) { + private func snap(_ name: String) { let attachment = XCTAttachment(screenshot: app.screenshot()) attachment.name = name attachment.lifetime = .keepAlways diff --git a/docs/plans/app-store-screenshots.md b/docs/plans/app-store-screenshots.md index e46ff09..70940bf 100644 --- a/docs/plans/app-store-screenshots.md +++ b/docs/plans/app-store-screenshots.md @@ -1,7 +1,7 @@ # App Store screenshots — direction & production plan **Date:** 2026-08-17 -**Status:** direction chosen (this doc); production is the next job (Opus, ultracode). +**Status:** direction chosen and PRODUCED — the en-US set is rendered (`Design/ScreenshotKit`, output in `build/upload/`). es-MX set deferred (owner: English-only for this release). **Owner decision needed:** none blocking — the choices below are recommendations with the alternatives kept; flip anything before production starts. @@ -208,3 +208,25 @@ the trend badge is genuinely negative. Fictional merchants only; no real names. Profile in v1, onboarding as the PPO alternate. - Any objection to the drawn warm-ink bezel (vs Apple's silver product bezel)? The ink bezel keeps "warmth by type and tint, not chrome" and is the same on both themes. + +--- + +## 8. Produced (2026-08-18) + +`Design/ScreenshotKit/` renders the set end to end: + +- `Savely/Utilities/ScreenshotSeed.swift` (DEBUG, `-SavelyScreenshotSeed`) wipes the + Simulator store and seeds the fictional dataset of §3 — Trip to Oaxaca 42 % + "On track · by Dec 12, 2026 · $125/wk"; Laptop 17 % "Behind · by Mar 1, 2027 · + $25/wk" with ETA "1+ year"; a $1,240 paycheck offering a $150 auto-move; a + −22 % month-over-month drop in the 7-month trend; fictional merchants only. +- `SavelyUITests/StoreScreenshotTourUITests` drives the eight screens and attaches + the raw 1320×2868 captures; `collect.sh` renames them. +- `compose.swift` renders each page with SwiftUI `ImageRenderer` on macOS, so the + type is the real New York / SF Pro. Copy for all eight frames lives in that file. +- Output flattened to alpha-free sRGB in `build/upload/` (8 × 1320×2868 PNG, + 138–309 KB). Upload to the 6.9" slot only; Apple scales the rest. + +Deferred: the es-MX set (needs the ~139 en-only strings and the hard-coded +`On track`/`Behind`/`1+ year`/`/wk`/greeting/date-pattern literals localized +first), the full-dark PPO treatment, and the payday-first order test.