@rightkit/onboarding 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,7 +27,39 @@ import "@rightkit/onboarding/onboarding.css";
27
27
  | `modelsIntro` | App copy shown at the top of the models step. |
28
28
  | `allowSkip` | Show "Set up later" (default `true`). |
29
29
  | `titlebar` | Node rendered in a strip along the top. |
30
- | `dragRegion` | Add `data-tauri-drag-region` to that strip (default `false`; height `--rk-onboarding-titlebar-h`, default 32px). |
30
+ | `dragRegion` | Add `data-tauri-drag-region` to that strip on the setup steps (default `false`; height `--rk-onboarding-titlebar-h`, default 32px). Never rendered on the legal stage. |
31
+ | `legalDragRegion` | Also render the drag region on the legal stage (default `false`; the legal gate has no strip, and `titlebar` content alone never adds the attribute). |
32
+ | `legalReceipt` | `{ read(), write(record) }`: the app's own acceptance receipt. See below. |
33
+
34
+ ## App-owned legal receipt
35
+
36
+ An app that already persists its own acceptance receipt passes `legalReceipt` and keeps that store as the source of truth. `read()` runs on load (sync or async) and returns a `LegalAcceptanceRecord` (`version`, `documentsHash`, `acceptedAt`, `eligibilityBasis`) or `null`. Its result replaces any acceptance in the settings backend: `null`, a throw, or an incomplete record all mean "not accepted", so the gate shows. `requiredVersion` / `isAccepted` then evaluate against the app's receipt, and a receipt for another version re-prompts with `variant: "re-acceptance"`. On agree, `write(record)` is awaited first; if it throws, the gate shows its save-failure alert and nothing else is persisted.
37
+
38
+ Mapping example for ScrapeRight's four-field `scraperight.legal-acceptance.v1` receipt (`legalAcceptanceVersion`, `legalAcceptedAt`, `legalEligibilityBasis`, `legalMaterialSha256`; pass `documentsHash={LEGAL_MATERIAL_SHA256}` so the hash round-trips):
39
+
40
+ ```ts
41
+ const legalReceipt: LegalReceiptPort = {
42
+ read() {
43
+ const r = loadLegalAcceptance(); // reads localStorage "scraperight.legal-acceptance.v1"
44
+ if (!r?.legalAcceptanceVersion || !r.legalAcceptedAt || !r.legalEligibilityBasis) return null;
45
+ return {
46
+ version: r.legalAcceptanceVersion,
47
+ acceptedAt: r.legalAcceptedAt,
48
+ eligibilityBasis: r.legalEligibilityBasis, // "individual" | "enterprise"
49
+ documentsHash: r.legalMaterialSha256 ?? "",
50
+ };
51
+ },
52
+ write(rec) {
53
+ saveLegalAcceptance({
54
+ legalAcceptanceVersion: rec.version,
55
+ legalAcceptedAt: rec.acceptedAt, // UTC ISO, ends in Z
56
+ legalEligibilityBasis: rec.eligibilityBasis,
57
+ legalMaterialSha256: rec.documentsHash,
58
+ });
59
+ },
60
+ };
61
+ <Onboarding legalReceipt={legalReceipt} requiredVersion={REQUIRED_LEGAL_ACCEPTANCE_VERSION} documentsHash={LEGAL_MATERIAL_SHA256} ... />
62
+ ```
31
63
 
32
64
  ## Custom steps and readiness
33
65
 
@@ -2,4 +2,4 @@ import type { OnboardingProps, PersistedOnboardingState } from "./types.js";
2
2
  /** True when the stored acceptance satisfies the host's requirement. */
3
3
  export declare function isLegalAccepted(state: PersistedOnboardingState, requiredVersion?: string, custom?: (state: PersistedOnboardingState) => boolean): boolean;
4
4
  export declare function onboardingStateKey(): string;
5
- export declare function Onboarding({ settings, legal, steps, permissions, models, license, onComplete, onSkip, className, requiredVersion, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, modelsIntro, allowSkip, titlebar, dragRegion }: OnboardingProps): import("react").JSX.Element;
5
+ export declare function Onboarding({ settings, legal, steps, permissions, models, license, onComplete, onSkip, className, requiredVersion, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, modelsIntro, allowSkip, titlebar, dragRegion, legalDragRegion, legalReceipt }: OnboardingProps): import("react").JSX.Element;
@@ -31,6 +31,13 @@ function readState(value) {
31
31
  ...(legalAcceptance ? { legalAcceptance } : {}),
32
32
  };
