@owlmeans/client-auth 0.1.18-rc.8 → 0.1.18
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 +283 -37
- package/agent-meta/manifest.json +50 -4
- package/agent-meta/skills/client-auth/SKILL.md +85 -23
- package/agent-meta/skills/client-auth/parts/login.md +77 -0
- package/agent-meta/skills/client-auth/parts/manager.md +37 -0
- package/build/components/dispatcher/component.d.ts.map +1 -1
- package/build/components/dispatcher/component.js +26 -6
- package/build/components/dispatcher/component.js.map +1 -1
- package/build/components/dispatcher/types.local.d.ts +7 -0
- package/build/components/dispatcher/types.local.d.ts.map +1 -0
- package/build/components/dispatcher/types.local.js +2 -0
- package/build/components/dispatcher/types.local.js.map +1 -0
- package/build/entrypoints.d.ts +7 -0
- package/build/entrypoints.d.ts.map +1 -0
- package/build/entrypoints.js +7 -0
- package/build/entrypoints.js.map +1 -0
- package/build/helper.d.ts +2 -1
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +28 -5
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/login/adopt/types.d.ts +26 -0
- package/build/login/adopt/types.d.ts.map +1 -0
- package/build/login/adopt/types.js +2 -0
- package/build/login/adopt/types.js.map +1 -0
- package/build/login/adopt.d.ts +7 -12
- package/build/login/adopt.d.ts.map +1 -1
- package/build/login/adopt.js +15 -14
- package/build/login/adopt.js.map +1 -1
- package/build/login/consts.d.ts +89 -4
- package/build/login/consts.d.ts.map +1 -1
- package/build/login/consts.js +93 -5
- package/build/login/consts.js.map +1 -1
- package/build/login/consts.local.d.ts +16 -0
- package/build/login/consts.local.d.ts.map +1 -0
- package/build/login/consts.local.js +30 -0
- package/build/login/consts.local.js.map +1 -0
- package/build/login/credit/types.d.ts +7 -0
- package/build/login/credit/types.d.ts.map +1 -0
- package/build/login/credit/types.js +2 -0
- package/build/login/credit/types.js.map +1 -0
- package/build/login/credit.d.ts +19 -0
- package/build/login/credit.d.ts.map +1 -0
- package/build/login/credit.js +58 -0
- package/build/login/credit.js.map +1 -0
- package/build/login/env/types.d.ts +35 -0
- package/build/login/env/types.d.ts.map +1 -0
- package/build/login/env/types.js +2 -0
- package/build/login/env/types.js.map +1 -0
- package/build/login/env.d.ts +5 -29
- package/build/login/env.d.ts.map +1 -1
- package/build/login/env.js +63 -80
- package/build/login/env.js.map +1 -1
- package/build/login/flow.d.ts +20 -0
- package/build/login/flow.d.ts.map +1 -0
- package/build/login/flow.js +39 -0
- package/build/login/flow.js.map +1 -0
- package/build/login/hook.d.ts +30 -4
- package/build/login/hook.d.ts.map +1 -1
- package/build/login/hook.js +53 -18
- package/build/login/hook.js.map +1 -1
- package/build/login/i18n/be.json +41 -0
- package/build/login/i18n/de.json +41 -0
- package/build/login/i18n/en.json +41 -0
- package/build/login/i18n/es.json +41 -0
- package/build/login/i18n/fr.json +41 -0
- package/build/login/i18n/pl.json +41 -0
- package/build/login/i18n/ru.json +41 -0
- package/build/login/i18n/uk.json +41 -0
- package/build/login/i18n.d.ts +2 -0
- package/build/login/i18n.d.ts.map +1 -0
- package/build/login/i18n.js +27 -0
- package/build/login/i18n.js.map +1 -0
- package/build/login/index.d.ts +20 -0
- package/build/login/index.d.ts.map +1 -1
- package/build/login/index.js +11 -0
- package/build/login/index.js.map +1 -1
- package/build/login/land/types.d.ts +52 -0
- package/build/login/land/types.d.ts.map +1 -0
- package/build/login/land/types.js +2 -0
- package/build/login/land/types.js.map +1 -0
- package/build/login/land.d.ts +6 -0
- package/build/login/land.d.ts.map +1 -0
- package/build/login/land.js +122 -0
- package/build/login/land.js.map +1 -0
- package/build/login/methods/types.d.ts +30 -0
- package/build/login/methods/types.d.ts.map +1 -0
- package/build/login/methods/types.js +2 -0
- package/build/login/methods/types.js.map +1 -0
- package/build/login/methods.d.ts +7 -0
- package/build/login/methods.d.ts.map +1 -0
- package/build/login/methods.js +87 -0
- package/build/login/methods.js.map +1 -0
- package/build/login/resume/types.d.ts +24 -0
- package/build/login/resume/types.d.ts.map +1 -0
- package/build/login/resume/types.js +2 -0
- package/build/login/resume/types.js.map +1 -0
- package/build/login/resume.d.ts +9 -0
- package/build/login/resume.d.ts.map +1 -0
- package/build/login/resume.js +38 -0
- package/build/login/resume.js.map +1 -0
- package/build/login/screen.d.ts +17 -0
- package/build/login/screen.d.ts.map +1 -0
- package/build/login/screen.js +66 -0
- package/build/login/screen.js.map +1 -0
- package/build/login/service.d.ts +6 -5
- package/build/login/service.d.ts.map +1 -1
- package/build/login/service.js +141 -15
- package/build/login/service.js.map +1 -1
- package/build/login/start/types.d.ts +29 -0
- package/build/login/start/types.d.ts.map +1 -0
- package/build/login/start/types.js +2 -0
- package/build/login/start/types.js.map +1 -0
- package/build/login/start.d.ts +5 -0
- package/build/login/start.d.ts.map +1 -0
- package/build/login/start.js +36 -0
- package/build/login/start.js.map +1 -0
- package/build/login/surrogate/types.d.ts +9 -0
- package/build/login/surrogate/types.d.ts.map +1 -0
- package/build/login/surrogate/types.js +2 -0
- package/build/login/surrogate/types.js.map +1 -0
- package/build/login/surrogate.d.ts +13 -0
- package/build/login/surrogate.d.ts.map +1 -0
- package/build/login/surrogate.js +33 -0
- package/build/login/surrogate.js.map +1 -0
- package/build/login/terms/types.d.ts +112 -0
- package/build/login/terms/types.d.ts.map +1 -0
- package/build/login/terms/types.js +2 -0
- package/build/login/terms/types.js.map +1 -0
- package/build/login/terms-config.d.ts +65 -0
- package/build/login/terms-config.d.ts.map +1 -0
- package/build/login/terms-config.js +2 -0
- package/build/login/terms-config.js.map +1 -0
- package/build/login/terms.d.ts +21 -0
- package/build/login/terms.d.ts.map +1 -0
- package/build/login/terms.js +235 -0
- package/build/login/terms.js.map +1 -0
- package/build/login/types.d.ts +223 -16
- package/build/login/types.d.ts.map +1 -1
- package/build/login/types.js +1 -16
- package/build/login/types.js.map +1 -1
- package/build/manager/components/authentication/control.d.ts.map +1 -1
- package/build/manager/components/authentication/control.js +3 -3
- package/build/manager/components/authentication/control.js.map +1 -1
- package/build/manager/components/authentication/types.d.ts +2 -2
- package/build/manager/components/authentication/types.d.ts.map +1 -1
- package/build/manager/components/tunnel-consumer.js +2 -2
- package/build/manager/components/tunnel-consumer.js.map +1 -1
- package/build/manager/entrypoints.d.ts +11 -0
- package/build/manager/entrypoints.d.ts.map +1 -0
- package/build/manager/entrypoints.js +17 -0
- package/build/manager/entrypoints.js.map +1 -0
- package/build/manager/errors.d.ts.map +1 -1
- package/build/manager/errors.js.map +1 -1
- package/build/manager/plugins/basic-ed25519.d.ts.map +1 -1
- package/build/manager/plugins/basic-ed25519.js +5 -0
- package/build/manager/plugins/basic-ed25519.js.map +1 -1
- package/build/manager/plugins/exports.d.ts +3 -0
- package/build/manager/plugins/exports.d.ts.map +1 -1
- package/build/manager/plugins/exports.js +2 -0
- package/build/manager/plugins/exports.js.map +1 -1
- package/build/manager/plugins/index.d.ts +2 -5
- package/build/manager/plugins/index.d.ts.map +1 -1
- package/build/manager/plugins/index.js +9 -1
- package/build/manager/plugins/index.js.map +1 -1
- package/build/manager/plugins/methods.d.ts +6 -0
- package/build/manager/plugins/methods.d.ts.map +1 -0
- package/build/manager/plugins/methods.js +65 -0
- package/build/manager/plugins/methods.js.map +1 -0
- package/build/manager/plugins/re-captcha.d.ts.map +1 -1
- package/build/manager/plugins/re-captcha.js +5 -0
- package/build/manager/plugins/re-captcha.js.map +1 -1
- package/build/manager/plugins/registry/types.d.ts +10 -0
- package/build/manager/plugins/registry/types.d.ts.map +1 -0
- package/build/manager/plugins/registry/types.js +2 -0
- package/build/manager/plugins/registry/types.js.map +1 -0
- package/build/manager/plugins/registry.d.ts +20 -0
- package/build/manager/plugins/registry.d.ts.map +1 -0
- package/build/manager/plugins/registry.js +23 -0
- package/build/manager/plugins/registry.js.map +1 -0
- package/build/manager/plugins/tunnel/wallet.d.ts.map +1 -1
- package/build/manager/plugins/tunnel-consumer.d.ts.map +1 -1
- package/build/manager/plugins/tunnel-consumer.js +10 -3
- package/build/manager/plugins/tunnel-consumer.js.map +1 -1
- package/build/manager/plugins/types.d.ts +53 -0
- package/build/manager/plugins/types.d.ts.map +1 -1
- package/build/service.d.ts.map +1 -1
- package/build/service.js +1 -1
- package/build/service.js.map +1 -1
- package/package.json +33 -30
- package/src/components/dispatcher/component.tsx +37 -14
- package/src/components/dispatcher/types.local.ts +7 -0
- package/src/entrypoints.ts +10 -0
- package/src/helper.ts +31 -5
- package/src/index.ts +1 -1
- package/src/login/adopt/types.ts +25 -0
- package/src/login/adopt.ts +21 -14
- package/src/login/consts.local.ts +34 -0
- package/src/login/consts.ts +105 -5
- package/src/login/credit/types.ts +6 -0
- package/src/login/credit.ts +69 -0
- package/src/login/env/types.ts +35 -0
- package/src/login/env.ts +60 -72
- package/src/login/flow.ts +44 -0
- package/src/login/hook.ts +58 -22
- package/src/login/i18n/be.json +41 -0
- package/src/login/i18n/de.json +41 -0
- package/src/login/i18n/en.json +41 -0
- package/src/login/i18n/es.json +41 -0
- package/src/login/i18n/fr.json +41 -0
- package/src/login/i18n/pl.json +41 -0
- package/src/login/i18n/ru.json +41 -0
- package/src/login/i18n/uk.json +41 -0
- package/src/login/i18n.ts +29 -0
- package/src/login/index.ts +21 -0
- package/src/login/land/types.ts +54 -0
- package/src/login/land.ts +149 -0
- package/src/login/methods/types.ts +32 -0
- package/src/login/methods.ts +109 -0
- package/src/login/resume/types.ts +24 -0
- package/src/login/resume.ts +45 -0
- package/src/login/screen.tsx +140 -0
- package/src/login/service.ts +181 -18
- package/src/login/start/types.ts +30 -0
- package/src/login/start.ts +44 -0
- package/src/login/surrogate/types.ts +9 -0
- package/src/login/surrogate.ts +37 -0
- package/src/login/terms/types.ts +126 -0
- package/src/login/terms-config.ts +55 -0
- package/src/login/terms.ts +315 -0
- package/src/login/types.ts +233 -17
- package/src/manager/README.md +37 -80
- package/src/manager/components/authentication/control.ts +3 -3
- package/src/manager/components/authentication/types.ts +2 -2
- package/src/manager/components/tunnel-consumer.tsx +2 -2
- package/src/manager/entrypoints.ts +18 -0
- package/src/manager/plugins/basic-ed25519.tsx +7 -0
- package/src/manager/plugins/exports.ts +3 -0
- package/src/manager/plugins/index.ts +11 -4
- package/src/manager/plugins/methods.ts +74 -0
- package/src/manager/plugins/re-captcha.tsx +7 -0
- package/src/manager/plugins/registry/types.ts +10 -0
- package/src/manager/plugins/registry.ts +33 -0
- package/src/manager/plugins/tunnel-consumer.tsx +12 -3
- package/src/manager/plugins/types.ts +54 -0
- package/src/service.ts +1 -1
- package/tests/context.ts +1 -2
- package/tests/i18n.spec.ts +68 -0
- package/tests/land.spec.ts +292 -0
- package/tests/login.spec.ts +332 -0
- package/tests/target.spec.ts +210 -0
- package/tests/terms-deferred.spec.ts +65 -0
- package/tests/terms.spec.ts +314 -0
- package/build/.gitkeep +0 -0
- package/src/manager/modules.ts +0 -18
- package/src/modules.ts +0 -13
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import en from './i18n/en.json' with { type: 'json' }
|
|
4
|
+
import pl from './i18n/pl.json' with { type: 'json' }
|
|
5
|
+
import ru from './i18n/ru.json' with { type: 'json' }
|
|
6
|
+
import be from './i18n/be.json' with { type: 'json' }
|
|
7
|
+
import uk from './i18n/uk.json' with { type: 'json' }
|
|
8
|
+
import es from './i18n/es.json' with { type: 'json' }
|
|
9
|
+
import de from './i18n/de.json' with { type: 'json' }
|
|
10
|
+
import fr from './i18n/fr.json' with { type: 'json' }
|
|
11
|
+
import { i18nHelper } from '@owlmeans/i18n'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The sign-in screen's copy, registered into the `auth` library resource.
|
|
15
|
+
*
|
|
16
|
+
* `_addI18n` PUSHES rather than replaces, so this coexists with `@owlmeans/web-client`'s own
|
|
17
|
+
* `auth` registration (which owns `dispatcher.*` and `surrogate.*`) and the two merge by tier and
|
|
18
|
+
* priority. Registering here rather than in a UI package is what lets the plain fallback screen
|
|
19
|
+
* and every styled one read the same keys — so the seven-language rule holds on every path a user
|
|
20
|
+
* can actually reach, not only the pretty one.
|
|
21
|
+
*/
|
|
22
|
+
i18nHelper.addI18nLib('en', 'auth', en)
|
|
23
|
+
i18nHelper.addI18nLib('pl', 'auth', pl)
|
|
24
|
+
i18nHelper.addI18nLib('ru', 'auth', ru)
|
|
25
|
+
i18nHelper.addI18nLib('be', 'auth', be)
|
|
26
|
+
i18nHelper.addI18nLib('uk', 'auth', uk)
|
|
27
|
+
i18nHelper.addI18nLib('es', 'auth', es)
|
|
28
|
+
i18nHelper.addI18nLib('de', 'auth', de)
|
|
29
|
+
i18nHelper.addI18nLib('fr', 'auth', fr)
|
package/src/login/index.ts
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
|
+
import './i18n.js'
|
|
2
|
+
|
|
1
3
|
export * from './consts.js'
|
|
2
4
|
export * from './types.js'
|
|
5
|
+
export type * from './env/types.js'
|
|
6
|
+
export type * from './adopt/types.js'
|
|
7
|
+
export type * from './methods/types.js'
|
|
8
|
+
export type * from './resume/types.js'
|
|
9
|
+
export type * from './terms/types.js'
|
|
10
|
+
export type * from './credit/types.js'
|
|
11
|
+
export type * from './surrogate/types.js'
|
|
12
|
+
export type * from './land/types.js'
|
|
13
|
+
export type * from './start/types.js'
|
|
3
14
|
export * from './env.js'
|
|
4
15
|
export * from './adopt.js'
|
|
16
|
+
export * from './methods.js'
|
|
17
|
+
export * from './flow.js'
|
|
18
|
+
export * from './resume.js'
|
|
19
|
+
export * from './terms-config.js'
|
|
20
|
+
export * from './terms.js'
|
|
21
|
+
export * from './credit.js'
|
|
22
|
+
export * from './surrogate.js'
|
|
23
|
+
export * from './screen.js'
|
|
5
24
|
export * from './service.js'
|
|
25
|
+
export * from './start.js'
|
|
6
26
|
export * from './hook.js'
|
|
27
|
+
export * from './land.js'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { LoginLandingParams } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/** Where a finished sign-in (or a pending step) lands. */
|
|
4
|
+
export interface LoginLanding {
|
|
5
|
+
alias: string
|
|
6
|
+
params?: LoginLandingParams
|
|
7
|
+
query?: LoginLandingParams
|
|
8
|
+
/** The step alias this landing came from, when it came from one. */
|
|
9
|
+
step?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Options for {@link LoginService.landAfterLogin}-shaped helpers (./land.js). */
|
|
13
|
+
export interface LandOptions {
|
|
14
|
+
/** Where to land when nothing else claims the flow. Defaults to `{ alias: HOME }`. */
|
|
15
|
+
fallback?: LoginLanding
|
|
16
|
+
/** `false` skips `resumeSuspendedFlow` — the caller already has its own concrete destination. */
|
|
17
|
+
resume?: boolean
|
|
18
|
+
/** Skip every step up to and including this alias — the resume point for a step's re-entry. */
|
|
19
|
+
after?: string
|
|
20
|
+
/**
|
|
21
|
+
* Overrides the step/hook timeout for this call. Mainly for tests; production callers leave it
|
|
22
|
+
* at the default (`LOGIN_STEP_TIMEOUT`).
|
|
23
|
+
*/
|
|
24
|
+
stepTimeout?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Where one context's finished sign-in goes: its pending steps, a suspended flow, or the fallback. */
|
|
28
|
+
export interface LoginLandingHelper {
|
|
29
|
+
/**
|
|
30
|
+
* Where a finished sign-in should go: the first pending step, else a suspended flow, else the
|
|
31
|
+
* fallback (ordinarily `HOME`).
|
|
32
|
+
*
|
|
33
|
+
* Re-checks `env().surrogate` even though {@link landAfterLogin} already did — this function is
|
|
34
|
+
* also called directly (e.g. from `useContinueLogin`, after the surrogate window has already
|
|
35
|
+
* closed), and the check is cheap insurance either way: **no step and no landing hook ever runs
|
|
36
|
+
* inside the surrogate popup.** It has nothing of its own to collect and nowhere to navigate —
|
|
37
|
+
* the popup's own `DispatcherHOC` has no `navigate() → HOME` at all.
|
|
38
|
+
*/
|
|
39
|
+
continueLogin: (opts?: LandOptions) => Promise<LoginLanding>
|
|
40
|
+
/**
|
|
41
|
+
* The whole post-sign-in landing decision: run any due landing hooks for a freshly seen token,
|
|
42
|
+
* then delegate to {@link continueLogin} for where the flow actually goes.
|
|
43
|
+
*
|
|
44
|
+
* This is what a plugin that just adopted a token calls in place of the old inline
|
|
45
|
+
* "`resumeSuspendedFlow` else `HOME`" — `DispatcherHOC`, the supervisor plugin and both Google
|
|
46
|
+
* plugins all replace their own copy of that logic with this one.
|
|
47
|
+
*/
|
|
48
|
+
landAfterLogin: (opts?: LandOptions) => Promise<LoginLanding>
|
|
49
|
+
/**
|
|
50
|
+
* The absolute URL a landing resolves to — the same shape the supervisor and Google plugins
|
|
51
|
+
* already build their own `window.location.href` from.
|
|
52
|
+
*/
|
|
53
|
+
landingUrl: (landing: LoginLanding) => Promise<string>
|
|
54
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { AuthService } from '@owlmeans/auth-common'
|
|
2
|
+
import { DISPATCHER } from '@owlmeans/auth'
|
|
3
|
+
import { HOME, memoHelper } from '@owlmeans/context'
|
|
4
|
+
import type { ClientContext } from '@owlmeans/client'
|
|
5
|
+
import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
|
|
6
|
+
import { DEFAULT_ALIAS as AUTH_ALIAS } from '../consts.js'
|
|
7
|
+
import { ensureLoginService } from './service.js'
|
|
8
|
+
import type { LoginContext, LoginService } from './types.js'
|
|
9
|
+
import type { LandOptions, LoginLanding, LoginLandingHelper } from './land/types.js'
|
|
10
|
+
import { LOGIN_LANDED_STORAGE, LOGIN_STEP_TIMEOUT } from './consts.js'
|
|
11
|
+
import { flowLandingOf } from '@owlmeans/client-flow'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Race a promise against a bound, resolving to `onTimeout` (never rejecting) if the bound wins.
|
|
15
|
+
*
|
|
16
|
+
* A synchronous throw from the wrapped call is turned into a rejection by the caller before this
|
|
17
|
+
* ever sees it, so the only two outcomes here are "settled in time" and "timed out". `onTimeout`
|
|
18
|
+
* defaults to `undefined` — every existing caller that omits it keeps its old fail-open meaning;
|
|
19
|
+
* `continueLogin` passes a step's own `required` flag through it so a step that hangs is read the
|
|
20
|
+
* same way as one that throws.
|
|
21
|
+
*/
|
|
22
|
+
const withTimeout = async <T>(
|
|
23
|
+
promise: Promise<T>, ms: number, onTimeout?: T
|
|
24
|
+
): Promise<T | undefined> => {
|
|
25
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
26
|
+
try {
|
|
27
|
+
return await Promise.race<T | undefined>([
|
|
28
|
+
promise,
|
|
29
|
+
new Promise<T | undefined>(resolve => { timer = setTimeout(() => resolve(onTimeout), ms) }),
|
|
30
|
+
])
|
|
31
|
+
} finally {
|
|
32
|
+
if (timer != null) {
|
|
33
|
+
clearTimeout(timer)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const makeLoginLandingHelper = (ctx: LoginContext): LoginLandingHelper => {
|
|
39
|
+
/**
|
|
40
|
+
* Run every registered landing hook, once per distinct token.
|
|
41
|
+
*
|
|
42
|
+
* The stored token is overwritten BEFORE the hooks run, not after: a hook that throws or hangs
|
|
43
|
+
* must never make the same token re-run the whole set on the next call, since surviving exactly
|
|
44
|
+
* that is what the bound-and-swallow contract is for. The raw token string is the marker rather
|
|
45
|
+
* than a digest of it — it never leaves the browser, and this package already keeps the terms
|
|
46
|
+
* acceptance marker the same way (`LOGIN_TERMS_STORAGE`).
|
|
47
|
+
*/
|
|
48
|
+
const runLandingHooks = async (login: LoginService, timeout: number): Promise<void> => {
|
|
49
|
+
const auth = ctx.service<AuthService>(AUTH_ALIAS)
|
|
50
|
+
const token = await auth.authenticated()
|
|
51
|
+
if (token == null) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let last: string | null = null
|
|
56
|
+
try {
|
|
57
|
+
last = window.localStorage.getItem(LOGIN_LANDED_STORAGE)
|
|
58
|
+
} catch {
|
|
59
|
+
// Storage can be unavailable (private modes, blocked cookies). Treated as "never landed",
|
|
60
|
+
// which just re-runs the hooks — the safe direction, same as the terms-acceptance marker.
|
|
61
|
+
}
|
|
62
|
+
if (last === token) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
window.localStorage.setItem(LOGIN_LANDED_STORAGE, token)
|
|
68
|
+
} catch { /* nothing to remember if storage was never available */ }
|
|
69
|
+
|
|
70
|
+
for (const hook of login.landingHooks()) {
|
|
71
|
+
await withTimeout(Promise.resolve().then(() => hook.landed(ctx)), timeout).catch(() => undefined)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const continueLogin = async (opts?: LandOptions): Promise<LoginLanding> => {
|
|
76
|
+
const login = ensureLoginService(ctx)
|
|
77
|
+
if (login.env().surrogate) {
|
|
78
|
+
return { alias: DISPATCHER }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const timeout = opts?.stepTimeout ?? LOGIN_STEP_TIMEOUT
|
|
82
|
+
const auth = ctx.service<AuthService>(AUTH_ALIAS)
|
|
83
|
+
const token = await auth.authenticated()
|
|
84
|
+
|
|
85
|
+
if (token != null) {
|
|
86
|
+
const steps = login.steps()
|
|
87
|
+
let start = 0
|
|
88
|
+
if (opts?.after != null) {
|
|
89
|
+
const index = steps.findIndex(step => step.alias === opts.after)
|
|
90
|
+
if (index >= 0) {
|
|
91
|
+
start = index + 1
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (let i = start; i < steps.length; i++) {
|
|
96
|
+
const step = steps[i]
|
|
97
|
+
// Registration proves only that some module reached the bundle — the step's own screen may
|
|
98
|
+
// not be bound in this tree (an older target, a partial import). Skipped, never thrown.
|
|
99
|
+
if (!ctx.hasEntrypoint(step.entrypoint)) {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A step that throws or hangs fails OPEN ("not pending") by default — UNLESS it declared
|
|
104
|
+
// itself `required`, in which case a broken read must show the step rather than let the
|
|
105
|
+
// person through it unconfirmed (a Terms box moved here from the sign-in screen).
|
|
106
|
+
const onBroken = step.required === true
|
|
107
|
+
const pending = await withTimeout(
|
|
108
|
+
Promise.resolve().then(() => step.pending(ctx)), timeout, onBroken
|
|
109
|
+
).catch(() => onBroken)
|
|
110
|
+
|
|
111
|
+
if (pending === true) {
|
|
112
|
+
return { alias: step.entrypoint, query: await step.query?.(ctx), step: step.alias }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (opts?.resume !== false) {
|
|
118
|
+
const landing = await flowLandingOf(ctx as unknown as ClientContext).resumeSuspendedFlow()
|
|
119
|
+
if (landing != null) {
|
|
120
|
+
return { alias: landing.entrypoint, query: landing.query }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return opts?.fallback ?? { alias: HOME }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const landAfterLogin = async (opts?: LandOptions): Promise<LoginLanding> => {
|
|
128
|
+
const login = ensureLoginService(ctx)
|
|
129
|
+
// Never run a step or a landing hook while THIS document is the surrogate popup — see the note
|
|
130
|
+
// on `continueLogin`.
|
|
131
|
+
if (login.env().surrogate) {
|
|
132
|
+
return { alias: DISPATCHER }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await runLandingHooks(login, opts?.stepTimeout ?? LOGIN_STEP_TIMEOUT)
|
|
136
|
+
|
|
137
|
+
return continueLogin(opts)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const landingUrl = async (landing: LoginLanding): Promise<string> =>
|
|
141
|
+
await ctx.entrypoint<ClientEntrypoint<string>>(landing.alias).url(
|
|
142
|
+
{ params: landing.params, query: landing.query }, { absolute: true }
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return { continueLogin, landAfterLogin, landingUrl }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The landing helper of a context — one per context. */
|
|
149
|
+
export const loginLandingOf = memoHelper.oncePer(makeLoginLandingHelper)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LoginScreenConfig } from '@owlmeans/config'
|
|
2
|
+
import type { LoginMethod, LoginMethodContext, LoginMethodSource } from '../types.js'
|
|
3
|
+
|
|
4
|
+
/** The sign-in methods on offer: the global source registry and the resolution of a screen's list. */
|
|
5
|
+
export interface LoginMethodsHelper {
|
|
6
|
+
/** Register a source globally — replacing an earlier one of the same alias. */
|
|
7
|
+
registerMethodSource: (source: LoginMethodSource) => void
|
|
8
|
+
/** The globally registered sources, in registration order. */
|
|
9
|
+
listMethodSources: () => LoginMethodSource[]
|
|
10
|
+
/**
|
|
11
|
+
* Everything the current environment may be offered, in the order it should be offered in.
|
|
12
|
+
*
|
|
13
|
+
* Resolution order matters and is not arbitrary:
|
|
14
|
+
*
|
|
15
|
+
* 1. Sources produce candidates. A source that throws is skipped rather than taking the screen
|
|
16
|
+
* down — a misconfigured provider list must not remove the sign-in method that does work.
|
|
17
|
+
* 2. `restricted` candidates are dropped unless the configuration named them, which is what makes
|
|
18
|
+
* an operator login opt-in rather than merely unadvertised.
|
|
19
|
+
* 3. An explicit `methods` list is both a filter AND the order, because an app that bothered to
|
|
20
|
+
* list them means that list.
|
|
21
|
+
*/
|
|
22
|
+
resolveLoginMethods: (
|
|
23
|
+
ctx: LoginMethodContext, cfg?: LoginScreenConfig, extra?: LoginMethodSource[]
|
|
24
|
+
) => LoginMethod[]
|
|
25
|
+
/**
|
|
26
|
+
* The method a screen highlights and focuses.
|
|
27
|
+
*
|
|
28
|
+
* The first `primary` one, else the first offered. It is highlighted, never started: requirement
|
|
29
|
+
* one of this whole feature is that nothing leaves the document without a click.
|
|
30
|
+
*/
|
|
31
|
+
primaryLoginMethod: (methods: LoginMethod[]) => LoginMethod | null
|
|
32
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { LoginMethodConfig, LoginScreenConfig } from '@owlmeans/config'
|
|
2
|
+
import { DEFAULT_METHOD_ORDER } from './consts.js'
|
|
3
|
+
import type { LoginMethod, LoginMethodContext, LoginMethodSource } from './types.js'
|
|
4
|
+
import type { LoginMethodsHelper } from './methods/types.js'
|
|
5
|
+
import { logger } from '@owlmeans/log'
|
|
6
|
+
|
|
7
|
+
const log = logger('client-auth:login')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The module-global registry of method SOURCES.
|
|
11
|
+
*
|
|
12
|
+
* A source, rather than a method, because the two things that offer sign-in methods answer
|
|
13
|
+
* different questions at different times: the plugin registry knows what code is loaded, and the
|
|
14
|
+
* OIDC configuration knows what an entity is federated with. Both are answered per render — a
|
|
15
|
+
* provider list can arrive from the api-config middleware after the first paint.
|
|
16
|
+
*
|
|
17
|
+
* Global, like the `AuthenticationPlugin` map it mirrors, because a package registers its source
|
|
18
|
+
* from a side-effect import that has no context to hang it on. A context-scoped registry exists
|
|
19
|
+
* alongside it on the login service, and both are merged by `resolveLoginMethods`.
|
|
20
|
+
*/
|
|
21
|
+
const sources: LoginMethodSource[] = []
|
|
22
|
+
|
|
23
|
+
export const createLoginMethodsHelper = (): LoginMethodsHelper => {
|
|
24
|
+
const registerMethodSource = (source: LoginMethodSource): void => {
|
|
25
|
+
const existing = sources.findIndex(candidate => candidate.alias === source.alias)
|
|
26
|
+
if (existing >= 0) {
|
|
27
|
+
sources.splice(existing, 1)
|
|
28
|
+
}
|
|
29
|
+
sources.push(source)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const listMethodSources = (): LoginMethodSource[] => [...sources]
|
|
33
|
+
|
|
34
|
+
/** Normalize a config entry, which may be a bare id or a full override object. */
|
|
35
|
+
const asConfig = (entry: string | LoginMethodConfig): LoginMethodConfig =>
|
|
36
|
+
typeof entry === 'string' ? { id: entry } : entry
|
|
37
|
+
|
|
38
|
+
const merge = (method: LoginMethod, override: LoginMethodConfig | undefined): LoginMethod =>
|
|
39
|
+
override == null ? method : {
|
|
40
|
+
...method,
|
|
41
|
+
...(override.label != null ? { label: override.label } : {}),
|
|
42
|
+
...(override.i18nKey != null ? { i18nKey: override.i18nKey } : {}),
|
|
43
|
+
...(override.icon != null ? { icon: override.icon } : {}),
|
|
44
|
+
...(override.order != null ? { order: override.order } : {}),
|
|
45
|
+
...(override.emphasis != null ? { emphasis: override.emphasis } : {}),
|
|
46
|
+
...(override.params != null ? { params: { ...method.params, ...override.params } } : {}),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolveLoginMethods = (
|
|
50
|
+
ctx: LoginMethodContext, cfg?: LoginScreenConfig, extra: LoginMethodSource[] = []
|
|
51
|
+
): LoginMethod[] => {
|
|
52
|
+
const candidates: LoginMethod[] = []
|
|
53
|
+
for (const source of [...sources, ...extra]) {
|
|
54
|
+
try {
|
|
55
|
+
candidates.push(...source.list(ctx))
|
|
56
|
+
} catch (e) {
|
|
57
|
+
log.warn('Login method source failed', { source: source.alias, error: e })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const overrides = cfg?.overrides ?? {}
|
|
62
|
+
const listed = cfg?.methods?.map(asConfig)
|
|
63
|
+
const listedIds = listed?.map(entry => entry.id).filter((id): id is string => id != null)
|
|
64
|
+
|
|
65
|
+
const offered = candidates
|
|
66
|
+
.filter(method => {
|
|
67
|
+
const override = overrides[method.id] ?? listed?.find(entry => entry.id === method.id)
|
|
68
|
+
if (override?.enabled === false || override?.hidden === true) {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
// A restricted method is offered only where the configuration asked for it — by naming it in
|
|
72
|
+
// `methods`, or by enabling it in `overrides`. Registering the plugin is deliberately not
|
|
73
|
+
// enough: an operator login that appears wherever its code happens to be bundled is an
|
|
74
|
+
// operator login in production.
|
|
75
|
+
if (method.restricted === true
|
|
76
|
+
&& listedIds?.includes(method.id) !== true
|
|
77
|
+
&& override?.enabled !== true) {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
return true
|
|
81
|
+
})
|
|
82
|
+
.map(method => merge(method, overrides[method.id]))
|
|
83
|
+
|
|
84
|
+
if (listedIds != null) {
|
|
85
|
+
return listedIds
|
|
86
|
+
.map(id => {
|
|
87
|
+
const method = offered.find(candidate => candidate.id === id)
|
|
88
|
+
return method != null ? merge(method, listed?.find(entry => entry.id === id)) : null
|
|
89
|
+
})
|
|
90
|
+
.filter((method): method is LoginMethod => method != null)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return offered.sort((a, b) => {
|
|
94
|
+
const order = (a.order ?? DEFAULT_METHOD_ORDER) - (b.order ?? DEFAULT_METHOD_ORDER)
|
|
95
|
+
return order !== 0 ? order : a.id.localeCompare(b.id)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const primaryLoginMethod = (methods: LoginMethod[]): LoginMethod | null =>
|
|
100
|
+
methods.find(method => method.emphasis === 'primary') ?? methods[0] ?? null
|
|
101
|
+
|
|
102
|
+
return { registerMethodSource, listMethodSources, resolveLoginMethods, primaryLoginMethod }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const loginMethodsHelper = createLoginMethodsHelper()
|
|
106
|
+
|
|
107
|
+
/** @deprecated compat:factory-refactor — use `loginMethodsHelper.primaryLoginMethod(…)` */
|
|
108
|
+
export const primaryLoginMethod = (methods: LoginMethod[]): LoginMethod | null =>
|
|
109
|
+
loginMethodsHelper.primaryLoginMethod(methods)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LoginOutcome, ResumeAction } from '../consts.js'
|
|
2
|
+
|
|
3
|
+
/** How a settled sign-in outcome is read: by a screen that stayed put, and by a resuming dispatcher. */
|
|
4
|
+
export interface LoginResumeHelper {
|
|
5
|
+
/**
|
|
6
|
+
* The message key a FINISHED sign-in attempt should show, or null when it actually went somewhere.
|
|
7
|
+
*
|
|
8
|
+
* A dispatcher may treat `Passed` as "carry on with the ordinary continuation", because it has one.
|
|
9
|
+
* A screen does not: the user clicked, the document did not move, and nothing rendered — which is
|
|
10
|
+
* indistinguishable from a broken button, and is exactly how a null authorization URL used to
|
|
11
|
+
* present itself. Every outcome that leaves the user looking at the same screen therefore has to
|
|
12
|
+
* say something.
|
|
13
|
+
*/
|
|
14
|
+
loginAttemptError: (outcome: LoginOutcome | null) => string | null
|
|
15
|
+
/**
|
|
16
|
+
* The one reading of a `resume` outcome, shared by every dispatcher.
|
|
17
|
+
*
|
|
18
|
+
* Shared because three dispatchers — `web-client`, `web-oidc-rp` and `mui-oidc-rp` — have to agree
|
|
19
|
+
* on it, and the last time they each held their own copy of a decision they drifted, which is how a
|
|
20
|
+
* popup came to render the application inside itself in two packages at once. It is also the whole
|
|
21
|
+
* rule, so it is testable without a DOM.
|
|
22
|
+
*/
|
|
23
|
+
resumeAction: (outcome: LoginOutcome) => ResumeAction
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { LoginOutcome, ResumeAction } from './consts.js'
|
|
2
|
+
import type { LoginResumeHelper } from './resume/types.js'
|
|
3
|
+
|
|
4
|
+
export const createLoginResumeHelper = (): LoginResumeHelper => {
|
|
5
|
+
const loginAttemptError = (outcome: LoginOutcome | null): string | null => {
|
|
6
|
+
switch (outcome) {
|
|
7
|
+
case LoginOutcome.Gesture:
|
|
8
|
+
case LoginOutcome.Blocked:
|
|
9
|
+
// The window never opened. That is the popup blocker, and it has its own copy.
|
|
10
|
+
return 'login.error.blocked'
|
|
11
|
+
case LoginOutcome.Failed:
|
|
12
|
+
case LoginOutcome.Passed:
|
|
13
|
+
return 'login.error.failed'
|
|
14
|
+
default:
|
|
15
|
+
// Handled, Redirected and Orphaned all did something; Orphaned reports itself elsewhere.
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const resumeAction = (outcome: LoginOutcome): ResumeAction => {
|
|
21
|
+
switch (outcome) {
|
|
22
|
+
case LoginOutcome.Handled:
|
|
23
|
+
case LoginOutcome.Redirected:
|
|
24
|
+
return ResumeAction.Stop
|
|
25
|
+
case LoginOutcome.Orphaned:
|
|
26
|
+
case LoginOutcome.Failed:
|
|
27
|
+
case LoginOutcome.Gesture:
|
|
28
|
+
case LoginOutcome.Blocked:
|
|
29
|
+
return ResumeAction.Render
|
|
30
|
+
default:
|
|
31
|
+
return ResumeAction.Navigate
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { loginAttemptError, resumeAction }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const loginResumeHelper = createLoginResumeHelper()
|
|
39
|
+
|
|
40
|
+
/** @deprecated compat:factory-refactor — use `loginResumeHelper.loginAttemptError(…)` */
|
|
41
|
+
export const loginAttemptError = (outcome: LoginOutcome | null): string | null =>
|
|
42
|
+
loginResumeHelper.loginAttemptError(outcome)
|
|
43
|
+
|
|
44
|
+
/** @deprecated compat:factory-refactor — use `loginResumeHelper.resumeAction(…)` */
|
|
45
|
+
export const resumeAction = (outcome: LoginOutcome): ResumeAction => loginResumeHelper.resumeAction(outcome)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useCallback, useState, type CSSProperties, type FC, type ReactNode } from 'react'
|
|
2
|
+
import { useContext } from '@owlmeans/client'
|
|
3
|
+
import type { CommonConfig } from '@owlmeans/config'
|
|
4
|
+
import type { LoginContext, LoginMethod, LoginScreenProps, LoginService } from './types.js'
|
|
5
|
+
import type { TermsSentencePart } from './terms/types.js'
|
|
6
|
+
import { LOGIN_SERVICE } from './consts.js'
|
|
7
|
+
import { loginMethodsHelper } from './methods.js'
|
|
8
|
+
import { loginTermsHelper } from './terms.js'
|
|
9
|
+
import { resolveCredit } from './credit.js'
|
|
10
|
+
import { box, page } from './consts.local.js'
|
|
11
|
+
|
|
12
|
+
const renderParts = (parts: TermsSentencePart[]): ReactNode =>
|
|
13
|
+
parts.map((part, index) => part.href != null
|
|
14
|
+
? <a key={index} href={part.href} target="_blank" rel="noreferrer noopener">{part.text}</a>
|
|
15
|
+
: <span key={index}>{part.text}</span>)
|
|
16
|
+
|
|
17
|
+
const button = (emphasis?: string): CSSProperties => ({
|
|
18
|
+
display: 'block', width: '100%', marginTop: '.5rem', padding: '.625rem 1rem',
|
|
19
|
+
fontSize: '1rem', cursor: 'pointer', borderRadius: '.375rem',
|
|
20
|
+
border: '1px solid currentColor',
|
|
21
|
+
...(emphasis === 'primary' ? { fontWeight: 600 } : {}),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The sign-in screen every application gets, whether or not it registered a styled one.
|
|
26
|
+
*
|
|
27
|
+
* It exists because a relying party must render SOMETHING rather than start a flow on its own, and
|
|
28
|
+
* a relying party may not depend on a UI family. So this is deliberately plain: raw elements and
|
|
29
|
+
* inline styles, no Tailwind class that a consumer's stylesheet would have to scan for, no shadcn
|
|
30
|
+
* primitive a consumer would have to vendor. An application that registers a real screen through
|
|
31
|
+
* `login().registerScreen(...)` never sees it.
|
|
32
|
+
*
|
|
33
|
+
* Because it is the floor rather than the design, it still has to be correct: it offers the same
|
|
34
|
+
* methods, enforces the same terms confirmation, centres the same way, and renders the same
|
|
35
|
+
* credit line.
|
|
36
|
+
*/
|
|
37
|
+
export const FallbackLoginScreen: FC<LoginScreenProps> = props => {
|
|
38
|
+
const context = useContext() as unknown as LoginContext
|
|
39
|
+
const t = props.translate ?? ((_key: string, defaultValue: string) => defaultValue)
|
|
40
|
+
const cfg = props.config ?? (context.cfg as CommonConfig).security?.auth?.login
|
|
41
|
+
const brand = (context.cfg as CommonConfig).brand
|
|
42
|
+
|
|
43
|
+
const login = context.service<LoginService>(LOGIN_SERVICE)
|
|
44
|
+
const env = login.env()
|
|
45
|
+
const resolved = loginTermsHelper.resolveTerms(props.terms ?? cfg?.terms)
|
|
46
|
+
const credit = resolveCredit(cfg?.credit, brand, context.cfg.service)
|
|
47
|
+
const resolveLabel = loginTermsHelper.termsLabelResolver(t, props.locale)
|
|
48
|
+
const deferred = loginTermsHelper.termsDeferred(context)
|
|
49
|
+
|
|
50
|
+
const [accepted, setAccepted] = useState(() => loginTermsHelper.termsAccepted(resolved))
|
|
51
|
+
const [attempted, setAttempted] = useState(false)
|
|
52
|
+
|
|
53
|
+
const all = login.methods({ context, env })
|
|
54
|
+
const methods = typeof props.methods === 'function'
|
|
55
|
+
? props.methods(all)
|
|
56
|
+
: props.methods ?? all
|
|
57
|
+
const primary = loginMethodsHelper.primaryLoginMethod(methods)
|
|
58
|
+
|
|
59
|
+
const blocked = resolved != null && resolved.required && !deferred && !accepted
|
|
60
|
+
|
|
61
|
+
const select = useCallback((method: LoginMethod) => {
|
|
62
|
+
if (blocked) {
|
|
63
|
+
setAttempted(true)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
void method.start({ context, env })
|
|
67
|
+
}, [blocked, context, env])
|
|
68
|
+
|
|
69
|
+
const onAccept = useCallback((value: boolean) => {
|
|
70
|
+
setAccepted(value)
|
|
71
|
+
setAttempted(false)
|
|
72
|
+
loginTermsHelper.acceptTerms(resolved, value)
|
|
73
|
+
}, [resolved])
|
|
74
|
+
|
|
75
|
+
return <div data-login-screen style={page}><div style={box}>
|
|
76
|
+
<h1 style={{ fontSize: '1.25rem', marginBottom: '.25rem' }}>
|
|
77
|
+
{props.title ?? t('login.title', 'Sign in')}
|
|
78
|
+
</h1>
|
|
79
|
+
<p style={{ opacity: .75, marginTop: 0 }}>
|
|
80
|
+
{props.subtitle ?? t('login.subtitle', 'Choose how you would like to continue.')}
|
|
81
|
+
</p>
|
|
82
|
+
|
|
83
|
+
{methods.length < 1
|
|
84
|
+
? <p role="status">{t('login.empty', 'No sign-in method is configured for this application.')}</p>
|
|
85
|
+
: methods.map(method => <button
|
|
86
|
+
key={method.id} type="button" data-login-method={method.id}
|
|
87
|
+
// `aria-disabled`, never `disabled`: a disabled button swallows the click, so a user who
|
|
88
|
+
// has not confirmed the terms would press it and be told nothing at all.
|
|
89
|
+
aria-disabled={blocked} data-blocked={blocked ? 'true' : undefined}
|
|
90
|
+
style={{ ...button(method.emphasis), opacity: blocked ? .6 : 1 }}
|
|
91
|
+
autoFocus={method.id === primary?.id}
|
|
92
|
+
onClick={() => select(method)}
|
|
93
|
+
>
|
|
94
|
+
{method.label ?? t(`login.method.${method.i18nKey ?? method.id}`, method.id)}
|
|
95
|
+
</button>)}
|
|
96
|
+
|
|
97
|
+
{/* When a registered, bound step confirms the terms instead, this screen shows neither the
|
|
98
|
+
checkbox nor its "last updated" line — that document is agreed to over there, and this
|
|
99
|
+
screen is never `blocked` on it. The privacy notice below still renders either way: it was
|
|
100
|
+
never inside the consented label, so deferring the checkbox does not touch it. */}
|
|
101
|
+
{resolved != null && !deferred && <p style={{ marginTop: '1rem', fontSize: '.875rem' }}>
|
|
102
|
+
{/* The ONLY place `data-login-terms` appears — never duplicated onto a second control. */}
|
|
103
|
+
<label>
|
|
104
|
+
<input
|
|
105
|
+
type="checkbox" checked={accepted} data-login-terms
|
|
106
|
+
onChange={event => onAccept(event.target.checked)}
|
|
107
|
+
/>{' '}
|
|
108
|
+
{renderParts(loginTermsHelper.termsSentence(
|
|
109
|
+
t('login.terms.accept', 'I have read and agree to the {{documents}}.'),
|
|
110
|
+
resolved, props.locale, resolveLabel
|
|
111
|
+
))}
|
|
112
|
+
</label>
|
|
113
|
+
</p>}
|
|
114
|
+
|
|
115
|
+
{resolved?.revisedAt != null && !deferred && <p data-login-revised style={{ fontSize: '.75rem', opacity: .7 }}>
|
|
116
|
+
{t('login.terms.revised', 'Last updated: {{date}}').split('{{date}}').join(resolved.revisedAt)}
|
|
117
|
+
</p>}
|
|
118
|
+
|
|
119
|
+
{/* Outside the checkbox's label: a privacy disclosure is not something it consents to. */}
|
|
120
|
+
{resolved != null && <p data-login-privacy style={{ fontSize: '.875rem' }}>
|
|
121
|
+
{renderParts(loginTermsHelper.termsSentence(
|
|
122
|
+
t('login.terms.notice', 'How we handle your personal data: {{notices}}.'),
|
|
123
|
+
resolved, props.locale, resolveLabel
|
|
124
|
+
))}
|
|
125
|
+
</p>}
|
|
126
|
+
|
|
127
|
+
{attempted && blocked && resolved != null && <p role="alert" style={{ color: '#b00', fontSize: '.875rem' }}>
|
|
128
|
+
{renderParts(loginTermsHelper.termsSentence(
|
|
129
|
+
t('login.terms.required', 'Please confirm the {{documents}} to continue.'),
|
|
130
|
+
resolved, props.locale, resolveLabel
|
|
131
|
+
))}
|
|
132
|
+
</p>}
|
|
133
|
+
|
|
134
|
+
{props.footer ?? <p style={{ marginTop: '2rem', fontSize: '.75rem', opacity: .7 }}>
|
|
135
|
+
{credit.poweredBy && <span>{t('login.credit.powered', 'Powered by OwlMeans')}</span>}
|
|
136
|
+
{credit.poweredBy && credit.line != null && ' · '}
|
|
137
|
+
{credit.line}
|
|
138
|
+
</p>}
|
|
139
|
+
</div></div>
|
|
140
|
+
}
|