Skip to content

feat(pro): Lemon Squeezy license layer for a Pro tier - #5

Draft
chiliec wants to merge 7 commits into
mainfrom
feat/pro-license-layer
Draft

chiliec wants to merge 7 commits into
mainfrom
feat/pro-license-layer

Conversation

@chiliec

@chiliec chiliec commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Why

First step of monetizing ClaudeBar: a freemium Pro tier. Everything that ships today stays free forever — Pro gates only new features. Payment via Lemon Squeezy (Merchant of Record: remits global tax, pays out to a non-US individual, no company needed).

What's in this PR (the entitlement core)

  • LicenseService — the 3 LS license calls (activate/validate/deactivate). The license key is the credential; no API token embedded in the app.
  • LicenseStore@Observable entitlement, persisted in the Keychain (mirrors KeychainService). Implements offline grace: a network blip never locks a paying user (14-day window), but an authoritative refund/expiry/disable locks immediately.
  • ProConfig — checkout URL, price string, and the Pro feature list (usage history+graphs, multi-account, spend alerts, low-credit alerts, CSV export).
  • LicenseStoreTests — activation success/limit/empty, authoritative revocation, and both grace edges (blip-within-grace stays Pro; past-grace locks).
  • docs/PRO_SETUP.md — the one-time LS dashboard setup + test-mode checklist.

Draft, because two things remain

  1. Your ~20-min Lemon Squeezy setup (create product, enable license keys, paste the buy URL into ProConfig) — see docs/PRO_SETUP.md.
  2. UI wiring follow-up — a Pro section in SettingsView (upgrade button + key field + deactivate) and per-feature isPro gates. Deliberately separate so this core compiles + is reviewed first.

Note

I can't build Swift locally — relying on CI to compile + run LicenseStoreTests. If anything's red I'll fix it.

Adds the entitlement core for a freemium ClaudeBar Pro. Everything that
ships today stays free; Pro will gate only NEW features.

- LicenseService: the 3 Lemon Squeezy license calls (activate/validate/
  deactivate). The license key is the credential (no API token in-app).
- LicenseStore: @observable entitlement persisted in the Keychain
  (mirrors KeychainService), with offline grace — a network blip never
  locks a paying user (14d), a refund/expiry locks immediately.
- ProConfig: checkout URL + price + the Pro feature list.
- LicenseStoreTests: activation, authoritative revocation, both grace edges.
- docs/PRO_SETUP.md: the Lemon Squeezy dashboard setup + test checklist.

UI wiring (Settings Pro section + per-feature gates) is a deliberate
follow-up so this core is reviewable and CI compiles it first.
ClaudeAPIClient already declares this extension in the same module; reuse it.
- ProSettingsSection: upgrade button (opens Lemon Squeezy checkout),
  license-key activation field, deactivate, and grace/status display.
- SettingsView + PopoverView thread a shared LicenseStore through.
- AppDelegate owns one LicenseStore and calls refresh() at launch.
- en/ru Localizable.strings: the settings.pro.* keys.

Feature GATING at each Pro feature's call site is the next step; this
lands the purchase + activation UI so the store is testable end-to-end.
- foregroundStyle ternary: unify to Color.secondary/Color.red (was mixing
  HierarchicalShapeStyle and Color).
- Drop the @mainactor LicenseStore() default arg from PopoverView/SettingsView
  inits (can't default-construct a main-actor type in a nonisolated init);
  callers (AppDelegate, previews) pass it explicitly.
Marking the PopoverView/SettingsView inits @mainactor lets the default
LicenseStore() argument be constructed safely (the views are only used
from MainActor contexts anyway), so existing ViewTests call sites that
omit the license argument keep compiling.
Swift evaluates default argument values in a nonisolated context even when
the init is @mainactor, so 'license: LicenseStore = LicenseStore()' can't
compile against the actor-isolated LicenseStore.init. Use an optional
'license: LicenseStore? = nil' and build the default in the init body
(which is @mainactor).
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.

1 participant