@rightkit/onboarding 0.5.0 → 0.6.0
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 +2 -2
- package/dist/Onboarding.d.ts +1 -1
- package/dist/Onboarding.js +54 -13
- package/dist/steps.d.ts +17 -2
- package/dist/steps.js +8 -4
- package/dist/types.d.ts +37 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ First-run onboarding framework for Right Suite apps. The legal agreement gate co
|
|
|
13
13
|
|
|
14
14
|
| Import | Contents |
|
|
15
15
|
|---|---|
|
|
16
|
-
| `@rightkit/onboarding` | `Onboarding`, `isLegalAccepted`, `onboardingStateKey`, `licenseStep`, `modelsStep`, `permissionsStep`, `signInStep` (Google/Apple/OAuth buttons that call the shared `rightkit-oauth` owner; copy says "Opens your browser"; never an in-app webview,
|
|
16
|
+
| `@rightkit/onboarding` | `Onboarding`, `isLegalAccepted`, `onboardingStateKey`, `licenseStep`, `modelsStep`, `permissionsStep`, `signInStep` (Google/Apple/OAuth buttons that call the shared `rightkit-oauth` owner; copy says "Opens your browser"; never an in-app webview, bogusyogi/rightkit#279); types from `types.ts`, `ModelsStepOptions` |
|
|
17
17
|
| `@rightkit/onboarding/onboarding.css` | Stylesheet |
|
|
18
18
|
|
|
19
19
|
## Peer dependencies
|
|
@@ -45,7 +45,7 @@ import "@rightkit/onboarding/onboarding.css";
|
|
|
45
45
|
|
|
46
46
|
Free or open-source apps without a legal gate pass `legalMode="none"` (no legal step, `legal` not needed, `appName` for the rail) or `legalMode="notice"` (no gate or acceptance; a first Notice step lists `legal.documents` as links). The default is `"gate"`.
|
|
47
47
|
|
|
48
|
-
Acceptance is stored as `legalAcceptance: { version, documentsHash, acceptedAt, eligibilityBasis }`. An app that already keeps its own acceptance receipt can pass `legalReceipt` (`read()` and `write(record)`) and keep that store as the source of truth. Steps can set `canContinue(context)
|
|
48
|
+
Acceptance is stored as `legalAcceptance: { version, documentsHash, acceptedAt, eligibilityBasis }`. An app that already keeps its own acceptance receipt can pass `legalReceipt` (`read()` and `write(record)`) and keep that store as the source of truth. Steps can set `canContinue(context)`, `onContinue(context)` (a veto: `false` or a throw keeps the step), `continueLabel`, and `skippable` (a per-step Skip button). `ctx.setResult(value)` stores a value that `onComplete({ completed, skipped, steps })` receives. `labels` overrides the button and status text, and `licenseStep(backend, { copy })` overrides the licence copy.
|
|
49
49
|
|
|
50
50
|
## When the legal gate re-prompts
|
|
51
51
|
|
package/dist/Onboarding.d.ts
CHANGED
|
@@ -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, requiredDocumentsHash?: string): boolean;
|
|
4
4
|
export declare function onboardingStateKey(): string;
|
|
5
|
-
export declare function Onboarding({ settings, legal, legalMode, appName, steps, permissions, models, license, onComplete, onSkip, className, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip, titlebar, dragRegion, legalDragRegion, legalReceipt }: OnboardingProps): import("react").JSX.Element | null;
|
|
5
|
+
export declare function Onboarding({ settings, legal, legalMode, appName, steps, permissions, models, license, onComplete, onSkip, className, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip, titlebar, dragRegion, legalDragRegion, legalReceipt, labels: labelOverrides }: OnboardingProps): import("react").JSX.Element | null;
|
package/dist/Onboarding.js
CHANGED
|
@@ -6,6 +6,7 @@ import { cx } from "./ui.js";
|
|
|
6
6
|
import { licenseStep, modelsStep, permissionsStep } from "./steps.js";
|
|
7
7
|
const KEY = "onboarding.state";
|
|
8
8
|
const INITIAL = { version: 1, currentStep: 0, completed: [], skipped: false, legalAccepted: false };
|
|
9
|
+
const DEFAULT_LABELS = { skip: "Set up later", skipStep: "Skip", back: "Back", continue: "Continue", finish: "Finish setup", loading: "Loading setup…" };
|
|
9
10
|
function readAcceptance(value) {
|
|
10
11
|
if (!value || typeof value !== "object")
|
|
11
12
|
return undefined;
|
|
@@ -29,6 +30,7 @@ function readState(value) {
|
|
|
29
30
|
legalAccepted: candidate.legalAccepted === true || legalAcceptance !== undefined,
|
|
30
31
|
eligibilityBasis: legalAcceptance?.eligibilityBasis ?? (candidate.eligibilityBasis === "individual" || candidate.eligibilityBasis === "enterprise" ? candidate.eligibilityBasis : undefined),
|
|
31
32
|
...(legalAcceptance ? { legalAcceptance } : {}),
|
|
33
|
+
...(Array.isArray(candidate.skippedSteps) ? { skippedSteps: candidate.skippedSteps.filter((x) => typeof x === "string") } : {}),
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
/** Replace the acceptance in `state` with the one from the app's receipt (`undefined`: not accepted). */
|
|
@@ -84,9 +86,10 @@ function NoticeList({ documents }) {
|
|
|
84
86
|
return _jsx("p", { className: "rk-onboarding__muted", children: "No notices to show." });
|
|
85
87
|
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)) });
|
|
86
88
|
}
|
|
87
|
-
export function Onboarding({ settings, legal, legalMode = "gate", appName, steps = [], permissions, models, license, onComplete, onSkip, className, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip = true, titlebar, dragRegion, legalDragRegion = false, legalReceipt }) {
|
|
89
|
+
export function Onboarding({ settings, legal, legalMode = "gate", appName, steps = [], permissions, models, license, onComplete, onSkip, className, requiredVersion, requiredDocumentsHash, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip = true, titlebar, dragRegion, legalDragRegion = false, legalReceipt, labels: labelOverrides }) {
|
|
88
90
|
if (legalMode === "gate" && !legal)
|
|
89
91
|
throw new Error("Onboarding: `legal` is required when legalMode is \"gate\"");
|
|
92
|
+
const L = { ...DEFAULT_LABELS, ...labelOverrides };
|
|
90
93
|
const gated = legalMode === "gate";
|
|
91
94
|
/** Leading steps the user cannot navigate to: the Agreement stand-in in gate mode. */
|
|
92
95
|
const lead = gated ? 1 : 0;
|
|
@@ -97,6 +100,9 @@ export function Onboarding({ settings, legal, legalMode = "gate", appName, steps
|
|
|
97
100
|
const [gateCleared, setGateCleared] = useState(false);
|
|
98
101
|
/** One auto-start per mount, even when the models step unmounts (Back/Next) or the step list is rebuilt. */
|
|
99
102
|
const modelsStarted = useRef(false);
|
|
103
|
+
/** Values passed to `ctx.setResult`, keyed by step id; handed to `onComplete`. */
|
|
104
|
+
const stepResults = useRef({});
|
|
105
|
+
const [continuing, setContinuing] = useState(false);
|
|
100
106
|
/** Persists a patch; throws when the settings backend rejects it. */
|
|
101
107
|
const persist = useCallback(async (patch) => {
|
|
102
108
|
setSaving(true);
|
|
@@ -169,6 +175,9 @@ export function Onboarding({ settings, legal, legalMode = "gate", appName, steps
|
|
|
169
175
|
const index = !accepted ? 0 : builtins.length > lead ? Math.max(lead, Math.min(activeState.currentStep, builtins.length - 1)) : 0;
|
|
170
176
|
const active = builtins[index];
|
|
171
177
|
const activeId = active?.id ?? "";
|
|
178
|
+
/** The step on screen now; an `onContinue` that resolves after the step changed is stale. */
|
|
179
|
+
const activeIdRef = useRef(activeId);
|
|
180
|
+
activeIdRef.current = activeId;
|
|
172
181
|
const setReady = useCallback((ready) => setReadyMap((m) => m[activeId] === ready ? m : { ...m, [activeId]: ready }), [activeId]);
|
|
173
182
|
useEffect(() => {
|
|
174
183
|
if (!needsGate) {
|
|
@@ -188,7 +197,8 @@ export function Onboarding({ settings, legal, legalMode = "gate", appName, steps
|
|
|
188
197
|
setGateCleared(true); })();
|
|
189
198
|
return () => { live = false; };
|
|
190
199
|
}, [needsGate, gateCleared, beforeLegalGate]);
|
|
191
|
-
const
|
|
200
|
+
const setResult = useCallback((value) => { stepResults.current[activeId] = value; }, [activeId]);
|
|
201
|
+
const context = { state: activeState, settings, setState: saveStep, permissions, models, license, ready: readyMap, setReady, setResult };
|
|
192
202
|
const canContinue = active?.canContinue ? active.canContinue(context) : true;
|
|
193
203
|
const runCallback = async (callback) => {
|
|
194
204
|
try {
|
|
@@ -198,24 +208,55 @@ export function Onboarding({ settings, legal, legalMode = "gate", appName, steps
|
|
|
198
208
|
setError(e instanceof Error ? e.message : String(e));
|
|
199
209
|
}
|
|
200
210
|
};
|
|
201
|
-
const
|
|
202
|
-
|
|
211
|
+
const resultOf = (completed, skipped) => ({ completed, skipped, steps: { ...stepResults.current } });
|
|
212
|
+
/** Marks `mark.completed` done or `mark.skipped` skipped (never both), then moves to `nextIndex`; completing the last step calls `onComplete`. */
|
|
213
|
+
const markAndMove = async (nextIndex, mark = {}) => {
|
|
214
|
+
const previousSkipped = activeState.skippedSteps ?? [];
|
|
215
|
+
// A step is completed or skipped, never both: a skip removes an earlier completion of the same step.
|
|
216
|
+
const completedBase = activeState.completed.filter((id) => id !== mark.skipped);
|
|
217
|
+
const completed = mark.completed && !completedBase.includes(mark.completed) ? [...completedBase, mark.completed] : completedBase;
|
|
218
|
+
const skippedBase = previousSkipped.filter((id) => id !== mark.completed);
|
|
219
|
+
const skippedSteps = mark.skipped && !skippedBase.includes(mark.skipped) ? [...skippedBase, mark.skipped] : skippedBase;
|
|
203
220
|
if (nextIndex >= builtins.length) {
|
|
204
|
-
if (await save({ completed, currentStep: Math.max(0, builtins.length - 1), skipped: false }))
|
|
205
|
-
await runCallback(onComplete);
|
|
221
|
+
if (await save({ completed, skippedSteps, currentStep: Math.max(0, builtins.length - 1), skipped: false }))
|
|
222
|
+
await runCallback(() => onComplete(resultOf(completed, skippedSteps)));
|
|
206
223
|
return;
|
|
207
224
|
}
|
|
208
|
-
await save({ completed, currentStep: nextIndex, skipped: false });
|
|
225
|
+
await save({ completed, skippedSteps, currentStep: nextIndex, skipped: false });
|
|
226
|
+
};
|
|
227
|
+
/** Primary button: runs the step's `onContinue` veto first; `false` or a throw keeps the step. */
|
|
228
|
+
const advance = async () => {
|
|
229
|
+
if (active.onContinue) {
|
|
230
|
+
const stepId = active.id;
|
|
231
|
+
setContinuing(true);
|
|
232
|
+
setError(null);
|
|
233
|
+
let proceed = true;
|
|
234
|
+
try {
|
|
235
|
+
proceed = (await active.onContinue(context)) !== false;
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
if (activeIdRef.current === stepId)
|
|
239
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
setContinuing(false);
|
|
244
|
+
}
|
|
245
|
+
// The step changed while onContinue was awaited: discard this completion rather than move or save from stale state.
|
|
246
|
+
if (!proceed || activeIdRef.current !== stepId)
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
await markAndMove(index + 1, { completed: active.id });
|
|
209
250
|
};
|
|
210
251
|
if (error && !state)
|
|
211
252
|
return _jsxs("div", { className: cx("rk-onboarding", className), children: [_jsx("p", { className: "rk-onboarding__error", role: "alert", children: error }), _jsx(Button, { onClick: () => window.location.reload(), children: "Retry" })] });
|
|
212
253
|
if (state && !active)
|
|
213
|
-
return _jsxs("main", { className: cx("rk-onboarding", className), "data-testid": "onboarding", children: [_jsx("p", { className: "rk-onboarding__muted", children: "Nothing to set up." }), _jsx(Button, { variant: "primary", onClick: () => void runCallback(onComplete), children:
|
|
254
|
+
return _jsxs("main", { className: cx("rk-onboarding", className), "data-testid": "onboarding", children: [_jsx("p", { className: "rk-onboarding__muted", children: "Nothing to set up." }), _jsx(Button, { variant: "primary", onClick: () => void runCallback(() => onComplete(resultOf(activeState.completed, activeState.skippedSteps ?? []))), children: L.finish })] });
|
|
214
255
|
if (!state || !active)
|
|
215
|
-
return _jsx("div", { className: cx("rk-onboarding", className), "aria-busy": "true", children: _jsx("p", { className: "rk-onboarding__muted", children:
|
|
256
|
+
return _jsx("div", { className: cx("rk-onboarding", className), "aria-busy": "true", children: _jsx("p", { className: "rk-onboarding__muted", children: L.loading }) });
|
|
216
257
|
if (needsGate) {
|
|
217
258
|
if (!gateCleared)
|
|
218
|
-
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:
|
|
259
|
+
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: L.loading })] });
|
|
219
260
|
if (!legal)
|
|
220
261
|
return null;
|
|
221
262
|
const reacceptance = activeState.legalAccepted || activeState.legalAcceptance !== undefined;
|
|
@@ -233,10 +274,10 @@ export function Onboarding({ settings, legal, legalMode = "gate", appName, steps
|
|
|
233
274
|
const progress = reacceptance ? {} : { completed: ["legal"], currentStep: builtins.length > 1 ? 1 : 0 };
|
|
234
275
|
await persist({ legalAccepted: true, eligibilityBasis, legalAcceptance, ...progress });
|
|
235
276
|
if (!reacceptance && builtins.length === 1)
|
|
236
|
-
await onComplete();
|
|
277
|
+
await onComplete(resultOf(["legal"], []));
|
|
237
278
|
} })] });
|
|
238
279
|
}
|
|
239
280
|
const body = active.render(context);
|
|
240
|
-
return _jsxs("main", { className: cx("rk-onboarding", className), "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, onClick: () => { if (i >= lead && (i <= index || canContinue))
|
|
241
|
-
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] }), _jsx("h1", { children: active.title }), 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, onClick: () => { void save({ currentStep: index, skipped: true }).then((ok) => ok ? runCallback(onSkip) : undefined); }, children:
|
|
281
|
+
return _jsxs("main", { className: cx("rk-onboarding", className), "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))
|
|
282
|
+
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) })] })] })] });
|
|
242
283
|
}
|
package/dist/steps.d.ts
CHANGED
|
@@ -21,11 +21,26 @@ export interface ModelsStepOptions extends ModelsAutoStart {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
export declare function modelsStep(backend: ModelsBackend, options?: ModelsStepOptions): OnboardingStep;
|
|
24
|
-
|
|
24
|
+
/** Copy overrides for the licence step. Each key defaults to the English text. */
|
|
25
|
+
export interface LicenseStepCopy {
|
|
26
|
+
/** Key field label and placeholder. Default "Licence key". */
|
|
27
|
+
keyLabel?: string;
|
|
28
|
+
/** Submit button. Default "Activate". */
|
|
29
|
+
activateLabel?: string;
|
|
30
|
+
/** Trial button. Default "Start trial". */
|
|
31
|
+
trialLabel?: string;
|
|
32
|
+
/** Plans link. Default "View plans". */
|
|
33
|
+
plansLabel?: string;
|
|
34
|
+
/** Status text when no licence is active. Default "You can continue without a licence.". */
|
|
35
|
+
noLicenceDetail?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface LicenseStepOptions {
|
|
25
38
|
label?: string;
|
|
26
39
|
title?: string;
|
|
27
40
|
description?: string;
|
|
28
|
-
|
|
41
|
+
copy?: LicenseStepCopy;
|
|
42
|
+
}
|
|
43
|
+
export declare function licenseStep(backend: LicenseBackend, options?: LicenseStepOptions): OnboardingStep;
|
|
29
44
|
/** Sign-in step. Every button hands off to the shared owner and opens the user's default browser; no in-app webview. */
|
|
30
45
|
export declare function signInStep(backend: SignInBackend, options?: {
|
|
31
46
|
label?: string;
|
package/dist/steps.js
CHANGED
|
@@ -100,18 +100,22 @@ export function modelsStep(backend, options = {}) {
|
|
|
100
100
|
render: (ctx) => _jsx(ModelsBody, { backend: backend, onReady: ctx.setReady, autoStartDownload: options.autoStartDownload, shouldAutoStart: options.shouldAutoStart, onModelsStepEnter: options.onModelsStepEnter, intro: options.intro, startGuard: options.startGuard }),
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
-
function LicenseBody({ backend }) {
|
|
103
|
+
function LicenseBody({ backend, copy }) {
|
|
104
104
|
const [state, setState] = useState({ status: "none" });
|
|
105
105
|
const [key, setKey] = useState("");
|
|
106
106
|
const [busy, setBusy] = useState(false);
|
|
107
107
|
const [error, setError] = useState(null);
|
|
108
108
|
useEffect(() => void backend.load().then(setState, (e) => setError(e instanceof Error ? e.message : String(e))), [backend]);
|
|
109
109
|
const run = (fn) => { setBusy(true); setError(null); void fn().then(setState, (e) => setError(e instanceof Error ? e.message : String(e))).finally(() => setBusy(false)); };
|
|
110
|
-
return _jsxs("div", { className: "rk-onboarding__card", "data-testid": "onboarding-license", children: [_jsxs("div", { className: "rk-onboarding__license-state", children: [_jsx(Badge, { tone: state.status === "active" || state.status === "trial" ? "ok" : state.status === "expired" || state.status === "invalid" ? "bad" : "neutral", children: state.label ?? state.status }), _jsx("span", { children: state.detail ??
|
|
111
|
-
run(() => backend.activate(key.trim())); }, children: [_jsx(Input, { "aria-label":
|
|
110
|
+
return _jsxs("div", { className: "rk-onboarding__card", "data-testid": "onboarding-license", children: [_jsxs("div", { className: "rk-onboarding__license-state", children: [_jsx(Badge, { tone: state.status === "active" || state.status === "trial" ? "ok" : state.status === "expired" || state.status === "invalid" ? "bad" : "neutral", children: state.label ?? state.status }), _jsx("span", { children: state.detail ?? copy.noLicenceDetail })] }), error ? _jsx("p", { className: "rk-onboarding__error", role: "alert", children: error }) : null, backend.activate ? _jsxs("form", { className: "rk-onboarding__form", onSubmit: (e) => { e.preventDefault(); if (key.trim())
|
|
111
|
+
run(() => backend.activate(key.trim())); }, children: [_jsx(Input, { "aria-label": copy.keyLabel, placeholder: copy.keyLabel, value: key, onChange: (e) => setKey(e.target.value), disabled: busy }), _jsx(Button, { size: "sm", variant: "primary", type: "submit", disabled: busy || !key.trim(), children: copy.activateLabel })] }) : null, _jsxs("div", { className: "rk-onboarding__row-actions", children: [backend.startTrial ? _jsx(Button, { size: "sm", variant: "secondary", disabled: busy, onClick: () => run(() => backend.startTrial()), children: copy.trialLabel }) : null, backend.purchaseUrl ? _jsx(Button, { size: "sm", variant: "ghost", onClick: () => void backend.openExternal?.(backend.purchaseUrl), children: copy.plansLabel }) : null] })] });
|
|
112
112
|
}
|
|
113
113
|
export function licenseStep(backend, options = {}) {
|
|
114
|
-
|
|
114
|
+
const copy = {
|
|
115
|
+
keyLabel: "Licence key", activateLabel: "Activate", trialLabel: "Start trial", plansLabel: "View plans",
|
|
116
|
+
noLicenceDetail: "You can continue without a licence.", ...options.copy,
|
|
117
|
+
};
|
|
118
|
+
return { id: "license", label: options.label ?? "Licence", title: options.title ?? "Choose how you’ll use this app", description: options.description ?? "Activate a licence, start a trial, or continue with the free plan.", skippable: true, render: () => _jsx(LicenseBody, { backend: backend, copy: copy }) };
|
|
115
119
|
}
|
|
116
120
|
function SignInBody({ backend }) {
|
|
117
121
|
const [account, setAccount] = useState({ signedIn: false });
|
package/dist/types.d.ts
CHANGED
|
@@ -108,6 +108,29 @@ export interface PersistedOnboardingState {
|
|
|
108
108
|
/** Legacy mirror of `legalAcceptance.eligibilityBasis`. */
|
|
109
109
|
eligibilityBasis?: EligibilityBasis;
|
|
110
110
|
legalAcceptance?: LegalAcceptanceRecord;
|
|
111
|
+
/** Step ids the user skipped with the per-step Skip button (not marked complete). */
|
|
112
|
+
skippedSteps?: string[];
|
|
113
|
+
}
|
|
114
|
+
/** What `onComplete` receives. `steps` holds values passed to `ctx.setResult` during this mount. */
|
|
115
|
+
export interface OnboardingResult {
|
|
116
|
+
completed: string[];
|
|
117
|
+
skipped: string[];
|
|
118
|
+
steps: Record<string, unknown>;
|
|
119
|
+
}
|
|
120
|
+
/** Button and status copy. Each key defaults to the English text shown in the table. */
|
|
121
|
+
export interface OnboardingLabels {
|
|
122
|
+
/** "Set up later". */
|
|
123
|
+
skip: string;
|
|
124
|
+
/** Per-step Skip button. */
|
|
125
|
+
skipStep: string;
|
|
126
|
+
/** "Back". */
|
|
127
|
+
back: string;
|
|
128
|
+
/** "Continue". */
|
|
129
|
+
continue: string;
|
|
130
|
+
/** "Finish setup" on the last step. */
|
|
131
|
+
finish: string;
|
|
132
|
+
/** "Loading setup…". */
|
|
133
|
+
loading: string;
|
|
111
134
|
}
|
|
112
135
|
export interface OnboardingStepContext {
|
|
113
136
|
state: PersistedOnboardingState;
|
|
@@ -121,6 +144,8 @@ export interface OnboardingStepContext {
|
|
|
121
144
|
ready: Readonly<Record<string, boolean>>;
|
|
122
145
|
/** Reports whether the active step is ready (used by `canContinue`). Stable per step. */
|
|
123
146
|
setReady: (ready: boolean) => void;
|
|
147
|
+
/** Stores a value for this step; it is passed to `onComplete` as `result.steps[step.id]`. */
|
|
148
|
+
setResult: (value: unknown) => void;
|
|
124
149
|
}
|
|
125
150
|
export interface OnboardingStep {
|
|
126
151
|
id: string;
|
|
@@ -132,6 +157,14 @@ export interface OnboardingStep {
|
|
|
132
157
|
canContinue?: (context: OnboardingStepContext) => boolean;
|
|
133
158
|
/** Shown beside the disabled Continue/Finish button. */
|
|
134
159
|
blockedHint?: string;
|
|
160
|
+
/** Primary button text for this step (e.g. "Open CodeRight"). Overrides Continue and Finish setup. */
|
|
161
|
+
continueLabel?: string;
|
|
162
|
+
/**
|
|
163
|
+
* Runs before Continue/Finish advances. Return `false` (or throw) to keep the step; a throw shows its message.
|
|
164
|
+
* `void` or `true` advances. Not run for Back, the rail, or Skip.
|
|
165
|
+
*/
|
|
166
|
+
onContinue?: (context: OnboardingStepContext) => void | boolean | Promise<void | boolean>;
|
|
167
|
+
/** Empty title hides the heading (e.g. a Welcome step). */
|
|
135
168
|
render: (context: OnboardingStepContext) => ReactNode;
|
|
136
169
|
}
|
|
137
170
|
export interface LegalStepConfig extends Omit<LegalGateProps, "variant" | "onAgree" | "onQuit"> {
|
|
@@ -157,8 +190,11 @@ export interface OnboardingProps {
|
|
|
157
190
|
permissions?: PermissionsBackend;
|
|
158
191
|
models?: ModelsBackend;
|
|
159
192
|
license?: LicenseBackend;
|
|
160
|
-
|
|
193
|
+
/** Called once when setup finishes, with the completed and skipped step ids and the step results. */
|
|
194
|
+
onComplete: (result: OnboardingResult) => void | Promise<void>;
|
|
161
195
|
onSkip?: () => void | Promise<void>;
|
|
196
|
+
/** Override any button or status text. Keys not given keep the English defaults. */
|
|
197
|
+
labels?: Partial<OnboardingLabels>;
|
|
162
198
|
className?: string;
|
|
163
199
|
/** Version the user must have accepted. A different (or missing) stored version re-prompts with `variant: "re-acceptance"`. */
|
|
164
200
|
requiredVersion?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/onboarding",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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,8 +29,8 @@
|
|
|
29
29
|
"react": ">=18 <21"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@rightkit/react-shell": "0.
|
|
33
|
-
"@rightkit/legal-ui": "0.
|
|
32
|
+
"@rightkit/react-shell": "0.7.0",
|
|
33
|
+
"@rightkit/legal-ui": "0.4.0",
|
|
34
34
|
"@rightkit/qa-chrome": "0.1.8",
|
|
35
35
|
"@testing-library/jest-dom": "^7.0.1",
|
|
36
36
|
"@testing-library/dom": "^10.4.2",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"repository": {
|
|
53
53
|
"type": "git",
|
|
54
|
-
"url": "git+https://github.com/
|
|
54
|
+
"url": "git+https://github.com/bogusyogi/rightkit.git",
|
|
55
55
|
"directory": "packages/onboarding"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|