Lanework Pro is a subscription — the tier seam, StoreKit 2, and Settings

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
This commit is contained in:
2026-07-30 18:14:58 -04:00
parent 2c6b8fe63a
commit 3b19883593
15 changed files with 1459 additions and 15 deletions
+8 -3
View File
@@ -15,8 +15,13 @@ import IndieAbout
/// one-app collapse (12, re-ruled 2026-07-30) makes Pro a subscription, so the line is a pointer
/// to something a subscriber has already taken up rather than to another download true either
/// way, and shown to everyone. It stays deliberately flat: what Pro adds, in one sentence, with
/// no call to action. It gains its "in Settings" when the Settings Pro section it would point at
/// actually exists (the StoreKit phase); until then a direction would be a dead end.
/// no call to action.
///
/// **The direction arrived with the section.** The line was written without its "in Settings"
/// while there was no Settings Pro section to point at a signpost to a dead end is worse than no
/// signpost. `ProSettingsSection` exists now, so the sentence ends where the subscription actually
/// lives. Still one sentence, still flat, still no verb aimed at the reader: it names the place, it
/// does not ask them to go there.
enum AboutBox {
/// The About window's content: version/build/date from the stamped Info.plist
@@ -26,7 +31,7 @@ enum AboutBox {
/// own single-line paragraphs.
static var configuration: AppInfoConfiguration {
AppInfoConfiguration(
copyrightText: "© 2026 rzen\nLanework Pro adds git-backed board history and sync.",
copyrightText: "© 2026 rzen\nLanework Pro adds git-backed board history and sync — see Settings.",
documents: [.license(extension: "md")],
changelogDocument: .changelog()
)