Socle for tier-based feature gating (UI-only soft-paywall).
- LicenseContext: machine-level provider (createContext<T|null>, useReducer,
throwing consumer hook), mounted above ProfileProvider in main.tsx so a
profile switch (BrowserRouter key remount) does not reload the license.
Loads edition + info once; exposes { status, edition, features, info, error,
refresh, submitKey }. Boot-error recovery (CWE-703): neutral state + capped
exponential-backoff retry, never the upsell.
- shared/entitlements.ts: FeatureKey (kebab-case), ENTITLEMENTS matrix, pure
isEntitled() fail-closed in Free (CWE-863) — the features[] override is
ignored before edition==="free" is checked.
- useEntitlement(f): { allowed, ready } (ready = status==="ready"), synchronous.
- useIsPremium + its test migrated onto the context (drops the per-call double
invoke); LicenseCard consumes the context. useLicense.ts removed (fully
replaced, no remaining consumers).
- services/entitlements.test.ts: matrix, features[] override, override ignored
in Free, unknown-feature deny-all.
Resolves #297
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 lines
427 B
TypeScript
11 lines
427 B
TypeScript
import { useLicenseContext } from "../contexts/LicenseContext";
|
|
|
|
/**
|
|
* Returns true if the active license edition is "premium".
|
|
* Ergonomic helper only — the server enforces entitlements independently (cf. ADR 0011 §UX).
|
|
* Reads the shared LicenseContext (single boot load) — no per-call invoke.
|
|
*/
|
|
export function useIsPremium(): boolean {
|
|
const { edition } = useLicenseContext();
|
|
return edition === "premium";
|
|
}
|