@owlmeans/web-panel 0.1.18-rc.60 → 0.1.18-rc.61
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 +4 -4
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-panel/SKILL.md +20 -12
- package/build/auth/exports.d.ts +2 -2
- package/build/auth/exports.d.ts.map +1 -1
- package/build/auth/exports.js +1 -1
- package/build/auth/exports.js.map +1 -1
- package/build/components/login/screen.d.ts.map +1 -1
- package/build/components/login/screen.js +5 -3
- package/build/components/login/screen.js.map +1 -1
- package/build/components/login/terms.d.ts +10 -0
- package/build/components/login/terms.d.ts.map +1 -1
- package/build/components/login/terms.js +14 -34
- package/build/components/login/terms.js.map +1 -1
- package/build/entrypoints.d.ts +4 -2
- package/build/entrypoints.d.ts.map +1 -1
- package/build/exports.d.ts +2 -1
- package/build/exports.d.ts.map +1 -1
- package/build/exports.js +1 -1
- package/build/exports.js.map +1 -1
- package/package.json +27 -27
- package/src/auth/exports.ts +2 -2
- package/src/components/login/screen.tsx +7 -3
- package/src/components/login/terms.tsx +24 -50
- package/src/exports.ts +2 -1
- package/tests/consent.spec.ts +15 -0
- package/tests/harness/lazy-screen.tsx +9 -0
- package/tests/harness/mount.tsx +65 -2
- package/tests/lazy.spec.ts +104 -0
- package/tests/login.spec.ts +49 -1
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.61
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Peer requirements (the consuming app provides these): `react`, `react-dom`,
|
|
@@ -351,8 +351,8 @@ The dialog's strings are the lib-tier `socket` namespace (`reload.title`, `reloa
|
|
|
351
351
|
| `SocketReloadDialog` | component | Blocking reload prompt for `'lost'` sockets |
|
|
352
352
|
| `LoginScreen`, `LocalizedLoginScreen` | component | Identity-provider choice screen; the localized one binds `translate` to the app's resources |
|
|
353
353
|
| `appendLoginScreen(ctx, setup?)`, `LoginScreenSetup` | function, type | Register the screen on the login service, with `Logo` and other `LoginScreenProps` |
|
|
354
|
-
| `LoginTerms`, `LoginCredit`, `LoginMethodIcon` | component | The screen's terms
|
|
355
|
-
| `LoginTermsProps`, `LoginCreditProps` | type | Their props |
|
|
354
|
+
| `LoginTerms`, `LoginPrivacyNotice`, `LoginCredit`, `LoginMethodIcon` | component | The screen's terms confirmation, its privacy-only disclosure (rendered alone once `termsDeferred` moves the checkbox to a post-login step — see `@owlmeans/client-auth/login`), the credit line and method icons |
|
|
355
|
+
| `LoginTermsProps`, `LoginPrivacyNoticeProps`, `LoginCreditProps` | type | Their props |
|
|
356
356
|
| `scalingToStyles(horizontal?, vertical?)` | function | `BlockScaling` to Tailwind width/height classes |
|
|
357
357
|
| `useBreakPoint()` | hook | The current Tailwind breakpoint name |
|
|
358
358
|
| `useMapBreakpoint(map, def?, breakpoint?)` | hook | The map entry for the current breakpoint; throws `SyntaxError` when nothing matches and no `def` is given |
|
|
@@ -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.39
|
|
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.61",
|
|
5
|
+
"generatedAt": "2026-09-24T14:00:35.085Z",
|
|
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.61"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
@@ -33,7 +33,7 @@ user-invocable: false
|
|
|
33
33
|
| `useBreakPoint()` | The current Tailwind breakpoint name, tracked on `resize` (`lg` when there is no `window`) |
|
|
34
34
|
| `useMapBreakpoint(map, def?, breakpoint?)` | The `map` entry for the current breakpoint (or for the `breakpoint` passed), falling back to `def`. It **throws a `SyntaxError`** when neither yields a value, so give it a `def` or cover every breakpoint |
|
|
35
35
|
| Re-exports from `@owlmeans/client-panel` | Cross-platform panel primitives, incl. `usePanelNav` and the `PanelNav*` types |
|
|
36
|
-
| Re-exports from `@owlmeans/client` / `@owlmeans/client-entrypoint` / `@owlmeans/route` | `bind`, `bindAll`, `bindScreen`, `handler`, `provideRequest`, `stab`, `route`, `croute`, `frontend`, `useNavigate`, `useEntrypoint`, `useValue` |
|
|
36
|
+
| Re-exports from `@owlmeans/client` / `@owlmeans/client-entrypoint` / `@owlmeans/route` | `bind`, `bindAll`, `bindScreen`, `handler`, `lazyHandler` / `lazyComponent` (+ `LazyHandler` / `LazyComponent` / `LazyComponentOptions` types — see the `client` skill), `provideRequest`, `stab`, `route`, `croute`, `frontend`, `useNavigate`, `useEntrypoint`, `useValue` |
|
|
37
37
|
| Re-exports from the surrounding layers | `config`, `service`, `addWebService`, `AppType` / `HOME` / `ROOT` / `BASE` / `GUEST`, `DISPATCHER`, `CAUTHEN_FLOW_ENTER`, `DAUTH_GUARD`, `bindExternalAuthentication`, `Dispatcher`, `appendWebAuthService`, `flow` / `configureFlows` / `useFlow` / `FLOW_PARAM` / `SERVICE_PARAM`, `useI18n*` / `useLanguage` / `composePrefix`, `addI18nApp` / `addI18nLib` / `SUPPORTED_LNGS` |
|
|
38
38
|
|
|
39
39
|
## Subpath Exports
|
|
@@ -421,20 +421,28 @@ 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`
|
|
424
|
+
### The terms confirmation — `LoginTerms`, `LoginPrivacyNotice`
|
|
425
425
|
|
|
426
426
|
`components/login/terms.tsx` renders `LoginTermsModel` (`@owlmeans/client-panel/auth`) via
|
|
427
|
-
`termsSentence` (`@owlmeans/client-auth/login`) rather than re-deriving
|
|
428
|
-
**`[data-login-terms]` marks exactly one checkbox
|
|
429
|
-
|
|
427
|
+
`termsSentence` + `termsLabelResolver` (`@owlmeans/client-auth/login`) rather than re-deriving
|
|
428
|
+
link/label pairs itself. **`[data-login-terms]` marks exactly one checkbox — or NONE, once
|
|
429
|
+
`model.terms.deferred` is true** (`LoginScreen` renders `LoginTerms` when not deferred,
|
|
430
|
+
`LoginPrivacyNotice` when it is): an e2e suite elsewhere in the platform treats the checkbox as a
|
|
431
|
+
strict, at-most-one-match locator, so a change here must never add a second one, whatever
|
|
430
432
|
billing/product/custom documents a configuration adds. `[data-login-privacy]` is a SIBLING
|
|
431
433
|
paragraph of the checkbox's `<label>`, never nested inside it — a privacy disclosure is not
|
|
432
|
-
something the checkbox consents to
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
434
|
+
something the checkbox consents to — and it is exported ON ITS OWN as `LoginPrivacyNotice`: it
|
|
435
|
+
still renders while deferred, since the disclosure was never the checkbox's to remove. A linked
|
|
436
|
+
document/notice fragment carries `data-login-document="<key>"`. `[data-login-revised]` renders only
|
|
437
|
+
when NOT deferred and the resolved terms carry a `revisedAt` (i.e. the configuration set
|
|
438
|
+
`showRevision: true` and at least one document has its own revision date) — deferred, that date
|
|
439
|
+
belongs to the document the checkbox agreed to, and there is no checkbox on this screen to attach it
|
|
440
|
+
to. `LoginTerms`/`LoginPrivacyNotice` take `locale?: string` — from `LoginScreen`'s own `locale`
|
|
441
|
+
prop, never read from context directly — for `Intl.ListFormat` and a custom document's locale-keyed
|
|
442
|
+
`labelMap`. Method buttons carry `aria-disabled={model.blocked || undefined}`, never a literal
|
|
443
|
+
`"false"` — a deferred screen (which never sets `blocked`) then renders identically to an unblocked
|
|
444
|
+
ordinary one, with no special case. Pinned by `tests/login.spec.ts` → the "with billing, product and
|
|
445
|
+
custom documents configured" and "the Terms confirmation deferred to a step" blocks.
|
|
438
446
|
|
|
439
447
|
## Subpath: `./consent`
|
|
440
448
|
|
package/build/auth/exports.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { handler, useEntrypoint, useValue, useNavigate } from '@owlmeans/client';
|
|
2
|
-
export type { Navigator } from '@owlmeans/client';
|
|
1
|
+
export { handler, lazyHandler, lazyComponent, useEntrypoint, useValue, useNavigate } from '@owlmeans/client';
|
|
2
|
+
export type { Navigator, LazyComponent, LazyComponentOptions, LazyHandler } from '@owlmeans/client';
|
|
3
3
|
export { config } from '@owlmeans/client-context';
|
|
4
4
|
export { service } from '@owlmeans/config';
|
|
5
5
|
export { EntrypointOutcome } from '@owlmeans/entrypoint';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exports.d.ts","sourceRoot":"","sources":["../../src/auth/exports.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"exports.d.ts","sourceRoot":"","sources":["../../src/auth/exports.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC5G,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnG,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAA;AAC7F,YAAY,EAAE,gBAAgB,IAAI,MAAM,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAA;AACvG,OAAO,EAAE,KAAK,IAAI,MAAM,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC9D,YAAY,EAAE,oBAAoB,IAAI,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC3E,OAAO,EAAE,aAAa,IAAI,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAExC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAEpE,cAAc,6BAA6B,CAAA;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAEnC,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAC/D,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C,OAAO,EAAE,aAAa,IAAI,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACnE,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,EAAE,WAAW,IAAI,UAAU,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAC7E,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA"}
|
package/build/auth/exports.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { handler, useEntrypoint, useValue, useNavigate } from '@owlmeans/client';
|
|
1
|
+
export { handler, lazyHandler, lazyComponent, useEntrypoint, useValue, useNavigate } from '@owlmeans/client';
|
|
2
2
|
export { config } from '@owlmeans/client-context';
|
|
3
3
|
export { service } from '@owlmeans/config';
|
|
4
4
|
export { EntrypointOutcome } from '@owlmeans/entrypoint';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exports.js","sourceRoot":"","sources":["../../src/auth/exports.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"exports.js","sourceRoot":"","sources":["../../src/auth/exports.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE5G,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAExD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAA;AAE7F,OAAO,EAAE,KAAK,IAAI,MAAM,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE9D,OAAO,EAAE,aAAa,IAAI,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAA;AAExC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAEpE,cAAc,6BAA6B,CAAA;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAEnC,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAG/D,OAAO,EAAE,aAAa,IAAI,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAEnE,OAAO,EAAE,WAAW,IAAI,UAAU,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAC7E,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA"}
|
|
@@ -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,
|
|
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,CAgG5C,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE,EAAE,CAAC,gBAAgB,CASrD,CAAA"}
|
|
@@ -8,7 +8,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';
|
|
11
|
-
import { LoginTerms } from './terms.js';
|
|
11
|
+
import { LoginPrivacyNotice, LoginTerms } from './terms.js';
|
|
12
12
|
import { LoginCredit } from './credit.js';
|
|
13
13
|
const VARIANT = {
|
|
14
14
|
primary: 'default',
|
|
@@ -50,13 +50,15 @@ export const LoginScreen = props => {
|
|
|
50
50
|
return _jsx("div", { "data-login-screen": true, style: { minHeight: '100dvh', ...props.style }, className: cn('flex w-full items-center justify-center p-4', props.className), children: _jsxs(Card, { className: cn('w-full max-w-sm', props.containerClassName), children: [_jsxs(CardHeader, { className: "items-center gap-2 text-center", children: [Logo != null && _jsx("div", { className: "flex justify-center pb-2", children: isValidElement(Logo) ? Logo : typeof Logo === 'function'
|
|
51
51
|
? _jsx(Logo, { className: "h-10 w-auto" }) : Logo }), _jsx(CardTitle, { className: "text-xl", children: props.title ?? t('login.title', 'Sign in') }), _jsx(CardDescription, { children: props.subtitle ?? t('login.subtitle', 'Choose how you would like to continue.') })] }), _jsxs(CardContent, { className: "flex flex-col gap-4", children: [model.methods.length < 1
|
|
52
52
|
? _jsx("p", { role: "status", className: "text-sm text-muted-foreground text-center", children: t('login.empty', 'No sign-in method is configured for this application.') })
|
|
53
|
-
: _jsx("div", { className: "flex flex-col gap-2", children: model.methods.map((method) => _jsxs(Button, { type: "button", "data-login-method": method.id, variant: VARIANT[method.emphasis ?? 'secondary'] ?? 'outline', "aria-disabled": model.blocked, "data-blocked": model.blocked ? 'true' : undefined,
|
|
53
|
+
: _jsx("div", { className: "flex flex-col gap-2", children: model.methods.map((method) => _jsxs(Button, { type: "button", "data-login-method": method.id, variant: VARIANT[method.emphasis ?? 'secondary'] ?? 'outline', "aria-disabled": model.blocked || undefined, "data-blocked": model.blocked ? 'true' : undefined,
|
|
54
54
|
// `cursor-pointer` explicitly: the package's private shadcn button stays compatible
|
|
55
55
|
// with the older primitive style too. Stating it here keeps the sign-in control
|
|
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 &&
|
|
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 && (model.terms.deferred
|
|
60
|
+
? _jsx(LoginPrivacyNotice, { model: model.terms, translate: t, locale: props.locale })
|
|
61
|
+
: _jsx(LoginTerms, { model: model.terms, translate: t, locale: props.locale })), (model.error ?? attemptError) != null
|
|
60
62
|
&& _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
63
|
};
|
|
62
64
|
/**
|
|
@@ -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,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAE/D,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"screen.js","sourceRoot":"","sources":["../../../src/components/login/screen.tsx"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,16 @@ export interface LoginTermsProps {
|
|
|
7
7
|
locale?: string;
|
|
8
8
|
className?: string;
|
|
9
9
|
}
|
|
10
|
+
export type LoginPrivacyNoticeProps = LoginTermsProps;
|
|
11
|
+
/**
|
|
12
|
+
* The privacy disclosure alone — `notices` (privacy, plus cookies per its own rule), never
|
|
13
|
+
* consented to and never nested inside a checkbox's `<label>`.
|
|
14
|
+
*
|
|
15
|
+
* Split out of {@link LoginTerms} so a sign-in screen whose Terms confirmation has moved to a
|
|
16
|
+
* post-login step (`termsDeferred`, `@owlmeans/client-auth/login`) can still show this line on its
|
|
17
|
+
* own — the disclosure does not move just because the confirmation did.
|
|
18
|
+
*/
|
|
19
|
+
export declare const LoginPrivacyNotice: FC<LoginPrivacyNoticeProps>;
|
|
10
20
|
/**
|
|
11
21
|
* The sign-in screen's terms confirmation: one consented checkbox for `documents` (terms, plus
|
|
12
22
|
* billing/product/custom when configured), and a SEPARATE, non-consented disclosure line for
|
|
@@ -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;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;
|
|
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;AAED,MAAM,MAAM,uBAAuB,GAAG,eAAe,CAAA;AASrD;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,EAAE,EAAE,CAAC,uBAAuB,CAS1D,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CA8C1C,CAAA"}
|
|
@@ -1,41 +1,21 @@
|
|
|
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
|
+
import { termsLabelResolver, termsSentence } from '@owlmeans/client-auth/login';
|
|
4
|
+
const renderParts = (parts) => parts.map((part, index) => part.href != null
|
|
5
|
+
? _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)
|
|
6
|
+
: _jsx("span", { children: part.text }, index));
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
8
|
+
* The privacy disclosure alone — `notices` (privacy, plus cookies per its own rule), never
|
|
9
|
+
* consented to and never nested inside a checkbox's `<label>`.
|
|
6
10
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
11
|
+
* Split out of {@link LoginTerms} so a sign-in screen whose Terms confirmation has moved to a
|
|
12
|
+
* post-login step (`termsDeferred`, `@owlmeans/client-auth/login`) can still show this line on its
|
|
13
|
+
* own — the disclosure does not move just because the confirmation did.
|
|
9
14
|
*/
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
privacy: '
|
|
13
|
-
cookies: 'Cookie Policy',
|
|
14
|
-
billing: 'Billing Terms',
|
|
15
|
-
product: '{{product}} Product Terms',
|
|
15
|
+
export const LoginPrivacyNotice = ({ model, translate, locale, className }) => {
|
|
16
|
+
const resolveLabel = termsLabelResolver(translate, locale);
|
|
17
|
+
return _jsx("p", { "data-login-privacy": true, className: cn('text-xs text-muted-foreground', className), children: renderParts(termsSentence(translate('login.terms.notice', 'How we handle your personal data: {{notices}}.'), model, locale, resolveLabel)) });
|
|
16
18
|
};
|
|
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
19
|
/**
|
|
40
20
|
* The sign-in screen's terms confirmation: one consented checkbox for `documents` (terms, plus
|
|
41
21
|
* billing/product/custom when configured), and a SEPARATE, non-consented disclosure line for
|
|
@@ -43,10 +23,10 @@ const renderParts = (parts) => parts.map((part, index) => part.href != null
|
|
|
43
23
|
* because nothing is being agreed to there.
|
|
44
24
|
*/
|
|
45
25
|
export const LoginTerms = ({ model, translate, locale, className }) => {
|
|
46
|
-
const resolveLabel =
|
|
26
|
+
const resolveLabel = termsLabelResolver(translate, locale);
|
|
47
27
|
// Centred, like every other row in the card. The checkbox stays at the start of the sentence
|
|
48
28
|
// rather than above it, so `justify-center` centres the pair and `text-center` centres the
|
|
49
29
|
// wrapped lines within it.
|
|
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(
|
|
30
|
+
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(LoginPrivacyNotice, { model: model, translate: translate, locale: locale }), 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)) })] });
|
|
51
31
|
};
|
|
52
32
|
//# 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;AAEzC,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"terms.js","sourceRoot":"","sources":["../../../src/components/login/terms.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,sBAAsB,CAAA;AAEzC,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAa/E,MAAM,WAAW,GAAG,CAAC,KAA0B,EAAa,EAAE,CAC5D,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI;IAC1C,CAAC,CAAC,YAAe,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAC,QAAQ,EAAC,GAAG,EAAC,qBAAqB,yBACpD,IAAI,CAAC,WAAW,EACrC,SAAS,EAAC,oDAAoD,YAAE,IAAI,CAAC,IAAI,IAFjE,KAAK,CAEiE;IAChF,CAAC,CAAC,yBAAmB,IAAI,CAAC,IAAI,IAAjB,KAAK,CAAoB,CAAC,CAAA;AAE3C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAgC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE;IACzG,MAAM,YAAY,GAAG,kBAAkB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IAE1D,OAAO,wCAAsB,SAAS,EAAE,EAAE,CAAC,+BAA+B,EAAE,SAAS,CAAC,YACnF,WAAW,CAAC,aAAa,CACxB,SAAS,CAAC,oBAAoB,EAAE,gDAAgD,CAAC,EACjF,KAAK,EAAE,MAAM,EAAE,YAAY,CAC5B,CAAC,GACA,CAAA;AACN,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAwB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE;IACzF,MAAM,YAAY,GAAG,kBAAkB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IAE1D,6FAA6F;IAC7F,2FAA2F;IAC3F,2BAA2B;IAC3B,OAAO,eAAK,SAAS,EAAE,EAAE,CAAC,gDAAgD,EAAE,SAAS,CAAC,aAKpF,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,CAAC,aAAa,CACxB,SAAS,CAAC,oBAAoB,EAAE,6CAA6C,CAAC,EAC9E,KAAK,EAAE,MAAM,EAAE,YAAY,CAC5B,CAAC,GACG,IACD,EAEP,KAAK,CAAC,SAAS,IAAI,IAAI,IAAI,wCAAsB,SAAS,EAAC,+BAA+B,YACxF,SAAS,CAAC,qBAAqB,EAAE,wBAAwB,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GACjG,EAEJ,KAAC,kBAAkB,IAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAI,EAEzE,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,YAAG,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,0BAA0B,YACxF,WAAW,CAAC,aAAa,CACxB,SAAS,CAAC,sBAAsB,EAAE,+CAA+C,CAAC,EAClF,KAAK,EAAE,MAAM,EAAE,YAAY,CAC5B,CAAC,GACA,IACA,CAAA;AACR,CAAC,CAAA"}
|
package/build/entrypoints.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
export declare const entrypoints: (import("@owlmeans/client-entrypoint").ClientProtocolEntrypoint<
|
|
1
|
+
export declare const entrypoints: (import("@owlmeans/client-entrypoint").ClientProtocolEntrypoint<import("@owlmeans/entrypoint").EntrypointProtocol<import("@owlmeans/entrypoint").OpenRequest, import("@owlmeans/entrypoint").OpenValue>> | import("@owlmeans/client-entrypoint").ClientProtocolEntrypoint<import("@owlmeans/entrypoint").EntrypointProtocol<{
|
|
2
2
|
query: import("@owlmeans/auth").AuthToken;
|
|
3
3
|
}, undefined>> | import("@owlmeans/client-entrypoint").ClientProtocolEntrypoint<import("@owlmeans/entrypoint").EntrypointProtocol<{
|
|
4
4
|
query: import("@owlmeans/auth-common").SurrogateQuery;
|
|
5
|
-
}, undefined>>)
|
|
5
|
+
}, undefined>> | import("@owlmeans/client-entrypoint").ClientProtocolEntrypoint<import("@owlmeans/entrypoint").EntrypointProtocol<{
|
|
6
|
+
body: import("@owlmeans/auth").AuthToken;
|
|
7
|
+
}, import("@owlmeans/auth").AuthToken>>)[];
|
|
6
8
|
//# sourceMappingURL=entrypoints.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entrypoints.d.ts","sourceRoot":"","sources":["../src/entrypoints.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,WAAW
|
|
1
|
+
{"version":3,"file":"entrypoints.d.ts","sourceRoot":"","sources":["../src/entrypoints.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,WAAW;;;;;;0CAA4C,CAAA"}
|
package/build/exports.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { handler, useNavigate, useValue, useEntrypoint } from '@owlmeans/client';
|
|
1
|
+
export { handler, lazyHandler, lazyComponent, useNavigate, useValue, useEntrypoint } from '@owlmeans/client';
|
|
2
|
+
export type { LazyComponent, LazyComponentOptions, LazyHandler } from '@owlmeans/client';
|
|
2
3
|
export { config } from '@owlmeans/client-context';
|
|
3
4
|
export { service } from '@owlmeans/config';
|
|
4
5
|
export { addWebService } from '@owlmeans/client-config';
|
package/build/exports.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exports.d.ts","sourceRoot":"","sources":["../src/exports.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"exports.d.ts","sourceRoot":"","sources":["../src/exports.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAC5G,YAAY,EAAE,aAAa,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACxF,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAA;AAC7F,YAAY,EAAE,gBAAgB,IAAI,MAAM,EAAE,MAAM,6BAA6B,CAAA;AAC7E,OAAO,EAAE,KAAK,IAAI,MAAM,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,aAAa,IAAI,WAAW,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAA;AAEhG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAC/D,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnG,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACvE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACrD,OAAO,EAAE,WAAW,IAAI,UAAU,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACtF,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA"}
|
package/build/exports.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { handler, useNavigate, useValue, useEntrypoint } from '@owlmeans/client';
|
|
1
|
+
export { handler, lazyHandler, lazyComponent, useNavigate, useValue, useEntrypoint } from '@owlmeans/client';
|
|
2
2
|
export { config } from '@owlmeans/client-context';
|
|
3
3
|
export { service } from '@owlmeans/config';
|
|
4
4
|
export { addWebService } from '@owlmeans/client-config';
|
package/build/exports.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exports.js","sourceRoot":"","sources":["../src/exports.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"exports.js","sourceRoot":"","sources":["../src/exports.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAE5G,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAA;AAE7F,OAAO,EAAE,KAAK,IAAI,MAAM,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,aAAa,IAAI,WAAW,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAA;AAEhG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAEpE,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAG/D,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnG,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACvE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACrD,OAAO,EAAE,WAAW,IAAI,UAAU,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACtF,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA"}
|
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.61",
|
|
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.
|
|
54
|
+
"@owlmeans/api-config-client": "^0.1.18-rc.36",
|
|
55
|
+
"@owlmeans/auth": "^0.1.18-rc.36",
|
|
56
|
+
"@owlmeans/auth-common": "^0.1.18-rc.38",
|
|
57
|
+
"@owlmeans/basic-envelope": "^0.1.18-rc.37",
|
|
58
|
+
"@owlmeans/client": "^0.1.18-rc.41",
|
|
59
|
+
"@owlmeans/client-auth": "^0.1.18-rc.48",
|
|
60
|
+
"@owlmeans/client-config": "^0.1.18-rc.38",
|
|
61
|
+
"@owlmeans/client-context": "^0.1.18-rc.39",
|
|
62
|
+
"@owlmeans/client-entrypoint": "^0.1.18-rc.38",
|
|
63
|
+
"@owlmeans/client-flow": "^0.1.18-rc.44",
|
|
64
|
+
"@owlmeans/client-i18n": "^0.1.18-rc.40",
|
|
65
|
+
"@owlmeans/client-panel": "^0.1.18-rc.48",
|
|
66
|
+
"@owlmeans/client-route": "^0.1.18-rc.39",
|
|
67
|
+
"@owlmeans/client-socket": "^0.1.18-rc.39",
|
|
68
|
+
"@owlmeans/config": "^0.1.18-rc.38",
|
|
69
69
|
"@owlmeans/context": "^0.1.18-rc.32",
|
|
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.
|
|
70
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.37",
|
|
71
|
+
"@owlmeans/error": "^0.1.18-rc.36",
|
|
72
|
+
"@owlmeans/flow": "^0.1.18-rc.39",
|
|
73
|
+
"@owlmeans/i18n": "^0.1.18-rc.37",
|
|
74
74
|
"@owlmeans/route": "^0.1.18-rc.33",
|
|
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.
|
|
75
|
+
"@owlmeans/state": "^0.1.18-rc.35",
|
|
76
|
+
"@owlmeans/web-client": "^0.1.18-rc.51",
|
|
77
|
+
"@owlmeans/web-consent": "^0.1.18-rc.32",
|
|
78
|
+
"@owlmeans/web-db": "^0.1.18-rc.39",
|
|
79
|
+
"@owlmeans/web-flow": "^0.1.18-rc.43",
|
|
80
|
+
"@owlmeans/web-router": "^0.1.18-rc.39",
|
|
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.39",
|
|
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",
|
package/src/auth/exports.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
|
-
export { handler, useEntrypoint, useValue, useNavigate } from '@owlmeans/client'
|
|
3
|
-
export type { Navigator } from '@owlmeans/client'
|
|
2
|
+
export { handler, lazyHandler, lazyComponent, useEntrypoint, useValue, useNavigate } from '@owlmeans/client'
|
|
3
|
+
export type { Navigator, LazyComponent, LazyComponentOptions, LazyHandler } from '@owlmeans/client'
|
|
4
4
|
export { config } from '@owlmeans/client-context'
|
|
5
5
|
export { service } from '@owlmeans/config'
|
|
6
6
|
export { EntrypointOutcome } from '@owlmeans/entrypoint'
|
|
@@ -9,7 +9,7 @@ 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'
|
|
11
11
|
import { LoginMethodIcon } from './icons.js'
|
|
12
|
-
import { LoginTerms } from './terms.js'
|
|
12
|
+
import { LoginPrivacyNotice, LoginTerms } from './terms.js'
|
|
13
13
|
import { LoginCredit } from './credit.js'
|
|
14
14
|
|
|
15
15
|
const VARIANT: Record<string, 'default' | 'outline' | 'link'> = {
|
|
@@ -84,7 +84,7 @@ export const LoginScreen: FC<LoginScreenProps> = props => {
|
|
|
84
84
|
// who has not confirmed the terms would press it and be told nothing at all — the
|
|
85
85
|
// screen would simply seem broken. Blocking happens in the handler, which then says
|
|
86
86
|
// why.
|
|
87
|
-
aria-disabled={model.blocked}
|
|
87
|
+
aria-disabled={model.blocked || undefined}
|
|
88
88
|
data-blocked={model.blocked ? 'true' : undefined}
|
|
89
89
|
// `cursor-pointer` explicitly: the package's private shadcn button stays compatible
|
|
90
90
|
// with the older primitive style too. Stating it here keeps the sign-in control
|
|
@@ -103,7 +103,11 @@ export const LoginScreen: FC<LoginScreenProps> = props => {
|
|
|
103
103
|
</Button>)}
|
|
104
104
|
</div>}
|
|
105
105
|
|
|
106
|
-
{
|
|
106
|
+
{/* Terms mode: the checkbox lives on a post-login step instead (`termsDeferred`) — this
|
|
107
|
+
screen shows the privacy disclosure alone and blocks nothing on it. */}
|
|
108
|
+
{model.terms.required && (model.terms.deferred
|
|
109
|
+
? <LoginPrivacyNotice model={model.terms} translate={t} locale={props.locale} />
|
|
110
|
+
: <LoginTerms model={model.terms} translate={t} locale={props.locale} />)}
|
|
107
111
|
|
|
108
112
|
{/*
|
|
109
113
|
A thrown message first, because it names the actual fault; otherwise whatever the
|
|
@@ -1,8 +1,8 @@
|
|
|
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 {
|
|
4
|
+
import { termsLabelResolver, termsSentence } from '@owlmeans/client-auth/login'
|
|
5
|
+
import type { TermsSentencePart } from '@owlmeans/client-auth/login'
|
|
6
6
|
|
|
7
7
|
export interface LoginTermsProps {
|
|
8
8
|
model: LoginTermsModel
|
|
@@ -12,43 +12,7 @@ export interface LoginTermsProps {
|
|
|
12
12
|
className?: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
* English fallbacks for a document's own translated label, keyed by `ResolvedTermsDocument.key`.
|
|
17
|
-
*
|
|
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.
|
|
20
|
-
*/
|
|
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
|
-
}
|
|
28
|
-
|
|
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)
|
|
43
|
-
|
|
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
|
-
}
|
|
15
|
+
export type LoginPrivacyNoticeProps = LoginTermsProps
|
|
52
16
|
|
|
53
17
|
const renderParts = (parts: TermsSentencePart[]): ReactNode =>
|
|
54
18
|
parts.map((part, index) => part.href != null
|
|
@@ -57,6 +21,25 @@ const renderParts = (parts: TermsSentencePart[]): ReactNode =>
|
|
|
57
21
|
className="underline underline-offset-2 hover:text-foreground">{part.text}</a>
|
|
58
22
|
: <span key={index}>{part.text}</span>)
|
|
59
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The privacy disclosure alone — `notices` (privacy, plus cookies per its own rule), never
|
|
26
|
+
* consented to and never nested inside a checkbox's `<label>`.
|
|
27
|
+
*
|
|
28
|
+
* Split out of {@link LoginTerms} so a sign-in screen whose Terms confirmation has moved to a
|
|
29
|
+
* post-login step (`termsDeferred`, `@owlmeans/client-auth/login`) can still show this line on its
|
|
30
|
+
* own — the disclosure does not move just because the confirmation did.
|
|
31
|
+
*/
|
|
32
|
+
export const LoginPrivacyNotice: FC<LoginPrivacyNoticeProps> = ({ model, translate, locale, className }) => {
|
|
33
|
+
const resolveLabel = termsLabelResolver(translate, locale)
|
|
34
|
+
|
|
35
|
+
return <p data-login-privacy className={cn('text-xs text-muted-foreground', className)}>
|
|
36
|
+
{renderParts(termsSentence(
|
|
37
|
+
translate('login.terms.notice', 'How we handle your personal data: {{notices}}.'),
|
|
38
|
+
model, locale, resolveLabel
|
|
39
|
+
))}
|
|
40
|
+
</p>
|
|
41
|
+
}
|
|
42
|
+
|
|
60
43
|
/**
|
|
61
44
|
* The sign-in screen's terms confirmation: one consented checkbox for `documents` (terms, plus
|
|
62
45
|
* billing/product/custom when configured), and a SEPARATE, non-consented disclosure line for
|
|
@@ -64,7 +47,7 @@ const renderParts = (parts: TermsSentencePart[]): ReactNode =>
|
|
|
64
47
|
* because nothing is being agreed to there.
|
|
65
48
|
*/
|
|
66
49
|
export const LoginTerms: FC<LoginTermsProps> = ({ model, translate, locale, className }) => {
|
|
67
|
-
const resolveLabel =
|
|
50
|
+
const resolveLabel = termsLabelResolver(translate, locale)
|
|
68
51
|
|
|
69
52
|
// Centred, like every other row in the card. The checkbox stays at the start of the sentence
|
|
70
53
|
// rather than above it, so `justify-center` centres the pair and `text-center` centres the
|
|
@@ -100,16 +83,7 @@ export const LoginTerms: FC<LoginTermsProps> = ({ model, translate, locale, clas
|
|
|
100
83
|
{translate('login.terms.revised', 'Last updated: {{date}}').split('{{date}}').join(model.revisedAt)}
|
|
101
84
|
</p>}
|
|
102
85
|
|
|
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>
|
|
86
|
+
<LoginPrivacyNotice model={model} translate={translate} locale={locale} />
|
|
113
87
|
|
|
114
88
|
{model.attempted && !model.accepted && <p role="alert" className="text-sm text-destructive">
|
|
115
89
|
{renderParts(termsSentence(
|
package/src/exports.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
|
|
2
|
-
export { handler, useNavigate, useValue, useEntrypoint } from '@owlmeans/client'
|
|
2
|
+
export { handler, lazyHandler, lazyComponent, useNavigate, useValue, useEntrypoint } from '@owlmeans/client'
|
|
3
|
+
export type { LazyComponent, LazyComponentOptions, LazyHandler } from '@owlmeans/client'
|
|
3
4
|
export { config } from '@owlmeans/client-context'
|
|
4
5
|
export { service } from '@owlmeans/config'
|
|
5
6
|
export { addWebService } from '@owlmeans/client-config'
|
package/tests/consent.spec.ts
CHANGED
|
@@ -44,4 +44,19 @@ describe('@owlmeans/web-panel/consent — the dialog and the menu widget', () =>
|
|
|
44
44
|
await close()
|
|
45
45
|
}
|
|
46
46
|
}, TIMEOUT)
|
|
47
|
+
|
|
48
|
+
test('linker passes through to the underlying dialog unchanged', async () => {
|
|
49
|
+
// `?domains=1` hands `PanelCookieConsent` a `linker` — nothing here re-derives it, so seeing
|
|
50
|
+
// the domain line pins that `{...props}` still carries it all the way to `CookieConsent`.
|
|
51
|
+
const { page, close } = await open('/prefs?consent=bare&footer=node&domains=1')
|
|
52
|
+
try {
|
|
53
|
+
await page.waitForSelector('#prefs')
|
|
54
|
+
await page.locator('[data-consent-dialog]').waitFor()
|
|
55
|
+
const domains = page.locator('[data-consent-domains]')
|
|
56
|
+
await domains.waitFor()
|
|
57
|
+
expect(await domains.textContent()).toContain('harness-partner.test')
|
|
58
|
+
} finally {
|
|
59
|
+
await close()
|
|
60
|
+
}
|
|
61
|
+
}, TIMEOUT)
|
|
47
62
|
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FC } from 'react'
|
|
2
|
+
import { useEntrypoint } from '@owlmeans/client'
|
|
3
|
+
|
|
4
|
+
/** The screen `mount.tsx` loads lazily — a separate module, so it is a separate request. */
|
|
5
|
+
export const LazyScreen: FC = () => {
|
|
6
|
+
const { alias } = useEntrypoint()
|
|
7
|
+
|
|
8
|
+
return <div id="lazy-screen" data-alias={alias}>lazy-screen</div>
|
|
9
|
+
}
|
package/tests/harness/mount.tsx
CHANGED
|
@@ -13,7 +13,8 @@ import { handler, useNavigate } from '@owlmeans/client'
|
|
|
13
13
|
import { toast } from 'sonner'
|
|
14
14
|
import type { PanelNavConfig, PanelNavLink } from '../../src/index.js'
|
|
15
15
|
import {
|
|
16
|
-
makeContext, useContext, entrypoints as baseEntrypoints, NavLayout, PanelApp, Toaster
|
|
16
|
+
makeContext, useContext, entrypoints as baseEntrypoints, NavLayout, PanelApp, Toaster,
|
|
17
|
+
lazyHandler, lazyComponent,
|
|
17
18
|
} from '../../src/index.js'
|
|
18
19
|
import { LoginScreen } from '../../src/components/login/index.js'
|
|
19
20
|
import {
|
|
@@ -36,6 +37,8 @@ const alias = {
|
|
|
36
37
|
prefs: `${SERVICE}:web:prefs`,
|
|
37
38
|
login: `${SERVICE}:web:login`,
|
|
38
39
|
socket: `${SERVICE}:web:socket`,
|
|
40
|
+
lazy: `${SERVICE}:web:lazy`,
|
|
41
|
+
lazyEntry: `${SERVICE}:web:lazy-entry`,
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/** A harness id for the ONE tracked connection this screen simulates — a real `ws()`/`useWs()`
|
|
@@ -115,6 +118,15 @@ const themeToggle = new URLSearchParams(window.location.search).get('themeToggle
|
|
|
115
118
|
// confirmation — the shape that pins the "still exactly one checkbox" and "documents render
|
|
116
119
|
// outside the notice" rules even when there is more than terms+privacy to show.
|
|
117
120
|
const extendedTerms = new URLSearchParams(window.location.search).get('terms') === 'extended'
|
|
121
|
+
// `?defer=bound` registers a step that CONFIRMS the terms, bound to a real screen — the sign-in
|
|
122
|
+
// screen must then show no checkbox, only the privacy notice, and block nothing.
|
|
123
|
+
// `?defer=unbound` registers the same step but never binds its entrypoint — the checkbox must
|
|
124
|
+
// stay (fail-closed): a step nobody can reach must never silently remove the confirmation.
|
|
125
|
+
const defer = new URLSearchParams(window.location.search).get('defer')
|
|
126
|
+
// `?domains=1` passes `linker` through to `PanelCookieConsent` — proving the prop reaches
|
|
127
|
+
// `@owlmeans/web-consent`'s `CookieConsent` with nothing lost in between (the props already just
|
|
128
|
+
// spread through; this is what pins that they keep doing so).
|
|
129
|
+
const withDomains = new URLSearchParams(window.location.search).get('domains') === '1'
|
|
118
130
|
|
|
119
131
|
/**
|
|
120
132
|
* The footer's "Cookie settings" control — the menu widget, rendered from an always-mounted
|
|
@@ -222,6 +234,37 @@ const ReportsIndex: FC = () => {
|
|
|
222
234
|
</div>
|
|
223
235
|
}
|
|
224
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Lazily-loaded screens, declared at MODULE SCOPE as the helpers require — the route renderer
|
|
239
|
+
* remounts a screen on every navigation, so a lazy object made during render would re-suspend
|
|
240
|
+
* each time. Both go through the package root, which is what an application imports.
|
|
241
|
+
*
|
|
242
|
+
* `flakyScreen` fails its first load on purpose: a rejected load must be retried, never cached.
|
|
243
|
+
* The globals let a spec drive `preload()` without a screen of its own.
|
|
244
|
+
*/
|
|
245
|
+
let flakyLoads = 0
|
|
246
|
+
const lazyScreen = lazyHandler(
|
|
247
|
+
() => import('./lazy-screen.js'), 'LazyScreen', { fallback: <div id="lazy-fallback">loading</div> }
|
|
248
|
+
)
|
|
249
|
+
const flakyScreen = lazyComponent(async () => {
|
|
250
|
+
if (++flakyLoads === 1) throw new Error('first-load-fails')
|
|
251
|
+
return import('./lazy-screen.js')
|
|
252
|
+
}, 'LazyScreen')
|
|
253
|
+
;(globalThis as unknown as { __lazyPreload: () => Promise<boolean> }).__lazyPreload =
|
|
254
|
+
async () => typeof await lazyScreen.preload() === 'function'
|
|
255
|
+
;(globalThis as unknown as { __flakyPreload: () => Promise<string> }).__flakyPreload =
|
|
256
|
+
() => flakyScreen.preload().then(() => 'loaded', (e: Error) => e.message)
|
|
257
|
+
|
|
258
|
+
/** The lazy screen's way in: an in-app navigation, so the layout around it is already mounted. */
|
|
259
|
+
const LazyEntry: FC = () => {
|
|
260
|
+
const nav = useNavigate()
|
|
261
|
+
|
|
262
|
+
return <div id="lazy-entry">
|
|
263
|
+
lazy-entry-screen
|
|
264
|
+
<button id="to-lazy" onClick={nav.press(alias.lazy)}>lazy</button>
|
|
265
|
+
</div>
|
|
266
|
+
}
|
|
267
|
+
|
|
225
268
|
// The panel context registers the api-config middleware, which resolves the advertise
|
|
226
269
|
// entrypoint during init — that entrypoint needs a declared backend service route, so both
|
|
227
270
|
// sides are declared here exactly as a real app declares them.
|
|
@@ -280,6 +323,19 @@ ensureLoginService(context as never).registerMethodSource({
|
|
|
280
323
|
],
|
|
281
324
|
})
|
|
282
325
|
|
|
326
|
+
if (defer != null) {
|
|
327
|
+
ensureLoginService(context as never).registerStep({
|
|
328
|
+
alias: 'harness-terms-step',
|
|
329
|
+
// "bound" points at a real screen of this harness (`/prefs`, already registered below) —
|
|
330
|
+
// `termsDeferred` only reads `ctx.hasEntrypoint`, so any bound alias proves the point.
|
|
331
|
+
// "unbound" points nowhere: `hasEntrypoint` answers false and the step stays unreachable.
|
|
332
|
+
entrypoint: defer === 'bound' ? alias.prefs : 'harness-terms-step-nowhere',
|
|
333
|
+
confirmsTerms: true,
|
|
334
|
+
required: true,
|
|
335
|
+
pending: async () => false,
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
283
339
|
const protocols = {
|
|
284
340
|
base: openProtocol(route(BASE, '/', frontend())),
|
|
285
341
|
home: openProtocol(route(HOME, '/', frontend({ default: true, parent: BASE }))),
|
|
@@ -290,6 +346,8 @@ const protocols = {
|
|
|
290
346
|
prefs: openProtocol(route(alias.prefs, '/prefs', frontend({ parent: BASE }))),
|
|
291
347
|
login: openProtocol(route(alias.login, '/login', frontend({ parent: BASE }))),
|
|
292
348
|
socket: openProtocol(route(alias.socket, '/socket', frontend({ parent: BASE }))),
|
|
349
|
+
lazy: openProtocol(route(alias.lazy, '/lazy', frontend({ parent: BASE }))),
|
|
350
|
+
lazyEntry: openProtocol(route(alias.lazyEntry, '/lazy-entry', frontend({ parent: BASE }))),
|
|
293
351
|
}
|
|
294
352
|
|
|
295
353
|
const entrypoints = [
|
|
@@ -308,6 +366,8 @@ const entrypoints = [
|
|
|
308
366
|
bindScreen(protocols.prefs, handler(PrefsScreen)),
|
|
309
367
|
bindScreen(protocols.login, handler(LoginHarness)),
|
|
310
368
|
bindScreen(protocols.socket, handler(SocketStatusScreen)),
|
|
369
|
+
bindScreen(protocols.lazy, lazyScreen),
|
|
370
|
+
bindScreen(protocols.lazyEntry, handler(LazyEntry)),
|
|
311
371
|
]
|
|
312
372
|
|
|
313
373
|
context.registerEntrypoints(entrypoints)
|
|
@@ -315,5 +375,8 @@ context.registerEntrypoints(entrypoints)
|
|
|
315
375
|
// The consent dialog is a sibling of the Router, as in an application — and only on the consent
|
|
316
376
|
// branches, since its first-visit overlay covers the page every other test clicks through.
|
|
317
377
|
createRoot(document.getElementById('root')!).render(<PanelApp context={context as never}>
|
|
318
|
-
{consentMode != null && <PanelCookieConsent
|
|
378
|
+
{consentMode != null && <PanelCookieConsent
|
|
379
|
+
policyHref="/cookies"
|
|
380
|
+
{...(withDomains ? { linker: { domains: ['harness-partner.test'] } } : {})}
|
|
381
|
+
/>}
|
|
319
382
|
</PanelApp>)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { closeBrowser, mountComponent } from '@owlmeans/test-ui'
|
|
3
|
+
import { HARNESS_URL } from './context.js'
|
|
4
|
+
|
|
5
|
+
// Browser work does not fit the 5s default: a cold harness compiles the app on first request.
|
|
6
|
+
const TIMEOUT = 30_000
|
|
7
|
+
|
|
8
|
+
// The lazy screen's alias as the harness declares it (`${SERVICE}:web:lazy`).
|
|
9
|
+
const LAZY_ALIAS = 'web-panel-test:web:lazy'
|
|
10
|
+
// The request the lazy screen's chunk arrives by — the harness's own module, served by Vite.
|
|
11
|
+
const LAZY_MODULE = '**/lazy-screen.tsx*'
|
|
12
|
+
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
await closeBrowser()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const open = async (path: string) => mountComponent({ url: `${HARNESS_URL.replace(/\/$/, '')}${path}` })
|
|
18
|
+
|
|
19
|
+
describe('@owlmeans/web-panel — lazily-loaded screens', () => {
|
|
20
|
+
test('the fallback renders inside the layout, and the layout survives the load', async () => {
|
|
21
|
+
const { page, close } = await open('/lazy-entry')
|
|
22
|
+
try {
|
|
23
|
+
await page.waitForSelector('#lazy-entry')
|
|
24
|
+
|
|
25
|
+
// Hold the chunk's request open, so the fallback is observable for as long as the test needs.
|
|
26
|
+
let release!: () => void
|
|
27
|
+
const held = new Promise<void>(resolve => { release = resolve })
|
|
28
|
+
await page.route(LAZY_MODULE, async route => {
|
|
29
|
+
await held
|
|
30
|
+
await route.continue()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
await page.locator('#to-lazy').click()
|
|
34
|
+
await page.waitForSelector('#lazy-fallback')
|
|
35
|
+
// The boundary is the lazy screen's own: the shell stays up and the fallback sits in it.
|
|
36
|
+
expect(await page.locator('header nav').first().isVisible()).toBe(true)
|
|
37
|
+
expect(await page.locator('main #lazy-fallback').count()).toBe(1)
|
|
38
|
+
|
|
39
|
+
// Keep the layout's header node itself — a remount would replace it with a fresh one.
|
|
40
|
+
await page.evaluate(() => {
|
|
41
|
+
const header = document.querySelector('header')!
|
|
42
|
+
header.setAttribute('data-lazy-mark', '1')
|
|
43
|
+
;(window as unknown as { __header: Element }).__header = header
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
release()
|
|
47
|
+
await page.waitForSelector('#lazy-screen')
|
|
48
|
+
|
|
49
|
+
expect(await page.locator('#lazy-fallback').count()).toBe(0)
|
|
50
|
+
expect(await page.locator('header[data-lazy-mark]').count()).toBe(1)
|
|
51
|
+
expect(await page.evaluate(() => {
|
|
52
|
+
const kept = (window as unknown as { __header: Element }).__header
|
|
53
|
+
return kept.isConnected && document.querySelector('header') === kept
|
|
54
|
+
})).toBe(true)
|
|
55
|
+
expect(await page.locator('#lazy-screen').getAttribute('data-alias')).toBe(LAZY_ALIAS)
|
|
56
|
+
} finally {
|
|
57
|
+
await close()
|
|
58
|
+
}
|
|
59
|
+
}, TIMEOUT)
|
|
60
|
+
|
|
61
|
+
test('a preloaded screen renders without its fallback', async () => {
|
|
62
|
+
const { page, close } = await open('/lazy-entry')
|
|
63
|
+
try {
|
|
64
|
+
await page.waitForSelector('#lazy-entry')
|
|
65
|
+
expect(await page.evaluate(() => (window as unknown as { __lazyPreload: () => Promise<boolean> }).__lazyPreload())).toBe(true)
|
|
66
|
+
|
|
67
|
+
// Records every node ever added, so a fallback that is mounted and replaced in the same
|
|
68
|
+
// task still counts — reading the DOM afterwards would miss it.
|
|
69
|
+
await page.evaluate(() => {
|
|
70
|
+
const flags = window as unknown as { __fallbackSeen: boolean }
|
|
71
|
+
flags.__fallbackSeen = false
|
|
72
|
+
new MutationObserver(records => {
|
|
73
|
+
for (const record of records) {
|
|
74
|
+
for (const node of Array.from(record.addedNodes)) {
|
|
75
|
+
if (node instanceof Element && (node.id === 'lazy-fallback' || node.querySelector('#lazy-fallback') != null)) {
|
|
76
|
+
flags.__fallbackSeen = true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}).observe(document.body, { childList: true, subtree: true })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
await page.locator('#to-lazy').click()
|
|
84
|
+
await page.waitForSelector('#lazy-screen')
|
|
85
|
+
expect(await page.evaluate(() => (window as unknown as { __fallbackSeen: boolean }).__fallbackSeen)).toBe(false)
|
|
86
|
+
expect(await page.locator('#lazy-screen').getAttribute('data-alias')).toBe(LAZY_ALIAS)
|
|
87
|
+
} finally {
|
|
88
|
+
await close()
|
|
89
|
+
}
|
|
90
|
+
}, TIMEOUT)
|
|
91
|
+
|
|
92
|
+
test('a failed load is retried, not cached', async () => {
|
|
93
|
+
const { page, close } = await open('/lazy-entry')
|
|
94
|
+
try {
|
|
95
|
+
await page.waitForSelector('#lazy-entry')
|
|
96
|
+
const preload = () => page.evaluate(() => (window as unknown as { __flakyPreload: () => Promise<string> }).__flakyPreload())
|
|
97
|
+
|
|
98
|
+
expect(await preload()).toBe('first-load-fails')
|
|
99
|
+
expect(await preload()).toBe('loaded')
|
|
100
|
+
} finally {
|
|
101
|
+
await close()
|
|
102
|
+
}
|
|
103
|
+
}, TIMEOUT)
|
|
104
|
+
})
|
package/tests/login.spec.ts
CHANGED
|
@@ -168,8 +168,11 @@ describe('@owlmeans/web-panel — the sign-in screen', () => {
|
|
|
168
168
|
await page.waitForSelector('[data-login-method]')
|
|
169
169
|
|
|
170
170
|
expect(await page.locator('[data-login-terms]').isChecked()).toBe(true)
|
|
171
|
+
// Omitted rather than `"false"`: `aria-disabled={blocked || undefined}` drops the attribute
|
|
172
|
+
// entirely once unblocked, which is what lets a deferred screen (no `blocked` state at all)
|
|
173
|
+
// render the same way with no special case.
|
|
171
174
|
expect(await page.locator('[data-login-method="primary"]').getAttribute('aria-disabled'))
|
|
172
|
-
.
|
|
175
|
+
.toBeNull()
|
|
173
176
|
} finally {
|
|
174
177
|
await close()
|
|
175
178
|
}
|
|
@@ -259,4 +262,49 @@ describe('@owlmeans/web-panel — the sign-in screen', () => {
|
|
|
259
262
|
}
|
|
260
263
|
}, TIMEOUT)
|
|
261
264
|
})
|
|
265
|
+
|
|
266
|
+
describe('the Terms confirmation deferred to a step', () => {
|
|
267
|
+
test('bound: no checkbox, the privacy notice still renders, and methods are never blocked', async () => {
|
|
268
|
+
const { page, close } = await open('?defer=bound')
|
|
269
|
+
try {
|
|
270
|
+
expect(await page.locator('[data-login-terms]').count()).toBe(0)
|
|
271
|
+
expect(await page.locator('[data-login-privacy]').count()).toBe(1)
|
|
272
|
+
expect(await page.locator('[role="alert"]').count()).toBe(0)
|
|
273
|
+
|
|
274
|
+
const disabled = await page.locator('[data-login-method="primary"]').getAttribute('aria-disabled')
|
|
275
|
+
expect(disabled).not.toBe('true')
|
|
276
|
+
|
|
277
|
+
await page.locator('[data-login-method="primary"]').click()
|
|
278
|
+
expect(await page.evaluate(() => (window as never as { __loginStarted: string[] }).__loginStarted))
|
|
279
|
+
.toEqual(['primary'])
|
|
280
|
+
} finally {
|
|
281
|
+
await close()
|
|
282
|
+
}
|
|
283
|
+
}, TIMEOUT)
|
|
284
|
+
|
|
285
|
+
test('bound + extended documents: still no checkbox, still no blocking', async () => {
|
|
286
|
+
const { page, close } = await open('?defer=bound&terms=extended')
|
|
287
|
+
try {
|
|
288
|
+
expect(await page.locator('[data-login-terms]').count()).toBe(0)
|
|
289
|
+
expect(await page.locator('[data-login-privacy]').count()).toBe(1)
|
|
290
|
+
|
|
291
|
+
await page.locator('[data-login-method="primary"]').click()
|
|
292
|
+
expect(await page.evaluate(() => (window as never as { __loginStarted: string[] }).__loginStarted))
|
|
293
|
+
.toEqual(['primary'])
|
|
294
|
+
} finally {
|
|
295
|
+
await close()
|
|
296
|
+
}
|
|
297
|
+
}, TIMEOUT)
|
|
298
|
+
|
|
299
|
+
test('unbound: the checkbox stays — a step nobody can reach never removes the confirmation', async () => {
|
|
300
|
+
const { page, close } = await open('?defer=unbound')
|
|
301
|
+
try {
|
|
302
|
+
expect(await page.locator('[data-login-terms]').count()).toBe(1)
|
|
303
|
+
expect(await page.locator('[data-login-method="primary"]').getAttribute('aria-disabled'))
|
|
304
|
+
.toBe('true')
|
|
305
|
+
} finally {
|
|
306
|
+
await close()
|
|
307
|
+
}
|
|
308
|
+
}, TIMEOUT)
|
|
309
|
+
})
|
|
262
310
|
})
|