33
33
  }
34
+ /** Replace the acceptance in `state` with the one from the app's receipt (`undefined`: not accepted). */
35
+ function withReceipt(state, record) {
36
+ const { legalAcceptance: _drop, eligibilityBasis: _basis, ...rest } = state;
37
+ void _drop;
38
+ void _basis;
39
+ return record ? { ...rest, legalAccepted: true, eligibilityBasis: record.eligibilityBasis, legalAcceptance: record } : { ...rest, legalAccepted: false };
40
+ }
34
41
  /** True when the stored acceptance satisfies the host's requirement. */
35
42
  export function isLegalAccepted(state, requiredVersion, custom) {
36
43
  if (custom)
@@ -66,7 +73,7 @@ function TitleStrip({ titlebar, dragRegion }) {
66
73
  return null;
67
74
  return _jsx("div", { className: "rk-onboarding__titlebar", "data-testid": "onboarding-titlebar", ...(dragRegion ? { "data-tauri-drag-region": "" } : {}), children: titlebar });
68
75
  }
69
- export function Onboarding({ settings, legal, steps = [], permissions, models, license, onComplete, onSkip, className, requiredVersion, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, modelsIntro, allowSkip = true, titlebar, dragRegion }) {
76
+ export function Onboarding({ settings, legal, steps = [], permissions, models, license, onComplete, onSkip, className, requiredVersion, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, modelsIntro, allowSkip = true, titlebar, dragRegion, legalDragRegion = false, legalReceipt }) {
70
77
  const [state, setState] = useState(null);
71
78
  const [saving, setSaving] = useState(false);
72
79
  const [error, setError] = useState(null);
@@ -100,9 +107,34 @@ export function Onboarding({ settings, legal, steps = [], permissions, models, l
100
107
  }
101
108
  }, [persist]);
102
109
  const saveStep = useCallback(async (patch) => { await save(patch); }, [save]);
103
- useEffect(() => { let live = true; void settings.load().then((snapshot) => { if (live)
104
- setState(readState(snapshot.values[KEY])); }, (e) => { if (live)
105
- setError(e instanceof Error ? e.message : String(e)); }); return () => { live = false; }; }, [settings]);
110
+ useEffect(() => {
111
+ let live = true;
112
+ void (async () => {
113
+ try {
114
+ const loaded = readState((await settings.load()).values[KEY]);
115
+ if (!legalReceipt) {
116
+ if (live)
117
+ setState(loaded);
118
+ return;
119
+ }
120
+ // The app's receipt is the source of truth for acceptance; an unreadable receipt counts as none.
121
+ let record;
122
+ try {
123
+ record = readAcceptance(await legalReceipt.read());
124
+ }
125
+ catch {
126
+ record = undefined;
127
+ }
128
+ if (live)
129
+ setState(withReceipt(loaded, record));
130
+ }
131
+ catch (e) {
132
+ if (live)
133
+ setError(e instanceof Error ? e.message : String(e));
134
+ }
135
+ })();
136
+ return () => { live = false; };
137
+ }, [settings, legalReceipt]);
106
138
  const builtins = useMemo(() => [
107
139
  { id: "legal", label: "Agreement", title: "Review agreement", skippable: false, render: () => _jsx("p", { className: "rk-onboarding__muted", children: "Agreement accepted." }) },
108
140
  ...(permissions ? [permissionsStep(permissions)] : []),
@@ -162,10 +194,19 @@ export function Onboarding({ settings, legal, steps = [], permissions, models, l
162
194
  return _jsx("div", { className: cx("rk-onboarding", className), "aria-busy": "true", children: _jsx("p", { className: "rk-onboarding__muted", children: "Loading setup\u2026" }) });
163
195
  if (needsGate) {
164
196
  if (!gateCleared)
165
- return _jsxs("div", { className: cx("rk-onboarding", className), "aria-busy": "true", children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: dragRegion }), _jsx("p", { className: "rk-onboarding__muted", children: "Loading setup\u2026" })] });
197
+ return _jsxs("div", { className: cx("rk-onboarding", className), "aria-busy": "true", children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: legalDragRegion }), _jsx("p", { className: "rk-onboarding__muted", children: "Loading setup\u2026" })] });
166
198
  const reacceptance = activeState.legalAccepted || activeState.legalAcceptance !== undefined;
167
- return _jsxs("div", { className: cx("rk-onboarding rk-onboarding--legal", className), "data-testid": "onboarding-legal", "data-variant": reacceptance ? "re-acceptance" : "first-run", children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: dragRegion }), _jsx(LegalGate, { ...legal, variant: reacceptance ? "re-acceptance" : "first-run", onQuit: () => void legal.onQuit?.(), onAgree: async ({ eligibilityBasis }) => {
199
+ return _jsxs("div", { className: cx("rk-onboarding rk-onboarding--legal", className), "data-testid": "onboarding-legal", "data-variant": reacceptance ? "re-acceptance" : "first-run", children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: legalDragRegion }), _jsx(LegalGate, { ...legal, variant: reacceptance ? "re-acceptance" : "first-run", onQuit: () => void legal.onQuit?.(), onAgree: async ({ eligibilityBasis }) => {
168
200
  const legalAcceptance = { version: requiredVersion ?? "1", documentsHash: documentsHash ?? await hashDocuments(legal.documents), acceptedAt: new Date().toISOString(), eligibilityBasis };
201
+ if (legalReceipt) {
202
+ try {
203
+ await legalReceipt.write(legalAcceptance);
204
+ }
205
+ catch (e) {
206
+ setError(e instanceof Error ? e.message : String(e));
207
+ throw e;
208
+ }
209
+ }
169
210
  const progress = reacceptance ? {} : { completed: ["legal"], currentStep: builtins.length > 1 ? 1 : 0 };
170
211
  await persist({ legalAccepted: true, eligibilityBasis, legalAcceptance, ...progress });
171
212
  if (!reacceptance && builtins.length === 1)
package/dist/types.d.ts CHANGED
@@ -66,6 +66,16 @@ export interface LegalAcceptanceRecord {
66
66
  acceptedAt: string;
67
67
  eligibilityBasis: EligibilityBasis;
68
68
  }
69
+ /**
70
+ * App-owned persistence for the legal acceptance. Lets an app keep its existing receipt (any shape, any store) and map it
71
+ * to and from the package's record. When supplied it is the source of truth: `read()` is consulted on load and overrides
72
+ * whatever acceptance the settings backend holds (`null` means "not accepted", so the gate shows), and `write()` is
73
+ * awaited when the user agrees, before the settings backend is patched.
74
+ */
75
+ export interface LegalReceiptPort {
76
+ read(): LegalAcceptanceRecord | null | Promise<LegalAcceptanceRecord | null>;
77
+ write(record: LegalAcceptanceRecord): void | Promise<void>;
78
+ }
69
79
  export interface PersistedOnboardingState {
70
80
  version: 1;
71
81
  currentStep: number;
@@ -133,7 +143,11 @@ export interface OnboardingProps {
133
143
  allowSkip?: boolean;
134
144
  /** Optional titlebar content rendered in a strip along the top. */
135
145
  titlebar?: ReactNode;
136
- /** Render a `data-tauri-drag-region` strip at the top. Default false. */
146
+ /** Render a `data-tauri-drag-region` strip at the top of the setup steps. Default false. Never applied to the legal stage; see `legalDragRegion`. */
137
147
  dragRegion?: boolean;
148
+ /** Also render the drag region on the legal stage (loading and gate). Default false: the legal gate has no strip. */
149
+ legalDragRegion?: boolean;
150
+ /** Read and write the legal acceptance in the app's own receipt instead of (overriding) the settings backend's copy. */
151
+ legalReceipt?: LegalReceiptPort;
138
152
  }
139
153
  export type { LegalGateDocument, LegalGateTheme };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/onboarding",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "First-run onboarding framework: ordered steps, progress, skip/resume, legal gate first, permissions, model download and licence/trial steps.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@rightkit/app-shell": "0.2.0",
33
- "@rightkit/legal-ui": "0.1.2",
33
+ "@rightkit/legal-ui": "0.1.3",
34
34
  "@testing-library/jest-dom": "^6.6.3",
35
35
  "@testing-library/react": "^16.1.0",
36
36
  "@testing-library/user-event": "^14.5.2",