@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 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`; types from `types.ts`, `ModelsStepOptions` |
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/app-shell` | `>=0.2.0 <1` |
23
+ | `@rightkit/react-shell` | `>=0.2.0 <1` |
24
24
  | `@rightkit/legal-ui` | `>=0.1.2 <1` |
25
- | `react` | `>=18 <20` |
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 `<20`, while `doc-render`, `i18n`, `media-ui` and `pdf-ui` accept `<21` (npm audit 2026-10-08).
56
+ - Peer React is `<21`, the same range as `doc-render`, `i18n`, `media-ui` and `pdf-ui` (`package.json:29`).
@@ -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;
@@ -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/app-shell/react";
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
- export function Onboarding({ settings, legal, steps = [], permissions, models, license, onComplete, onSkip, className, requiredVersion, isAccepted, documentsHash, beforeLegalGate, requireModel, autoStartDownload, shouldAutoStart, onModelsStepEnter, modelsIntro, allowSkip = true, titlebar, dragRegion, legalDragRegion = false, legalReceipt }) {
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 > 1 ? Math.max(1, Math.min(activeState.currentStep, builtins.length - 1)) : 0;
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.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: i === 0 && builtins.length > 1, onClick: () => { if (i > 0 && (i <= index || canContinue))
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 <= 1 || 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" })] })] })] });
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/app-shell/react";
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
- legal: LegalStepConfig;
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
- /** Hash of the legal documents recorded with the acceptance. Default: hash of document titles and URLs. */
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.2",
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/app-shell": ">=0.2.0 <1",
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/app-shell": "0.4.1",
33
- "@rightkit/legal-ui": "0.2.0",
34
- "@rightkit/qa-chrome": "0.1.3",
35
- "@testing-library/jest-dom": "^6.6.3",
36
- "@testing-library/react": "^16.1.0",
37
- "@testing-library/user-event": "^14.5.2",
38
- "@types/react": "^19.0.0",
39
- "@types/react-dom": "^19.0.0",
40
- "jsdom": "^25.0.1",
41
- "react": "^19.0.0",
42
- "react-dom": "^19.0.0",
43
- "typescript": "^5.7.0",
44
- "vite": "^6.0.0",
45
- "vitest": "^2.1.8"
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/bogusyogi/rightkit.git",
54
+ "url": "git+https://github.com/Orthic-Labs/rightkit.git",
54
55
  "directory": "packages/onboarding"
55
56
  },
56
57
  "scripts": {