Repository navigation
Get Spellbreak App Store ready: trial, smart breaks, every display - #1
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
--deep.Selling
-DAPP_STORE); the website build is untouched. Only scheduled breaks are gated.Behaviour
Docs & tooling
APP_STORE.md(paste-ready listing, IAP table, review notes) andAPP_STORE_SUBMISSION.md(walkthrough and TestFlight checklist); privacy policy updated.Test plan
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