@atproto/oauth-provider-ui 0.0.2
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/.linguirc +57 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +6 -0
- package/LICENSE.txt +7 -0
- package/dist/assets/COdVzed-.css +3 -0
- package/dist/assets/COdVzed-.js +100 -0
- package/dist/assets/COdVzed-.js.map +1 -0
- package/dist/assets/Cqnfnbvc.js +6 -0
- package/dist/assets/Cqnfnbvc.js.map +1 -0
- package/dist/assets/bundle-manifest.json +630 -0
- package/dist/assets/error-view-Bu4y7Nd8.js +208 -0
- package/dist/assets/error-view-Bu4y7Nd8.js.map +1 -0
- package/dist/assets/index-DXlCRM6V.js +36 -0
- package/dist/assets/index-DXlCRM6V.js.map +1 -0
- package/dist/assets/messages-2GoTm2qL.js +4 -0
- package/dist/assets/messages-2GoTm2qL.js.map +1 -0
- package/dist/assets/messages-6Cn2Jbhw.js +4 -0
- package/dist/assets/messages-6Cn2Jbhw.js.map +1 -0
- package/dist/assets/messages-75hFgOK2.js +4 -0
- package/dist/assets/messages-75hFgOK2.js.map +1 -0
- package/dist/assets/messages-B3OK4k0O.js +4 -0
- package/dist/assets/messages-B3OK4k0O.js.map +1 -0
- package/dist/assets/messages-BNXlPzKV.js +4 -0
- package/dist/assets/messages-BNXlPzKV.js.map +1 -0
- package/dist/assets/messages-BUygB8mD.js +4 -0
- package/dist/assets/messages-BUygB8mD.js.map +1 -0
- package/dist/assets/messages-BVPPcwNr.js +4 -0
- package/dist/assets/messages-BVPPcwNr.js.map +1 -0
- package/dist/assets/messages-BbbWUQS8.js +4 -0
- package/dist/assets/messages-BbbWUQS8.js.map +1 -0
- package/dist/assets/messages-BibKCYyW.js +4 -0
- package/dist/assets/messages-BibKCYyW.js.map +1 -0
- package/dist/assets/messages-BlPrr9_7.js +4 -0
- package/dist/assets/messages-BlPrr9_7.js.map +1 -0
- package/dist/assets/messages-ByVCw40U.js +4 -0
- package/dist/assets/messages-ByVCw40U.js.map +1 -0
- package/dist/assets/messages-C5DU1neP.js +4 -0
- package/dist/assets/messages-C5DU1neP.js.map +1 -0
- package/dist/assets/messages-C6IgUtbX.js +4 -0
- package/dist/assets/messages-C6IgUtbX.js.map +1 -0
- package/dist/assets/messages-C92Zzt2o.js +4 -0
- package/dist/assets/messages-C92Zzt2o.js.map +1 -0
- package/dist/assets/messages-CGZqYT14.js +4 -0
- package/dist/assets/messages-CGZqYT14.js.map +1 -0
- package/dist/assets/messages-CGlsy4wt.js +4 -0
- package/dist/assets/messages-CGlsy4wt.js.map +1 -0
- package/dist/assets/messages-CPT1nd0u.js +4 -0
- package/dist/assets/messages-CPT1nd0u.js.map +1 -0
- package/dist/assets/messages-CTTdXyw_.js +4 -0
- package/dist/assets/messages-CTTdXyw_.js.map +1 -0
- package/dist/assets/messages-ChK_C_Pj.js +4 -0
- package/dist/assets/messages-ChK_C_Pj.js.map +1 -0
- package/dist/assets/messages-CjJbk7Uf.js +4 -0
- package/dist/assets/messages-CjJbk7Uf.js.map +1 -0
- package/dist/assets/messages-CoiLjLYO.js +4 -0
- package/dist/assets/messages-CoiLjLYO.js.map +1 -0
- package/dist/assets/messages-Cwx6B4Ti.js +4 -0
- package/dist/assets/messages-Cwx6B4Ti.js.map +1 -0
- package/dist/assets/messages-D0uXAp_H.js +4 -0
- package/dist/assets/messages-D0uXAp_H.js.map +1 -0
- package/dist/assets/messages-DG0_arU0.js +4 -0
- package/dist/assets/messages-DG0_arU0.js.map +1 -0
- package/dist/assets/messages-DOXFJh9K.js +4 -0
- package/dist/assets/messages-DOXFJh9K.js.map +1 -0
- package/dist/assets/messages-DPK7nOoC.js +4 -0
- package/dist/assets/messages-DPK7nOoC.js.map +1 -0
- package/dist/assets/messages-Duccgtu0.js +4 -0
- package/dist/assets/messages-Duccgtu0.js.map +1 -0
- package/dist/assets/messages-DxTqgsHq.js +4 -0
- package/dist/assets/messages-DxTqgsHq.js.map +1 -0
- package/dist/assets/messages-E5_lTg7A.js +4 -0
- package/dist/assets/messages-E5_lTg7A.js.map +1 -0
- package/dist/assets/messages-UhunAjh1.js +4 -0
- package/dist/assets/messages-UhunAjh1.js.map +1 -0
- package/dist/assets/messages-Xg_3YLGw.js +4 -0
- package/dist/assets/messages-Xg_3YLGw.js.map +1 -0
- package/dist/assets/messages-iliBQHY2.js +4 -0
- package/dist/assets/messages-iliBQHY2.js.map +1 -0
- package/dist/assets/messages-lRprpIl-.js +4 -0
- package/dist/assets/messages-lRprpIl-.js.map +1 -0
- package/dist/assets/messages-pbPHQbz1.js +4 -0
- package/dist/assets/messages-pbPHQbz1.js.map +1 -0
- package/dist/assets/messages-q-O7ZQGs.js +4 -0
- package/dist/assets/messages-q-O7ZQGs.js.map +1 -0
- package/dist/lib/index.d.ts +19 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +47 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/tsconfig.backend.tsbuildinfo +1 -0
- package/lib/index.ts +72 -0
- package/package.json +73 -0
- package/rollup.config.js +102 -0
- package/src/authorization-page.html +183 -0
- package/src/authorization-page.tsx +55 -0
- package/src/backend-data.ts +35 -0
- package/src/components/forms/button-toggle-visibility.tsx +43 -0
- package/src/components/forms/button.tsx +60 -0
- package/src/components/forms/fieldset.tsx +55 -0
- package/src/components/forms/form-card-async.tsx +103 -0
- package/src/components/forms/form-card.tsx +49 -0
- package/src/components/forms/input-checkbox.tsx +78 -0
- package/src/components/forms/input-container.tsx +107 -0
- package/src/components/forms/input-email-address.tsx +65 -0
- package/src/components/forms/input-new-password.tsx +62 -0
- package/src/components/forms/input-password.tsx +87 -0
- package/src/components/forms/input-text.tsx +82 -0
- package/src/components/forms/input-token.tsx +94 -0
- package/src/components/forms/wizard-card.tsx +116 -0
- package/src/components/layouts/layout-title-page.tsx +77 -0
- package/src/components/layouts/layout-welcome.tsx +73 -0
- package/src/components/utils/account-identifier.tsx +23 -0
- package/src/components/utils/account-image.tsx +33 -0
- package/src/components/utils/admonition.tsx +52 -0
- package/src/components/utils/client-name.tsx +45 -0
- package/src/components/utils/error-card.tsx +93 -0
- package/src/components/utils/error-message.tsx +88 -0
- package/src/components/utils/help-card.tsx +46 -0
- package/src/components/utils/icons.tsx +88 -0
- package/src/components/utils/link-anchor.tsx +28 -0
- package/src/components/utils/link-title.tsx +26 -0
- package/src/components/utils/multi-lang-string.tsx +56 -0
- package/src/components/utils/password-strength-label.tsx +37 -0
- package/src/components/utils/password-strength-meter.tsx +58 -0
- package/src/components/utils/url-viewer.tsx +73 -0
- package/src/cookies.ts +11 -0
- package/src/error-page.html +125 -0
- package/src/error-page.tsx +29 -0
- package/src/hooks/use-api.ts +182 -0
- package/src/hooks/use-async-action.ts +120 -0
- package/src/hooks/use-bound-dispatch.ts +5 -0
- package/src/hooks/use-browser-color-scheme.ts +31 -0
- package/src/hooks/use-csrf-token.ts +5 -0
- package/src/hooks/use-random-string.ts +37 -0
- package/src/hooks/use-stepper.ts +87 -0
- package/src/index.html +13 -0
- package/src/lib/api.ts +234 -0
- package/src/lib/backend-data.ts +6 -0
- package/src/lib/clsx.ts +6 -0
- package/src/lib/json-client.ts +97 -0
- package/src/lib/password.ts +98 -0
- package/src/lib/ref.ts +17 -0
- package/src/lib/util.ts +13 -0
- package/src/locales/an/messages.po +487 -0
- package/src/locales/ast/messages.po +487 -0
- package/src/locales/ca/messages.po +487 -0
- package/src/locales/da/messages.po +487 -0
- package/src/locales/de/messages.po +487 -0
- package/src/locales/el/messages.po +487 -0
- package/src/locales/en/messages.po +487 -0
- package/src/locales/en-GB/messages.po +487 -0
- package/src/locales/es/messages.po +487 -0
- package/src/locales/eu/messages.po +487 -0
- package/src/locales/fi/messages.po +487 -0
- package/src/locales/fr/messages.po +487 -0
- package/src/locales/ga/messages.po +487 -0
- package/src/locales/gl/messages.po +487 -0
- package/src/locales/hi/messages.po +487 -0
- package/src/locales/hu/messages.po +487 -0
- package/src/locales/ia/messages.po +487 -0
- package/src/locales/id/messages.po +487 -0
- package/src/locales/it/messages.po +487 -0
- package/src/locales/ja/messages.po +487 -0
- package/src/locales/km/messages.po +487 -0
- package/src/locales/ko/messages.po +487 -0
- package/src/locales/load.ts +8 -0
- package/src/locales/locale-context.ts +19 -0
- package/src/locales/locale-provider.tsx +112 -0
- package/src/locales/locale-selector.tsx +58 -0
- package/src/locales/locales.ts +168 -0
- package/src/locales/ne/messages.po +487 -0
- package/src/locales/nl/messages.po +487 -0
- package/src/locales/pl/messages.po +487 -0
- package/src/locales/pt-BR/messages.po +487 -0
- package/src/locales/ro/messages.po +487 -0
- package/src/locales/ru/messages.po +487 -0
- package/src/locales/sv/messages.po +487 -0
- package/src/locales/th/messages.po +487 -0
- package/src/locales/tr/messages.po +487 -0
- package/src/locales/uk/messages.po +487 -0
- package/src/locales/vi/messages.po +487 -0
- package/src/locales/zh-CN/messages.po +487 -0
- package/src/locales/zh-HK/messages.po +487 -0
- package/src/locales/zh-TW/messages.po +487 -0
- package/src/styles.css +33 -0
- package/src/views/authorize/accept/accept-form.tsx +150 -0
- package/src/views/authorize/accept/accept-view.tsx +70 -0
- package/src/views/authorize/authorize-view.tsx +183 -0
- package/src/views/authorize/reset-password/reset-password-confirm-form.tsx +88 -0
- package/src/views/authorize/reset-password/reset-password-request-form.tsx +80 -0
- package/src/views/authorize/reset-password/reset-password-view.tsx +127 -0
- package/src/views/authorize/sign-in/sign-in-form.tsx +242 -0
- package/src/views/authorize/sign-in/sign-in-picker.tsx +116 -0
- package/src/views/authorize/sign-in/sign-in-view.tsx +145 -0
- package/src/views/authorize/sign-up/sign-up-account-form.tsx +142 -0
- package/src/views/authorize/sign-up/sign-up-disclaimer.tsx +51 -0
- package/src/views/authorize/sign-up/sign-up-handle-form.tsx +287 -0
- package/src/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +108 -0
- package/src/views/authorize/sign-up/sign-up-view.tsx +158 -0
- package/src/views/authorize/welcome/welcome-view.tsx +56 -0
- package/src/views/error/error-view.tsx +31 -0
- package/tailwind.config.js +31 -0
- package/tsconfig.backend.json +8 -0
- package/tsconfig.frontend.json +10 -0
- package/tsconfig.frontend.tsbuildinfo +1 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tools.json +8 -0
- package/tsconfig.tools.tsbuildinfo +1 -0
- package/vite.config.mjs +16 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useLingui } from '@lingui/react/macro'
|
|
2
|
+
import { ChangeEvent, useState } from 'react'
|
|
3
|
+
import { Override } from '../../lib/util.ts'
|
|
4
|
+
import { TokenIcon } from '../utils/icons.tsx'
|
|
5
|
+
import { InputText, InputTextProps } from './input-text.tsx'
|
|
6
|
+
|
|
7
|
+
export type InputTokenProps = Override<
|
|
8
|
+
Omit<
|
|
9
|
+
InputTextProps,
|
|
10
|
+
| 'type'
|
|
11
|
+
| 'pattern'
|
|
12
|
+
| 'autoCapitalize'
|
|
13
|
+
| 'autoCorrect'
|
|
14
|
+
| 'autoComplete'
|
|
15
|
+
| 'spellCheck'
|
|
16
|
+
| 'minLength'
|
|
17
|
+
| 'maxLength'
|
|
18
|
+
| 'placeholder'
|
|
19
|
+
| 'dir'
|
|
20
|
+
>,
|
|
21
|
+
{
|
|
22
|
+
example?: string
|
|
23
|
+
onToken?: (code: string | null) => void
|
|
24
|
+
}
|
|
25
|
+
>
|
|
26
|
+
|
|
27
|
+
export const OTP_CODE_EXAMPLE = 'XXXXX-XXXXX'
|
|
28
|
+
|
|
29
|
+
export function InputToken({
|
|
30
|
+
example = OTP_CODE_EXAMPLE,
|
|
31
|
+
onToken,
|
|
32
|
+
|
|
33
|
+
// InputTextProps
|
|
34
|
+
icon = <TokenIcon className="w-5" />,
|
|
35
|
+
title = example,
|
|
36
|
+
onChange,
|
|
37
|
+
value,
|
|
38
|
+
defaultValue = value,
|
|
39
|
+
...props
|
|
40
|
+
}: InputTokenProps) {
|
|
41
|
+
const { t } = useLingui()
|
|
42
|
+
const [token, setToken] = useState<string>(
|
|
43
|
+
typeof defaultValue === 'string' ? defaultValue : '',
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<InputText
|
|
48
|
+
{...props}
|
|
49
|
+
type="text"
|
|
50
|
+
autoCapitalize="none"
|
|
51
|
+
autoCorrect="off"
|
|
52
|
+
autoComplete="off"
|
|
53
|
+
spellCheck="false"
|
|
54
|
+
minLength={11}
|
|
55
|
+
maxLength={11}
|
|
56
|
+
dir="auto"
|
|
57
|
+
icon={icon}
|
|
58
|
+
pattern="^[A-Z2-7]{5}-[A-Z2-7]{5}$"
|
|
59
|
+
placeholder={t`Looks like ${example}`}
|
|
60
|
+
title={title}
|
|
61
|
+
value={token}
|
|
62
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
|
63
|
+
const { value, selectionEnd, selectionStart } = event.currentTarget
|
|
64
|
+
|
|
65
|
+
const fixedValue = fix(value)
|
|
66
|
+
|
|
67
|
+
event.currentTarget.value = fixedValue
|
|
68
|
+
|
|
69
|
+
// Move the cursor back where it was relative to the original value
|
|
70
|
+
const pos = selectionEnd ?? selectionStart
|
|
71
|
+
if (pos != null) {
|
|
72
|
+
const fixedSlicedValue = fix(value.slice(0, pos))
|
|
73
|
+
event.currentTarget.selectionStart =
|
|
74
|
+
event.currentTarget.selectionEnd = fixedSlicedValue.length
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setToken(fixedValue)
|
|
78
|
+
onChange?.(event)
|
|
79
|
+
|
|
80
|
+
if (!event.isDefaultPrevented()) {
|
|
81
|
+
onToken?.(fixedValue.length === 11 ? fixedValue : null)
|
|
82
|
+
}
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function fix(value: string) {
|
|
89
|
+
const normalized = value.toUpperCase().replaceAll(/[^A-Z2-7]/g, '')
|
|
90
|
+
|
|
91
|
+
if (normalized.length <= 5) return normalized
|
|
92
|
+
|
|
93
|
+
return `${normalized.slice(0, 5)}-${normalized.slice(5, 10)}`
|
|
94
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Trans } from '@lingui/react/macro'
|
|
2
|
+
import { JSX, ReactNode, useCallback } from 'react'
|
|
3
|
+
import { DisabledStep, Step, useStepper } from '../../hooks/use-stepper.ts'
|
|
4
|
+
import { clsx } from '../../lib/clsx.ts'
|
|
5
|
+
import { Override } from '../../lib/util.ts'
|
|
6
|
+
|
|
7
|
+
export type DoneFn = (...a: any) => unknown
|
|
8
|
+
|
|
9
|
+
export type WizardRenderProps<TDone extends DoneFn> = {
|
|
10
|
+
/**
|
|
11
|
+
* Indicates wether the render function being invoked corresponds to the step
|
|
12
|
+
* currently active. The steps titles could, for example, be rendered in a
|
|
13
|
+
* list of links, where the current step is highlighted (based on `current`).
|
|
14
|
+
*
|
|
15
|
+
* Another use for this is to render the next/previous steps in order to
|
|
16
|
+
* provide animated transitions between steps. In this case, `current` would
|
|
17
|
+
* be used to disable any form interaction with the form transitioning in/out.
|
|
18
|
+
*/
|
|
19
|
+
current: boolean
|
|
20
|
+
invalid: boolean
|
|
21
|
+
|
|
22
|
+
prev?: () => void
|
|
23
|
+
prevLabel: ReactNode
|
|
24
|
+
|
|
25
|
+
// On the last step, the "next()" function will actually be the done function
|
|
26
|
+
next: (() => void) | TDone
|
|
27
|
+
nextLabel: ReactNode
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type WizardRenderFn<TDone extends DoneFn> = (
|
|
31
|
+
data: WizardRenderProps<TDone>,
|
|
32
|
+
) => ReactNode
|
|
33
|
+
|
|
34
|
+
export type WizardStep<TDone extends DoneFn> = Step & {
|
|
35
|
+
titleRender?: WizardRenderFn<TDone>
|
|
36
|
+
contentRender: WizardRenderFn<TDone>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type WizardCardProps<TDone extends DoneFn> = Override<
|
|
40
|
+
Omit<JSX.IntrinsicElements['div'], 'children'>,
|
|
41
|
+
{
|
|
42
|
+
prevLabel?: ReactNode
|
|
43
|
+
nextLabel?: ReactNode
|
|
44
|
+
|
|
45
|
+
onBack?: () => void
|
|
46
|
+
backLabel?: ReactNode
|
|
47
|
+
|
|
48
|
+
onDone: TDone
|
|
49
|
+
doneLabel?: ReactNode
|
|
50
|
+
|
|
51
|
+
steps: readonly (WizardStep<TDone> | DisabledStep)[]
|
|
52
|
+
}
|
|
53
|
+
>
|
|
54
|
+
|
|
55
|
+
export function WizardCard<TDone extends DoneFn>({
|
|
56
|
+
prevLabel,
|
|
57
|
+
nextLabel,
|
|
58
|
+
|
|
59
|
+
onBack,
|
|
60
|
+
backLabel,
|
|
61
|
+
|
|
62
|
+
onDone,
|
|
63
|
+
doneLabel,
|
|
64
|
+
|
|
65
|
+
steps,
|
|
66
|
+
className,
|
|
67
|
+
|
|
68
|
+
...props
|
|
69
|
+
}: WizardCardProps<TDone>) {
|
|
70
|
+
const {
|
|
71
|
+
atFirst,
|
|
72
|
+
atLast,
|
|
73
|
+
count,
|
|
74
|
+
current,
|
|
75
|
+
currentPosition,
|
|
76
|
+
completed,
|
|
77
|
+
toNext,
|
|
78
|
+
toPrev,
|
|
79
|
+
toRequired,
|
|
80
|
+
} = useStepper(steps)
|
|
81
|
+
|
|
82
|
+
// Memoized to avoid re-renders in child (rendered) components
|
|
83
|
+
const onNext = useCallback(() => {
|
|
84
|
+
// If already at last step, go to the first incomplete (required) step
|
|
85
|
+
if (!toNext()) toRequired()
|
|
86
|
+
}, [toNext, toRequired])
|
|
87
|
+
|
|
88
|
+
const data: WizardRenderProps<TDone> = {
|
|
89
|
+
// The current UI only displays the current title & content.
|
|
90
|
+
current: true,
|
|
91
|
+
invalid: current ? current.invalid : false,
|
|
92
|
+
|
|
93
|
+
prevLabel: (atFirst && backLabel) || prevLabel || <Trans>Back</Trans>,
|
|
94
|
+
prev: atFirst ? onBack : toPrev,
|
|
95
|
+
|
|
96
|
+
nextLabel: (atLast && doneLabel) || nextLabel || <Trans>Next</Trans>,
|
|
97
|
+
next: atLast && completed ? onDone : onNext,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const stepTitle = current?.titleRender?.(data)
|
|
101
|
+
const stepContent = current?.contentRender?.(data)
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className={clsx(className, 'flex flex-col')} {...props}>
|
|
105
|
+
<p className="text-slate-500 dark:text-slate-400">
|
|
106
|
+
<Trans>
|
|
107
|
+
Step {currentPosition} of {count}
|
|
108
|
+
</Trans>
|
|
109
|
+
</p>
|
|
110
|
+
|
|
111
|
+
{stepTitle && <h2 className="font-medium text-xl mb-4">{stepTitle}</h2>}
|
|
112
|
+
|
|
113
|
+
{stepContent}
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { JSX, ReactNode } from 'react'
|
|
2
|
+
import { clsx } from '../../lib/clsx.ts'
|
|
3
|
+
import { Override } from '../../lib/util.ts'
|
|
4
|
+
import { LocaleSelector } from '../../locales/locale-selector.tsx'
|
|
5
|
+
|
|
6
|
+
export type LayoutTitlePageProps = Override<
|
|
7
|
+
JSX.IntrinsicElements['div'],
|
|
8
|
+
{
|
|
9
|
+
title?: string
|
|
10
|
+
subtitle?: ReactNode
|
|
11
|
+
children?: ReactNode
|
|
12
|
+
}
|
|
13
|
+
>
|
|
14
|
+
|
|
15
|
+
export function LayoutTitlePage({
|
|
16
|
+
children,
|
|
17
|
+
title,
|
|
18
|
+
subtitle,
|
|
19
|
+
|
|
20
|
+
// HTMLDivElement
|
|
21
|
+
className,
|
|
22
|
+
...props
|
|
23
|
+
}: LayoutTitlePageProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
{...props}
|
|
27
|
+
className={clsx(
|
|
28
|
+
className,
|
|
29
|
+
'flex flex-col items-center',
|
|
30
|
+
'md:flex md:flex-row md:justify-stretch md:items-center',
|
|
31
|
+
'min-h-screen min-w-screen',
|
|
32
|
+
'bg-white text-slate-900',
|
|
33
|
+
'dark:bg-slate-900 dark:text-slate-100',
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
{title && <title>{title}</title>}
|
|
37
|
+
|
|
38
|
+
<div
|
|
39
|
+
className={clsx(
|
|
40
|
+
'px-6 pt-4',
|
|
41
|
+
'w-full',
|
|
42
|
+
'md:max-w-lg',
|
|
43
|
+
'flex flex-row md:flex-col',
|
|
44
|
+
'md:self-stretch',
|
|
45
|
+
'md:w-1/2 md:max-w-fix md:p-4',
|
|
46
|
+
'md:text-right',
|
|
47
|
+
'md:dark:border-r md:dark:border-slate-700',
|
|
48
|
+
'md:bg-slate-100 md:dark:bg-slate-800',
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
<div className="flex-grow grid content-center md:justify-items-end">
|
|
52
|
+
{title && (
|
|
53
|
+
<h1
|
|
54
|
+
key="title"
|
|
55
|
+
className="text-xl md:text-2xl lg:text-5xl md:my-4 font-semibold text-brand"
|
|
56
|
+
>
|
|
57
|
+
{title}
|
|
58
|
+
</h1>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{subtitle && (
|
|
62
|
+
<p
|
|
63
|
+
key="subtitle"
|
|
64
|
+
className="hidden md:block max-w-xs text-slate-600 dark:text-slate-400"
|
|
65
|
+
>
|
|
66
|
+
{subtitle}
|
|
67
|
+
</p>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<LocaleSelector key="localeSelector" className="m-1 md:m-2" />
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<main className="w-full p-6 md:max-w-3xl md:px-12">{children}</main>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { JSX } from 'react'
|
|
2
|
+
import type { CustomizationData } from '@atproto/oauth-provider-api'
|
|
3
|
+
import { clsx } from '../../lib/clsx.ts'
|
|
4
|
+
import { Override } from '../../lib/util.ts'
|
|
5
|
+
import { LocaleSelector } from '../../locales/locale-selector.tsx'
|
|
6
|
+
import { LinkAnchor } from '../utils/link-anchor.tsx'
|
|
7
|
+
|
|
8
|
+
export type LayoutWelcomeProps = Override<
|
|
9
|
+
JSX.IntrinsicElements['div'],
|
|
10
|
+
{
|
|
11
|
+
customizationData: CustomizationData | undefined
|
|
12
|
+
title?: string
|
|
13
|
+
}
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
export function LayoutWelcome({
|
|
17
|
+
customizationData: { logo, name, links } = {},
|
|
18
|
+
title = name,
|
|
19
|
+
|
|
20
|
+
// div
|
|
21
|
+
className,
|
|
22
|
+
children,
|
|
23
|
+
...props
|
|
24
|
+
}: LayoutWelcomeProps) {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
{...props}
|
|
28
|
+
className={clsx(
|
|
29
|
+
'min-h-screen w-full',
|
|
30
|
+
'flex items-center justify-center flex-col',
|
|
31
|
+
'bg-white text-slate-900',
|
|
32
|
+
'dark:bg-slate-900 dark:text-slate-100',
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
{title && <title>{title}</title>}
|
|
37
|
+
|
|
38
|
+
<main className="w-full overflow-hidden flex-grow flex flex-col items-center justify-center p-6">
|
|
39
|
+
{logo && (
|
|
40
|
+
<img
|
|
41
|
+
src={logo}
|
|
42
|
+
alt={name || `Logo`}
|
|
43
|
+
aria-hidden
|
|
44
|
+
className="w-16 h-16 md:w-24 md:h-24 mb-4 md:mb-8"
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
{name && (
|
|
49
|
+
<h1 className="text-2xl md:text-4xl mb-4 md:mb-8 mx-4 text-center font-bold">
|
|
50
|
+
{name}
|
|
51
|
+
</h1>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
{children}
|
|
55
|
+
</main>
|
|
56
|
+
|
|
57
|
+
<nav className="w-full overflow-hidden border-t border-t-slate-200 dark:border-t-slate-700 flex flex-wrap justify-center content-center">
|
|
58
|
+
{links?.map((link, i) => (
|
|
59
|
+
<LinkAnchor
|
|
60
|
+
key={i}
|
|
61
|
+
link={link}
|
|
62
|
+
className="m-2 md:m-4 text-xs md:text-sm text-brand hover:underline"
|
|
63
|
+
/>
|
|
64
|
+
))}
|
|
65
|
+
|
|
66
|
+
<LocaleSelector
|
|
67
|
+
className="m-1 md:m-2 text-xs md:text-sm"
|
|
68
|
+
key="localeSelector"
|
|
69
|
+
/>
|
|
70
|
+
</nav>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { JSX } from 'react'
|
|
2
|
+
import type { Account } from '@atproto/oauth-provider-api'
|
|
3
|
+
import { Override } from '../../lib/util.ts'
|
|
4
|
+
|
|
5
|
+
export type AccountIdentifierProps = Override<
|
|
6
|
+
Omit<JSX.IntrinsicElements['b'], 'children'>,
|
|
7
|
+
{
|
|
8
|
+
account: Account
|
|
9
|
+
}
|
|
10
|
+
>
|
|
11
|
+
|
|
12
|
+
export function AccountIdentifier({
|
|
13
|
+
account,
|
|
14
|
+
|
|
15
|
+
// b
|
|
16
|
+
...props
|
|
17
|
+
}: AccountIdentifierProps) {
|
|
18
|
+
return (
|
|
19
|
+
<b {...props}>
|
|
20
|
+
{account.preferred_username || account.email || account.sub}
|
|
21
|
+
</b>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { AccountIcon } from './icons.tsx'
|
|
3
|
+
|
|
4
|
+
export type AccountIconProps = {
|
|
5
|
+
src?: string
|
|
6
|
+
alt: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function AccountImage({ src, alt }: AccountIconProps) {
|
|
10
|
+
const [errored, setErrored] = useState(false)
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setErrored(false)
|
|
14
|
+
}, [src])
|
|
15
|
+
|
|
16
|
+
return src && !errored ? (
|
|
17
|
+
<img
|
|
18
|
+
aria-hidden
|
|
19
|
+
crossOrigin="anonymous"
|
|
20
|
+
src={src}
|
|
21
|
+
alt={alt}
|
|
22
|
+
className="-ml-1 w-6 h-6 rounded-full"
|
|
23
|
+
onError={() => setErrored(true)}
|
|
24
|
+
/>
|
|
25
|
+
) : (
|
|
26
|
+
<div
|
|
27
|
+
aria-hidden
|
|
28
|
+
className="h-6 w-6 text-white bg-brand rounded-full border-solid border-2 border-brand overflow-hidden"
|
|
29
|
+
>
|
|
30
|
+
<AccountIcon className="-mx-1 -mb-1" />
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { JSX, memo } from 'react'
|
|
2
|
+
import { clsx } from '../../lib/clsx.ts'
|
|
3
|
+
import { Override } from '../../lib/util.ts'
|
|
4
|
+
import { AlertIcon, EyeIcon } from './icons.tsx'
|
|
5
|
+
|
|
6
|
+
export type AdmonitionProps = Override<
|
|
7
|
+
JSX.IntrinsicElements['div'],
|
|
8
|
+
{
|
|
9
|
+
role: 'alert' | 'status' | 'info'
|
|
10
|
+
}
|
|
11
|
+
>
|
|
12
|
+
|
|
13
|
+
export const Admonition = memo(function Admonition({
|
|
14
|
+
role = 'alert',
|
|
15
|
+
children,
|
|
16
|
+
className,
|
|
17
|
+
...props
|
|
18
|
+
}: AdmonitionProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
{...props}
|
|
22
|
+
role={role}
|
|
23
|
+
className={clsx(
|
|
24
|
+
'flex flex-row',
|
|
25
|
+
'gap-2',
|
|
26
|
+
'p-3',
|
|
27
|
+
'rounded-lg',
|
|
28
|
+
'border',
|
|
29
|
+
'border-gray-300 dark:border-gray-700',
|
|
30
|
+
role === 'alert' && 'bg-error text-error-c',
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
{role === 'info' ? (
|
|
35
|
+
<EyeIcon
|
|
36
|
+
aria-hidden
|
|
37
|
+
className={clsx('fill-current h-6 w-6', 'text-brand')}
|
|
38
|
+
/>
|
|
39
|
+
) : (
|
|
40
|
+
<AlertIcon
|
|
41
|
+
aria-hidden
|
|
42
|
+
className={clsx(
|
|
43
|
+
'fill-current h-6 w-6',
|
|
44
|
+
role === 'alert' ? 'text-inherit' : 'text-brand',
|
|
45
|
+
)}
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<div className="flex flex-1 flex-col">{children}</div>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Trans } from '@lingui/react/macro'
|
|
2
|
+
import { JSX } from 'react'
|
|
3
|
+
import type { OAuthClientMetadata } from '@atproto/oauth-types'
|
|
4
|
+
import { Override } from '../../lib/util.ts'
|
|
5
|
+
import { UrlViewer } from './url-viewer.tsx'
|
|
6
|
+
|
|
7
|
+
export type ClientNameProps = Override<
|
|
8
|
+
Omit<JSX.IntrinsicElements['span'], 'children'>,
|
|
9
|
+
{
|
|
10
|
+
clientId: string
|
|
11
|
+
clientMetadata: OAuthClientMetadata
|
|
12
|
+
clientTrusted: boolean
|
|
13
|
+
}
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
export function ClientName({
|
|
17
|
+
clientId,
|
|
18
|
+
clientMetadata,
|
|
19
|
+
clientTrusted,
|
|
20
|
+
|
|
21
|
+
// span
|
|
22
|
+
...attrs
|
|
23
|
+
}: ClientNameProps) {
|
|
24
|
+
if (clientTrusted && clientMetadata.client_name) {
|
|
25
|
+
return <span {...attrs}>{clientMetadata.client_name}</span>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// @NOTE: not using isOAuthClientIdLoopback & isOAuthClientIdDiscoverable from
|
|
29
|
+
// @atproto/oauth-types here because 1) we don't need to validate here and 2)
|
|
30
|
+
// we prefer not to import un-necessary code to improve bundle size.
|
|
31
|
+
|
|
32
|
+
if (clientId.startsWith('http://')) {
|
|
33
|
+
return (
|
|
34
|
+
<span {...attrs}>
|
|
35
|
+
<Trans>An application on your device</Trans>
|
|
36
|
+
</span>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (clientId.startsWith('https://')) {
|
|
41
|
+
return <UrlViewer {...attrs} url={clientId} path />
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return <span {...attrs}>{clientId}</span>
|
|
45
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Trans } from '@lingui/react/macro'
|
|
2
|
+
import { memo, useEffect, useMemo, useState } from 'react'
|
|
3
|
+
import { useRandomString } from '../../hooks/use-random-string.ts'
|
|
4
|
+
import { Api } from '../../lib/api.ts'
|
|
5
|
+
import { JsonErrorResponse } from '../../lib/json-client.ts'
|
|
6
|
+
import { Override } from '../../lib/util.ts'
|
|
7
|
+
import { Admonition, AdmonitionProps } from './admonition.tsx'
|
|
8
|
+
import { ErrorMessage } from './error-message.tsx'
|
|
9
|
+
|
|
10
|
+
export type ErrorCardProps = Override<
|
|
11
|
+
Omit<AdmonitionProps, 'role'>,
|
|
12
|
+
{
|
|
13
|
+
error: unknown
|
|
14
|
+
}
|
|
15
|
+
>
|
|
16
|
+
export const ErrorCard = memo(function ErrorCard({
|
|
17
|
+
error,
|
|
18
|
+
|
|
19
|
+
// Admonition
|
|
20
|
+
children,
|
|
21
|
+
onClick,
|
|
22
|
+
onKeyDown,
|
|
23
|
+
...props
|
|
24
|
+
}: ErrorCardProps) {
|
|
25
|
+
const [inputCount, setInputCount] = useState(0)
|
|
26
|
+
// Every 5th input will toggle showing the details
|
|
27
|
+
const showDetails = ((inputCount / 5) | 0) % 2 === 1
|
|
28
|
+
|
|
29
|
+
const detailsDivId = useRandomString('error-card-')
|
|
30
|
+
|
|
31
|
+
const parsedError = useMemo(
|
|
32
|
+
() =>
|
|
33
|
+
error instanceof JsonErrorResponse
|
|
34
|
+
? // Already parsed:
|
|
35
|
+
error
|
|
36
|
+
: // If "error" is a json object, try parsing it as a JsonErrorResponse:
|
|
37
|
+
Api.parseError(error) ?? error,
|
|
38
|
+
[error],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
// For debugging purposes
|
|
43
|
+
console.warn('Displayed error details:', parsedError)
|
|
44
|
+
|
|
45
|
+
// Reset the input count when the error changes
|
|
46
|
+
setInputCount(0)
|
|
47
|
+
}, [parsedError])
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Admonition
|
|
51
|
+
role="alert"
|
|
52
|
+
aria-controls={detailsDivId}
|
|
53
|
+
tabIndex={0}
|
|
54
|
+
onKeyDown={(event) => {
|
|
55
|
+
onKeyDown?.(event)
|
|
56
|
+
if (!event.defaultPrevented) {
|
|
57
|
+
setInputCount((c) => c + 1)
|
|
58
|
+
}
|
|
59
|
+
}}
|
|
60
|
+
onClick={(event) => {
|
|
61
|
+
onClick?.(event)
|
|
62
|
+
if (!event.defaultPrevented) {
|
|
63
|
+
setInputCount((c) => c + 1)
|
|
64
|
+
}
|
|
65
|
+
}}
|
|
66
|
+
{...props}
|
|
67
|
+
>
|
|
68
|
+
<ErrorMessage error={parsedError} />
|
|
69
|
+
|
|
70
|
+
{children && <div className="mt-2">{children}</div>}
|
|
71
|
+
|
|
72
|
+
<div hidden={!showDetails} id={detailsDivId} aria-hidden={!showDetails}>
|
|
73
|
+
{parsedError instanceof JsonErrorResponse ? (
|
|
74
|
+
<dl className="mt-2 grid grid-cols-[auto,1fr] gap-x-2 text-sm">
|
|
75
|
+
<dt className="font-semibold">
|
|
76
|
+
<Trans>Code</Trans>
|
|
77
|
+
</dt>
|
|
78
|
+
<dd>
|
|
79
|
+
<code>{parsedError.error}</code>
|
|
80
|
+
</dd>
|
|
81
|
+
|
|
82
|
+
<dt className="font-semibold">
|
|
83
|
+
<Trans>Description</Trans>
|
|
84
|
+
</dt>
|
|
85
|
+
<dd>{parsedError.description}</dd>
|
|
86
|
+
</dl>
|
|
87
|
+
) : (
|
|
88
|
+
<pre className="text-xs">{JSON.stringify(parsedError, null, 2)}</pre>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</Admonition>
|
|
92
|
+
)
|
|
93
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Trans } from '@lingui/react/macro'
|
|
2
|
+
import { ReactNode, memo } from 'react'
|
|
3
|
+
import {
|
|
4
|
+
AccessDeniedError,
|
|
5
|
+
EmailTakenError,
|
|
6
|
+
HandleUnavailableError,
|
|
7
|
+
InvalidCredentialsError,
|
|
8
|
+
InvalidInviteCodeError,
|
|
9
|
+
InvalidRequestError,
|
|
10
|
+
RequestExpiredError,
|
|
11
|
+
SecondAuthenticationFactorRequiredError,
|
|
12
|
+
UnknownRequestUriError,
|
|
13
|
+
} from '../../lib/api.ts'
|
|
14
|
+
import { JsonErrorResponse } from '../../lib/json-client.ts'
|
|
15
|
+
|
|
16
|
+
export type ApiErrorMessageProps = {
|
|
17
|
+
error: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ErrorMessage = memo(function ErrorMessage({
|
|
21
|
+
error,
|
|
22
|
+
}: ApiErrorMessageProps): ReactNode {
|
|
23
|
+
// Matches the order of the error checks in the API's parseError method (must
|
|
24
|
+
// be from most specific to least specific to avoid unreachable code paths).
|
|
25
|
+
|
|
26
|
+
if (error instanceof SecondAuthenticationFactorRequiredError) {
|
|
27
|
+
return <Trans>A second authentication factor is required</Trans>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (error instanceof InvalidCredentialsError) {
|
|
31
|
+
return <Trans>Wrong identifier or password</Trans>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (error instanceof InvalidInviteCodeError) {
|
|
35
|
+
return <Trans>The invite code is not valid</Trans>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (error instanceof HandleUnavailableError) {
|
|
39
|
+
switch (error.reason) {
|
|
40
|
+
case 'syntax':
|
|
41
|
+
return <Trans>The handle is invalid</Trans>
|
|
42
|
+
case 'domain':
|
|
43
|
+
return <Trans>The domain name is not allowed</Trans>
|
|
44
|
+
case 'slur':
|
|
45
|
+
return <Trans>The handle contains inappropriate language</Trans>
|
|
46
|
+
case 'taken':
|
|
47
|
+
if (error.description === 'Reserved handle') {
|
|
48
|
+
return <Trans>This handle is reserved</Trans>
|
|
49
|
+
}
|
|
50
|
+
return <Trans>The handle is already in use</Trans>
|
|
51
|
+
default:
|
|
52
|
+
return <Trans>That handle cannot be used</Trans>
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (error instanceof EmailTakenError) {
|
|
57
|
+
return <Trans>This email is already used</Trans>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
error instanceof UnknownRequestUriError ||
|
|
62
|
+
error instanceof RequestExpiredError
|
|
63
|
+
) {
|
|
64
|
+
return <Trans>This sign-in session has expired</Trans>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (error instanceof InvalidRequestError) {
|
|
68
|
+
return (
|
|
69
|
+
<Trans>
|
|
70
|
+
The data you submitted is invalid. Please check the form and try again.
|
|
71
|
+
</Trans>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (error instanceof AccessDeniedError) {
|
|
76
|
+
return (
|
|
77
|
+
<Trans>
|
|
78
|
+
This authorization request has been denied. Please try again.
|
|
79
|
+
</Trans>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (error instanceof JsonErrorResponse) {
|
|
84
|
+
return <Trans>Unexpected server response</Trans>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return <Trans>An unknown error occurred</Trans>
|
|
88
|
+
})
|