@owlmeans/web-panel 0.1.18-rc.57 → 0.1.18-rc.58

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
@@ -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.57
14
+ bun add @owlmeans/web-panel@^0.1.18-rc.58
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.34
499
+ npx @owlmeans/agent-skills@^0.1.18-rc.35
500
500
  ```
501
501
 
502
502
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-panel",
4
- "version": "0.1.18-rc.57",
5
- "generatedAt": "2026-09-22T20:45:24.070Z",
4
+ "version": "0.1.18-rc.58",
5
+ "generatedAt": "2026-09-22T21:57:18.836Z",
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.57"` in `dependencies`
11
+ **Install:** `"@owlmeans/web-panel": "^0.1.18-rc.58"` 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,CAOrD,CAAA"}
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
- return _jsx(LoginScreen, { ...props, translate: props.translate ?? ((key, defaultValue) => t(key, { defaultValue })) });
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":"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"}
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;AAElE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,CAAA;IACtB,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAuBD,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CA0C1C,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
- * Split a translated sentence around its two link placeholders.
5
+ * English fallbacks for a document's own translated label, keyed by `ResolvedTermsDocument.key`.
5
6
  *
6
- * The sentence is translated as ONE string with `{{terms}}` and `{{privacy}}` in it, rather than
7
- * assembled from fragments, because word order differs between languages and an assembled sentence
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 interpolate = (sentence, slots) => sentence
13
- .split(/(\{\{[a-z]+\}\})/i)
14
- .map((part, index) => {
15
- const match = /^\{\{([a-z]+)\}\}$/i.exec(part);
16
- return match != null
17
- ? _jsx("span", { children: slots[match[1]] ?? part }, index)
18
- : _jsx("span", { children: part }, index);
19
- });
20
- export const LoginTerms = ({ model, translate, className }) => {
21
- const link = (href, label) => _jsx("a", { href: href, target: "_blank", rel: "noreferrer noopener", className: "underline underline-offset-2 hover:text-foreground", children: label });
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: interpolate(translate('login.terms.agreement', 'I have read and agree to the {{terms}} and the {{privacy}}.'), {
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;AASzC;;;;;;;;GAQG;AACH,MAAM,WAAW,GAAG,CAClB,QAAgB,EAAE,KAAgC,EACrC,EAAE,CAAC,QAAQ;KACvB,KAAK,CAAC,mBAAmB,CAAC;KAC1B,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;IACnB,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAE9C,OAAO,KAAK,IAAI,IAAI;QAClB,CAAC,CAAC,yBAAmB,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,IAA/B,KAAK,CAAkC;QACpD,CAAC,CAAC,yBAAmB,IAAI,IAAZ,KAAK,CAAe,CAAA;AACrC,CAAC,CAAC,CAAA;AAEJ,MAAM,CAAC,MAAM,UAAU,GAAwB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE;IACjF,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,KAAa,EAAa,EAAE,CACtD,YAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAC,QAAQ,EAAC,GAAG,EAAC,qBAAqB,EACtD,SAAS,EAAC,oDAAoD,YAAE,KAAK,GAAK,CAAA;IAE9E,6FAA6F;IAC7F,2FAA2F;IAC3F,2BAA2B;IAC3B,OAAO,eAAK,SAAS,EAAE,EAAE,CAAC,gDAAgD,EAAE,SAAS,CAAC,aACpF,iBAAO,SAAS,EAAC,oFAAoF,aAOnG,gBACE,IAAI,EAAC,UAAU,4BACf,SAAS,EAAC,uCAAuC,EACjD,OAAO,EAAE,KAAK,CAAC,QAAQ,kBACT,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,QAAQ,EAChD,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,GACrD,EACF,yBACG,WAAW,CACV,SAAS,CAAC,uBAAuB,EAC/B,6DAA6D,CAAC,EAChE;4BACE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,SAAS,CAAC,mBAAmB,EAAE,oBAAoB,CAAC,CAAC;4BACnF,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,qBAAqB,EAAE,gBAAgB,CAAC,CAAC;4BACrF,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI;gCACjC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,qBAAqB,EAAE,eAAe,CAAC,CAAC;gCAC7E,CAAC,CAAC,IAAI;yBACT,CACF,GACI,IACD,EACP,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,YAAG,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,0BAA0B,YACxF,SAAS,CAAC,sBAAsB,EAC/B,2EAA2E,CAAC,GAC5E,IACA,CAAA;AACR,CAAC,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.57",
3
+ "version": "0.1.18-rc.58",
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.32",
55
- "@owlmeans/auth": "^0.1.18-rc.32",
56
- "@owlmeans/auth-common": "^0.1.18-rc.34",
57
- "@owlmeans/basic-envelope": "^0.1.18-rc.33",
58
- "@owlmeans/client": "^0.1.18-rc.37",
59
- "@owlmeans/client-auth": "^0.1.18-rc.44",
60
- "@owlmeans/client-config": "^0.1.18-rc.34",
61
- "@owlmeans/client-context": "^0.1.18-rc.35",
62
- "@owlmeans/client-entrypoint": "^0.1.18-rc.34",
63
- "@owlmeans/client-flow": "^0.1.18-rc.40",
64
- "@owlmeans/client-i18n": "^0.1.18-rc.36",
65
- "@owlmeans/client-panel": "^0.1.18-rc.44",
66
- "@owlmeans/client-route": "^0.1.18-rc.35",
67
- "@owlmeans/client-socket": "^0.1.18-rc.35",
68
- "@owlmeans/config": "^0.1.18-rc.34",
69
- "@owlmeans/context": "^0.1.18-rc.30",
70
- "@owlmeans/entrypoint": "^0.1.18-rc.33",
71
- "@owlmeans/error": "^0.1.18-rc.32",
72
- "@owlmeans/flow": "^0.1.18-rc.35",
73
- "@owlmeans/i18n": "^0.1.18-rc.33",
74
- "@owlmeans/route": "^0.1.18-rc.31",
75
- "@owlmeans/state": "^0.1.18-rc.31",
76
- "@owlmeans/web-client": "^0.1.18-rc.47",
77
- "@owlmeans/web-consent": "^0.1.18-rc.28",
78
- "@owlmeans/web-db": "^0.1.18-rc.35",
79
- "@owlmeans/web-flow": "^0.1.18-rc.39",
80
- "@owlmeans/web-router": "^0.1.18-rc.35",
54
+ "@owlmeans/api-config-client": "^0.1.18-rc.33",
55
+ "@owlmeans/auth": "^0.1.18-rc.33",
56
+ "@owlmeans/auth-common": "^0.1.18-rc.35",
57
+ "@owlmeans/basic-envelope": "^0.1.18-rc.34",
58
+ "@owlmeans/client": "^0.1.18-rc.38",
59
+ "@owlmeans/client-auth": "^0.1.18-rc.45",
60
+ "@owlmeans/client-config": "^0.1.18-rc.35",
61
+ "@owlmeans/client-context": "^0.1.18-rc.36",
62
+ "@owlmeans/client-entrypoint": "^0.1.18-rc.35",
63
+ "@owlmeans/client-flow": "^0.1.18-rc.41",
64
+ "@owlmeans/client-i18n": "^0.1.18-rc.37",
65
+ "@owlmeans/client-panel": "^0.1.18-rc.45",
66
+ "@owlmeans/client-route": "^0.1.18-rc.36",
67
+ "@owlmeans/client-socket": "^0.1.18-rc.36",
68
+ "@owlmeans/config": "^0.1.18-rc.35",
69
+ "@owlmeans/context": "^0.1.18-rc.31",
70
+ "@owlmeans/entrypoint": "^0.1.18-rc.34",
71
+ "@owlmeans/error": "^0.1.18-rc.33",
72
+ "@owlmeans/flow": "^0.1.18-rc.36",
73
+ "@owlmeans/i18n": "^0.1.18-rc.34",
74
+ "@owlmeans/route": "^0.1.18-rc.32",
75
+ "@owlmeans/state": "^0.1.18-rc.32",
76
+ "@owlmeans/web-client": "^0.1.18-rc.48",
77
+ "@owlmeans/web-consent": "^0.1.18-rc.29",
78
+ "@owlmeans/web-db": "^0.1.18-rc.36",
79
+ "@owlmeans/web-flow": "^0.1.18-rc.40",
80
+ "@owlmeans/web-router": "^0.1.18-rc.36",
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.35",
106
+ "@owlmeans/test-ui": "^0.1.18-rc.36",
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
- * Split a translated sentence around its two link placeholders.
16
+ * English fallbacks for a document's own translated label, keyed by `ResolvedTermsDocument.key`.
13
17
  *
14
- * The sentence is translated as ONE string with `{{terms}}` and `{{privacy}}` in it, rather than
15
- * assembled from fragments, because word order differs between languages and an assembled sentence
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 interpolate = (
21
- sentence: string, slots: Record<string, ReactNode>
22
- ): ReactNode[] => sentence
23
- .split(/(\{\{[a-z]+\}\})/i)
24
- .map((part, index) => {
25
- const match = /^\{\{([a-z]+)\}\}$/i.exec(part)
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
- return match != null
28
- ? <span key={index}>{slots[match[1]] ?? part}</span>
29
- : <span key={index}>{part}</span>
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
- export const LoginTerms: FC<LoginTermsProps> = ({ model, translate, className }) => {
33
- const link = (href: string, label: string): ReactNode =>
34
- <a href={href} target="_blank" rel="noreferrer noopener"
35
- className="underline underline-offset-2 hover:text-foreground">{label}</a>
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
- {interpolate(
57
- translate('login.terms.agreement',
58
- 'I have read and agree to the {{terms}} and the {{privacy}}.'),
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
- {translate('login.terms.required',
71
- 'Please confirm the Terms & Conditions and the Privacy Policy to continue.')}
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
  }
@@ -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: { required: true, terms: 'https://example.test/terms', privacy: 'https://example.test/privacy' },
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
  },
@@ -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
  })