Skip to content

Get Spellbreak App Store ready: trial, smart breaks, every display - #1

Merged
pibulus merged 13 commits into
mainfrom
claude/laughing-lamport-qihyhl
Oct 4, 2026
Merged

pibulus merged 13 commits into
mainfrom
claude/laughing-lamport-qihyhl

Conversation

@pibulus

@pibulus pibulus commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Summary

Everything needed to submit Spellbreak to the Mac App Store, plus the easy wins from the competitor comparison. Main's Oct 1–2 copy changes are already merged in (main's plainer voice wins on words), so this merges cleanly.

Would have blocked submission

  • MAS signing: app ID + team ID entitlements (ITMS-90886), Xcode toolchain keys stamped into Info.plist, fail-fast preflight (profile, identities, universal binary), no --deep.
  • App icon: the 2x-oversized iconset regression (an icns with 6 of 10 reps) fixed in the PNGs and in the generator. The icon now sits on the macOS grid, so Tahoe doesn't shrink it onto a grey tile.
  • Store metadata: no prices in copy or screenshots (2.3.7), keywords within 100 characters, every screenshot shows the app, images cropped not stretched.

Selling

  • Free download → 7-day trial (Apple's sanctioned $0 "7-Day Trial" in-app purchase) → one-time Unlock, US$9.99 / A$12.99. App Store builds only (-DAPP_STORE); the website build is untouched. Only scheduled breaks are gated.

Behaviour

  • Breaks cover every display: the interactive break on the pointer's screen, quiet mirrors on the rest.
  • Sleep and time away count as a break, so a break no longer lands the moment you sit down.
  • A due break waits for a pause in typing (30 seconds at most).
  • Today's break count in both menus; Settings fits small screens; messages notice skips and don't repeat; full-moon lines on the right days; a VoiceOver skip action.

Docs & tooling

  • APP_STORE.md (paste-ready listing, IAP table, review notes) and APP_STORE_SUBMISSION.md (walkthrough and TestFlight checklist); privacy policy updated.
  • CI on GitHub's macOS runners: website and App Store builds with Xcode 26.3 and 16.4, bundle checks (universal binary, plist, signature, all ten icon reps), the ScreenBusy check, and the card generator.

Test plan

  • CI green: run 2: Xcode 26.3 and 16.4, website and App Store builds, bundle checks
  • TestFlight checklist in APP_STORE_SUBMISSION.md §8, for what CI can't reach: the sandboxed mic check, StoreKit sandbox purchases, multiple displays, sleep/away/typing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf


Generated by Claude Code

claude added 13 commits October 3, 2026 21:44
build-mas.sh was one upload away from bouncing:
- Signed with the sandbox-only entitlements, so the signature never carried
  com.apple.application-identifier / team-identifier that the provisioning
  profile names (ITMS-90886, TestFlight-ineligible). New
  Spellbreak.mas.entitlements adds both; Developer ID builds keep the old file.
- `swift build` writes none of the DT* keys App Store Connect reads to decide
  which Xcode/SDK built the app. Stamp them from the Xcode that did the build,
  and refuse to run on Command Line Tools alone.
- Preflight before the two-arch build: profile present, for this app ID, not a
  development profile; both signing identities in the keychain.
- Refuse a non-universal binary (build-app.sh silently falls back to arm64).
- Drop --deep, verify strictly, prove the app ID landed in the signature,
  check the pkg signature.

Info.plist gains NSHighResolutionCapable (run.sh had it, the real plist didn't)
and CFBundleSupportedPlatforms. The committed AppIcon.icns was a stale 6-of-10
rep build that nothing copied — build-app.sh compiles the iconset — so it goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
3bbf4e3 re-ran generate-icon.swift and brought back the bug 9e222a2 fixed:
every iconset PNG at double its slot size, so iconutil built a 6-of-10 rep icns
with no true 16px or 128px. The generator itself was never fixed — it still
rendered through lockFocus at the screen's backing scale. Now it draws straight
into exact-size bitmaps, vector-rendered at every size.

The art also filled the whole 1024 canvas edge to edge. On macOS 13–15 that
reads ~24% bigger than every other icon in the Dock; on Tahoe, pixels outside
the squircle get the icon shrunk onto a grey tile. Same artwork now sits on the
grid: continuous-corner body inside the 824pt slot (drawn at 816 so nothing
pokes past the mask), transparent margin.

PNGs regenerated from the existing 2048px master, checked per size: exact
dimensions, zero alpha outside the 824 body, no holes inside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
The store copy and About tab promised things the generator didn't do:
- "Skip a few and it notices, gently" — generateMessage was handed skip count,
  break count and time since the last break, and ignored all three. Now a few
  skips, a long stretch without a break, or a clean run of breaks each add a
  small set of noticing lines (35% when they apply), in the same voice: noticed,
  never scolded.
- "Never the same line twice" — lines were drawn with replacement; ~9% of breaks
  repeated a line from the same day, sometimes back to back. The last 60 lines
  are now remembered and skipped, re-drawing inside the rolled mode so the
  30/30/20/20 mix holds (a longer memory starves the 18 observations). Noticing
  lines never repeat within 24 breaks. Simulated: zero repeats inside 60 breaks.
- "A full moon shows off a little" — the full-moon window sat at day 18–20 of
  the cycle, so "Full moon pull" showed four days after the full moon; new moon
  missed the end of the cycle. Checked against known 2024/2025 full and new moons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
…ll screens

- Sleep past the interval (lid shut over lunch, overnight) and both the overdue
  break timer and the 1s status tick fired the moment the Mac woke — straight
  into a break after hours away from the screen. Wall clock keeps running
  through sleep and systemUptime doesn't; when the gap is at least a break long,
  the interval restarts instead. Whichever tick runs first after the wake
  absorbs it; startTimer resets the baseline so a paused gap never counts.
- Paused countdown kept its frozen number until a wake zeroed it on screen.
- Settings is ~810pt tall and not resizable — taller than the usable height of
  a 13" M1 Air at default scaling with the Dock showing, so Test Break sat
  under the Dock. Sized to the screen now, scrolling only when it has to.
- "Not While Fullscreen" also governs call detection; renamed Smart Pauses,
  subtitle says calls.
- Preferences → Settings (macOS 13+ naming) in the window title and menus.
- Plain menu's Pause reset the interval instead of pausing (toggleTimer now).
- VoiceOver gets a "Skip break" action — it can't press-and-hold the ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
APP_STORE.md, rewritten paste-ready:
- Quoted lines the app can actually show ("Your shoulders holding court since
  Tuesday?" and "Screen's got your number." weren't in the generator).
- No prices anywhere — they differ per storefront and 2.3.7 rejects them.
- Keywords were 144 chars against a 100 limit and repeated subtitle words;
  now 96, commas only. "pomodoro" dropped (max break is 3 min).
- Age-rating answers for the 2025 questionnaire, privacy label, export
  compliance, content rights, App Review notes, claim→code table.
- Price recommendation US$9.99 (reasoning in the walkthrough).

APP_STORE_SUBMISSION.md (new): business setup that gates paid apps (agreements,
tax, Small Business Program, EU trader status), certificates and profile, app
record, build/upload, screenshots, a TestFlight test list for the sandboxed
build, trademark note, known multi-display limitation.

PRIVACY.md says what Smart Pauses actually checks (mic busy yes/no, never
audio) instead of a flat "no microphone access". SIGNING_GUIDE drops the altool
line (AC_PASSWORD is a notarytool profile altool can't read) for Transporter.
README and launch hints lose "never the same line twice". Version → 1.2.0: the
App Store binary differs from the website's 1.1.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
The old set had problems a reviewer or buyer would see:
- 03 printed "HOLD TO SKIP" over the break message (garbled overlap) on a big
  centre ring the app doesn't have. Now the real ring — 64pt, bottom centre,
  percentage inside, at the picture's own scale — with a close-up loupe.
- 04 drew the 16:10 overlays into portrait frames, squashing text and waves.
  Every image is now aspect-filled and cropped, never stretched.
- 02 and 05 didn't show the app at all (2.3.3 bait), and 05 said "$19.99"
  twice — wrong in every non-US storefront and a 2.3.7 rejection on its own.
- 02 led with "Never the same line twice", which wasn't true.

New generator (Python + Pillow) replaces the Swift one so the cards can be
rendered and checked anywhere; on a Mac it uses SF Pro. Real captures dropped
into screenshots/raw/ override the re-drawn overlays automatically, and
Settings / menu bar captures add their own cards. Four cards today, RGB (no
alpha), exact 2880×1800 and 1440×900.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
The identity and entitlement checks piped into grep -q. An early-exiting grep
can SIGPIPE the writer, and pipefail then reports a match as a miss. Capture
the output and match in bash instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
Pablo's Oct 1–2 commits on main rewrote the listing, Settings labels, About
text and card copy in a plainer, review-safe voice and set the price to US$9.99.
That's the newer intent, so it wins on words:

- PreferencesView, APP_STORE.md listing copy, keywords, category, price: main's,
  verbatim, except "fills your displays" → "fills the screen" (the overlay
  likely covers one display; see the submission guide).
- APP_STORE.md keeps the branch's submission metadata (age rating answers,
  privacy label, export compliance, review notes, URLs), with "Unskippable"
  and "Smart Pauses" renamed to main's "Lock mode" and "Pause while busy".
- Cards: main's five slugs, headlines, labels and pillars, ported into the
  Python generator (the Swift generator stays deleted) and re-rendered on the
  fixed layouts. Card 02 swaps the text-only tarot cards for a close-up of a
  real break line plus three real generated lines as captions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
Every serious break app lets people try first — LookAway (free download + IAP,
free trial), Restier (14-day trial), Time Out and the open-source ones (free).
A paid-upfront unknown app at $9.99 asks for the money before anyone has seen
a break land, and the break is what sells Spellbreak.

Apple has no trial for paid apps, but guideline 3.1.1 sanctions this shape for
one-time purchases: a free non-consumable named "7-Day Trial" whose purchase
date starts the clock (an App Store transaction, so a reinstall can't reset it),
then a paid "Unlock". Store.swift does it with StoreKit 2:

- Gates scheduled breaks only. Test Break and Break Now always work, and while
  StoreKit is still answering nothing is locked.
- Trial ending: the break that was due becomes the unlock prompt, once, and
  the timer stops. Starting the trial or unlocking starts breaks.
- Settings puts the trial/unlock button beside Test Break, with the duration,
  what stops, and the localized price in plain sight before the trial can
  start (3.1.1 asks for all three). Restore Purchase sits beside it. Paying
  users see exactly the old row.
- Prices only ever come from StoreKit (displayPrice), never constants.
- App Store builds only: build-mas.sh sets SPELLBREAK_APP_STORE=1 →
  -Xswiftc -DAPP_STORE, in separate scratch dirs. Website builds compile the
  store as always-unlocked and never ask StoreKit anything.
- SPELLBREAK_TRIAL_SECONDS shortens the trial for TestFlight testing; it can
  never lengthen it.

Copy: the app is Free; US$9.99 base with Australia set to A$12.99 lives on the
Unlock IAP. Listing, review notes, IAP table (names/descriptions within App
Store Connect's limits), privacy policy and card 05 say "try it for a week,
then one purchase" — no "free" or prices in screenshots (2.3.7). Submission
guide gets the competitor table, proceeds per storefront, the two-price setup
and TestFlight checks for trial start, ending, unlock and restore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
The easy wins from the competitor gap list:

- Away counts as a break. Three minutes with no keyboard or mouse (or a break's
  length, if longer) holds a due break and restarts the interval on return, the
  same treatment sleep already gets. Still hands with something holding the
  display awake (a video, a call) aren't away: that's someone watching, the
  trance a break is for. Keep-awake apps hold it too, which just switches
  away detection off for those users. No stuck states: it ends when they're back.
- Typing waits for the sentence. Under "Pause while busy", a break that comes due
  mid-typing waits for two quiet seconds on the keys, 30 seconds at most so a
  non-stop typist still gets it. Both checks are Quartz timestamps, no
  permission prompt: when, never which key.
- Every display. One overlay window per screen instead of one stretched across
  them: with "Displays have separate Spaces" on (the default), a window can't
  span displays, so the break only ever covered one. The interactive break
  goes on the pointer's display, the others get the same aurora quietly, with
  one palette rolled per break so Surprise matches, and they fade out together.
  Single-display behaviour is unchanged.
- Today's tally. Completed and skipped counts were tracked, never shown, and lost
  on relaunch. Now persisted and shown as "3 breaks today · 1 skipped" in the
  popover and right-click menu.

Skipped on purpose: windowed video detection (that's the trance), screen-share
detection (no public API; calls are already caught), short/long rhythm (needs
settings UI). Privacy policy, listing, review notes, glossary and TestFlight
checks updated; "fills your displays" is true again, so it's back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
…names

- New step-0 decision: the website. A free, unlocked DMG on the site the app
  links to undercuts every App Store sale; recommend pointing the site at the
  App Store, keeping /privacy and a support route live for review, and no
  buy-elsewhere links (3.1.1). Notes the public repo too.
- App Preview video spec (1920×1080, 15–30s, captured from the app) — the
  strongest asset for something that's mostly motion.
- TestFlight checks use main's names (Lock mode, Pause while busy); fixed the
  app-name anchor; status line says the Swift is still uncompiled.
- What's New 1.2.0 lists everything since 1.1.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
None of the recent Swift had been compiled — this sandbox has no Swift
toolchain. GitHub's macOS runners do, free for a public repo. The workflow
runs the website build (universal bundle, icon, ad-hoc signature) and the App
Store build (-DAPP_STORE, with the trial and unlock), then checks what the
submission depends on: Info.plist lints, both architectures present, the
signature verifies, and the icns carries all ten reps (the 2x-iconset bug
dropped four, twice). Also runs the ScreenBusy check (informational) and the
store card generator against the system fonts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
Run 1 was green on the runner's default Xcode 16.4, but App Store uploads
are built with Xcode 26 now, so a matrix builds with 26.3 and 16.4. The App
Store step runs SPELLBREAK_APP_STORE=1 ./build-app.sh in full — exercising
the flag pass-through under macOS's bash 3.2 — and the bundle checks run on
that bundle, the one that would be submitted. checkout v4 → v5 (Node 24).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUektDzcDV9krNU4aDdyAf
@pibulus
pibulus merged commit 3f41240 into main Oct 4, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants