@rightkit/onboarding 0.6.3 → 0.6.4
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/dist/Onboarding.d.ts +16 -2
- package/dist/Onboarding.js +65 -7
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/legal-receipt.d.ts +27 -2
- package/dist/legal-receipt.js +68 -11
- package/dist/types.d.ts +24 -2
- package/onboarding.css +20 -0
- package/package.json +5 -17
package/dist/Onboarding.d.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import type { LegalAcceptanceRecord, OnboardingProps, PersistedOnboardingState } from "./types.js";
|
|
1
|
+
import type { LegalAcceptanceRecord, OnboardingProps, PersistedOnboardingState, SettingsBackend } from "./types.js";
|
|
2
2
|
/** @internal Validates one stored or returned acceptance record; `undefined` when it is not a well-formed record. */
|
|
3
3
|
export declare function readAcceptance(value: unknown): LegalAcceptanceRecord | undefined;
|
|
4
4
|
/** @internal Normalises the persisted `onboarding.state` value. */
|
|
5
5
|
export declare function readState(value: unknown): PersistedOnboardingState;
|
|
6
6
|
/** True when the stored acceptance satisfies the host's requirement. */
|
|
7
7
|
export declare function isLegalAccepted(state: PersistedOnboardingState, requiredVersion?: string, custom?: (state: PersistedOnboardingState) => boolean, requiredDocumentsHash?: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* True when the setup step `stepId` is settled in a persisted onboarding state: the step is completed or skipped, or the
|
|
10
|
+
* whole setup was set aside ("Set up later", `skipped: true`). Reads the raw persisted value, so callers need no cast.
|
|
11
|
+
* Missing or malformed input is false; it never throws. Which steps gate an app stays with the app.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isStepSettled(state: unknown, stepId: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Restarts the setup at its first step ("Show onboarding again"). The setup progress (completed and skipped steps, the
|
|
16
|
+
* set-aside flag) is cleared, but the legal acceptance is kept: the agreement is not asked again, and `legal` stays
|
|
17
|
+
* completed. Reads the stored value first, so the rest of the settings are untouched.
|
|
18
|
+
*/
|
|
19
|
+
export declare function restartOnboardingSetup({ settings }: {
|
|
20
|
+
settings: SettingsBackend;
|
|
21
|
+
}): Promise<void>;
|
|
8
22
|
export declare function onboardingStateKey(): string;
|
|
9
|
-
export declare function Onboarding({ settings, legal, legalMode, appName, steps, permissions, models, license, signIn, receiptDocuments, onComplete, onSkip, className, layout, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip, titlebar, dragRegion, legalDragRegion, legalReceipt, labels: labelOverrides }: OnboardingProps): import("react").JSX.Element | null;
|
|
23
|
+
export declare function Onboarding({ settings, legal, legalMode, appName, steps, permissions, models, license, signIn, receiptDocuments, onComplete, onSkip, className, layout, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip, titlebar, dragRegion, legalDragRegion, legalReceipt, labels: labelOverrides, frame, density, trafficLightInset }: OnboardingProps): import("react").JSX.Element | null;
|
package/dist/Onboarding.js
CHANGED
|
@@ -50,12 +50,24 @@ export function readState(value) {
|
|
|
50
50
|
...(Array.isArray(candidate.skippedSteps) ? { skippedSteps: candidate.skippedSteps.filter((x) => typeof x === "string") } : {}),
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Replace the acceptance in `state` with the one from the app's receipt (`undefined`: not accepted). A `historical` record
|
|
55
|
+
* (`HistoricalLegalReceipt`) is kept as the acceptance for the re-acceptance variant but never counts as accepted: it is
|
|
56
|
+
* flagged reopened, so the gate shows again and the setup progress is left alone.
|
|
57
|
+
*/
|
|
58
|
+
function withReceipt(state, record, historical = false) {
|
|
55
59
|
const { legalAcceptance: _drop, eligibilityBasis: _basis, ...rest } = state;
|
|
56
60
|
void _drop;
|
|
57
61
|
void _basis;
|
|
58
|
-
|
|
62
|
+
if (!record)
|
|
63
|
+
return { ...rest, legalAccepted: false };
|
|
64
|
+
if (historical)
|
|
65
|
+
return { ...rest, legalAccepted: false, legalReopened: true, eligibilityBasis: record.eligibilityBasis, legalAcceptance: record };
|
|
66
|
+
return { ...rest, legalAccepted: true, eligibilityBasis: record.eligibilityBasis, legalAcceptance: record };
|
|
67
|
+
}
|
|
68
|
+
/** True when a `LegalReceiptPort.read()` value is a historical receipt (`{ record, current: false }`). */
|
|
69
|
+
function isHistoricalReceipt(value) {
|
|
70
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "record" in value && value.current === false;
|
|
59
71
|
}
|
|
60
72
|
/** True when the stored acceptance satisfies the host's requirement. */
|
|
61
73
|
export function isLegalAccepted(state, requiredVersion, custom, requiredDocumentsHash) {
|
|
@@ -78,6 +90,39 @@ export function isLegalAccepted(state, requiredVersion, custom, requiredDocument
|
|
|
78
90
|
// `legalAccepted` flag carries no version, time or basis, so it re-prompts once (as re-acceptance).
|
|
79
91
|
return state.legalAcceptance !== undefined;
|
|
80
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* True when the setup step `stepId` is settled in a persisted onboarding state: the step is completed or skipped, or the
|
|
95
|
+
* whole setup was set aside ("Set up later", `skipped: true`). Reads the raw persisted value, so callers need no cast.
|
|
96
|
+
* Missing or malformed input is false; it never throws. Which steps gate an app stays with the app.
|
|
97
|
+
*/
|
|
98
|
+
export function isStepSettled(state, stepId) {
|
|
99
|
+
try {
|
|
100
|
+
if (typeof state !== "object" || state === null || typeof stepId !== "string")
|
|
101
|
+
return false;
|
|
102
|
+
const candidate = state;
|
|
103
|
+
if (candidate.skipped === true)
|
|
104
|
+
return true;
|
|
105
|
+
const completed = Array.isArray(candidate.completed) && candidate.completed.includes(stepId);
|
|
106
|
+
const skipped = Array.isArray(candidate.skippedSteps) && candidate.skippedSteps.includes(stepId);
|
|
107
|
+
return completed || skipped;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Restarts the setup at its first step ("Show onboarding again"). The setup progress (completed and skipped steps, the
|
|
115
|
+
* set-aside flag) is cleared, but the legal acceptance is kept: the agreement is not asked again, and `legal` stays
|
|
116
|
+
* completed. Reads the stored value first, so the rest of the settings are untouched.
|
|
117
|
+
*/
|
|
118
|
+
export async function restartOnboardingSetup({ settings }) {
|
|
119
|
+
const key = onboardingStateKey();
|
|
120
|
+
const snapshot = await settings.load();
|
|
121
|
+
const state = readState(snapshot.values[key]);
|
|
122
|
+
await settings.patch({
|
|
123
|
+
[key]: { ...state, currentStep: 0, skipped: false, skippedSteps: [], completed: state.completed.filter((id) => id === "legal") },
|
|
124
|
+
}, snapshot.revision);
|
|
125
|
+
}
|
|
81
126
|
/** FNV-1a fallback when SubtleCrypto is unavailable. */
|
|
82
127
|
function fnv(text) {
|
|
83
128
|
let h = 0x811c9dc5;
|
|
@@ -110,7 +155,7 @@ function NoticeList({ documents }) {
|
|
|
110
155
|
return _jsx("p", { className: "rk-onboarding__muted", children: "No notices to show." });
|
|
111
156
|
return _jsx("ul", { className: "rk-onboarding__notices", children: documents.map((d) => _jsx("li", { children: d.onOpen ? _jsx("button", { type: "button", onClick: d.onOpen, children: d.title }) : d.url ? _jsx("a", { href: d.url, target: "_blank", rel: "noreferrer", children: d.title }) : d.title }, d.title)) });
|
|
112
157
|
}
|
|
113
|
-
export function Onboarding({ settings, legal, legalMode = "gate", appName, steps = [], permissions, models, license, signIn, receiptDocuments, onComplete, onSkip, className, layout = "window", requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip = true, titlebar, dragRegion, legalDragRegion = false, legalReceipt, labels: labelOverrides }) {
|
|
158
|
+
export function Onboarding({ settings, legal, legalMode = "gate", appName, steps = [], permissions, models, license, signIn, receiptDocuments, onComplete, onSkip, className, layout = "window", requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip = true, titlebar, dragRegion, legalDragRegion = false, legalReceipt, labels: labelOverrides, frame = "rail", density = "default", trafficLightInset = false }) {
|
|
114
159
|
if (legalMode === "gate" && !legal)
|
|
115
160
|
throw new Error("Onboarding: `legal` is required when legalMode is \"gate\"");
|
|
116
161
|
const L = { ...DEFAULT_LABELS, ...labelOverrides };
|
|
@@ -169,14 +214,22 @@ export function Onboarding({ settings, legal, legalMode = "gate", appName, steps
|
|
|
169
214
|
}
|
|
170
215
|
// The app's receipt is the source of truth for acceptance; an unreadable receipt counts as none.
|
|
171
216
|
let record;
|
|
217
|
+
let historical = false;
|
|
172
218
|
try {
|
|
173
|
-
|
|
219
|
+
const value = await legalReceipt.read();
|
|
220
|
+
if (isHistoricalReceipt(value)) {
|
|
221
|
+
record = readAcceptance(value.record);
|
|
222
|
+
historical = record !== undefined;
|
|
223
|
+
}
|
|
224
|
+
else
|
|
225
|
+
record = readAcceptance(value);
|
|
174
226
|
}
|
|
175
227
|
catch {
|
|
176
228
|
record = undefined;
|
|
229
|
+
historical = false;
|
|
177
230
|
}
|
|
178
231
|
if (live)
|
|
179
|
-
setState(withReceipt(loaded, record));
|
|
232
|
+
setState(withReceipt(loaded, record, historical));
|
|
180
233
|
}
|
|
181
234
|
catch (e) {
|
|
182
235
|
if (live)
|
|
@@ -275,7 +328,7 @@ export function Onboarding({ settings, legal, legalMode = "gate", appName, steps
|
|
|
275
328
|
}
|
|
276
329
|
await markAndMove(index + 1, { completed: active.id });
|
|
277
330
|
};
|
|
278
|
-
const rootClass = cx("rk-onboarding", layout === "contained" && "rk-onboarding--contained", className);
|
|
331
|
+
const rootClass = cx("rk-onboarding", layout === "contained" && "rk-onboarding--contained", density === "compact" && "rk-onboarding--compact", trafficLightInset && "rk-onboarding--lights", frame === "card" && "rk-onboarding--card", className);
|
|
279
332
|
if (error && !state)
|
|
280
333
|
return _jsxs("div", { className: rootClass, children: [_jsx("p", { className: "rk-onboarding__error", role: "alert", children: error }), _jsx(Button, { onClick: () => window.location.reload(), children: "Retry" })] });
|
|
281
334
|
if (state && !active)
|
|
@@ -334,6 +387,11 @@ export function Onboarding({ settings, legal, legalMode = "gate", appName, steps
|
|
|
334
387
|
} })] });
|
|
335
388
|
}
|
|
336
389
|
const body = active.render(context);
|
|
390
|
+
if (frame === "card") {
|
|
391
|
+
// The HeardRight v5 frame: one centred card, the app name on top, a dot pager and no step rail.
|
|
392
|
+
return _jsxs("main", { className: rootClass, "data-testid": "onboarding", "data-step-id": active.id, "data-frame": "card", children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: dragRegion }), _jsxs("section", { className: "rk-onboarding__main rk-onboarding__main--card", children: [_jsx("div", { className: "rk-onboarding__brand rk-onboarding__brand--card", children: appName ?? legal?.appName }), _jsxs("div", { className: "rk-onboarding__frame", children: [active.title ? _jsx("h1", { children: active.title }) : null, active.description ? _jsx("p", { className: "rk-onboarding__description", children: active.description }) : null, _jsx("div", { className: "rk-onboarding__content", children: body }), error ? _jsx("p", { className: "rk-onboarding__error", role: "alert", children: error }) : null, _jsxs("footer", { children: [_jsx(Button, { variant: "secondary", disabled: index <= lead || saving || continuing, onClick: () => void save({ currentStep: index - 1 }), children: L.back }), _jsx("nav", { className: "rk-onboarding__dots", "aria-label": "Setup progress", children: builtins.map((step, i) => _jsx("button", { type: "button", className: cx("rk-onboarding__dot", i === index && "is-active", activeState.completed.includes(step.id) && "is-done"), "aria-label": `Step ${i + 1}: ${step.label}`, "aria-current": i === index ? "step" : undefined, disabled: (gated && i === 0 && builtins.length > 1) || continuing, onClick: () => { if (!continuing && i >= lead && (i <= index || canContinue))
|
|
393
|
+
void save({ currentStep: i }); } }, step.id)) }), _jsx("span", { className: "rk-onboarding__hint", children: !canContinue && active.blockedHint ? active.blockedHint : null }), allowSkip ? _jsx(Button, { variant: "ghost", disabled: saving || continuing, onClick: () => { void save({ currentStep: index, skipped: true }).then((ok) => ok ? runCallback(onSkip) : undefined); }, children: L.skip }) : null, active.skippable ? _jsx(Button, { variant: "ghost", disabled: saving || continuing, onClick: () => void markAndMove(index + 1, { skipped: active.id }), children: L.skipStep }) : null, _jsx(Button, { variant: "primary", disabled: saving || continuing || !canContinue, onClick: () => void advance(), children: active.continueLabel ?? (index === builtins.length - 1 ? L.finish : L.continue) })] })] })] })] });
|
|
394
|
+
}
|
|
337
395
|
return _jsxs("main", { className: rootClass, "data-testid": "onboarding", "data-step-id": active.id, children: [_jsx(TitleStrip, { titlebar: titlebar, dragRegion: dragRegion }), _jsxs("aside", { className: "rk-onboarding__rail", children: [_jsx("div", { className: "rk-onboarding__brand", children: appName ?? legal?.appName }), _jsx("nav", { "aria-label": "Setup progress", children: builtins.map((step, i) => _jsxs("button", { type: "button", className: cx("rk-onboarding__step", i === index && "is-active", activeState.completed.includes(step.id) && "is-done"), "aria-current": i === index ? "step" : undefined, disabled: (gated && i === 0 && builtins.length > 1) || continuing, onClick: () => { if (!continuing && i >= lead && (i <= index || canContinue))
|
|
338
396
|
void save({ currentStep: i }); }, children: [_jsx("span", { children: activeState.completed.includes(step.id) ? "✓" : i + 1 }), step.label] }, step.id)) })] }), _jsxs("section", { className: "rk-onboarding__main", children: [_jsxs("header", { children: [_jsxs("p", { className: "rk-onboarding__eyebrow", children: ["Step ", index + 1, " of ", builtins.length] }), active.title ? _jsx("h1", { children: active.title }) : null, active.description ? _jsx("p", { className: "rk-onboarding__description", children: active.description }) : null] }), _jsx("div", { className: "rk-onboarding__content", children: body }), error ? _jsx("p", { className: "rk-onboarding__error", role: "alert", children: error }) : null, _jsxs("footer", { children: [allowSkip ? _jsx(Button, { variant: "ghost", disabled: saving || continuing, onClick: () => { void save({ currentStep: index, skipped: true }).then((ok) => ok ? runCallback(onSkip) : undefined); }, children: L.skip }) : null, active.skippable ? _jsx(Button, { variant: "ghost", disabled: saving || continuing, onClick: () => void markAndMove(index + 1, { skipped: active.id }), children: L.skipStep }) : null, _jsx("span", { className: "rk-onboarding__hint", children: !canContinue && active.blockedHint ? active.blockedHint : null }), _jsx(Button, { variant: "secondary", disabled: index <= lead || saving || continuing, onClick: () => void save({ currentStep: index - 1 }), children: L.back }), _jsx(Button, { variant: "primary", disabled: saving || continuing || !canContinue, onClick: () => void advance(), children: active.continueLabel ?? (index === builtins.length - 1 ? L.finish : L.continue) })] })] })] });
|
|
339
397
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { Onboarding, isLegalAccepted, onboardingStateKey } from "./Onboarding.js";
|
|
1
|
+
export { Onboarding, isLegalAccepted, isStepSettled, onboardingStateKey, restartOnboardingSetup } from "./Onboarding.js";
|
|
2
2
|
export { licenseStep, modelsStep, permissionsStep, signInStep } from "./steps.js";
|
|
3
3
|
export type { LicenseStepCopy, LicenseStepOptions, ModelsStepOptions } from "./steps.js";
|
|
4
4
|
export { licenseBackendFromView, licenseStateFromView, signInBackendFromOAuth } from "./adapters.js";
|
|
5
5
|
export type { LicenseViewBackendOptions, LicenseViewClient, LicenseViewLike, OAuthSignInPort } from "./adapters.js";
|
|
6
|
-
export { LEGAL_RECEIPT_COMMANDS, createNativeLegalReceiptPort, reopenLegalAcceptance, resetLegalAcceptance } from "./legal-receipt.js";
|
|
7
|
-
export type { LegalReceiptInvoke, NativeLegalReceiptPortOptions } from "./legal-receipt.js";
|
|
6
|
+
export { LEGAL_RECEIPT_COMMANDS, createNativeLegalReceiptPort, importLegacyAcceptance, reopenLegalAcceptance, resetLegalAcceptance } from "./legal-receipt.js";
|
|
7
|
+
export type { LegacyAcceptanceImportOptions, LegalReceiptInvoke, NativeLegalReceiptPortOptions } from "./legal-receipt.js";
|
|
8
8
|
export type * from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Onboarding, isLegalAccepted, onboardingStateKey } from "./Onboarding.js";
|
|
1
|
+
export { Onboarding, isLegalAccepted, isStepSettled, onboardingStateKey, restartOnboardingSetup } from "./Onboarding.js";
|
|
2
2
|
export { licenseStep, modelsStep, permissionsStep, signInStep } from "./steps.js";
|
|
3
3
|
export { licenseBackendFromView, licenseStateFromView, signInBackendFromOAuth } from "./adapters.js";
|
|
4
|
-
export { LEGAL_RECEIPT_COMMANDS, createNativeLegalReceiptPort, reopenLegalAcceptance, resetLegalAcceptance } from "./legal-receipt.js";
|
|
4
|
+
export { LEGAL_RECEIPT_COMMANDS, createNativeLegalReceiptPort, importLegacyAcceptance, reopenLegalAcceptance, resetLegalAcceptance } from "./legal-receipt.js";
|
package/dist/legal-receipt.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LegalReceiptPort, SettingsBackend } from "./types.js";
|
|
1
|
+
import type { LegalAcceptanceRecord, LegalReceiptCreateRequest, LegalReceiptPort, SettingsBackend } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* The commands of the native legal-receipt host service. The native side registers them once under the host caller policy
|
|
4
4
|
* (`rightkit-tauri-shell` host services); the trusted window is the one that shows the legal gate. Names are provisional
|
|
@@ -8,6 +8,8 @@ export declare const LEGAL_RECEIPT_COMMANDS: Readonly<{
|
|
|
8
8
|
read: "rightkit_legal_receipt_read";
|
|
9
9
|
create: "rightkit_legal_receipt_create";
|
|
10
10
|
reset: "rightkit_legal_receipt_reset";
|
|
11
|
+
/** Imports a renderer-held legacy acceptance once (see `importLegacyAcceptance`). */
|
|
12
|
+
import: "rightkit_legal_receipt_import";
|
|
11
13
|
}>;
|
|
12
14
|
/** Tauri's `invoke`, structurally (`@tauri-apps/api/core`). No import, so the package stays free of Tauri. */
|
|
13
15
|
export type LegalReceiptInvoke = (command: string, args?: Record<string, unknown>) => Promise<unknown>;
|
|
@@ -18,7 +20,9 @@ export interface NativeLegalReceiptPortOptions {
|
|
|
18
20
|
/**
|
|
19
21
|
* `LegalReceiptPort` over the native legal-receipt service.
|
|
20
22
|
*
|
|
21
|
-
* - `read` returns the stored record, or `null` when there is none or the stored value is not a well-formed record.
|
|
23
|
+
* - `read` returns the stored record, or `null` when there is none or the stored value is not a well-formed record. A
|
|
24
|
+
* historical receipt (a version or digest that no longer matches) comes back as `{ record, current: false }`, which keeps
|
|
25
|
+
* the re-acceptance variant and the setup progress; the native service offers it that way.
|
|
22
26
|
* - `create` sends the declaration (`version`, `documentsHash`, `documents`, `eligibilityBasis`, `surface`) and NO time. The
|
|
23
27
|
* service compares it with the manifest it embeds, stamps native UTC time, stores the receipt and returns the record; a
|
|
24
28
|
* refusal or a malformed answer throws, which keeps the gate closed.
|
|
@@ -26,6 +30,27 @@ export interface NativeLegalReceiptPortOptions {
|
|
|
26
30
|
* There is no `write`: the renderer never supplies a stored record.
|
|
27
31
|
*/
|
|
28
32
|
export declare function createNativeLegalReceiptPort({ invoke, commands }: NativeLegalReceiptPortOptions): LegalReceiptPort;
|
|
33
|
+
export interface LegacyAcceptanceImportOptions {
|
|
34
|
+
invoke: LegalReceiptInvoke;
|
|
35
|
+
/** Key of the renderer-held legacy receipt, for example `scraperight.legal-acceptance.v1`. */
|
|
36
|
+
storageKey: string;
|
|
37
|
+
/**
|
|
38
|
+
* Maps the stored text to the declaration the native service verifies against its manifest (hashes, versions, documents).
|
|
39
|
+
* `null`: not an importable record; nothing is sent and the legacy key stays in place.
|
|
40
|
+
*/
|
|
41
|
+
parse: (raw: string) => LegalReceiptCreateRequest | null;
|
|
42
|
+
/** Storage that holds the legacy key. Default `localStorage` when present. `null`: no legacy storage to read. */
|
|
43
|
+
storage?: Pick<Storage, "getItem" | "removeItem"> | null;
|
|
44
|
+
commands?: Partial<typeof LEGAL_RECEIPT_COMMANDS>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One-time import of a renderer-held (legacy) acceptance into the native receipt. The legacy record is submitted once to the
|
|
48
|
+
* native `import` command, which verifies it against the manifest and stamps the import with its own clock. Only after the
|
|
49
|
+
* native service confirms the record is the legacy key removed. A refusal or a failed call throws and leaves the key in place,
|
|
50
|
+
* so the next launch retries; no acceptance is invented here. Returns the imported record, or `null` when there is nothing to
|
|
51
|
+
* import.
|
|
52
|
+
*/
|
|
53
|
+
export declare function importLegacyAcceptance({ invoke, storageKey, parse, storage, commands }: LegacyAcceptanceImportOptions): Promise<LegalAcceptanceRecord | null>;
|
|
29
54
|
/**
|
|
30
55
|
* Re-opens the agreement: the stored acceptance stops counting, so the gate shows as a re-acceptance on the next mount of
|
|
31
56
|
* `Onboarding`. The old record (and the app's receipt) stays until the user agrees again; agreeing creates the new receipt and
|
package/dist/legal-receipt.js
CHANGED
|
@@ -8,6 +8,8 @@ export const LEGAL_RECEIPT_COMMANDS = Object.freeze({
|
|
|
8
8
|
read: "rightkit_legal_receipt_read",
|
|
9
9
|
create: "rightkit_legal_receipt_create",
|
|
10
10
|
reset: "rightkit_legal_receipt_reset",
|
|
11
|
+
/** Imports a renderer-held legacy acceptance once (see `importLegacyAcceptance`). */
|
|
12
|
+
import: "rightkit_legal_receipt_import",
|
|
11
13
|
});
|
|
12
14
|
/** Host commands reject with `{ code, message }`; the gate and steps show `Error#message`. */
|
|
13
15
|
function hostError(error) {
|
|
@@ -24,10 +26,26 @@ async function call(invoke, command, args) {
|
|
|
24
26
|
throw hostError(error);
|
|
25
27
|
}
|
|
26
28
|
}
|
|
29
|
+
/** The declaration the native service compares with its manifest: never a time. */
|
|
30
|
+
function declaration(request) {
|
|
31
|
+
return {
|
|
32
|
+
version: request.version,
|
|
33
|
+
documentsHash: request.documentsHash,
|
|
34
|
+
documents: request.documents,
|
|
35
|
+
eligibilityBasis: request.eligibilityBasis,
|
|
36
|
+
surface: request.surface,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** True for a historical receipt value (`{ record, current: false }`) as the native read may return it. */
|
|
40
|
+
function isHistoricalValue(value) {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "record" in value && value.current === false;
|
|
42
|
+
}
|
|
27
43
|
/**
|
|
28
44
|
* `LegalReceiptPort` over the native legal-receipt service.
|
|
29
45
|
*
|
|
30
|
-
* - `read` returns the stored record, or `null` when there is none or the stored value is not a well-formed record.
|
|
46
|
+
* - `read` returns the stored record, or `null` when there is none or the stored value is not a well-formed record. A
|
|
47
|
+
* historical receipt (a version or digest that no longer matches) comes back as `{ record, current: false }`, which keeps
|
|
48
|
+
* the re-acceptance variant and the setup progress; the native service offers it that way.
|
|
31
49
|
* - `create` sends the declaration (`version`, `documentsHash`, `documents`, `eligibilityBasis`, `surface`) and NO time. The
|
|
32
50
|
* service compares it with the manifest it embeds, stamps native UTC time, stores the receipt and returns the record; a
|
|
33
51
|
* refusal or a malformed answer throws, which keeps the gate closed.
|
|
@@ -39,18 +57,16 @@ export function createNativeLegalReceiptPort({ invoke, commands }) {
|
|
|
39
57
|
return {
|
|
40
58
|
async read() {
|
|
41
59
|
const value = await call(invoke, names.read);
|
|
42
|
-
|
|
60
|
+
if (value === null || value === undefined)
|
|
61
|
+
return null;
|
|
62
|
+
if (isHistoricalValue(value)) {
|
|
63
|
+
const record = readAcceptance(value.record);
|
|
64
|
+
return record ? { record, current: false } : null;
|
|
65
|
+
}
|
|
66
|
+
return readAcceptance(value) ?? null;
|
|
43
67
|
},
|
|
44
68
|
async create(request) {
|
|
45
|
-
const created = readAcceptance(await call(invoke, names.create, {
|
|
46
|
-
request: {
|
|
47
|
-
version: request.version,
|
|
48
|
-
documentsHash: request.documentsHash,
|
|
49
|
-
documents: request.documents,
|
|
50
|
-
eligibilityBasis: request.eligibilityBasis,
|
|
51
|
-
surface: request.surface,
|
|
52
|
-
},
|
|
53
|
-
}));
|
|
69
|
+
const created = readAcceptance(await call(invoke, names.create, { request: declaration(request) }));
|
|
54
70
|
if (!created)
|
|
55
71
|
throw new Error("The legal receipt service returned an invalid record.");
|
|
56
72
|
return created;
|
|
@@ -58,6 +74,47 @@ export function createNativeLegalReceiptPort({ invoke, commands }) {
|
|
|
58
74
|
async reset() { await call(invoke, names.reset); },
|
|
59
75
|
};
|
|
60
76
|
}
|
|
77
|
+
function browserStorage() {
|
|
78
|
+
try {
|
|
79
|
+
return typeof localStorage === "undefined" ? null : localStorage;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* One-time import of a renderer-held (legacy) acceptance into the native receipt. The legacy record is submitted once to the
|
|
87
|
+
* native `import` command, which verifies it against the manifest and stamps the import with its own clock. Only after the
|
|
88
|
+
* native service confirms the record is the legacy key removed. A refusal or a failed call throws and leaves the key in place,
|
|
89
|
+
* so the next launch retries; no acceptance is invented here. Returns the imported record, or `null` when there is nothing to
|
|
90
|
+
* import.
|
|
91
|
+
*/
|
|
92
|
+
export async function importLegacyAcceptance({ invoke, storageKey, parse, storage, commands }) {
|
|
93
|
+
const names = { ...LEGAL_RECEIPT_COMMANDS, ...commands };
|
|
94
|
+
const backing = storage === undefined ? browserStorage() : storage;
|
|
95
|
+
let raw;
|
|
96
|
+
try {
|
|
97
|
+
raw = backing?.getItem(storageKey) ?? null;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
raw = null;
|
|
101
|
+
}
|
|
102
|
+
if (raw === null)
|
|
103
|
+
return null;
|
|
104
|
+
const request = parse(raw);
|
|
105
|
+
if (!request)
|
|
106
|
+
return null;
|
|
107
|
+
const imported = readAcceptance(await call(invoke, names.import, { request: declaration(request) }));
|
|
108
|
+
if (!imported)
|
|
109
|
+
throw new Error("The legal receipt service returned an invalid record.");
|
|
110
|
+
try {
|
|
111
|
+
backing?.removeItem(storageKey);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* the native receipt is stored; the native import is idempotent, so a later run only repeats the confirmation */
|
|
115
|
+
}
|
|
116
|
+
return imported;
|
|
117
|
+
}
|
|
61
118
|
/**
|
|
62
119
|
* Re-opens the agreement: the stored acceptance stops counting, so the gate shows as a re-acceptance on the next mount of
|
|
63
120
|
* `Onboarding`. The old record (and the app's receipt) stays until the user agrees again; agreeing creates the new receipt and
|
package/dist/types.d.ts
CHANGED
|
@@ -130,8 +130,17 @@ export interface LegalReceiptCreateRequest {
|
|
|
130
130
|
* `reset()` clears the stored receipt (used by `resetLegalAcceptance`). A port needs `create` or `write`; with neither,
|
|
131
131
|
* agreeing fails and the gate stays closed.
|
|
132
132
|
*/
|
|
133
|
+
/**
|
|
134
|
+
* A receipt that exists but no longer counts: its version or documents differ from the current agreement. `read()` may
|
|
135
|
+
* return it so the gate shows as a re-acceptance and the setup progress is kept (`current: false`). Any other return
|
|
136
|
+
* value is a current record or `null`.
|
|
137
|
+
*/
|
|
138
|
+
export interface HistoricalLegalReceipt {
|
|
139
|
+
record: LegalAcceptanceRecord;
|
|
140
|
+
current: false;
|
|
141
|
+
}
|
|
133
142
|
export interface LegalReceiptPort {
|
|
134
|
-
read(): LegalAcceptanceRecord | null | Promise<LegalAcceptanceRecord | null>;
|
|
143
|
+
read(): LegalAcceptanceRecord | HistoricalLegalReceipt | null | Promise<LegalAcceptanceRecord | HistoricalLegalReceipt | null>;
|
|
135
144
|
/** Native receipt creation: returns the stored record, stamped with native time. Preferred over `write`. */
|
|
136
145
|
create?(request: LegalReceiptCreateRequest): LegalAcceptanceRecord | Promise<LegalAcceptanceRecord>;
|
|
137
146
|
/** @deprecated Renderer-built record with webview time. Implement `create` on a native receipt service instead. */
|
|
@@ -226,6 +235,19 @@ export type LegalMode = "gate" | "notice" | "none";
|
|
|
226
235
|
export interface OnboardingProps {
|
|
227
236
|
/** Window-sized by default; use `contained` when the host sizes the setup surface. */
|
|
228
237
|
layout?: "window" | "contained";
|
|
238
|
+
/**
|
|
239
|
+
* Setup chrome. `rail` (default): a left step rail with a "Step n of N" eyebrow. `card`: one centred card with the app
|
|
240
|
+
* name on top and a dot pager, no rail (the HeardRight v5 onboarding mock). Applies to the setup steps only; the legal
|
|
241
|
+
* gate keeps its own layout.
|
|
242
|
+
*/
|
|
243
|
+
frame?: "rail" | "card";
|
|
244
|
+
/** Vertical density of the setup steps. `compact` (rail frame): main padding 18px top, content 8px, rail 188px wide. Default `default`. */
|
|
245
|
+
density?: "default" | "compact";
|
|
246
|
+
/**
|
|
247
|
+
* Clears the macOS traffic lights: the rail (or card brand row) and the titlebar strip start past them. Use with a
|
|
248
|
+
* `titlebar` or `dragRegion` on macOS windows with overlay title bars. Default false.
|
|
249
|
+
*/
|
|
250
|
+
trafficLightInset?: boolean;
|
|
229
251
|
settings: SettingsBackend;
|
|
230
252
|
/** Required when `legalMode` is `gate` (default). Optional for `notice` (the documents shown) and `none`. */
|
|
231
253
|
legal?: LegalStepConfig;
|
|
@@ -277,7 +299,7 @@ export interface OnboardingProps {
|
|
|
277
299
|
modelsIntro?: ReactNode;
|
|
278
300
|
/** Show "Set up later". Default true. */
|
|
279
301
|
allowSkip?: boolean;
|
|
280
|
-
/** Optional titlebar content rendered in a strip along the top. */
|
|
302
|
+
/** Optional titlebar content rendered in a strip along the top. On macOS with `trafficLightInset`, the strip starts past the lights. */
|
|
281
303
|
titlebar?: ReactNode;
|
|
282
304
|
/** Render a `data-tauri-drag-region` strip at the top of the setup steps. Default false. Never applied to the legal stage; see `legalDragRegion`. */
|
|
283
305
|
dragRegion?: boolean;
|
package/onboarding.css
CHANGED
|
@@ -40,3 +40,23 @@
|
|
|
40
40
|
.rk-onboarding__titlebar { position: absolute; inset: 0 0 auto 0; z-index: 2; display: flex; align-items: center; height: var(--rk-onboarding-titlebar-h, 32px); padding: 0 12px; }
|
|
41
41
|
.rk-onboarding__hint { color: var(--rk-ink-2, #6b655c); font-size: 12px; text-align: right; }
|
|
42
42
|
.rk-onboarding__intro { margin-bottom: 12px; color: var(--rk-ink-2, #6b655c); }
|
|
43
|
+
/* Density (HeardRight: onboarding too tall). compact: rail 188px, main 18px top, content 8px. */
|
|
44
|
+
.rk-onboarding--compact { grid-template-columns: 188px minmax(0, 1fr); }
|
|
45
|
+
.rk-onboarding--compact .rk-onboarding__main { padding-top: 18px; }
|
|
46
|
+
.rk-onboarding--compact .rk-onboarding__content { padding: 8px 0; }
|
|
47
|
+
/* macOS traffic lights: the rail and the titlebar strip start past them (lights-top: rail top; lights width from shell.css). */
|
|
48
|
+
.rk-onboarding--lights .rk-onboarding__rail { padding-top: var(--rk-onboarding-lights-top, 40px); }
|
|
49
|
+
.rk-onboarding--lights .rk-onboarding__titlebar { padding-left: calc(var(--rk-lights-width, 72px) + 6px); }
|
|
50
|
+
.rk-onboarding--lights .rk-onboarding__brand--card { padding-left: calc(var(--rk-lights-width, 72px) + 6px); }
|
|
51
|
+
/* card frame (HeardRight v5 mock): one centred card, the app name on top, a dot pager and no rail. */
|
|
52
|
+
.rk-onboarding--card { display: flex; flex-direction: column; align-items: stretch; }
|
|
53
|
+
.rk-onboarding--card .rk-onboarding__main--card { flex: 1; display: flex; flex-direction: column; align-items: center; max-width: none; width: 100%; padding: 0 clamp(24px, 5vw, 56px) 24px; }
|
|
54
|
+
.rk-onboarding__brand--card { align-self: stretch; padding: 18px 20px; font-size: 18px; font-weight: 900; letter-spacing: -.03em; }
|
|
55
|
+
.rk-onboarding__frame { display: flex; flex-direction: column; flex: 1; width: min(860px, 100%); min-height: 0; padding-top: 24px; }
|
|
56
|
+
.rk-onboarding--card .rk-onboarding__content { flex: 1; }
|
|
57
|
+
.rk-onboarding--card footer .rk-onboarding__hint { flex: none; }
|
|
58
|
+
.rk-onboarding__dots { display: flex; align-items: center; justify-content: center; gap: 10px; flex: 1; }
|
|
59
|
+
.rk-onboarding__dot { width: 10px; height: 10px; padding: 0; border: 0; border-radius: 999px; background: var(--rk-line-strong, rgba(35,31,26,.18)); cursor: pointer; transition: width .2s ease, background .2s ease; }
|
|
60
|
+
.rk-onboarding__dot.is-done { background: var(--rk-ok, #1e7f3c); }
|
|
61
|
+
.rk-onboarding__dot.is-active { width: 42px; background: var(--rk-accent, #7040c4); }
|
|
62
|
+
.rk-onboarding__dot:disabled { cursor: not-allowed; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/onboarding",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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",
|
|
@@ -29,21 +29,11 @@
|
|
|
29
29
|
"react": ">=18 <21"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@rightkit/react-shell": "0.7.
|
|
33
|
-
"@rightkit/legal-ui": "0.4.
|
|
34
|
-
"@rightkit/qa-chrome": "0.1.9",
|
|
35
|
-
"@testing-library/jest-dom": "^7.0.1",
|
|
36
|
-
"@testing-library/dom": "^10.4.2",
|
|
37
|
-
"@testing-library/react": "^16.3.3",
|
|
38
|
-
"@testing-library/user-event": "^14.6.8",
|
|
32
|
+
"@rightkit/react-shell": "0.7.3",
|
|
33
|
+
"@rightkit/legal-ui": "0.4.2",
|
|
39
34
|
"@types/react": "^19.3.0",
|
|
40
|
-
"@types/react-dom": "^19.3.0",
|
|
41
|
-
"jsdom": "^30.1.3",
|
|
42
35
|
"react": "^19.3.1",
|
|
43
|
-
"
|
|
44
|
-
"typescript": "^7.0.2",
|
|
45
|
-
"vite": "^8.3.4",
|
|
46
|
-
"vitest": "^5.0.5"
|
|
36
|
+
"typescript": "^7.0.2"
|
|
47
37
|
},
|
|
48
38
|
"publishConfig": {
|
|
49
39
|
"registry": "https://registry.npmjs.org",
|
|
@@ -56,8 +46,6 @@
|
|
|
56
46
|
},
|
|
57
47
|
"scripts": {
|
|
58
48
|
"build": "tsc -p tsconfig.json",
|
|
59
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
60
|
-
"pretest": "pnpm --filter \"@rightkit/onboarding^...\" run build",
|
|
61
|
-
"test": "vitest run"
|
|
49
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
62
50
|
}
|
|
63
51
|
}
|