Phase 3 of the one-app pivot (DESIGN 12 ▸ The entitlement / Distribution, ruled 2026-07-30; card c3a3ddd5). New Kanban/Tier/: Tier (.free/.pro — deliberately no .lapsed case; unsubscribed and lapsed are one state) and the pure decision Tier.resolve(from:now:) over SubscriptionFacts (expiration + willAutoRenew), unit-tested through all five named states: free, active, lapsed, offline-grace, never-online. The facts are a persisted cache (standard defaults), not a live view: StoreKit ages an expired subscription out of currentEntitlements locally, so an offline device and a real lapse are indistinguishable from that property alone — the cache holds the last answer, empty entitlements read as silence, and holds end only on a definitive answer (revocation, or the subscription-group status read Settings performs). That is 12's offline-grace trade, resolved toward the paying user. ProEntitlement is the local adapter (currentEntitlements + Transaction.updates, started from launch, never from a test host); ProStorefront holds everything networked (product load, purchase, AppStore.sync) and only the Settings section ever constructs one — the split is the enforcement of "never network on the open path". beginSession reads the tier once at composition; BoardSession.tier is a let with no path back in, so a lapse never rebinds an open session. makeHistoryProvider now takes the tier; both tiers bind the native stack until pro-m1 builds the git provider — the seam's consumer is named, not invented early. Settings gains the Pro section (subscribe with localized price, manage, restore; a quiet unreachable line, no indefinite spinner) — the third of the exactly-three Pro mentions; the About line gains its "…in Settings" pointer now that there is a Settings to point at. A successful purchase or restore offers once to reopen open boards (close + reopen through the ordinary paths). Configuration.storekit wired into the scheme's run action for ASC-free exercise; RELEASE.md gains the pro-m1 store-side steps and the rule that the product must not be configured before then. 1901 tests in 319 suites green. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
69 lines
8.0 KiB
Markdown
69 lines
8.0 KiB
Markdown
# Distribution and release
|
||
|
||
## The channel decision (recorded 2026-07-29)
|
||
|
||
**Lanework ships on the Mac App Store**, as version 2.0 of the existing `dev.rzen.indie.Kanban` App Store record — the 1.x app already listed there under the name Lanework. This was not an open question by the time this document was written; three prior rulings settle it:
|
||
|
||
- The one-app collapse (DESIGN/12-editions.md, re-ruled 2026-07-30, superseding the 2026-07-27 two-app split) puts the whole family on **one record**: `dev.rzen.indie.Kanban`, the id 1.x already ships under, so 2.0 is simply its update. Lanework Pro arrives inside it as a subscription after pro-m1/pro-m2, never as a second record.
|
||
- The marketing version has been 2.0 since m3 — the successor numbering of the shipped 1.x.
|
||
- The portfolio's whole release tooling (appstore-publish skill, `asc-*.swift` scripts, the account's API key) is App-Store-shaped, and the 1.x pipeline for this very record already exists and works.
|
||
|
||
Notarized direct distribution is deliberately **not** pursued for 2.0: a second channel means a second update path and a second set of listing assets for no current audience, and nothing in the app requires escaping the App Store's sandbox rules (the app is already sandboxed with user-selected file access, and its network-client entitlement stays dormant until Pro's remotes use it). Revisit only if App Review rejects something architectural.
|
||
|
||
## The pipeline
|
||
|
||
Carried over from the 1.x release (same record, same scheme names — the pathfinder repo's pipeline, now living here):
|
||
|
||
- `scripts/release.sh` — XcodeGen regenerate, `xcodebuild archive` (Release, generic/platform=macOS), `-exportArchive` with `scripts/ExportOptions-macOS.plist` (`destination=upload` — straight to App Store Connect). Build number = git commit count, stamped by the existing post-build phase.
|
||
- `.env.release` (gitignored; `.env.release.example` committed) — account-level App Store Connect API credentials.
|
||
- `scripts/metadata/` — the listing source of truth (name, subtitle, description, keywords, what's-new, URLs, categories, pricing, availability, review notes). Pushed with the canonical scripts, **always with `--platform macos`**:
|
||
|
||
```sh
|
||
set -a; source .env.release; set +a; swift ../indie-skills/skills/appstore-publish/scripts/asc-metadata.swift --bundle dev.rzen.indie.Kanban --version 2.0 --platform macos --dry-run
|
||
```
|
||
|
||
- Screenshots: `scripts/metadata/screenshots/en-US/APP_DESKTOP/` — needs fresh 2.0 captures (see checklist).
|
||
|
||
## First-release (2.0) checklist
|
||
|
||
Run top to bottom; nothing below "PUSH POINT" may run without a deliberate decision — every step after it writes to the live App Store record of a shipping 1.x app.
|
||
|
||
1. [ ] All milestone work committed and pushed (build number = commit count).
|
||
2. [ ] Unit suite green: `xcodebuild test -project Kanban.xcodeproj -scheme Kanban -destination 'platform=macOS,arch=arm64' -only-testing:KanbanTests`.
|
||
3. [ ] UI test pass on a real display (KanbanUITests: accessibility audits + end-to-end golden flows), plus the manual checklists (`KanbanUITests/AccessibilityVerification.md`, toolbar/undo/trash/drag-preview lists).
|
||
4. [ ] CHANGELOG.md current (it is the in-app release notes) — What's New (`scripts/metadata/version/en-US/whats_new.txt`) tells the same story.
|
||
5. [ ] Screenshots: capture the 2.0 board on a real display (APP_DESKTOP sizes come straight from screenshots of the running app; numeric filename prefixes set the store order).
|
||
6. [ ] Archive dry run: `scripts/release.sh` at least through the archive step completes locally.
|
||
7. **— PUSH POINT (everything below touches the live record) —**
|
||
8. [ ] `scripts/release.sh` — archive + upload the build.
|
||
9. [ ] Metadata dry run, then push (`asc-metadata.swift --platform macos`, commands above).
|
||
10. [ ] Verify pricing (currently free, carried from 1.x) and availability (ALL territories) still match intent — Pro will be the paid tier later.
|
||
11. [ ] Stage: `asc-submit.swift --bundle dev.rzen.indie.Kanban --version 2.0 --platform macos` (waits for build processing, attaches, stages the review submission — does not submit).
|
||
12. [ ] Review the staged listing at appstoreconnect.apple.com (age rating, privacy labels, and content rights carry over from 1.x — the stage step's errors will name anything missing).
|
||
13. [ ] Submit: the same command with `--submit`.
|
||
|
||
## Lanework Pro (the subscription)
|
||
|
||
Pro is **a subscription inside this app, not a second record** (DESIGN/12-editions.md ▸ Distribution, re-ruled 2026-07-30): the two-app split that once wanted its own bundle id, metadata folder and scheme here is retired. Nothing in the pipeline above changes for it — same scheme, same archive, same upload. The added work is App Store Connect configuration and review materials, and it is all below.
|
||
|
||
The **app side is built** (the entitlement, the tier seam, the Settings Pro section, the local `Configuration.storekit`). The **store side is not** and must not be until pro-m1 ships something to sell: a live subscription product against a build whose Pro tier binds the same native undo stack the free tier does would be selling nothing. So every step here is future work for the **pro-m1 release**, not for 2.0.
|
||
|
||
### What exists in the repo today
|
||
|
||
- `Kanban/Tier/ProProducts.swift` — the identifiers, mirrored from App Store Connect: product `dev.rzen.indie.kanban.pro.monthly`, group reference name `Lanework Pro`. The group's numeric id is read off the loaded product at runtime, never hardcoded.
|
||
- `Configuration.storekit` (repo root) — the same group and product as an Xcode-local StoreKit configuration, attached to the `Kanban` scheme's **run** action (`project.yml` ▸ `schemes.Kanban.run.storeKitConfiguration`). It makes the subscribe/manage/restore flow exercisable with no ASC product and no sandbox account. `buildPhase: none` — it never ships.
|
||
- Entitlements: no in-app-purchase key is needed. In-app purchase is an App Store Connect fact about the record, not a sandbox capability; the app calls StoreKit with the entitlements it already has.
|
||
|
||
### Checklist — pro-m1 release (not 2.0)
|
||
|
||
Steps 1–4 are App Store Connect work done by hand at appstoreconnect.apple.com; there is no script for them and no API step in this repo's tooling. They are all **below the PUSH POINT in spirit** — each one touches the live record of a shipping app.
|
||
|
||
1. [ ] Create the subscription group on `dev.rzen.indie.Kanban`: reference name **Lanework Pro**. One group — a second would let a user hold two Pro subscriptions at once.
|
||
2. [ ] Create the auto-renewable subscription in it: product id **`dev.rzen.indie.kanban.pro.monthly`**, reference name "Lanework Pro Monthly", duration **1 month**. The product id must match `ProProducts.monthly` exactly and can never be changed or reused once created.
|
||
3. [ ] Set the price (choose the tier; all territories, matching the app's ALL-territories availability) and the localized display name and description. The app never spells a price — it renders `product.displayPrice`, so the store is the only source.
|
||
4. [ ] Subscription review materials, which are separate from the app's: a **review screenshot of the Settings Pro section** showing the price, Subscribe and Restore Purchases; review notes naming Settings (⌘,) as the path to the purchase surface, that the subscription unlocks git-backed history and sync, and that there is no account or login of any kind.
|
||
5. [ ] Confirm the app's own listing carries the required subscription furniture: a link to the privacy policy and to the terms of use (EULA), both reachable from the listing, plus the auto-renewing terms in the description. Apple rejects auto-renewable subscriptions without them.
|
||
6. [ ] Update `Configuration.storekit`'s `displayPrice` to whatever step 3 set, so a development run and the store agree.
|
||
7. [ ] Verify in a **sandbox** account before submitting: subscribe, confirm the Settings section flips to "Subscribed — renews …", confirm the reopen offer appears with boards open, then Manage Subscription and Restore Purchases.
|
||
8. [ ] Then run the ordinary release checklist above for the pro-m1 version. The subscription is attached to that version's review submission.
|