@owlmeans/web-auth-email 0.0.0-stage → 0.1.19
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 +116 -2
- package/THIRD-PARTY-NOTICES.txt +24 -0
- package/agent-meta/manifest.json +16 -0
- package/agent-meta/skills/web-auth-email/SKILL.md +119 -0
- package/build/@/components/ui/alert.d.ts +10 -0
- package/build/@/components/ui/alert.d.ts.map +1 -0
- package/build/@/components/ui/alert.js +26 -0
- package/build/@/components/ui/alert.js.map +1 -0
- package/build/@/components/ui/button.d.ts +11 -0
- package/build/@/components/ui/button.d.ts.map +1 -0
- package/build/@/components/ui/button.js +32 -0
- package/build/@/components/ui/button.js.map +1 -0
- package/build/@/components/ui/card.d.ts +10 -0
- package/build/@/components/ui/card.d.ts.map +1 -0
- package/build/@/components/ui/card.js +25 -0
- package/build/@/components/ui/card.js.map +1 -0
- package/build/@/components/ui/input.d.ts +4 -0
- package/build/@/components/ui/input.d.ts.map +1 -0
- package/build/@/components/ui/input.js +7 -0
- package/build/@/components/ui/input.js.map +1 -0
- package/build/@/components/ui/label.d.ts +5 -0
- package/build/@/components/ui/label.d.ts.map +1 -0
- package/build/@/components/ui/label.js +8 -0
- package/build/@/components/ui/label.js.map +1 -0
- package/build/@/lib/utils.d.ts +3 -0
- package/build/@/lib/utils.d.ts.map +1 -0
- package/build/@/lib/utils.js +6 -0
- package/build/@/lib/utils.js.map +1 -0
- package/build/append.d.ts +23 -0
- package/build/append.d.ts.map +1 -0
- package/build/append.js +49 -0
- package/build/append.js.map +1 -0
- package/build/components/connected.d.ts +8 -0
- package/build/components/connected.d.ts.map +1 -0
- package/build/components/connected.js +16 -0
- package/build/components/connected.js.map +1 -0
- package/build/components/credit.d.ts +5 -0
- package/build/components/credit.d.ts.map +1 -0
- package/build/components/credit.js +16 -0
- package/build/components/credit.js.map +1 -0
- package/build/components/fields.d.ts +23 -0
- package/build/components/fields.d.ts.map +1 -0
- package/build/components/fields.js +39 -0
- package/build/components/fields.js.map +1 -0
- package/build/components/index.d.ts +9 -0
- package/build/components/index.d.ts.map +1 -0
- package/build/components/index.js +8 -0
- package/build/components/index.js.map +1 -0
- package/build/components/offer.d.ts +9 -0
- package/build/components/offer.d.ts.map +1 -0
- package/build/components/offer.js +99 -0
- package/build/components/offer.js.map +1 -0
- package/build/components/panel.d.ts +12 -0
- package/build/components/panel.d.ts.map +1 -0
- package/build/components/panel.js +129 -0
- package/build/components/panel.js.map +1 -0
- package/build/components/route.d.ts +9 -0
- package/build/components/route.d.ts.map +1 -0
- package/build/components/route.js +17 -0
- package/build/components/route.js.map +1 -0
- package/build/components/screen.d.ts +17 -0
- package/build/components/screen.d.ts.map +1 -0
- package/build/components/screen.js +79 -0
- package/build/components/screen.js.map +1 -0
- package/build/components/types.d.ts +81 -0
- package/build/components/types.d.ts.map +1 -0
- package/build/components/types.js +2 -0
- package/build/components/types.js.map +1 -0
- package/build/consts.d.ts +37 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +43 -0
- package/build/consts.js.map +1 -0
- package/build/entrypoints.d.ts +10 -0
- package/build/entrypoints.d.ts.map +1 -0
- package/build/entrypoints.js +14 -0
- package/build/entrypoints.js.map +1 -0
- package/build/hooks/consts.local.d.ts +4 -0
- package/build/hooks/consts.local.d.ts.map +1 -0
- package/build/hooks/consts.local.js +9 -0
- package/build/hooks/consts.local.js.map +1 -0
- package/build/hooks/types.d.ts +78 -0
- package/build/hooks/types.d.ts.map +1 -0
- package/build/hooks/types.js +2 -0
- package/build/hooks/types.js.map +1 -0
- package/build/hooks/use-email-login.d.ts +13 -0
- package/build/hooks/use-email-login.d.ts.map +1 -0
- package/build/hooks/use-email-login.js +208 -0
- package/build/hooks/use-email-login.js.map +1 -0
- package/build/hooks/use-password.d.ts +13 -0
- package/build/hooks/use-password.d.ts.map +1 -0
- package/build/hooks/use-password.js +118 -0
- package/build/hooks/use-password.js.map +1 -0
- package/build/hooks/use-translate.d.ts +7 -0
- package/build/hooks/use-translate.d.ts.map +1 -0
- package/build/hooks/use-translate.js +12 -0
- package/build/hooks/use-translate.js.map +1 -0
- package/build/i18n/auth/be.json +7 -0
- package/build/i18n/auth/de.json +7 -0
- package/build/i18n/auth/en.json +7 -0
- package/build/i18n/auth/es.json +7 -0
- package/build/i18n/auth/fr.json +7 -0
- package/build/i18n/auth/pl.json +7 -0
- package/build/i18n/auth/ru.json +7 -0
- package/build/i18n/auth/uk.json +7 -0
- package/build/i18n/be.json +100 -0
- package/build/i18n/de.json +100 -0
- package/build/i18n/en.json +100 -0
- package/build/i18n/es.json +100 -0
- package/build/i18n/fr.json +100 -0
- package/build/i18n/pl.json +100 -0
- package/build/i18n/ru.json +100 -0
- package/build/i18n/uk.json +100 -0
- package/build/i18n.d.ts +2 -0
- package/build/i18n.d.ts.map +1 -0
- package/build/i18n.js +40 -0
- package/build/i18n.js.map +1 -0
- package/build/index.d.ts +23 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +17 -0
- package/build/index.js.map +1 -0
- package/build/password/types.d.ts +39 -0
- package/build/password/types.d.ts.map +1 -0
- package/build/password/types.js +2 -0
- package/build/password/types.js.map +1 -0
- package/build/password.d.ts +4 -0
- package/build/password.d.ts.map +1 -0
- package/build/password.js +45 -0
- package/build/password.js.map +1 -0
- package/build/plugin.d.ts +10 -0
- package/build/plugin.d.ts.map +1 -0
- package/build/plugin.js +21 -0
- package/build/plugin.js.map +1 -0
- package/build/refusal/consts.local.d.ts +3 -0
- package/build/refusal/consts.local.d.ts.map +1 -0
- package/build/refusal/consts.local.js +3 -0
- package/build/refusal/consts.local.js.map +1 -0
- package/build/refusal/types.d.ts +38 -0
- package/build/refusal/types.d.ts.map +1 -0
- package/build/refusal/types.js +2 -0
- package/build/refusal/types.js.map +1 -0
- package/build/refusal.d.ts +4 -0
- package/build/refusal.d.ts.map +1 -0
- package/build/refusal.js +131 -0
- package/build/refusal.js.map +1 -0
- package/build/service.d.ts +12 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +66 -0
- package/build/service.js.map +1 -0
- package/build/state/types.d.ts +90 -0
- package/build/state/types.d.ts.map +1 -0
- package/build/state/types.js +2 -0
- package/build/state/types.js.map +1 -0
- package/build/state.d.ts +4 -0
- package/build/state.d.ts.map +1 -0
- package/build/state.js +98 -0
- package/build/state.js.map +1 -0
- package/build/step.d.ts +16 -0
- package/build/step.d.ts.map +1 -0
- package/build/step.js +31 -0
- package/build/step.js.map +1 -0
- package/build/storage/types.d.ts +42 -0
- package/build/storage/types.d.ts.map +1 -0
- package/build/storage/types.js +2 -0
- package/build/storage/types.js.map +1 -0
- package/build/storage.d.ts +4 -0
- package/build/storage.d.ts.map +1 -0
- package/build/storage.js +100 -0
- package/build/storage.js.map +1 -0
- package/build/types.d.ts +102 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/account.d.ts +4 -0
- package/build/utils/account.d.ts.map +1 -0
- package/build/utils/account.js +11 -0
- package/build/utils/account.js.map +1 -0
- package/build/utils/locale.d.ts +6 -0
- package/build/utils/locale.d.ts.map +1 -0
- package/build/utils/locale.js +10 -0
- package/build/utils/locale.js.map +1 -0
- package/build/utils/text/types.d.ts +23 -0
- package/build/utils/text/types.d.ts.map +1 -0
- package/build/utils/text/types.js +2 -0
- package/build/utils/text/types.js.map +1 -0
- package/build/utils/text.d.ts +4 -0
- package/build/utils/text.d.ts.map +1 -0
- package/build/utils/text.js +26 -0
- package/build/utils/text.js.map +1 -0
- package/build/utils.d.ts +8 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +9 -0
- package/build/utils.js.map +1 -0
- package/components.json +21 -0
- package/package.json +71 -4
- package/src/@/components/ui/alert.tsx +70 -0
- package/src/@/components/ui/button.tsx +60 -0
- package/src/@/components/ui/card.tsx +93 -0
- package/src/@/components/ui/input.tsx +22 -0
- package/src/@/components/ui/label.tsx +23 -0
- package/src/@/globals.css +57 -0
- package/src/@/lib/utils.ts +6 -0
- package/src/append.ts +65 -0
- package/src/components/connected.tsx +36 -0
- package/src/components/credit.tsx +30 -0
- package/src/components/fields.tsx +112 -0
- package/src/components/index.ts +8 -0
- package/src/components/offer.tsx +197 -0
- package/src/components/panel.tsx +299 -0
- package/src/components/route.tsx +20 -0
- package/src/components/screen.tsx +253 -0
- package/src/components/types.ts +85 -0
- package/src/consts.ts +53 -0
- package/src/entrypoints.ts +15 -0
- package/src/hooks/consts.local.ts +12 -0
- package/src/hooks/types.ts +79 -0
- package/src/hooks/use-email-login.ts +234 -0
- package/src/hooks/use-password.ts +137 -0
- package/src/hooks/use-translate.ts +14 -0
- package/src/i18n/auth/be.json +7 -0
- package/src/i18n/auth/de.json +7 -0
- package/src/i18n/auth/en.json +7 -0
- package/src/i18n/auth/es.json +7 -0
- package/src/i18n/auth/fr.json +7 -0
- package/src/i18n/auth/pl.json +7 -0
- package/src/i18n/auth/ru.json +7 -0
- package/src/i18n/auth/uk.json +7 -0
- package/src/i18n/be.json +100 -0
- package/src/i18n/de.json +100 -0
- package/src/i18n/en.json +100 -0
- package/src/i18n/es.json +100 -0
- package/src/i18n/fr.json +100 -0
- package/src/i18n/pl.json +100 -0
- package/src/i18n/ru.json +100 -0
- package/src/i18n/uk.json +100 -0
- package/src/i18n.ts +43 -0
- package/src/index.ts +22 -0
- package/src/password/types.ts +43 -0
- package/src/password.ts +65 -0
- package/src/plugin.ts +22 -0
- package/src/refusal/consts.local.ts +2 -0
- package/src/refusal/types.ts +64 -0
- package/src/refusal.ts +145 -0
- package/src/service.ts +95 -0
- package/src/state/types.ts +72 -0
- package/src/state.ts +115 -0
- package/src/step.ts +38 -0
- package/src/storage/types.ts +43 -0
- package/src/storage.ts +113 -0
- package/src/types.ts +111 -0
- package/src/utils/account.ts +12 -0
- package/src/utils/locale.ts +11 -0
- package/src/utils/text/types.ts +24 -0
- package/src/utils/text.ts +34 -0
- package/src/utils.ts +9 -0
- package/tests/helpers.ts +22 -0
- package/tests/i18n.spec.ts +94 -0
- package/tests/package-boundary.spec.ts +35 -0
- package/tests/password.spec.ts +56 -0
- package/tests/refusal.spec.ts +91 -0
- package/tests/state.spec.ts +99 -0
- package/tests/step.spec.ts +82 -0
- package/tests/storage.spec.ts +83 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { useId, useState, type FC, type FormEvent } from 'react'
|
|
2
|
+
import { Loader2 } from 'lucide-react'
|
|
3
|
+
import { EMAIL_PASSWORD_MAX_LENGTH, EMAIL_PIN_LENGTH } from '@owlmeans/auth-email'
|
|
4
|
+
import { Button } from '../@/components/ui/button.js'
|
|
5
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../@/components/ui/card.js'
|
|
6
|
+
import { Label } from '../@/components/ui/label.js'
|
|
7
|
+
import { cn } from '../@/lib/utils.js'
|
|
8
|
+
import { passwordPanelHelper } from '../password.js'
|
|
9
|
+
import { textUtils } from '../utils/text.js'
|
|
10
|
+
import type { EmailPasswordAction } from '../password/types.js'
|
|
11
|
+
import type { EmailRefusal } from '../refusal/types.js'
|
|
12
|
+
import type { Translate } from '../types.js'
|
|
13
|
+
import { PasswordInput, PinInput, RefusalAlert, SubmitButton } from './fields.js'
|
|
14
|
+
import type { PasswordPanelProps } from './types.js'
|
|
15
|
+
|
|
16
|
+
const english: Translate = (_key, defaultValue) => defaultValue
|
|
17
|
+
|
|
18
|
+
/** A record's moment in the reader's language, or null when it has none worth showing. */
|
|
19
|
+
const dateOf = (value: string | undefined, locale?: string): string | null => {
|
|
20
|
+
if (value == null) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
const date = new Date(value)
|
|
24
|
+
if (Number.isNaN(date.getTime())) {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(date)
|
|
29
|
+
} catch {
|
|
30
|
+
return date.toISOString().slice(0, 10)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The account's password: off on this deployment (a short notice), none yet (Set), or one (Change,
|
|
36
|
+
* Remove). Every action is confirmed by a PIN mailed to the account's own address: "Send code",
|
|
37
|
+
* then the code (and the new password, twice, for set and change), then Confirm.
|
|
38
|
+
*
|
|
39
|
+
* Presentational by contract — it performs no I/O, renders what it is given and reports what was
|
|
40
|
+
* pressed. `ConnectedPasswordPanel` is this panel wired to `usePassword`.
|
|
41
|
+
*/
|
|
42
|
+
export const PasswordPanel: FC<PasswordPanelProps> = ({
|
|
43
|
+
status, loading = false, busy = false, pending = null, refusal = null, done = null,
|
|
44
|
+
onSendPin, onConfirm, onCancel, onReload, title, description, translate, locale, className,
|
|
45
|
+
}) => {
|
|
46
|
+
const t = translate ?? english
|
|
47
|
+
const id = useId()
|
|
48
|
+
const [action, setAction] = useState<EmailPasswordAction | null>(null)
|
|
49
|
+
const [pin, setPin] = useState('')
|
|
50
|
+
const [password, setPassword] = useState('')
|
|
51
|
+
const [repeat, setRepeat] = useState('')
|
|
52
|
+
const [reveal, setReveal] = useState(false)
|
|
53
|
+
const [local, setLocal] = useState<EmailRefusal | null>(null)
|
|
54
|
+
|
|
55
|
+
const state = passwordPanelHelper.stateOf(status, loading)
|
|
56
|
+
const actions = passwordPanelHelper.actionsOf(status)
|
|
57
|
+
const sent = action != null && pending === passwordPanelHelper.purposeOf(action)
|
|
58
|
+
const withPassword = action != null && passwordPanelHelper.needsPassword(action)
|
|
59
|
+
const shown = local ?? refusal
|
|
60
|
+
|
|
61
|
+
const reset = (): void => {
|
|
62
|
+
setAction(null)
|
|
63
|
+
setPin('')
|
|
64
|
+
setPassword('')
|
|
65
|
+
setRepeat('')
|
|
66
|
+
setReveal(false)
|
|
67
|
+
setLocal(null)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const open = (next: EmailPasswordAction): void => {
|
|
71
|
+
reset()
|
|
72
|
+
setAction(next)
|
|
73
|
+
if (pending != null) {
|
|
74
|
+
onCancel()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const close = (): void => {
|
|
79
|
+
reset()
|
|
80
|
+
onCancel()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const send = async (): Promise<void> => {
|
|
84
|
+
if (action == null || busy) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
setLocal(null)
|
|
88
|
+
setPin('')
|
|
89
|
+
await onSendPin(passwordPanelHelper.purposeOf(action))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const submit = async (event: FormEvent): Promise<void> => {
|
|
93
|
+
event.preventDefault()
|
|
94
|
+
if (action == null || busy) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (!sent) {
|
|
98
|
+
await send()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const block = passwordPanelHelper.confirmBlock(action, { pin, password, repeat })
|
|
102
|
+
if (block != null) {
|
|
103
|
+
setLocal(block)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
setLocal(null)
|
|
107
|
+
if (await onConfirm(action, withPassword ? { pin, password } : { pin })) {
|
|
108
|
+
reset()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const updated = state === 'set' ? dateOf(status?.updatedAt, locale) : null
|
|
113
|
+
const control = 'h-11 text-base'
|
|
114
|
+
|
|
115
|
+
const summary = (() => {
|
|
116
|
+
switch (state) {
|
|
117
|
+
case 'loading':
|
|
118
|
+
return <p role="status" className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
119
|
+
<Loader2 className="size-4 animate-spin" aria-hidden />
|
|
120
|
+
{t('password.loading', 'Loading…')}
|
|
121
|
+
</p>
|
|
122
|
+
case 'unreadable':
|
|
123
|
+
return <div className="flex flex-col items-start gap-1 text-sm">
|
|
124
|
+
<p className="text-destructive">{t('password.unreadable', "We couldn't load your password settings.")}</p>
|
|
125
|
+
{onReload != null && <Button
|
|
126
|
+
type="button" variant="link" data-email-password-retry className="h-11 px-0"
|
|
127
|
+
onClick={() => void onReload()}
|
|
128
|
+
>
|
|
129
|
+
{t('password.retry', 'Try again')}
|
|
130
|
+
</Button>}
|
|
131
|
+
</div>
|
|
132
|
+
case 'off':
|
|
133
|
+
return <p data-email-password-notice className="text-sm text-muted-foreground">
|
|
134
|
+
{t('password.off', 'Password sign-in is not available here. Sign in with a code sent to your email.')}
|
|
135
|
+
</p>
|
|
136
|
+
case 'unset':
|
|
137
|
+
return <p className="text-sm">
|
|
138
|
+
{t('password.unset', "You don't have a password yet. You sign in with a code sent to your email.")}
|
|
139
|
+
</p>
|
|
140
|
+
default:
|
|
141
|
+
return <p className="text-sm">
|
|
142
|
+
{t('password.set', 'You have a password.')}
|
|
143
|
+
{updated != null && <> {textUtils.fill(t('password.updated', 'Last changed {{date}}.'), { date: updated })}</>}
|
|
144
|
+
</p>
|
|
145
|
+
}
|
|
146
|
+
})()
|
|
147
|
+
|
|
148
|
+
return <Card data-email-password-panel data-state={state} className={className}>
|
|
149
|
+
<CardHeader>
|
|
150
|
+
<CardTitle>{title ?? t('password.title', 'Password')}</CardTitle>
|
|
151
|
+
<CardDescription>
|
|
152
|
+
{description ?? t('password.description', 'Sign in with your email and a password instead of a code.')}
|
|
153
|
+
</CardDescription>
|
|
154
|
+
</CardHeader>
|
|
155
|
+
|
|
156
|
+
<CardContent className="flex flex-col gap-4">
|
|
157
|
+
{summary}
|
|
158
|
+
|
|
159
|
+
{done != null && action == null && <p role="status" data-email-password-success={done} className="text-sm">
|
|
160
|
+
{done === 'remove'
|
|
161
|
+
? t('password.done.remove', 'Your password has been removed.')
|
|
162
|
+
: done === 'change'
|
|
163
|
+
? t('password.done.change', 'Your password has been changed.')
|
|
164
|
+
: t('password.done.set', 'Your password has been set.')}
|
|
165
|
+
{' '}
|
|
166
|
+
{t('password.devices', 'Other devices will need a code the next time they sign in.')}
|
|
167
|
+
</p>}
|
|
168
|
+
|
|
169
|
+
{action == null && actions.length > 0 && <div className="flex flex-col gap-2 sm:flex-row">
|
|
170
|
+
{actions.map(next => <Button
|
|
171
|
+
key={next}
|
|
172
|
+
type="button"
|
|
173
|
+
variant="outline"
|
|
174
|
+
data-email-password-action={next}
|
|
175
|
+
className={cn(control, next === 'remove' && 'text-destructive hover:text-destructive')}
|
|
176
|
+
onClick={() => open(next)}
|
|
177
|
+
>
|
|
178
|
+
{next === 'remove'
|
|
179
|
+
? t('password.action.remove', 'Remove password')
|
|
180
|
+
: next === 'change'
|
|
181
|
+
? t('password.action.change', 'Change password')
|
|
182
|
+
: t('password.action.set', 'Set a password')}
|
|
183
|
+
</Button>)}
|
|
184
|
+
</div>}
|
|
185
|
+
|
|
186
|
+
{action != null && <form
|
|
187
|
+
noValidate
|
|
188
|
+
data-email-password-form={action}
|
|
189
|
+
data-sent={sent ? 'true' : undefined}
|
|
190
|
+
className="flex flex-col gap-4"
|
|
191
|
+
onSubmit={event => void submit(event)}
|
|
192
|
+
>
|
|
193
|
+
<p className="text-sm text-muted-foreground">
|
|
194
|
+
{action === 'remove'
|
|
195
|
+
? t('password.explain.remove', "Without a password you sign in with a code sent to your email. We'll email you a code to confirm.")
|
|
196
|
+
: action === 'change'
|
|
197
|
+
? t('password.explain.change', "To change your password, confirm it's you with a code we'll email you.")
|
|
198
|
+
: t('password.explain.set', "To set a password, confirm it's you with a code we'll email you.")}
|
|
199
|
+
</p>
|
|
200
|
+
|
|
201
|
+
{sent && <>
|
|
202
|
+
<p role="status" data-email-password-sent className="text-sm">
|
|
203
|
+
{t('password.sent', 'We sent a code to your email.')}
|
|
204
|
+
</p>
|
|
205
|
+
<div className="flex flex-col gap-2">
|
|
206
|
+
<Label htmlFor={`${id}-pin`}>{t('password.pin-label', 'Code from the email')}</Label>
|
|
207
|
+
<PinInput
|
|
208
|
+
id={`${id}-pin`}
|
|
209
|
+
name="pin"
|
|
210
|
+
data-email-password-pin
|
|
211
|
+
length={EMAIL_PIN_LENGTH}
|
|
212
|
+
value={pin}
|
|
213
|
+
readOnly={busy}
|
|
214
|
+
autoFocus
|
|
215
|
+
aria-invalid={shown?.kind === 'code' || shown?.kind === 'pin-format' || undefined}
|
|
216
|
+
className="max-w-60"
|
|
217
|
+
onChange={event => { setPin(event.target.value.replace(/\D/g, '').slice(0, EMAIL_PIN_LENGTH)); setLocal(null) }}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
{withPassword && <>
|
|
221
|
+
<div className="flex flex-col gap-2">
|
|
222
|
+
<Label htmlFor={`${id}-new`}>{t('password.new-label', 'New password')}</Label>
|
|
223
|
+
<PasswordInput
|
|
224
|
+
id={`${id}-new`}
|
|
225
|
+
name="new-password"
|
|
226
|
+
data-email-password-new
|
|
227
|
+
autoComplete="new-password"
|
|
228
|
+
maxLength={EMAIL_PASSWORD_MAX_LENGTH}
|
|
229
|
+
value={password}
|
|
230
|
+
readOnly={busy}
|
|
231
|
+
aria-invalid={shown?.kind === 'weak' || shown?.kind === 'password-empty' || undefined}
|
|
232
|
+
reveal={reveal}
|
|
233
|
+
onReveal={setReveal}
|
|
234
|
+
showLabel={t('login.show-password', 'Show password')}
|
|
235
|
+
hideLabel={t('login.hide-password', 'Hide password')}
|
|
236
|
+
toggleProps={{ 'data-email-password-toggle': '' }}
|
|
237
|
+
onChange={event => { setPassword(event.target.value); setLocal(null) }}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
<div className="flex flex-col gap-2">
|
|
241
|
+
<Label htmlFor={`${id}-repeat`}>{t('password.repeat-label', 'Repeat the password')}</Label>
|
|
242
|
+
<PasswordInput
|
|
243
|
+
id={`${id}-repeat`}
|
|
244
|
+
name="repeat-password"
|
|
245
|
+
data-email-password-repeat
|
|
246
|
+
autoComplete="new-password"
|
|
247
|
+
maxLength={EMAIL_PASSWORD_MAX_LENGTH}
|
|
248
|
+
value={repeat}
|
|
249
|
+
readOnly={busy}
|
|
250
|
+
aria-invalid={shown?.kind === 'mismatch' || undefined}
|
|
251
|
+
reveal={reveal}
|
|
252
|
+
onReveal={setReveal}
|
|
253
|
+
showLabel={t('login.show-password', 'Show password')}
|
|
254
|
+
hideLabel={t('login.hide-password', 'Hide password')}
|
|
255
|
+
onChange={event => { setRepeat(event.target.value); setLocal(null) }}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
</>}
|
|
259
|
+
</>}
|
|
260
|
+
|
|
261
|
+
<RefusalAlert refusal={shown} t={t} locale={locale} marker="data-email-password-error" className="text-left" />
|
|
262
|
+
|
|
263
|
+
<div className="flex flex-col gap-2 sm:flex-row">
|
|
264
|
+
{sent
|
|
265
|
+
? <SubmitButton
|
|
266
|
+
data-email-password-confirm
|
|
267
|
+
busy={busy}
|
|
268
|
+
blocked={passwordPanelHelper.confirmBlock(action, { pin, password, repeat }) != null}
|
|
269
|
+
className="sm:w-auto"
|
|
270
|
+
>
|
|
271
|
+
{busy ? t('password.confirming', 'Saving…') : t('password.confirm', 'Confirm')}
|
|
272
|
+
</SubmitButton>
|
|
273
|
+
: <SubmitButton data-email-password-send busy={busy} className="sm:w-auto">
|
|
274
|
+
{busy ? t('password.sending', 'Sending…') : t('password.send', 'Send code')}
|
|
275
|
+
</SubmitButton>}
|
|
276
|
+
{sent && <Button
|
|
277
|
+
type="button"
|
|
278
|
+
variant="link"
|
|
279
|
+
data-email-password-resend
|
|
280
|
+
aria-disabled={busy || undefined}
|
|
281
|
+
className={control}
|
|
282
|
+
onClick={() => void send()}
|
|
283
|
+
>
|
|
284
|
+
{t('password.resend', 'Send a new code')}
|
|
285
|
+
</Button>}
|
|
286
|
+
<Button
|
|
287
|
+
type="button"
|
|
288
|
+
variant="ghost"
|
|
289
|
+
data-email-password-cancel
|
|
290
|
+
className={control}
|
|
291
|
+
onClick={close}
|
|
292
|
+
>
|
|
293
|
+
{t('password.cancel', 'Cancel')}
|
|
294
|
+
</Button>
|
|
295
|
+
</div>
|
|
296
|
+
</form>}
|
|
297
|
+
</CardContent>
|
|
298
|
+
</Card>
|
|
299
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FC } from 'react'
|
|
2
|
+
import { useContext } from '@owlmeans/client'
|
|
3
|
+
import type { AuthenticationRendererProps } from '@owlmeans/client-auth/manager'
|
|
4
|
+
import { EMAIL_LOGIN_SERVICE } from '@owlmeans/auth-email'
|
|
5
|
+
import type { EmailLoginClientService } from '../types.js'
|
|
6
|
+
import { LocalizedEmailLoginScreen } from './screen.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What the e-mail method's authentication route renders: the screen registered for this context
|
|
10
|
+
* (`appendEmailLogin({ screen })`, or a replacement from `registerEmailLoginScreen`), else the
|
|
11
|
+
* package's own.
|
|
12
|
+
*/
|
|
13
|
+
export const EmailLoginRoute: FC<Partial<AuthenticationRendererProps>> = () => {
|
|
14
|
+
const ctx = useContext()
|
|
15
|
+
const Screen = ctx.hasService(EMAIL_LOGIN_SERVICE)
|
|
16
|
+
? ctx.service<EmailLoginClientService>(EMAIL_LOGIN_SERVICE).screen() ?? LocalizedEmailLoginScreen
|
|
17
|
+
: LocalizedEmailLoginScreen
|
|
18
|
+
|
|
19
|
+
return <Screen />
|
|
20
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { isValidElement, useId, type FC, type FormEvent } from 'react'
|
|
2
|
+
import { Loader2 } from 'lucide-react'
|
|
3
|
+
import { useLanguage } from '@owlmeans/client-i18n'
|
|
4
|
+
import { EMAIL_ADDRESS_MAX_LENGTH } from '@owlmeans/auth-email'
|
|
5
|
+
import { Button } from '../@/components/ui/button.js'
|
|
6
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../@/components/ui/card.js'
|
|
7
|
+
import { Input } from '../@/components/ui/input.js'
|
|
8
|
+
import { Label } from '../@/components/ui/label.js'
|
|
9
|
+
import { cn } from '../@/lib/utils.js'
|
|
10
|
+
import { useEmailLogin } from '../hooks/use-email-login.js'
|
|
11
|
+
import { useEmailAuthTranslate } from '../hooks/use-translate.js'
|
|
12
|
+
import { emailLoginStateHelper } from '../state.js'
|
|
13
|
+
import { textUtils } from '../utils/text.js'
|
|
14
|
+
import type { EmailLoginScreenProps, Translate } from '../types.js'
|
|
15
|
+
import { CaptchaNotice, PasswordInput, PinInput, RefusalAlert, SubmitButton } from './fields.js'
|
|
16
|
+
import { EmailLoginCredit } from './credit.js'
|
|
17
|
+
|
|
18
|
+
const english: Translate = (_key, defaultValue) => defaultValue
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The e-mail sign-in card — the chooser card's twin (`@owlmeans/web-panel`'s `LoginScreen`): the
|
|
22
|
+
* same centred card, logo, title, subtitle and footer slot, the same inline viewport height.
|
|
23
|
+
*
|
|
24
|
+
* Three steps share it: the address ("Receive authentication PIN code"), the mailed PIN, and — only
|
|
25
|
+
* where the deployment offers passwords — the address with a password. Enter submits the step on
|
|
26
|
+
* screen. Nothing on it is ever natively `disabled`.
|
|
27
|
+
*
|
|
28
|
+
* Pure with respect to i18n: `translate` is a prop (English without it), `locale` too.
|
|
29
|
+
* `LocalizedEmailLoginScreen` binds both to the application's own.
|
|
30
|
+
*/
|
|
31
|
+
export const EmailLoginScreen: FC<EmailLoginScreenProps> = props => {
|
|
32
|
+
const t = props.translate ?? english
|
|
33
|
+
const model = useEmailLogin({ locale: props.locale })
|
|
34
|
+
const { state } = model
|
|
35
|
+
const id = useId()
|
|
36
|
+
const busy = state.busy != null
|
|
37
|
+
const Logo = props.Logo
|
|
38
|
+
|
|
39
|
+
const onSubmit = (event: FormEvent): void => {
|
|
40
|
+
event.preventDefault()
|
|
41
|
+
void model.submit()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const subtitle = props.subtitle ?? (state.step === 'password'
|
|
45
|
+
? t('login.password-subtitle', 'Sign in with your email and password.')
|
|
46
|
+
: state.step === 'pin'
|
|
47
|
+
? t('login.pin-subtitle', 'Check your inbox for the code.')
|
|
48
|
+
: t('login.subtitle', "We'll email you a code to sign in. New here? The same code creates your account."))
|
|
49
|
+
|
|
50
|
+
const emailField = (autoFocus: boolean) => <div className="flex flex-col gap-2 text-left">
|
|
51
|
+
<Label htmlFor={`${id}-email`}>{t('login.email-label', 'Email')}</Label>
|
|
52
|
+
<Input
|
|
53
|
+
id={`${id}-email`}
|
|
54
|
+
name="email"
|
|
55
|
+
data-email-login-email
|
|
56
|
+
type="email"
|
|
57
|
+
inputMode="email"
|
|
58
|
+
autoComplete="username"
|
|
59
|
+
autoCapitalize="none"
|
|
60
|
+
autoCorrect="off"
|
|
61
|
+
spellCheck={false}
|
|
62
|
+
maxLength={EMAIL_ADDRESS_MAX_LENGTH}
|
|
63
|
+
placeholder={t('login.email-placeholder', 'you@example.com')}
|
|
64
|
+
value={state.email}
|
|
65
|
+
readOnly={busy}
|
|
66
|
+
autoFocus={autoFocus}
|
|
67
|
+
aria-invalid={state.refusal?.kind === 'email' || undefined}
|
|
68
|
+
className="h-11 text-base"
|
|
69
|
+
onChange={event => model.setEmail(event.target.value)}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
const link = 'h-11 px-2 text-sm'
|
|
74
|
+
// A tertiary button: no fill and no visible border until hovered, the primary text colour kept.
|
|
75
|
+
const tertiary = 'h-11 px-3 text-sm text-primary border border-transparent hover:border-border hover:text-primary'
|
|
76
|
+
|
|
77
|
+
const step = (() => {
|
|
78
|
+
switch (state.step) {
|
|
79
|
+
case 'pin':
|
|
80
|
+
return <form noValidate onSubmit={onSubmit} data-email-login-form="pin" className="flex flex-col gap-4">
|
|
81
|
+
<p role="status" data-email-login-notice={state.notice ?? 'sent'} className="text-center text-sm">
|
|
82
|
+
{state.notice === 'device'
|
|
83
|
+
? t('login.device', "We don't recognise this device. Enter the code we sent to your email.")
|
|
84
|
+
: textUtils.fill(t('login.sent', 'We sent a sign-in code to {{email}}.'), {
|
|
85
|
+
email: emailLoginStateHelper.normalizeEmail(state.email),
|
|
86
|
+
})}
|
|
87
|
+
</p>
|
|
88
|
+
<div className="flex flex-col gap-2 text-left">
|
|
89
|
+
<Label htmlFor={`${id}-pin`}>{t('login.pin-label', 'Code from the email')}</Label>
|
|
90
|
+
<PinInput
|
|
91
|
+
id={`${id}-pin`}
|
|
92
|
+
name="pin"
|
|
93
|
+
data-email-login-pin
|
|
94
|
+
length={model.pinLength}
|
|
95
|
+
value={state.pin}
|
|
96
|
+
readOnly={busy}
|
|
97
|
+
autoFocus
|
|
98
|
+
aria-invalid={state.refusal?.kind === 'code' || state.refusal?.kind === 'pin-format' || undefined}
|
|
99
|
+
onChange={event => model.setPin(event.target.value)}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
<SubmitButton
|
|
103
|
+
data-email-login-submit
|
|
104
|
+
data-mode="pin"
|
|
105
|
+
busy={state.busy === 'sign-in'}
|
|
106
|
+
blocked={emailLoginStateHelper.verifyBlock(state) != null}
|
|
107
|
+
>
|
|
108
|
+
{state.busy === 'sign-in' ? t('login.verifying', 'Signing in…') : t('login.verify', 'Sign in')}
|
|
109
|
+
</SubmitButton>
|
|
110
|
+
<div className="flex flex-col items-center justify-center gap-1 sm:flex-row sm:gap-4">
|
|
111
|
+
<Button
|
|
112
|
+
type="button"
|
|
113
|
+
variant="link"
|
|
114
|
+
data-email-login-resend
|
|
115
|
+
aria-busy={state.busy === 'send' || undefined}
|
|
116
|
+
className={link}
|
|
117
|
+
onClick={() => void model.send()}
|
|
118
|
+
>
|
|
119
|
+
{state.busy === 'send' ? t('login.sending', 'Sending…') : t('login.resend', 'Send a new code')}
|
|
120
|
+
</Button>
|
|
121
|
+
<Button type="button" variant="link" data-email-login-change className={link} onClick={model.changeEmail}>
|
|
122
|
+
{t('login.change-email', 'Change email')}
|
|
123
|
+
</Button>
|
|
124
|
+
</div>
|
|
125
|
+
</form>
|
|
126
|
+
|
|
127
|
+
case 'password':
|
|
128
|
+
return <form noValidate onSubmit={onSubmit} data-email-login-form="password" className="flex flex-col gap-4">
|
|
129
|
+
{emailField(false)}
|
|
130
|
+
<div className="flex flex-col gap-2 text-left">
|
|
131
|
+
<Label htmlFor={`${id}-password`}>{t('login.password-label', 'Password')}</Label>
|
|
132
|
+
<PasswordInput
|
|
133
|
+
id={`${id}-password`}
|
|
134
|
+
name="password"
|
|
135
|
+
data-email-login-password
|
|
136
|
+
autoComplete="current-password"
|
|
137
|
+
value={state.password}
|
|
138
|
+
readOnly={busy}
|
|
139
|
+
autoFocus
|
|
140
|
+
aria-invalid={state.refusal?.kind === 'credentials' || state.refusal?.kind === 'password-empty'
|
|
141
|
+
|| undefined}
|
|
142
|
+
reveal={state.reveal}
|
|
143
|
+
onReveal={model.setReveal}
|
|
144
|
+
showLabel={t('login.show-password', 'Show password')}
|
|
145
|
+
hideLabel={t('login.hide-password', 'Hide password')}
|
|
146
|
+
toggleProps={{ 'data-email-login-password-toggle': '' }}
|
|
147
|
+
onChange={event => model.setPassword(event.target.value)}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
<SubmitButton
|
|
151
|
+
data-email-login-submit
|
|
152
|
+
data-mode="password"
|
|
153
|
+
busy={state.busy === 'sign-in'}
|
|
154
|
+
blocked={emailLoginStateHelper.signInBlock(state) != null}
|
|
155
|
+
>
|
|
156
|
+
{state.busy === 'sign-in' ? t('login.verifying', 'Signing in…') : t('login.password-submit', 'Sign in')}
|
|
157
|
+
</SubmitButton>
|
|
158
|
+
<Button type="button" variant="link" data-email-login-use-pin className={link} onClick={model.choosePin}>
|
|
159
|
+
{t('login.use-pin', 'Sign in with a code instead')}
|
|
160
|
+
</Button>
|
|
161
|
+
</form>
|
|
162
|
+
|
|
163
|
+
case 'done':
|
|
164
|
+
return <p role="status" data-email-login-done className="flex items-center justify-center gap-2 text-sm">
|
|
165
|
+
<Loader2 className="size-4 animate-spin" aria-hidden />
|
|
166
|
+
{t('login.done', 'Signing you in…')}
|
|
167
|
+
</p>
|
|
168
|
+
|
|
169
|
+
default:
|
|
170
|
+
return <form noValidate onSubmit={onSubmit} data-email-login-form="email" className="flex flex-col gap-4">
|
|
171
|
+
{emailField(true)}
|
|
172
|
+
<SubmitButton
|
|
173
|
+
data-email-login-send
|
|
174
|
+
busy={state.busy === 'send'}
|
|
175
|
+
blocked={emailLoginStateHelper.sendBlock(state) != null}
|
|
176
|
+
>
|
|
177
|
+
{state.busy === 'send' ? t('login.sending', 'Sending…') : t('login.send', 'Receive authentication PIN code')}
|
|
178
|
+
</SubmitButton>
|
|
179
|
+
{model.passwordOffered && <Button
|
|
180
|
+
type="button"
|
|
181
|
+
variant="ghost"
|
|
182
|
+
data-email-login-use-password
|
|
183
|
+
className={tertiary}
|
|
184
|
+
onClick={model.choosePassword}
|
|
185
|
+
>
|
|
186
|
+
{t('login.use-password', 'Already set a password? Use it')}
|
|
187
|
+
</Button>}
|
|
188
|
+
</form>
|
|
189
|
+
}
|
|
190
|
+
})()
|
|
191
|
+
|
|
192
|
+
const card = <Card data-email-login-card className={cn('w-full max-w-sm', props.containerClassName)}>
|
|
193
|
+
<CardHeader className="items-center gap-2 text-center">
|
|
194
|
+
{Logo != null && <div className="flex justify-center pb-2">
|
|
195
|
+
{isValidElement(Logo) ? Logo : typeof Logo === 'function' ? <Logo className="h-10 w-auto" /> : Logo}
|
|
196
|
+
</div>}
|
|
197
|
+
<CardTitle className="text-xl">{props.title ?? t('login.title', 'Sign in with email')}</CardTitle>
|
|
198
|
+
<CardDescription>{subtitle}</CardDescription>
|
|
199
|
+
</CardHeader>
|
|
200
|
+
|
|
201
|
+
<CardContent className="flex flex-col gap-4">
|
|
202
|
+
{step}
|
|
203
|
+
|
|
204
|
+
<RefusalAlert refusal={state.refusal} t={t} locale={props.locale} marker="data-email-login-error" />
|
|
205
|
+
|
|
206
|
+
{state.optionsFailed && state.options == null && state.refusal == null
|
|
207
|
+
&& <div data-email-login-options-error className="flex flex-col items-center gap-1 text-center text-sm">
|
|
208
|
+
<p role="alert" className="text-destructive">
|
|
209
|
+
{t('login.options-failed', "We couldn't load the sign-in settings.")}
|
|
210
|
+
</p>
|
|
211
|
+
<Button type="button" variant="link" data-email-login-retry className={link} onClick={() => void model.retryOptions()}>
|
|
212
|
+
{t('login.retry', 'Try again')}
|
|
213
|
+
</Button>
|
|
214
|
+
</div>}
|
|
215
|
+
|
|
216
|
+
{model.captcha && state.step !== 'pin' && state.step !== 'done' && <CaptchaNotice t={t} />}
|
|
217
|
+
|
|
218
|
+
{model.otherOptions != null && state.step !== 'done' && <a
|
|
219
|
+
href={model.otherOptions.href}
|
|
220
|
+
data-email-login-other
|
|
221
|
+
className="inline-flex h-11 items-center justify-center text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline"
|
|
222
|
+
onClick={model.otherOptions.go}
|
|
223
|
+
>
|
|
224
|
+
{t('login.other', 'Other sign-in options')}
|
|
225
|
+
</a>}
|
|
226
|
+
</CardContent>
|
|
227
|
+
|
|
228
|
+
<div className="px-6">
|
|
229
|
+
{props.footer ?? <EmailLoginCredit t={t} />}
|
|
230
|
+
</div>
|
|
231
|
+
</Card>
|
|
232
|
+
|
|
233
|
+
// The viewport height is INLINE, like the chooser's: this screen is rendered straight out of the
|
|
234
|
+
// authentication route into a chain that carries no height, and a utility class new to the
|
|
235
|
+
// consumer's stylesheet is exactly what a stale `@source` scan drops. `props.style` still wins.
|
|
236
|
+
return <div
|
|
237
|
+
data-email-login-screen
|
|
238
|
+
data-step={state.step}
|
|
239
|
+
data-busy={state.busy ?? undefined}
|
|
240
|
+
style={{ minHeight: '100dvh', ...props.style }}
|
|
241
|
+
className={cn('flex w-full items-center justify-center p-4', props.className)}
|
|
242
|
+
>
|
|
243
|
+
{card}
|
|
244
|
+
</div>
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** {@link EmailLoginScreen} bound to the application's own translations and language. */
|
|
248
|
+
export const LocalizedEmailLoginScreen: FC<EmailLoginScreenProps> = props => {
|
|
249
|
+
const t = useEmailAuthTranslate()
|
|
250
|
+
const [lng] = useLanguage()
|
|
251
|
+
|
|
252
|
+
return <EmailLoginScreen {...props} translate={props.translate ?? t} locale={props.locale ?? lng} />
|
|
253
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
2
|
+
import type { EmailPasswordPurpose, EmailPasswordStatusView } from '@owlmeans/auth-email'
|
|
3
|
+
import type { EmailRefusal } from '../refusal/types.js'
|
|
4
|
+
import type { EmailPasswordAction } from '../password/types.js'
|
|
5
|
+
import type { EmailPasswordAliases, Translate } from '../types.js'
|
|
6
|
+
|
|
7
|
+
/** A password field with a show/hide toggle; every other prop goes to the `<input>`. */
|
|
8
|
+
export interface PasswordInputProps extends Omit<ComponentProps<'input'>, 'type'> {
|
|
9
|
+
reveal: boolean
|
|
10
|
+
onReveal: (value: boolean) => void
|
|
11
|
+
/** The toggle's accessible name while the password is hidden. */
|
|
12
|
+
showLabel: string
|
|
13
|
+
/** The toggle's accessible name while the password is shown. */
|
|
14
|
+
hideLabel: string
|
|
15
|
+
/** `data-*` hooks for the toggle button. */
|
|
16
|
+
toggleProps?: Record<`data-${string}`, string>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A PIN field: digits only, the one-time-code autofill, as many characters as the PIN has. */
|
|
20
|
+
export interface PinInputProps extends Omit<ComponentProps<'input'>, 'type' | 'inputMode' | 'autoComplete' | 'maxLength'> {
|
|
21
|
+
length: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A primary submit button that is never natively `disabled` — see the screen. */
|
|
25
|
+
export interface SubmitButtonProps extends ComponentProps<'button'> {
|
|
26
|
+
busy: boolean
|
|
27
|
+
/** The action cannot go yet; pressing it says why (`aria-disabled`, never `disabled`). */
|
|
28
|
+
blocked?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RefusalAlertProps {
|
|
32
|
+
refusal: EmailRefusal | null
|
|
33
|
+
t: Translate
|
|
34
|
+
locale?: string
|
|
35
|
+
/** The `data-*` attribute that marks this alert for tests (`data-email-login-error`, …). */
|
|
36
|
+
marker: `data-${string}`
|
|
37
|
+
className?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CaptchaNoticeProps {
|
|
41
|
+
t: Translate
|
|
42
|
+
className?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface EmailLoginCreditProps {
|
|
46
|
+
t: Translate
|
|
47
|
+
className?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The password surface, as a function of what it is given — no I/O: it renders what it is handed
|
|
52
|
+
* and reports what was pressed. `ConnectedPasswordPanel` is this panel wired to `usePassword`.
|
|
53
|
+
*/
|
|
54
|
+
export interface PasswordPanelProps {
|
|
55
|
+
status: EmailPasswordStatusView | null
|
|
56
|
+
loading?: boolean
|
|
57
|
+
/** A PIN request or a confirmation is in flight. */
|
|
58
|
+
busy?: boolean
|
|
59
|
+
/** The purpose a confirmation PIN was mailed for; the panel shows the PIN step while it is set. */
|
|
60
|
+
pending?: EmailPasswordPurpose | null
|
|
61
|
+
refusal?: EmailRefusal | null
|
|
62
|
+
/** The last action that went through. */
|
|
63
|
+
done?: EmailPasswordAction | null
|
|
64
|
+
/** Mail a confirmation PIN. Resolves whether it was sent. */
|
|
65
|
+
onSendPin: (purpose: EmailPasswordPurpose) => Promise<boolean>
|
|
66
|
+
/** Confirm the open action. Resolves whether it went through; the panel closes the action then. */
|
|
67
|
+
onConfirm: (action: EmailPasswordAction, input: { pin: string, password?: string }) => Promise<boolean>
|
|
68
|
+
/** The open action was abandoned. */
|
|
69
|
+
onCancel: () => void
|
|
70
|
+
/** Read the status again after it could not be read. */
|
|
71
|
+
onReload?: () => Promise<void> | void
|
|
72
|
+
/** Already-translated header copy; the package's own by default. */
|
|
73
|
+
title?: ReactNode
|
|
74
|
+
description?: ReactNode
|
|
75
|
+
translate?: Translate
|
|
76
|
+
locale?: string
|
|
77
|
+
className?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ConnectedPasswordPanelProps extends Pick<PasswordPanelProps, 'title' | 'description' | 'className'> {
|
|
81
|
+
/** Where the password routes are mounted; see `EmailPasswordAliases`. */
|
|
82
|
+
aliases?: EmailPasswordAliases
|
|
83
|
+
/** Overrides the package's own translator. */
|
|
84
|
+
translate?: Translate
|
|
85
|
+
}
|