@owlmeans/web-panel 0.1.18-rc.57 → 0.1.18-rc.59
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/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-panel/SKILL.md +17 -2
- package/build/components/login/screen.d.ts.map +1 -1
- package/build/components/login/screen.js +4 -3
- package/build/components/login/screen.js.map +1 -1
- package/build/components/login/terms.d.ts +8 -0
- package/build/components/login/terms.d.ts.map +1 -1
- package/build/components/login/terms.js +42 -23
- package/build/components/login/terms.js.map +1 -1
- package/package.json +29 -29
- package/src/components/login/screen.tsx +4 -2
- package/src/components/login/terms.tsx +80 -33
- package/tests/harness/mount.tsx +16 -1
- package/tests/login.spec.ts +74 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ but never started.
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
bun add @owlmeans/web-panel@^0.1.18-rc.
|
|
14
|
+
bun add @owlmeans/web-panel@^0.1.18-rc.59
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Peer requirements (the consuming app provides these): `react`, `react-dom`,
|
|
@@ -496,7 +496,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
496
496
|
your project's skill store (`.agents/skills/`):
|
|
497
497
|
|
|
498
498
|
```sh
|
|
499
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
499
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.36
|
|
500
500
|
```
|
|
501
501
|
|
|
502
502
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/web-panel",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.59",
|
|
5
|
+
"generatedAt": "2026-09-22T22:22:19.449Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/web-panel
|
|
9
9
|
|
|
10
10
|
**Layer:** Web (React)
|
|
11
|
-
**Install:** `"@owlmeans/web-panel": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/web-panel": "^0.1.18-rc.59"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
@@ -21,7 +21,7 @@ user-invocable: false
|
|
|
21
21
|
| `ThemeToggle` / `useColorScheme` | The light/dark switcher (`ThemeToggleProps`, `ThemeToggleLabels`) and the hook behind it (`ColorSchemeModel`) — see *Light and dark* below |
|
|
22
22
|
| `SocketReloadDialog` | The global "connection lost — try again / reload" prompt (opt in via `cfg.socket.reloadDialog`) `PanelApp` mounts automatically — see below. The hooks it reads, `useSocketStatus` and `useSocketRetry`, are imported from `@owlmeans/client-socket`, not from this package |
|
|
23
23
|
| `Link` | An `<a>` addressing an entrypoint alias (or a literal `src`), with the label taken from i18n |
|
|
24
|
-
| `LoginScreen` / `LocalizedLoginScreen` / `appendLoginScreen` | The identity-provider choice screen — see `login-methods` |
|
|
24
|
+
| `LoginScreen` / `LocalizedLoginScreen` / `appendLoginScreen` | The identity-provider choice screen — see `login-methods`. `LoginScreen` is pure w.r.t. `locale` too, exactly like `translate`: it is a prop, defaulting to nothing, never an implicit `useLanguage()` read, because a component that reaches for i18n context directly crashes an app mounted without one. `LocalizedLoginScreen` supplies `useLanguage()`'s value when the caller does not pass its own |
|
|
25
25
|
| `render(context, opts?)` | Mounts the tree inside `PanelApp`, with the browser language detector installed on the i18n instance. `opts` is `RenderOptions` plus `rootClassName` |
|
|
26
26
|
| `PanelApp` | That wrapper on its own — the themed root `div` plus the i18n provider — for a host that mounts the tree itself |
|
|
27
27
|
| `useContext<C, T>()` | The current context, from React. `AppContext` adds `context.flow()` over `@owlmeans/web-client`'s |
|
|
@@ -421,6 +421,21 @@ import { Form, TextInput, SubmitButton, Button } from '@owlmeans/web-panel'
|
|
|
421
421
|
`contained` and the rest `outlined`, calling `onSelect(option)`. `name` prefixes each option's
|
|
422
422
|
label key as `<name>.<option>`.
|
|
423
423
|
|
|
424
|
+
### The terms confirmation — `LoginTerms`
|
|
425
|
+
|
|
426
|
+
`components/login/terms.tsx` renders `LoginTermsModel` (`@owlmeans/client-panel/auth`) via
|
|
427
|
+
`termsSentence` (`@owlmeans/client-auth/login`) rather than re-deriving link/label pairs itself.
|
|
428
|
+
**`[data-login-terms]` marks exactly one checkbox, always** — an e2e suite elsewhere in the platform
|
|
429
|
+
treats it as a strict locator, so a change here must never add a second one, whatever
|
|
430
|
+
billing/product/custom documents a configuration adds. `[data-login-privacy]` is a SIBLING
|
|
431
|
+
paragraph of the checkbox's `<label>`, never nested inside it — a privacy disclosure is not
|
|
432
|
+
something the checkbox consents to. A linked document/notice fragment carries
|
|
433
|
+
`data-login-document="<key>"`. `[data-login-revised]` renders only when the resolved terms carry a
|
|
434
|
+
`revisedAt` (i.e. the configuration set `showRevision: true` and at least one document has its own
|
|
435
|
+
revision date). `LoginTerms` takes `locale?: string` — from `LoginScreen`'s own `locale` prop, never
|
|
436
|
+
read from context directly — for `Intl.ListFormat` and a custom document's locale-keyed `labelMap`.
|
|
437
|
+
Pinned by `tests/login.spec.ts` → the "with billing, product and custom documents configured" block.
|
|
438
|
+
|
|
424
439
|
## Subpath: `./consent`
|
|
425
440
|
|
|
426
441
|
`PanelCookieConsent` and `PanelCookiePolicy` — `@owlmeans/web-consent`'s components bound to this
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screen.d.ts","sourceRoot":"","sources":["../../../src/components/login/screen.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAQ/B,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAWhF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CA4F5C,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE,EAAE,CAAC,gBAAgB,
|
|
1
|
+
{"version":3,"file":"screen.d.ts","sourceRoot":"","sources":["../../../src/components/login/screen.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAQ/B,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAWhF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CA4F5C,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE,EAAE,CAAC,gBAAgB,CASrD,CAAA"}
|
|
@@ -4,7 +4,7 @@ import { Loader2 } from 'lucide-react';
|
|
|
4
4
|
import { Button } from '../../@/components/ui/button.js';
|
|
5
5
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../../@/components/ui/card.js';
|
|
6
6
|
import { cn } from '../../@/lib/utils.js';
|
|
7
|
-
import { useI18nLib } from '@owlmeans/client-i18n';
|
|
7
|
+
import { useI18nLib, useLanguage } from '@owlmeans/client-i18n';
|
|
8
8
|
import { useLoginMethods } from '@owlmeans/client-panel/auth';
|
|
9
9
|
import { loginAttemptError } from '@owlmeans/client-auth/login';
|
|
10
10
|
import { LoginMethodIcon } from './icons.js';
|
|
@@ -56,7 +56,7 @@ export const LoginScreen = props => {
|
|
|
56
56
|
// visibly actionable independently of a consumer's shadcn setup.
|
|
57
57
|
className: cn('w-full justify-center gap-2 cursor-pointer', model.blocked && 'opacity-60'), autoFocus: method.id === model.primary?.id, onClick: () => model.select(method), children: [model.busy === method.id
|
|
58
58
|
? _jsx(Loader2, { className: "size-4 animate-spin" })
|
|
59
|
-
: _jsx(LoginMethodIcon, { name: method.icon, className: "size-4" }), method.label ?? t(`login.method.${method.i18nKey ?? method.id}`, method.id)] }, method.id)) }), model.terms.required && _jsx(LoginTerms, { model: model.terms, translate: t }), (model.error ?? attemptError) != null
|
|
59
|
+
: _jsx(LoginMethodIcon, { name: method.icon, className: "size-4" }), method.label ?? t(`login.method.${method.i18nKey ?? method.id}`, method.id)] }, method.id)) }), model.terms.required && _jsx(LoginTerms, { model: model.terms, translate: t, locale: props.locale }), (model.error ?? attemptError) != null
|
|
60
60
|
&& _jsx("p", { role: "alert", "data-login-error": true, className: "text-sm text-destructive text-center", children: model.error ?? t(attemptError, 'Sign-in did not complete. Please try again.') })] }), _jsx("div", { className: "px-6", children: props.footer ?? _jsx(LoginCredit, { model: model.credit, translate: t }) })] }) });
|
|
61
61
|
};
|
|
62
62
|
/**
|
|
@@ -68,6 +68,7 @@ export const LoginScreen = props => {
|
|
|
68
68
|
*/
|
|
69
69
|
export const LocalizedLoginScreen = props => {
|
|
70
70
|
const t = useI18nLib('auth');
|
|
71
|
-
|
|
71
|
+
const [lng] = useLanguage();
|
|
72
|
+
return _jsx(LoginScreen, { ...props, translate: props.translate ?? ((key, defaultValue) => t(key, { defaultValue })), locale: props.locale ?? lng });
|
|
72
73
|
};
|
|
73
74
|
//# sourceMappingURL=screen.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screen.js","sourceRoot":"","sources":["../../../src/components/login/screen.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,iCAAiC,CAAA;AACxD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AACzG,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"screen.js","sourceRoot":"","sources":["../../../src/components/login/screen.tsx"],"names":[],"mappings":"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"}
|
|
@@ -3,7 +3,15 @@ import type { LoginTermsModel } from '@owlmeans/client-panel/auth';
|
|
|
3
3
|
export interface LoginTermsProps {
|
|
4
4
|
model: LoginTermsModel;
|
|
5
5
|
translate: (key: string, defaultValue: string) => string;
|
|
6
|
+
/** The current language, for `Intl.ListFormat` and a document's own locale-keyed label. */
|
|
7
|
+
locale?: string;
|
|
6
8
|
className?: string;
|
|
7
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* The sign-in screen's terms confirmation: one consented checkbox for `documents` (terms, plus
|
|
12
|
+
* billing/product/custom when configured), and a SEPARATE, non-consented disclosure line for
|
|
13
|
+
* `notices` (privacy, plus cookies per its own rule) — never nested inside the checkbox's label,
|
|
14
|
+
* because nothing is being agreed to there.
|
|
15
|
+
*/
|
|
8
16
|
export declare const LoginTerms: FC<LoginTermsProps>;
|
|
9
17
|
//# sourceMappingURL=terms.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terms.d.ts","sourceRoot":"","sources":["../../../src/components/login/terms.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAA;AAE1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"terms.d.ts","sourceRoot":"","sources":["../../../src/components/login/terms.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAA;AAE1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAIlE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,CAAA;IACtB,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACxD,2FAA2F;IAC3F,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA+CD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CAuD1C,CAAA"}
|
|
@@ -1,33 +1,52 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from '../../@/lib/utils.js';
|
|
3
|
+
import { termsSentence } from '@owlmeans/client-auth/login';
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* English fallbacks for a document's own translated label, keyed by `ResolvedTermsDocument.key`.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* forces every translator into English's. Splitting the already-translated string keeps that
|
|
9
|
-
* freedom while keeping the i18n contract at `(key, defaultValue) => string` — no ReactNode ever
|
|
10
|
-
* comes out of a translation, so an app with no i18n provider still renders a correct sentence.
|
|
7
|
+
* Only the keys this package's own resolver ever produces need one — a custom document always
|
|
8
|
+
* carries its own `label`/`labelMap`, or a caller's own `i18nKey` with its own bundle entry.
|
|
11
9
|
*/
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
const DEFAULT_LABEL = {
|
|
11
|
+
terms: 'Terms & Conditions',
|
|
12
|
+
privacy: 'Privacy Policy',
|
|
13
|
+
cookies: 'Cookie Policy',
|
|
14
|
+
billing: 'Billing Terms',
|
|
15
|
+
product: '{{product}} Product Terms',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* A document's own label: a caller's literal `label`, else its `labelMap` for the current locale,
|
|
19
|
+
* else its `i18nKey` translated — each with `params` (e.g. `{ product: 'Acme' }`) interpolated
|
|
20
|
+
* afterwards, since the `translate` contract this whole package shares is a plain
|
|
21
|
+
* `(key, defaultValue) => string` with no interpolation option of its own.
|
|
22
|
+
*/
|
|
23
|
+
const resolveLabelFor = (translate, locale) => (doc) => {
|
|
24
|
+
const fromMap = doc.labelMap != null
|
|
25
|
+
? (locale != null ? doc.labelMap[locale] : undefined) ?? Object.values(doc.labelMap)[0]
|
|
26
|
+
: undefined;
|
|
27
|
+
let label = doc.label ?? fromMap
|
|
28
|
+
?? (doc.i18nKey != null ? translate(doc.i18nKey, DEFAULT_LABEL[doc.key] ?? doc.key) : doc.key);
|
|
29
|
+
if (doc.params != null) {
|
|
30
|
+
for (const [key, value] of Object.entries(doc.params)) {
|
|
31
|
+
label = label.split(`{{${key}}}`).join(value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return label;
|
|
35
|
+
};
|
|
36
|
+
const renderParts = (parts) => parts.map((part, index) => part.href != null
|
|
37
|
+
? _jsx("a", { href: part.href, target: "_blank", rel: "noreferrer noopener", "data-login-document": part.documentKey, className: "underline underline-offset-2 hover:text-foreground", children: part.text }, index)
|
|
38
|
+
: _jsx("span", { children: part.text }, index));
|
|
39
|
+
/**
|
|
40
|
+
* The sign-in screen's terms confirmation: one consented checkbox for `documents` (terms, plus
|
|
41
|
+
* billing/product/custom when configured), and a SEPARATE, non-consented disclosure line for
|
|
42
|
+
* `notices` (privacy, plus cookies per its own rule) — never nested inside the checkbox's label,
|
|
43
|
+
* because nothing is being agreed to there.
|
|
44
|
+
*/
|
|
45
|
+
export const LoginTerms = ({ model, translate, locale, className }) => {
|
|
46
|
+
const resolveLabel = resolveLabelFor(translate, locale);
|
|
22
47
|
// Centred, like every other row in the card. The checkbox stays at the start of the sentence
|
|
23
48
|
// rather than above it, so `justify-center` centres the pair and `text-center` centres the
|
|
24
49
|
// wrapped lines within it.
|
|
25
|
-
return _jsxs("div", { className: cn('flex flex-col items-center gap-1.5 text-center', className), children: [_jsxs("label", { className: "flex items-start justify-center gap-2 text-sm text-muted-foreground cursor-pointer", children: [_jsx("input", { type: "checkbox", "data-login-terms": true, className: "mt-0.5 size-4 shrink-0 accent-primary", checked: model.accepted, "aria-invalid": model.attempted && !model.accepted, onChange: event => model.accept(event.target.checked) }), _jsx("span", { children:
|
|
26
|
-
terms: link(model.urls.terms, translate('login.terms.terms', 'Terms & Conditions')),
|
|
27
|
-
privacy: link(model.urls.privacy, translate('login.terms.privacy', 'Privacy Policy')),
|
|
28
|
-
cookies: model.urls.cookies != null
|
|
29
|
-
? link(model.urls.cookies, translate('login.terms.cookies', 'Cookie Policy'))
|
|
30
|
-
: null,
|
|
31
|
-
}) })] }), model.attempted && !model.accepted && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: translate('login.terms.required', 'Please confirm the Terms & Conditions and the Privacy Policy to continue.') })] });
|
|
50
|
+
return _jsxs("div", { className: cn('flex flex-col items-center gap-1.5 text-center', className), children: [_jsxs("label", { className: "flex items-start justify-center gap-2 text-sm text-muted-foreground cursor-pointer", children: [_jsx("input", { type: "checkbox", "data-login-terms": true, className: "mt-0.5 size-4 shrink-0 accent-primary", checked: model.accepted, "aria-invalid": model.attempted && !model.accepted, onChange: event => model.accept(event.target.checked) }), _jsx("span", { children: renderParts(termsSentence(translate('login.terms.accept', 'I have read and agree to the {{documents}}.'), model, locale, resolveLabel)) })] }), model.revisedAt != null && _jsx("p", { "data-login-revised": true, className: "text-xs text-muted-foreground", children: translate('login.terms.revised', 'Last updated: {{date}}').split('{{date}}').join(model.revisedAt) }), _jsx("p", { "data-login-privacy": true, className: "text-xs text-muted-foreground", children: renderParts(termsSentence(translate('login.terms.notice', 'How we handle your personal data: {{notices}}.'), model, locale, resolveLabel)) }), model.attempted && !model.accepted && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: renderParts(termsSentence(translate('login.terms.required', 'Please confirm the {{documents}} to continue.'), model, locale, resolveLabel)) })] });
|
|
32
51
|
};
|
|
33
52
|
//# sourceMappingURL=terms.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terms.js","sourceRoot":"","sources":["../../../src/components/login/terms.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"terms.js","sourceRoot":"","sources":["../../../src/components/login/terms.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/web-panel",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.59",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -51,33 +51,33 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@hookform/resolvers": "^5.2.2",
|
|
54
|
-
"@owlmeans/api-config-client": "^0.1.18-rc.
|
|
55
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
56
|
-
"@owlmeans/auth-common": "^0.1.18-rc.
|
|
57
|
-
"@owlmeans/basic-envelope": "^0.1.18-rc.
|
|
58
|
-
"@owlmeans/client": "^0.1.18-rc.
|
|
59
|
-
"@owlmeans/client-auth": "^0.1.18-rc.
|
|
60
|
-
"@owlmeans/client-config": "^0.1.18-rc.
|
|
61
|
-
"@owlmeans/client-context": "^0.1.18-rc.
|
|
62
|
-
"@owlmeans/client-entrypoint": "^0.1.18-rc.
|
|
63
|
-
"@owlmeans/client-flow": "^0.1.18-rc.
|
|
64
|
-
"@owlmeans/client-i18n": "^0.1.18-rc.
|
|
65
|
-
"@owlmeans/client-panel": "^0.1.18-rc.
|
|
66
|
-
"@owlmeans/client-route": "^0.1.18-rc.
|
|
67
|
-
"@owlmeans/client-socket": "^0.1.18-rc.
|
|
68
|
-
"@owlmeans/config": "^0.1.18-rc.
|
|
69
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
70
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
71
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
72
|
-
"@owlmeans/flow": "^0.1.18-rc.
|
|
73
|
-
"@owlmeans/i18n": "^0.1.18-rc.
|
|
74
|
-
"@owlmeans/route": "^0.1.18-rc.
|
|
75
|
-
"@owlmeans/state": "^0.1.18-rc.
|
|
76
|
-
"@owlmeans/web-client": "^0.1.18-rc.
|
|
77
|
-
"@owlmeans/web-consent": "^0.1.18-rc.
|
|
78
|
-
"@owlmeans/web-db": "^0.1.18-rc.
|
|
79
|
-
"@owlmeans/web-flow": "^0.1.18-rc.
|
|
80
|
-
"@owlmeans/web-router": "^0.1.18-rc.
|
|
54
|
+
"@owlmeans/api-config-client": "^0.1.18-rc.34",
|
|
55
|
+
"@owlmeans/auth": "^0.1.18-rc.34",
|
|
56
|
+
"@owlmeans/auth-common": "^0.1.18-rc.36",
|
|
57
|
+
"@owlmeans/basic-envelope": "^0.1.18-rc.35",
|
|
58
|
+
"@owlmeans/client": "^0.1.18-rc.39",
|
|
59
|
+
"@owlmeans/client-auth": "^0.1.18-rc.46",
|
|
60
|
+
"@owlmeans/client-config": "^0.1.18-rc.36",
|
|
61
|
+
"@owlmeans/client-context": "^0.1.18-rc.37",
|
|
62
|
+
"@owlmeans/client-entrypoint": "^0.1.18-rc.36",
|
|
63
|
+
"@owlmeans/client-flow": "^0.1.18-rc.42",
|
|
64
|
+
"@owlmeans/client-i18n": "^0.1.18-rc.38",
|
|
65
|
+
"@owlmeans/client-panel": "^0.1.18-rc.46",
|
|
66
|
+
"@owlmeans/client-route": "^0.1.18-rc.37",
|
|
67
|
+
"@owlmeans/client-socket": "^0.1.18-rc.37",
|
|
68
|
+
"@owlmeans/config": "^0.1.18-rc.36",
|
|
69
|
+
"@owlmeans/context": "^0.1.18-rc.32",
|
|
70
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.35",
|
|
71
|
+
"@owlmeans/error": "^0.1.18-rc.34",
|
|
72
|
+
"@owlmeans/flow": "^0.1.18-rc.37",
|
|
73
|
+
"@owlmeans/i18n": "^0.1.18-rc.35",
|
|
74
|
+
"@owlmeans/route": "^0.1.18-rc.33",
|
|
75
|
+
"@owlmeans/state": "^0.1.18-rc.33",
|
|
76
|
+
"@owlmeans/web-client": "^0.1.18-rc.49",
|
|
77
|
+
"@owlmeans/web-consent": "^0.1.18-rc.30",
|
|
78
|
+
"@owlmeans/web-db": "^0.1.18-rc.37",
|
|
79
|
+
"@owlmeans/web-flow": "^0.1.18-rc.41",
|
|
80
|
+
"@owlmeans/web-router": "^0.1.18-rc.37",
|
|
81
81
|
"i18next-browser-languagedetector": "^8.0.0",
|
|
82
82
|
"qrcode.react": "^4.2.0",
|
|
83
83
|
"react-google-recaptcha": "^3.1.0",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
},
|
|
104
104
|
"devDependencies": {
|
|
105
105
|
"@owlmeans/dep-config": "workspace:*",
|
|
106
|
-
"@owlmeans/test-ui": "^0.1.18-rc.
|
|
106
|
+
"@owlmeans/test-ui": "^0.1.18-rc.37",
|
|
107
107
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
108
108
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
109
109
|
"@radix-ui/react-label": "^2.1.0",
|
|
@@ -4,7 +4,7 @@ import { Loader2 } from 'lucide-react'
|
|
|
4
4
|
import { Button } from '../../@/components/ui/button.js'
|
|
5
5
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../../@/components/ui/card.js'
|
|
6
6
|
import { cn } from '../../@/lib/utils.js'
|
|
7
|
-
import { useI18nLib } from '@owlmeans/client-i18n'
|
|
7
|
+
import { useI18nLib, useLanguage } from '@owlmeans/client-i18n'
|
|
8
8
|
import { useLoginMethods } from '@owlmeans/client-panel/auth'
|
|
9
9
|
import { loginAttemptError } from '@owlmeans/client-auth/login'
|
|
10
10
|
import type { LoginMethod, LoginScreenProps } from '@owlmeans/client-auth/login'
|
|
@@ -103,7 +103,7 @@ export const LoginScreen: FC<LoginScreenProps> = props => {
|
|
|
103
103
|
</Button>)}
|
|
104
104
|
</div>}
|
|
105
105
|
|
|
106
|
-
{model.terms.required && <LoginTerms model={model.terms} translate={t} />}
|
|
106
|
+
{model.terms.required && <LoginTerms model={model.terms} translate={t} locale={props.locale} />}
|
|
107
107
|
|
|
108
108
|
{/*
|
|
109
109
|
A thrown message first, because it names the actual fault; otherwise whatever the
|
|
@@ -132,9 +132,11 @@ export const LoginScreen: FC<LoginScreenProps> = props => {
|
|
|
132
132
|
*/
|
|
133
133
|
export const LocalizedLoginScreen: FC<LoginScreenProps> = props => {
|
|
134
134
|
const t = useI18nLib('auth')
|
|
135
|
+
const [lng] = useLanguage()
|
|
135
136
|
|
|
136
137
|
return <LoginScreen
|
|
137
138
|
{...props}
|
|
138
139
|
translate={props.translate ?? ((key, defaultValue) => t(key, { defaultValue }))}
|
|
140
|
+
locale={props.locale ?? lng}
|
|
139
141
|
/>
|
|
140
142
|
}
|
|
@@ -1,43 +1,79 @@
|
|
|
1
1
|
import type { FC, ReactNode } from 'react'
|
|
2
2
|
import { cn } from '../../@/lib/utils.js'
|
|
3
3
|
import type { LoginTermsModel } from '@owlmeans/client-panel/auth'
|
|
4
|
+
import { termsSentence } from '@owlmeans/client-auth/login'
|
|
5
|
+
import type { ResolvedTermsDocument, TermsSentencePart } from '@owlmeans/client-auth/login'
|
|
4
6
|
|
|
5
7
|
export interface LoginTermsProps {
|
|
6
8
|
model: LoginTermsModel
|
|
7
9
|
translate: (key: string, defaultValue: string) => string
|
|
10
|
+
/** The current language, for `Intl.ListFormat` and a document's own locale-keyed label. */
|
|
11
|
+
locale?: string
|
|
8
12
|
className?: string
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
16
|
+
* English fallbacks for a document's own translated label, keyed by `ResolvedTermsDocument.key`.
|
|
13
17
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* forces every translator into English's. Splitting the already-translated string keeps that
|
|
17
|
-
* freedom while keeping the i18n contract at `(key, defaultValue) => string` — no ReactNode ever
|
|
18
|
-
* comes out of a translation, so an app with no i18n provider still renders a correct sentence.
|
|
18
|
+
* Only the keys this package's own resolver ever produces need one — a custom document always
|
|
19
|
+
* carries its own `label`/`labelMap`, or a caller's own `i18nKey` with its own bundle entry.
|
|
19
20
|
*/
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const DEFAULT_LABEL: Record<string, string> = {
|
|
22
|
+
terms: 'Terms & Conditions',
|
|
23
|
+
privacy: 'Privacy Policy',
|
|
24
|
+
cookies: 'Cookie Policy',
|
|
25
|
+
billing: 'Billing Terms',
|
|
26
|
+
product: '{{product}} Product Terms',
|
|
27
|
+
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* A document's own label: a caller's literal `label`, else its `labelMap` for the current locale,
|
|
31
|
+
* else its `i18nKey` translated — each with `params` (e.g. `{ product: 'Acme' }`) interpolated
|
|
32
|
+
* afterwards, since the `translate` contract this whole package shares is a plain
|
|
33
|
+
* `(key, defaultValue) => string` with no interpolation option of its own.
|
|
34
|
+
*/
|
|
35
|
+
const resolveLabelFor = (
|
|
36
|
+
translate: (key: string, defaultValue: string) => string, locale: string | undefined
|
|
37
|
+
) => (doc: ResolvedTermsDocument): string => {
|
|
38
|
+
const fromMap = doc.labelMap != null
|
|
39
|
+
? (locale != null ? doc.labelMap[locale] : undefined) ?? Object.values(doc.labelMap)[0]
|
|
40
|
+
: undefined
|
|
41
|
+
let label = doc.label ?? fromMap
|
|
42
|
+
?? (doc.i18nKey != null ? translate(doc.i18nKey, DEFAULT_LABEL[doc.key] ?? doc.key) : doc.key)
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
if (doc.params != null) {
|
|
45
|
+
for (const [key, value] of Object.entries(doc.params)) {
|
|
46
|
+
label = label.split(`{{${key}}}`).join(value)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return label
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const renderParts = (parts: TermsSentencePart[]): ReactNode =>
|
|
54
|
+
parts.map((part, index) => part.href != null
|
|
55
|
+
? <a key={index} href={part.href} target="_blank" rel="noreferrer noopener"
|
|
56
|
+
data-login-document={part.documentKey}
|
|
57
|
+
className="underline underline-offset-2 hover:text-foreground">{part.text}</a>
|
|
58
|
+
: <span key={index}>{part.text}</span>)
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The sign-in screen's terms confirmation: one consented checkbox for `documents` (terms, plus
|
|
62
|
+
* billing/product/custom when configured), and a SEPARATE, non-consented disclosure line for
|
|
63
|
+
* `notices` (privacy, plus cookies per its own rule) — never nested inside the checkbox's label,
|
|
64
|
+
* because nothing is being agreed to there.
|
|
65
|
+
*/
|
|
66
|
+
export const LoginTerms: FC<LoginTermsProps> = ({ model, translate, locale, className }) => {
|
|
67
|
+
const resolveLabel = resolveLabelFor(translate, locale)
|
|
36
68
|
|
|
37
69
|
// Centred, like every other row in the card. The checkbox stays at the start of the sentence
|
|
38
70
|
// rather than above it, so `justify-center` centres the pair and `text-center` centres the
|
|
39
71
|
// wrapped lines within it.
|
|
40
72
|
return <div className={cn('flex flex-col items-center gap-1.5 text-center', className)}>
|
|
73
|
+
{/*
|
|
74
|
+
The ONLY place `data-login-terms` appears — an e2e suite in the consuming product treats it
|
|
75
|
+
as a strict (exactly-one-match) locator, so it must never be duplicated onto a second control.
|
|
76
|
+
*/}
|
|
41
77
|
<label className="flex items-start justify-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
|
42
78
|
{/*
|
|
43
79
|
A NATIVE checkbox, deliberately. `web-panel` ships no `checkbox` primitive, and requiring
|
|
@@ -53,22 +89,33 @@ export const LoginTerms: FC<LoginTermsProps> = ({ model, translate, className })
|
|
|
53
89
|
onChange={event => model.accept(event.target.checked)}
|
|
54
90
|
/>
|
|
55
91
|
<span>
|
|
56
|
-
{
|
|
57
|
-
translate('login.terms.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
terms: link(model.urls.terms, translate('login.terms.terms', 'Terms & Conditions')),
|
|
61
|
-
privacy: link(model.urls.privacy, translate('login.terms.privacy', 'Privacy Policy')),
|
|
62
|
-
cookies: model.urls.cookies != null
|
|
63
|
-
? link(model.urls.cookies, translate('login.terms.cookies', 'Cookie Policy'))
|
|
64
|
-
: null,
|
|
65
|
-
}
|
|
66
|
-
)}
|
|
92
|
+
{renderParts(termsSentence(
|
|
93
|
+
translate('login.terms.accept', 'I have read and agree to the {{documents}}.'),
|
|
94
|
+
model, locale, resolveLabel
|
|
95
|
+
))}
|
|
67
96
|
</span>
|
|
68
97
|
</label>
|
|
98
|
+
|
|
99
|
+
{model.revisedAt != null && <p data-login-revised className="text-xs text-muted-foreground">
|
|
100
|
+
{translate('login.terms.revised', 'Last updated: {{date}}').split('{{date}}').join(model.revisedAt)}
|
|
101
|
+
</p>}
|
|
102
|
+
|
|
103
|
+
{/*
|
|
104
|
+
Outside the checkbox's `<label>` on purpose: a privacy disclosure is not something the
|
|
105
|
+
checkbox consents to, so it is a sibling paragraph rather than nested inside it.
|
|
106
|
+
*/}
|
|
107
|
+
<p data-login-privacy className="text-xs text-muted-foreground">
|
|
108
|
+
{renderParts(termsSentence(
|
|
109
|
+
translate('login.terms.notice', 'How we handle your personal data: {{notices}}.'),
|
|
110
|
+
model, locale, resolveLabel
|
|
111
|
+
))}
|
|
112
|
+
</p>
|
|
113
|
+
|
|
69
114
|
{model.attempted && !model.accepted && <p role="alert" className="text-sm text-destructive">
|
|
70
|
-
{
|
|
71
|
-
'Please confirm the
|
|
115
|
+
{renderParts(termsSentence(
|
|
116
|
+
translate('login.terms.required', 'Please confirm the {{documents}} to continue.'),
|
|
117
|
+
model, locale, resolveLabel
|
|
118
|
+
))}
|
|
72
119
|
</p>}
|
|
73
120
|
</div>
|
|
74
121
|
}
|
package/tests/harness/mount.tsx
CHANGED
|
@@ -111,6 +111,10 @@ const consentMode = new URLSearchParams(window.location.search).get('consent')
|
|
|
111
111
|
const skipOff = new URLSearchParams(window.location.search).get('skip') === 'off'
|
|
112
112
|
// `?themeToggle=1` asks for the footer's light/dark switcher. Absent, the prop is not passed.
|
|
113
113
|
const themeToggle = new URLSearchParams(window.location.search).get('themeToggle') === '1'
|
|
114
|
+
// `?terms=extended` adds billing/product/custom documents and a revision date to the terms
|
|
115
|
+
// confirmation — the shape that pins the "still exactly one checkbox" and "documents render
|
|
116
|
+
// outside the notice" rules even when there is more than terms+privacy to show.
|
|
117
|
+
const extendedTerms = new URLSearchParams(window.location.search).get('terms') === 'extended'
|
|
114
118
|
|
|
115
119
|
/**
|
|
116
120
|
* The footer's "Cookie settings" control — the menu widget, rendered from an always-mounted
|
|
@@ -229,7 +233,18 @@ base.security = {
|
|
|
229
233
|
login: {
|
|
230
234
|
// Confirmation required, which is the case that matters: a method must be blocked until it
|
|
231
235
|
// is given, and blocking must SAY so rather than swallow the click.
|
|
232
|
-
terms: {
|
|
236
|
+
terms: {
|
|
237
|
+
required: true, terms: 'https://example.test/terms', privacy: 'https://example.test/privacy',
|
|
238
|
+
...(extendedTerms ? {
|
|
239
|
+
billing: { href: 'https://example.test/billing', revisedAt: '2026-01-01' },
|
|
240
|
+
product: { name: 'Harness', href: 'https://example.test/product' },
|
|
241
|
+
documents: [
|
|
242
|
+
{ key: 'custom-a', href: 'https://example.test/custom-a', label: 'Custom A' },
|
|
243
|
+
{ key: 'custom-b', href: 'https://example.test/custom-b', label: 'Custom B' },
|
|
244
|
+
],
|
|
245
|
+
showRevision: true,
|
|
246
|
+
} : {}),
|
|
247
|
+
},
|
|
233
248
|
credit: { poweredBy: true, product: 'Harness', organization: 'Acme' },
|
|
234
249
|
},
|
|
235
250
|
},
|
package/tests/login.spec.ts
CHANGED
|
@@ -8,8 +8,8 @@ afterAll(async () => {
|
|
|
8
8
|
await closeBrowser()
|
|
9
9
|
})
|
|
10
10
|
|
|
11
|
-
const open = async () => {
|
|
12
|
-
const mounted = await mountComponent({ url: `${HARNESS_URL}login` })
|
|
11
|
+
const open = async (query?: string) => {
|
|
12
|
+
const mounted = await mountComponent({ url: `${HARNESS_URL}login${query ?? ''}` })
|
|
13
13
|
await mounted.page.waitForSelector('[data-login-method]')
|
|
14
14
|
// A fresh acceptance for every test: the confirmation is remembered per browser, and a test
|
|
15
15
|
// that inherited the previous one would assert the unblocked path while claiming the other.
|
|
@@ -187,4 +187,76 @@ describe('@owlmeans/web-panel — the sign-in screen', () => {
|
|
|
187
187
|
await close()
|
|
188
188
|
}
|
|
189
189
|
}, TIMEOUT)
|
|
190
|
+
|
|
191
|
+
test('[data-login-revised] is absent when the configuration never asked to show it', async () => {
|
|
192
|
+
const { page, close } = await open()
|
|
193
|
+
try {
|
|
194
|
+
expect(await page.locator('[data-login-revised]').count()).toBe(0)
|
|
195
|
+
} finally {
|
|
196
|
+
await close()
|
|
197
|
+
}
|
|
198
|
+
}, TIMEOUT)
|
|
199
|
+
|
|
200
|
+
describe('with billing, product and custom documents configured', () => {
|
|
201
|
+
test('there is still exactly one [data-login-terms] checkbox', async () => {
|
|
202
|
+
const { page, close } = await open('?terms=extended')
|
|
203
|
+
try {
|
|
204
|
+
expect(await page.locator('[data-login-terms]').count()).toBe(1)
|
|
205
|
+
} finally {
|
|
206
|
+
await close()
|
|
207
|
+
}
|
|
208
|
+
}, TIMEOUT)
|
|
209
|
+
|
|
210
|
+
test('[data-login-privacy] renders OUTSIDE the checkbox\'s label', async () => {
|
|
211
|
+
const { page, close } = await open('?terms=extended')
|
|
212
|
+
try {
|
|
213
|
+
expect(await page.locator('label:has([data-login-terms]) [data-login-privacy]').count())
|
|
214
|
+
.toBe(0)
|
|
215
|
+
expect(await page.locator('[data-login-privacy]').count()).toBe(1)
|
|
216
|
+
} finally {
|
|
217
|
+
await close()
|
|
218
|
+
}
|
|
219
|
+
}, TIMEOUT)
|
|
220
|
+
|
|
221
|
+
test('[data-login-revised] shows the latest revision date', async () => {
|
|
222
|
+
const { page, close } = await open('?terms=extended')
|
|
223
|
+
try {
|
|
224
|
+
expect(await page.locator('[data-login-revised]').count()).toBe(1)
|
|
225
|
+
expect(await page.locator('[data-login-revised]').textContent()).toContain('2026-01-01')
|
|
226
|
+
} finally {
|
|
227
|
+
await close()
|
|
228
|
+
}
|
|
229
|
+
}, TIMEOUT)
|
|
230
|
+
|
|
231
|
+
test('every configured document renders its own [data-login-document] link', async () => {
|
|
232
|
+
const { page, close } = await open('?terms=extended')
|
|
233
|
+
try {
|
|
234
|
+
const hrefByKey = async (key: string) =>
|
|
235
|
+
await page.locator(`[data-login-document="${key}"]`).getAttribute('href')
|
|
236
|
+
|
|
237
|
+
expect(await hrefByKey('terms')).toBe('https://example.test/terms')
|
|
238
|
+
expect(await hrefByKey('billing')).toBe('https://example.test/billing')
|
|
239
|
+
expect(await hrefByKey('product')).toBe('https://example.test/product')
|
|
240
|
+
expect(await hrefByKey('custom-a')).toBe('https://example.test/custom-a')
|
|
241
|
+
expect(await hrefByKey('custom-b')).toBe('https://example.test/custom-b')
|
|
242
|
+
// The notice line, not the consented one.
|
|
243
|
+
expect(await hrefByKey('privacy')).toBe('https://example.test/privacy')
|
|
244
|
+
} finally {
|
|
245
|
+
await close()
|
|
246
|
+
}
|
|
247
|
+
}, TIMEOUT)
|
|
248
|
+
|
|
249
|
+
test('confirming the single checkbox still unblocks the methods', async () => {
|
|
250
|
+
const { page, close } = await open('?terms=extended')
|
|
251
|
+
try {
|
|
252
|
+
await page.locator('[data-login-terms]').check()
|
|
253
|
+
await page.locator('[data-login-method="primary"]').click()
|
|
254
|
+
|
|
255
|
+
expect(await page.evaluate(() => (window as never as { __loginStarted: string[] }).__loginStarted))
|
|
256
|
+
.toEqual(['primary'])
|
|
257
|
+
} finally {
|
|
258
|
+
await close()
|
|
259
|
+
}
|
|
260
|
+
}, TIMEOUT)
|
|
261
|
+
})
|
|
190
262
|
})
|