@rightkit/onboarding 0.3.2 → 0.5.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 +10 -4
- package/dist/Onboarding.d.ts +2 -2
- package/dist/Onboarding.js +31 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/steps.d.ts +7 -1
- package/dist/steps.js +27 -2
- package/dist/types.d.ts +44 -3
- package/package.json +18 -17
package/README.md
CHANGED
|
@@ -13,16 +13,16 @@ 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
|
|
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, Orthic-Labs/rightkit#279); types from `types.ts`, `ModelsStepOptions` |
|
|
17
17
|
| `@rightkit/onboarding/onboarding.css` | Stylesheet |
|
|
18
18
|
|
|
19
19
|
## Peer dependencies
|
|
20
20
|
|
|
21
21
|
| Peer | Range |
|
|
22
22
|
|---|---|
|
|
23
|
-
| `@rightkit/
|
|
23
|
+
| `@rightkit/react-shell` | `>=0.2.0 <1` |
|
|
24
24
|
| `@rightkit/legal-ui` | `>=0.1.2 <1` |
|
|
25
|
-
| `react` | `>=18 <
|
|
25
|
+
| `react` | `>=18 <21` |
|
|
26
26
|
|
|
27
27
|
## Example
|
|
28
28
|
|
|
@@ -43,8 +43,14 @@ import "@rightkit/onboarding/onboarding.css";
|
|
|
43
43
|
/>
|
|
44
44
|
```
|
|
45
45
|
|
|
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
|
+
|
|
46
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)`.
|
|
47
49
|
|
|
50
|
+
## When the legal gate re-prompts
|
|
51
|
+
|
|
52
|
+
`isLegalAccepted(state, requiredVersion?, isAccepted?, requiredDocumentsHash?)` decides it, in this order: a custom `isAccepted` wins outright; a `requiredDocumentsHash` must equal the stored acceptance's `documentsHash`; a `requiredVersion` must equal its `version`; with neither, the stored `legalAccepted` flag is used. Pass `requiredDocumentsHash` (the hash of the material documents, from `@rightkit/legal`) to catch a text change that did not bump the version: without it only the version is compared. `documentsHash` is recorded with the acceptance and never compared; by default it covers document titles and URLs, not their text.
|
|
53
|
+
|
|
48
54
|
## Known issues
|
|
49
55
|
|
|
50
|
-
- Peer React is `<
|
|
56
|
+
- Peer React is `<21`, the same range as `doc-render`, `i18n`, `media-ui` and `pdf-ui` (`package.json:29`).
|
package/dist/Onboarding.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OnboardingProps, PersistedOnboardingState } from "./types.js";
|
|
2
2
|
/** True when the stored acceptance satisfies the host's requirement. */
|
|
3
|
-
export declare function isLegalAccepted(state: PersistedOnboardingState, requiredVersion?: string, custom?: (state: PersistedOnboardingState) => boolean): boolean;
|
|
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, steps, permissions, models, license, onComplete, onSkip, className, requiredVersion, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip, titlebar, dragRegion, legalDragRegion, legalReceipt }: OnboardingProps): import("react").JSX.Element;
|
|
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;
|
package/dist/Onboarding.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { LegalGate } from "@rightkit/legal-ui";
|
|
4
|
-
import { Button } from "@rightkit/
|
|
4
|
+
import { Button } from "@rightkit/react-shell/react";
|
|
5
5
|
import { cx } from "./ui.js";
|
|
6
6
|
import { licenseStep, modelsStep, permissionsStep } from "./steps.js";
|
|
7
7
|
const KEY = "onboarding.state";
|
|
@@ -39,11 +39,17 @@ function withReceipt(state, record) {
|
|
|
39
39
|
return record ? { ...rest, legalAccepted: true, eligibilityBasis: record.eligibilityBasis, legalAcceptance: record } : { ...rest, legalAccepted: false };
|
|
40
40
|
}
|
|
41
41
|
/** True when the stored acceptance satisfies the host's requirement. */
|
|
42
|
-
export function isLegalAccepted(state, requiredVersion, custom) {
|
|
42
|
+
export function isLegalAccepted(state, requiredVersion, custom, requiredDocumentsHash) {
|
|
43
43
|
if (custom)
|
|
44
44
|
return custom(state);
|
|
45
|
+
// A host that supplies the documents' hash is asking for the text itself to be what was accepted:
|
|
46
|
+
// a changed text re-prompts even when the version string did not move.
|
|
47
|
+
if (requiredDocumentsHash !== undefined && state.legalAcceptance?.documentsHash !== requiredDocumentsHash)
|
|
48
|
+
return false;
|
|
45
49
|
if (requiredVersion !== undefined)
|
|
46
50
|
return state.legalAcceptance?.version === requiredVersion;
|
|
51
|
+
if (requiredDocumentsHash !== undefined)
|
|
52
|
+
return state.legalAcceptance !== undefined;
|
|
47
53
|
return state.legalAccepted;
|
|
48
54
|
}
|
|
49
55
|
/** FNV-1a fallback when SubtleCrypto is unavailable. */
|
|
@@ -73,7 +79,17 @@ function TitleStrip({ titlebar, dragRegion }) {
|
|
|
73
79
|
return null;
|
|
74
80
|
return _jsx("div", { className: "rk-onboarding__titlebar", "data-testid": "onboarding-titlebar", ...(dragRegion ? { "data-tauri-drag-region": "" } : {}), children: titlebar });
|
|
75
81
|
}
|
|
76
|
-
|
|
82
|
+
function NoticeList({ documents }) {
|
|
83
|
+
if (!documents.length)
|
|
84
|
+
return _jsx("p", { className: "rk-onboarding__muted", children: "No notices to show." });
|
|
85
|
+
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
|
+
}
|
|
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 }) {
|
|
88
|
+
if (legalMode === "gate" && !legal)
|
|
89
|
+
throw new Error("Onboarding: `legal` is required when legalMode is \"gate\"");
|
|
90
|
+
const gated = legalMode === "gate";
|
|
91
|
+
/** Leading steps the user cannot navigate to: the Agreement stand-in in gate mode. */
|
|
92
|
+
const lead = gated ? 1 : 0;
|
|
77
93
|
const [state, setState] = useState(null);
|
|
78
94
|
const [saving, setSaving] = useState(false);
|
|
79
95
|
const [error, setError] = useState(null);
|
|
@@ -138,18 +154,19 @@ export function Onboarding({ settings, legal, steps = [], permissions, models, l
|
|
|
138
154
|
return () => { live = false; };
|
|
139
155
|
}, [settings, legalReceipt]);
|
|
140
156
|
const builtins = useMemo(() => [
|
|
141
|
-
{ id: "legal", label: "Agreement", title: "Review agreement", skippable: false, render: () => _jsx("p", { className: "rk-onboarding__muted", children: "Agreement accepted." }) },
|
|
157
|
+
...(gated ? [{ id: "legal", label: "Agreement", title: "Review agreement", skippable: false, render: () => _jsx("p", { className: "rk-onboarding__muted", children: "Agreement accepted." }) }] : []),
|
|
158
|
+
...(legalMode === "notice" ? [{ id: "notice", label: "Notice", title: "Notices", skippable: false, render: () => _jsx(NoticeList, { documents: legal?.documents ?? [] }) }] : []),
|
|
142
159
|
...(permissions ? [permissionsStep(permissions)] : []),
|
|
143
160
|
...(models ? [modelsStep(models, { required: requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, intro: modelsIntro, startGuard: modelsStarted })] : []),
|
|
144
161
|
...(license ? [licenseStep(license)] : []),
|
|
145
162
|
...steps,
|
|
146
|
-
], [license, models, permissions, steps, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro]);
|
|
163
|
+
], [gated, legalMode, legal?.documents, license, models, permissions, steps, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro]);
|
|
147
164
|
const activeState = state ?? INITIAL;
|
|
148
|
-
const accepted = isLegalAccepted(activeState, requiredVersion, isAccepted);
|
|
165
|
+
const accepted = !gated || isLegalAccepted(activeState, requiredVersion, isAccepted, requiredDocumentsHash);
|
|
149
166
|
const needsGate = state !== null && !accepted;
|
|
150
167
|
// The agreement gate does not depend on the saved step: until it is accepted, the active step is the first.
|
|
151
168
|
// Once accepted, the Agreement step (index 0) is never shown again when other steps exist.
|
|
152
|
-
const index = !accepted ? 0 : builtins.length >
|
|
169
|
+
const index = !accepted ? 0 : builtins.length > lead ? Math.max(lead, Math.min(activeState.currentStep, builtins.length - 1)) : 0;
|
|
153
170
|
const active = builtins[index];
|
|
154
171
|
const activeId = active?.id ?? "";
|
|
155
172
|
const setReady = useCallback((ready) => setReadyMap((m) => m[activeId] === ready ? m : { ...m, [activeId]: ready }), [activeId]);
|
|
@@ -192,14 +209,18 @@ export function Onboarding({ settings, legal, steps = [], permissions, models, l
|
|
|
192
209
|
};
|
|
193
210
|
if (error && !state)
|
|
194
211
|
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
|
+
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: "Finish setup" })] });
|
|
195
214
|
if (!state || !active)
|
|
196
215
|
return _jsx("div", { className: cx("rk-onboarding", className), "aria-busy": "true", children: _jsx("p", { className: "rk-onboarding__muted", children: "Loading setup\u2026" }) });
|
|
197
216
|
if (needsGate) {
|
|
198
217
|
if (!gateCleared)
|
|
199
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: "Loading setup\u2026" })] });
|
|
219
|
+
if (!legal)
|
|
220
|
+
return null;
|
|
200
221
|
const reacceptance = activeState.legalAccepted || activeState.legalAcceptance !== undefined;
|
|
201
222
|
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 }) => {
|
|
202
|
-
const legalAcceptance = { version: requiredVersion ?? "1", documentsHash: documentsHash ?? await hashDocuments(legal.documents), acceptedAt: new Date().toISOString(), eligibilityBasis };
|
|
223
|
+
const legalAcceptance = { version: requiredVersion ?? "1", documentsHash: documentsHash ?? requiredDocumentsHash ?? await hashDocuments(legal.documents), acceptedAt: new Date().toISOString(), eligibilityBasis };
|
|
203
224
|
if (legalReceipt) {
|
|
204
225
|
try {
|
|
205
226
|
await legalReceipt.write(legalAcceptance);
|
|
@@ -216,6 +237,6 @@ export function Onboarding({ settings, legal, steps = [], permissions, models, l
|
|
|
216
237
|
} })] });
|
|
217
238
|
}
|
|
218
239
|
const body = active.render(context);
|
|
219
|
-
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: legal
|
|
220
|
-
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: "Set up later" }) : null, _jsx("span", { className: "rk-onboarding__hint", children: !canContinue && active.blockedHint ? active.blockedHint : null }), _jsx(Button, { variant: "secondary", disabled: index <=
|
|
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: "Set up later" }) : null, _jsx("span", { className: "rk-onboarding__hint", children: !canContinue && active.blockedHint ? active.blockedHint : null }), _jsx(Button, { variant: "secondary", disabled: index <= lead || saving, onClick: () => void save({ currentStep: index - 1 }), children: "Back" }), _jsx(Button, { variant: "primary", disabled: saving || !canContinue, onClick: () => void markAndMove(index + 1, active.id), children: index === builtins.length - 1 ? "Finish setup" : "Continue" })] })] })] });
|
|
221
242
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { Onboarding, isLegalAccepted, onboardingStateKey } from "./Onboarding.js";
|
|
2
|
-
export { licenseStep, modelsStep, permissionsStep } from "./steps.js";
|
|
2
|
+
export { licenseStep, modelsStep, permissionsStep, signInStep } from "./steps.js";
|
|
3
3
|
export type { ModelsStepOptions } from "./steps.js";
|
|
4
4
|
export type * from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Onboarding, isLegalAccepted, onboardingStateKey } from "./Onboarding.js";
|
|
2
|
-
export { licenseStep, modelsStep, permissionsStep } from "./steps.js";
|
|
2
|
+
export { licenseStep, modelsStep, permissionsStep, signInStep } from "./steps.js";
|
package/dist/steps.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import type { LicenseBackend, ModelsAutoStart, ModelsBackend, OnboardingStep, PermissionsBackend } from "./types.js";
|
|
2
|
+
import type { SignInBackend, LicenseBackend, ModelsAutoStart, ModelsBackend, OnboardingStep, PermissionsBackend } from "./types.js";
|
|
3
3
|
export declare function permissionsStep(backend: PermissionsBackend, options?: {
|
|
4
4
|
label?: string;
|
|
5
5
|
title?: string;
|
|
@@ -26,3 +26,9 @@ export declare function licenseStep(backend: LicenseBackend, options?: {
|
|
|
26
26
|
title?: string;
|
|
27
27
|
description?: string;
|
|
28
28
|
}): OnboardingStep;
|
|
29
|
+
/** Sign-in step. Every button hands off to the shared owner and opens the user's default browser; no in-app webview. */
|
|
30
|
+
export declare function signInStep(backend: SignInBackend, options?: {
|
|
31
|
+
label?: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
}): OnboardingStep;
|
package/dist/steps.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
-
import { Button, Input, Badge } from "@rightkit/
|
|
3
|
+
import { Button, Input, Badge } from "@rightkit/react-shell/react";
|
|
4
4
|
function ProgressBar({ value, label }) {
|
|
5
5
|
const pct = Math.max(0, Math.min(1, value));
|
|
6
6
|
return _jsx("div", { className: "rk-onboarding__progress", role: "progressbar", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(pct * 100), children: _jsx("i", { style: { width: `${pct * 100}%` } }) });
|
|
@@ -113,3 +113,28 @@ function LicenseBody({ backend }) {
|
|
|
113
113
|
export function licenseStep(backend, options = {}) {
|
|
114
114
|
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 }) };
|
|
115
115
|
}
|
|
116
|
+
function SignInBody({ backend }) {
|
|
117
|
+
const [account, setAccount] = useState({ signedIn: false });
|
|
118
|
+
const [busy, setBusy] = useState(null);
|
|
119
|
+
const [error, setError] = useState(null);
|
|
120
|
+
useEffect(() => { let live = true; backend.load().then((a) => { if (live)
|
|
121
|
+
setAccount(a); }, () => undefined); return () => { live = false; }; }, [backend]);
|
|
122
|
+
const start = async (provider) => {
|
|
123
|
+
setBusy(provider);
|
|
124
|
+
setError(null);
|
|
125
|
+
try {
|
|
126
|
+
setAccount(await backend.start(provider));
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
setError(e instanceof Error ? e.message : "Sign-in did not finish.");
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
setBusy(null);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
return _jsxs("div", { className: "rk-onboarding__signin", children: [account.signedIn ? _jsxs("p", { children: ["Signed in", account.account ? _jsxs(_Fragment, { children: [" as ", _jsx("strong", { children: account.account })] }) : null, "."] }) : null, _jsx("div", { className: "rk-onboarding__row-actions", children: backend.providers.map((p) => _jsx(Button, { size: "sm", variant: "secondary", disabled: busy !== null, onClick: () => void start(p.id), children: busy === p.id ? "Waiting for your browser…" : p.label ?? `Sign in with ${p.id === "google" ? "Google" : p.id === "apple" ? "Apple" : "your account"}` }, p.id)) }), _jsx("p", { className: "rk-onboarding__hint", children: _jsx("small", { children: "Opens your browser. You stay signed in to the accounts you already use there." }) }), error ? _jsx("p", { role: "alert", children: error }) : null] });
|
|
136
|
+
}
|
|
137
|
+
/** Sign-in step. Every button hands off to the shared owner and opens the user's default browser; no in-app webview. */
|
|
138
|
+
export function signInStep(backend, options = {}) {
|
|
139
|
+
return { id: "sign-in", label: options.label ?? "Sign in", title: options.title ?? "Sign in", description: options.description ?? "Sign-in opens your default browser, so you stay signed in to the accounts you already use there.", skippable: true, render: () => _jsx(SignInBody, { backend: backend }) };
|
|
140
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface SettingsSnapshot {
|
|
|
5
5
|
values: SettingsValues;
|
|
6
6
|
revision: number;
|
|
7
7
|
}
|
|
8
|
-
/** Structural port shared with @rightkit/settings-ui and rightkit-settings. */
|
|
8
|
+
/** Structural port shared with @rightkit/settings-ui and rightkit-app-settings. */
|
|
9
9
|
export interface SettingsBackend {
|
|
10
10
|
load(): Promise<SettingsSnapshot>;
|
|
11
11
|
patch(patch: SettingsValues, expectedRevision?: number): Promise<SettingsSnapshot>;
|
|
@@ -57,6 +57,28 @@ export interface LicenseBackend {
|
|
|
57
57
|
purchaseUrl?: string;
|
|
58
58
|
openExternal?: (url: string) => void | Promise<void>;
|
|
59
59
|
}
|
|
60
|
+
export type SignInProvider = "google" | "apple" | "oauth";
|
|
61
|
+
/**
|
|
62
|
+
* Sign-in owned by `rightkit-oauth`. Every provider opens the user's DEFAULT browser (never an in-app webview, so existing
|
|
63
|
+
* browser sessions are kept); `start` must call the shared owner (`OAuthClient::sign_in_with_browser` through the app's
|
|
64
|
+
* command), not open a window of its own. Orthic-Labs/rightkit#279.
|
|
65
|
+
*/
|
|
66
|
+
export interface SignInBackend {
|
|
67
|
+
providers: readonly {
|
|
68
|
+
id: SignInProvider;
|
|
69
|
+
label?: string;
|
|
70
|
+
}[];
|
|
71
|
+
/** Account already signed in, if any. */
|
|
72
|
+
load(): Promise<{
|
|
73
|
+
signedIn: boolean;
|
|
74
|
+
account?: string;
|
|
75
|
+
}>;
|
|
76
|
+
/** Open the default browser for `provider` and resolve when the sign-in finishes (or reject / throw on failure or cancel). */
|
|
77
|
+
start(provider: SignInProvider): Promise<{
|
|
78
|
+
signedIn: boolean;
|
|
79
|
+
account?: string;
|
|
80
|
+
}>;
|
|
81
|
+
}
|
|
60
82
|
export type EligibilityBasis = "individual" | "enterprise";
|
|
61
83
|
/** Full record of one legal acceptance, persisted so version changes can re-prompt. */
|
|
62
84
|
export interface LegalAcceptanceRecord {
|
|
@@ -94,6 +116,7 @@ export interface OnboardingStepContext {
|
|
|
94
116
|
permissions?: PermissionsBackend;
|
|
95
117
|
models?: ModelsBackend;
|
|
96
118
|
license?: LicenseBackend;
|
|
119
|
+
signIn?: SignInBackend;
|
|
97
120
|
/** Readiness reported by steps via `setReady`, keyed by step id. */
|
|
98
121
|
ready: Readonly<Record<string, boolean>>;
|
|
99
122
|
/** Reports whether the active step is ready (used by `canContinue`). Stable per step. */
|
|
@@ -114,9 +137,21 @@ export interface OnboardingStep {
|
|
|
114
137
|
export interface LegalStepConfig extends Omit<LegalGateProps, "variant" | "onAgree" | "onQuit"> {
|
|
115
138
|
onQuit?: () => void;
|
|
116
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* How legal documents take part in onboarding.
|
|
142
|
+
* - `gate` (default): the blocking agreement gate comes first and is recorded (needs `legal`).
|
|
143
|
+
* - `notice`: no gate and no acceptance. A first "Notice" step lists the `legal` documents as links; Continue moves on (free or open-source apps that still hold their notice).
|
|
144
|
+
* - `none`: no legal step at all; `legal` is not needed.
|
|
145
|
+
*/
|
|
146
|
+
export type LegalMode = "gate" | "notice" | "none";
|
|
117
147
|
export interface OnboardingProps {
|
|
118
148
|
settings: SettingsBackend;
|
|
119
|
-
|
|
149
|
+
/** Required when `legalMode` is `gate` (default). Optional for `notice` (the documents shown) and `none`. */
|
|
150
|
+
legal?: LegalStepConfig;
|
|
151
|
+
/** Default `gate`. See `LegalMode`. */
|
|
152
|
+
legalMode?: LegalMode;
|
|
153
|
+
/** Name shown in the step rail. Default: `legal.appName`. */
|
|
154
|
+
appName?: string;
|
|
120
155
|
/** App-specific steps are appended after built-in steps, in supplied order. */
|
|
121
156
|
steps?: readonly OnboardingStep[];
|
|
122
157
|
permissions?: PermissionsBackend;
|
|
@@ -129,7 +164,13 @@ export interface OnboardingProps {
|
|
|
129
164
|
requiredVersion?: string;
|
|
130
165
|
/** Custom acceptance check; takes precedence over `requiredVersion`. */
|
|
131
166
|
isAccepted?: (state: PersistedOnboardingState) => boolean;
|
|
132
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* Hash the stored acceptance must carry. A stored acceptance with a different (or no) hash re-prompts even when
|
|
169
|
+
* the version is unchanged, so a text change without a version bump is caught. Pass the hash of the MATERIAL
|
|
170
|
+
* documents only (see `@rightkit/legal`), not of appendices that regenerate on dependency bumps.
|
|
171
|
+
*/
|
|
172
|
+
requiredDocumentsHash?: string;
|
|
173
|
+
/** Hash of the legal documents recorded with the acceptance. Default: `requiredDocumentsHash`, else a hash of document titles and URLs. Recorded only; never compared. */
|
|
133
174
|
documentsHash?: string;
|
|
134
175
|
/** Runs (and is awaited) before the legal gate is shown, e.g. to resize or reveal the window. */
|
|
135
176
|
beforeLegalGate?: () => void | Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/onboarding",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -24,25 +24,26 @@
|
|
|
24
24
|
"*.css"
|
|
25
25
|
],
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@rightkit/
|
|
27
|
+
"@rightkit/react-shell": ">=0.2.0 <1",
|
|
28
28
|
"@rightkit/legal-ui": ">=0.1.2 <1",
|
|
29
29
|
"react": ">=18 <21"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@rightkit/
|
|
33
|
-
"@rightkit/legal-ui": "0.
|
|
34
|
-
"@rightkit/qa-chrome": "0.1.
|
|
35
|
-
"@testing-library/jest-dom": "^
|
|
36
|
-
"@testing-library/
|
|
37
|
-
"@testing-library/
|
|
38
|
-
"@
|
|
39
|
-
"@types/react
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"react
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
32
|
+
"@rightkit/react-shell": "0.6.8",
|
|
33
|
+
"@rightkit/legal-ui": "0.3.0",
|
|
34
|
+
"@rightkit/qa-chrome": "0.1.8",
|
|
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",
|
|
39
|
+
"@types/react": "^19.3.0",
|
|
40
|
+
"@types/react-dom": "^19.3.0",
|
|
41
|
+
"jsdom": "^30.1.3",
|
|
42
|
+
"react": "^19.3.1",
|
|
43
|
+
"react-dom": "^19.3.1",
|
|
44
|
+
"typescript": "^7.0.2",
|
|
45
|
+
"vite": "^8.3.4",
|
|
46
|
+
"vitest": "^5.0.5"
|
|
46
47
|
},
|
|
47
48
|
"publishConfig": {
|
|
48
49
|
"registry": "https://registry.npmjs.org",
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
},
|
|
51
52
|
"repository": {
|
|
52
53
|
"type": "git",
|
|
53
|
-
"url": "git+https://github.com/
|
|
54
|
+
"url": "git+https://github.com/Orthic-Labs/rightkit.git",
|
|
54
55
|
"directory": "packages/onboarding"
|
|
55
56
|
},
|
|
56
57
|
"scripts": {
|