@atproto/oauth-provider-ui 0.0.2 → 0.1.0
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/CHANGELOG.md +10 -0
- package/{src/authorization-page.html → authorization-page.html} +36 -33
- package/{src/error-page.html → error-page.html} +17 -24
- package/package.json +28 -37
- package/src/authorization-page.tsx +21 -27
- package/src/components/forms/button.tsx +8 -8
- package/src/components/forms/fieldset.tsx +1 -1
- package/src/components/forms/form-card-async.tsx +1 -1
- package/src/components/forms/form-card.tsx +1 -1
- package/src/components/forms/input-checkbox.tsx +3 -3
- package/src/components/forms/input-container.tsx +12 -12
- package/src/components/forms/input-text.tsx +2 -2
- package/src/components/forms/wizard-card.tsx +2 -2
- package/src/components/layouts/layout-title-page.tsx +9 -8
- package/src/components/layouts/layout-welcome.tsx +21 -16
- package/src/components/utils/account-image.tsx +2 -2
- package/src/components/utils/admonition.tsx +5 -5
- package/src/components/utils/client-name.tsx +30 -4
- package/src/components/utils/error-card.tsx +1 -1
- package/src/components/utils/help-card.tsx +3 -3
- package/src/components/utils/multi-lang-string.tsx +14 -8
- package/src/components/utils/password-strength-meter.tsx +3 -3
- package/src/components/utils/url-viewer.tsx +1 -1
- package/src/error-page.tsx +8 -14
- package/src/hooks/use-api.ts +65 -45
- package/src/hydration-data.d.ts +42 -0
- package/src/lib/api.ts +12 -21
- package/src/{cookies.ts → lib/cookies.ts} +8 -2
- package/src/lib/json-client.ts +62 -18
- package/src/lib/util.ts +2 -1
- package/src/locales/an/messages.po +35 -28
- package/src/locales/ast/messages.po +35 -28
- package/src/locales/ca/messages.po +35 -28
- package/src/locales/da/messages.po +35 -28
- package/src/locales/de/messages.po +35 -28
- package/src/locales/el/messages.po +35 -28
- package/src/locales/en/messages.po +35 -28
- package/src/locales/en-GB/messages.po +35 -28
- package/src/locales/es/messages.po +35 -28
- package/src/locales/eu/messages.po +35 -28
- package/src/locales/fi/messages.po +35 -28
- package/src/locales/fr/messages.po +35 -28
- package/src/locales/ga/messages.po +35 -28
- package/src/locales/gl/messages.po +35 -28
- package/src/locales/hi/messages.po +35 -28
- package/src/locales/hu/messages.po +35 -28
- package/src/locales/ia/messages.po +35 -28
- package/src/locales/id/messages.po +35 -28
- package/src/locales/it/messages.po +35 -28
- package/src/locales/ja/messages.po +35 -28
- package/src/locales/km/messages.po +35 -28
- package/src/locales/ko/messages.po +35 -28
- package/src/locales/locale-provider.tsx +55 -59
- package/src/locales/locale-selector.tsx +13 -14
- package/src/locales/locales.ts +161 -146
- package/src/locales/ne/messages.po +35 -28
- package/src/locales/nl/messages.po +35 -28
- package/src/locales/pl/messages.po +35 -28
- package/src/locales/pt-BR/messages.po +35 -28
- package/src/locales/ro/messages.po +35 -28
- package/src/locales/ru/messages.po +35 -28
- package/src/locales/sv/messages.po +35 -28
- package/src/locales/th/messages.po +35 -28
- package/src/locales/tr/messages.po +35 -28
- package/src/locales/uk/messages.po +35 -28
- package/src/locales/vi/messages.po +35 -28
- package/src/locales/zh-CN/messages.po +35 -28
- package/src/locales/zh-HK/messages.po +35 -28
- package/src/locales/zh-TW/messages.po +35 -28
- package/src/style.css +219 -0
- package/src/views/authorize/accept/accept-form.tsx +11 -6
- package/src/views/authorize/authorize-view.tsx +13 -10
- package/src/views/authorize/reset-password/reset-password-view.tsx +2 -2
- package/src/views/authorize/sign-in/sign-in-form.tsx +39 -41
- package/src/views/authorize/sign-in/sign-in-picker.tsx +3 -3
- package/src/views/authorize/sign-up/sign-up-disclaimer.tsx +3 -3
- package/src/views/authorize/sign-up/sign-up-handle-form.tsx +6 -6
- package/src/views/authorize/sign-up/sign-up-view.tsx +3 -3
- package/src/views/authorize/welcome/welcome-view.tsx +5 -5
- package/tsconfig.json +1 -2
- package/{tsconfig.frontend.json → tsconfig.src.json} +4 -1
- package/tsconfig.src.tsbuildinfo +1 -0
- package/tsconfig.tools.json +1 -1
- package/tsconfig.tools.tsbuildinfo +1 -1
- package/vite.config.mjs +38 -7
- package/dist/assets/COdVzed-.css +0 -3
- package/dist/assets/COdVzed-.js +0 -100
- package/dist/assets/COdVzed-.js.map +0 -1
- package/dist/assets/Cqnfnbvc.js +0 -6
- package/dist/assets/Cqnfnbvc.js.map +0 -1
- package/dist/assets/bundle-manifest.json +0 -630
- package/dist/assets/error-view-Bu4y7Nd8.js +0 -208
- package/dist/assets/error-view-Bu4y7Nd8.js.map +0 -1
- package/dist/assets/index-DXlCRM6V.js +0 -36
- package/dist/assets/index-DXlCRM6V.js.map +0 -1
- package/dist/assets/messages-2GoTm2qL.js +0 -4
- package/dist/assets/messages-2GoTm2qL.js.map +0 -1
- package/dist/assets/messages-6Cn2Jbhw.js +0 -4
- package/dist/assets/messages-6Cn2Jbhw.js.map +0 -1
- package/dist/assets/messages-75hFgOK2.js +0 -4
- package/dist/assets/messages-75hFgOK2.js.map +0 -1
- package/dist/assets/messages-B3OK4k0O.js +0 -4
- package/dist/assets/messages-B3OK4k0O.js.map +0 -1
- package/dist/assets/messages-BNXlPzKV.js +0 -4
- package/dist/assets/messages-BNXlPzKV.js.map +0 -1
- package/dist/assets/messages-BUygB8mD.js +0 -4
- package/dist/assets/messages-BUygB8mD.js.map +0 -1
- package/dist/assets/messages-BVPPcwNr.js +0 -4
- package/dist/assets/messages-BVPPcwNr.js.map +0 -1
- package/dist/assets/messages-BbbWUQS8.js +0 -4
- package/dist/assets/messages-BbbWUQS8.js.map +0 -1
- package/dist/assets/messages-BibKCYyW.js +0 -4
- package/dist/assets/messages-BibKCYyW.js.map +0 -1
- package/dist/assets/messages-BlPrr9_7.js +0 -4
- package/dist/assets/messages-BlPrr9_7.js.map +0 -1
- package/dist/assets/messages-ByVCw40U.js +0 -4
- package/dist/assets/messages-ByVCw40U.js.map +0 -1
- package/dist/assets/messages-C5DU1neP.js +0 -4
- package/dist/assets/messages-C5DU1neP.js.map +0 -1
- package/dist/assets/messages-C6IgUtbX.js +0 -4
- package/dist/assets/messages-C6IgUtbX.js.map +0 -1
- package/dist/assets/messages-C92Zzt2o.js +0 -4
- package/dist/assets/messages-C92Zzt2o.js.map +0 -1
- package/dist/assets/messages-CGZqYT14.js +0 -4
- package/dist/assets/messages-CGZqYT14.js.map +0 -1
- package/dist/assets/messages-CGlsy4wt.js +0 -4
- package/dist/assets/messages-CGlsy4wt.js.map +0 -1
- package/dist/assets/messages-CPT1nd0u.js +0 -4
- package/dist/assets/messages-CPT1nd0u.js.map +0 -1
- package/dist/assets/messages-CTTdXyw_.js +0 -4
- package/dist/assets/messages-CTTdXyw_.js.map +0 -1
- package/dist/assets/messages-ChK_C_Pj.js +0 -4
- package/dist/assets/messages-ChK_C_Pj.js.map +0 -1
- package/dist/assets/messages-CjJbk7Uf.js +0 -4
- package/dist/assets/messages-CjJbk7Uf.js.map +0 -1
- package/dist/assets/messages-CoiLjLYO.js +0 -4
- package/dist/assets/messages-CoiLjLYO.js.map +0 -1
- package/dist/assets/messages-Cwx6B4Ti.js +0 -4
- package/dist/assets/messages-Cwx6B4Ti.js.map +0 -1
- package/dist/assets/messages-D0uXAp_H.js +0 -4
- package/dist/assets/messages-D0uXAp_H.js.map +0 -1
- package/dist/assets/messages-DG0_arU0.js +0 -4
- package/dist/assets/messages-DG0_arU0.js.map +0 -1
- package/dist/assets/messages-DOXFJh9K.js +0 -4
- package/dist/assets/messages-DOXFJh9K.js.map +0 -1
- package/dist/assets/messages-DPK7nOoC.js +0 -4
- package/dist/assets/messages-DPK7nOoC.js.map +0 -1
- package/dist/assets/messages-Duccgtu0.js +0 -4
- package/dist/assets/messages-Duccgtu0.js.map +0 -1
- package/dist/assets/messages-DxTqgsHq.js +0 -4
- package/dist/assets/messages-DxTqgsHq.js.map +0 -1
- package/dist/assets/messages-E5_lTg7A.js +0 -4
- package/dist/assets/messages-E5_lTg7A.js.map +0 -1
- package/dist/assets/messages-UhunAjh1.js +0 -4
- package/dist/assets/messages-UhunAjh1.js.map +0 -1
- package/dist/assets/messages-Xg_3YLGw.js +0 -4
- package/dist/assets/messages-Xg_3YLGw.js.map +0 -1
- package/dist/assets/messages-iliBQHY2.js +0 -4
- package/dist/assets/messages-iliBQHY2.js.map +0 -1
- package/dist/assets/messages-lRprpIl-.js +0 -4
- package/dist/assets/messages-lRprpIl-.js.map +0 -1
- package/dist/assets/messages-pbPHQbz1.js +0 -4
- package/dist/assets/messages-pbPHQbz1.js.map +0 -1
- package/dist/assets/messages-q-O7ZQGs.js +0 -4
- package/dist/assets/messages-q-O7ZQGs.js.map +0 -1
- package/dist/lib/index.d.ts +0 -19
- package/dist/lib/index.d.ts.map +0 -1
- package/dist/lib/index.js +0 -47
- package/dist/lib/index.js.map +0 -1
- package/dist/tsconfig.backend.tsbuildinfo +0 -1
- package/lib/index.ts +0 -72
- package/rollup.config.js +0 -102
- package/src/backend-data.ts +0 -35
- package/src/hooks/use-csrf-token.ts +0 -5
- package/src/lib/backend-data.ts +0 -6
- package/src/lib/clsx.ts +0 -6
- package/src/locales/locale-context.ts +0 -19
- package/src/styles.css +0 -33
- package/tailwind.config.js +0 -31
- package/tsconfig.backend.json +0 -8
- package/tsconfig.frontend.tsbuildinfo +0 -1
- /package/{src/index.html → index.html} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { useLingui } from '@lingui/react/macro'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
1
3
|
import { JSX } from 'react'
|
|
2
4
|
import type { CustomizationData } from '@atproto/oauth-provider-api'
|
|
3
|
-
import { clsx } from '../../lib/clsx.ts'
|
|
4
5
|
import { Override } from '../../lib/util.ts'
|
|
5
6
|
import { LocaleSelector } from '../../locales/locale-selector.tsx'
|
|
6
7
|
import { LinkAnchor } from '../utils/link-anchor.tsx'
|
|
@@ -22,12 +23,14 @@ export function LayoutWelcome({
|
|
|
22
23
|
children,
|
|
23
24
|
...props
|
|
24
25
|
}: LayoutWelcomeProps) {
|
|
26
|
+
const { t } = useLingui()
|
|
27
|
+
|
|
25
28
|
return (
|
|
26
29
|
<div
|
|
27
30
|
{...props}
|
|
28
31
|
className={clsx(
|
|
29
32
|
'min-h-screen w-full',
|
|
30
|
-
'flex items-center justify-center
|
|
33
|
+
'flex flex-col items-center justify-center',
|
|
31
34
|
'bg-white text-slate-900',
|
|
32
35
|
'dark:bg-slate-900 dark:text-slate-100',
|
|
33
36
|
className,
|
|
@@ -35,18 +38,18 @@ export function LayoutWelcome({
|
|
|
35
38
|
>
|
|
36
39
|
{title && <title>{title}</title>}
|
|
37
40
|
|
|
38
|
-
<main className="w-full
|
|
41
|
+
<main className="flex w-full grow flex-col items-center justify-center overflow-hidden p-6">
|
|
39
42
|
{logo && (
|
|
40
43
|
<img
|
|
41
44
|
src={logo}
|
|
42
|
-
alt={name || `Logo`}
|
|
45
|
+
alt={name || t`Logo`}
|
|
43
46
|
aria-hidden
|
|
44
|
-
className="
|
|
47
|
+
className="mb-4 h-16 w-16 md:mb-8 md:h-24 md:w-24"
|
|
45
48
|
/>
|
|
46
49
|
)}
|
|
47
50
|
|
|
48
51
|
{name && (
|
|
49
|
-
<h1 className="text-
|
|
52
|
+
<h1 className="mx-4 mb-4 text-center text-2xl font-bold md:mb-8 md:text-4xl">
|
|
50
53
|
{name}
|
|
51
54
|
</h1>
|
|
52
55
|
)}
|
|
@@ -54,20 +57,22 @@ export function LayoutWelcome({
|
|
|
54
57
|
{children}
|
|
55
58
|
</main>
|
|
56
59
|
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
<footer className="bg-contrast-25 dark:bg-contrast-50 flex w-full flex-wrap items-center justify-between overflow-hidden px-4 md:px-6">
|
|
61
|
+
<nav className="flex flex-wrap items-center justify-start">
|
|
62
|
+
{links?.map((link, i) => (
|
|
63
|
+
<LinkAnchor
|
|
64
|
+
key={i}
|
|
65
|
+
link={link}
|
|
66
|
+
className="text-text-light m-2 text-xs hover:underline md:m-4 md:text-sm"
|
|
67
|
+
/>
|
|
68
|
+
))}
|
|
69
|
+
</nav>
|
|
65
70
|
|
|
66
71
|
<LocaleSelector
|
|
67
|
-
className="m-1 md:m-2
|
|
72
|
+
className="m-1 text-xs md:m-2 md:text-sm"
|
|
68
73
|
key="localeSelector"
|
|
69
74
|
/>
|
|
70
|
-
</
|
|
75
|
+
</footer>
|
|
71
76
|
</div>
|
|
72
77
|
)
|
|
73
78
|
}
|
|
@@ -19,13 +19,13 @@ export function AccountImage({ src, alt }: AccountIconProps) {
|
|
|
19
19
|
crossOrigin="anonymous"
|
|
20
20
|
src={src}
|
|
21
21
|
alt={alt}
|
|
22
|
-
className="-ml-1
|
|
22
|
+
className="-ml-1 h-6 w-6 rounded-full"
|
|
23
23
|
onError={() => setErrored(true)}
|
|
24
24
|
/>
|
|
25
25
|
) : (
|
|
26
26
|
<div
|
|
27
27
|
aria-hidden
|
|
28
|
-
className="h-6 w-6
|
|
28
|
+
className="bg-primary border-primary h-6 w-6 overflow-hidden rounded-full border-2 border-solid text-white"
|
|
29
29
|
>
|
|
30
30
|
<AccountIcon className="-mx-1 -mb-1" />
|
|
31
31
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
1
2
|
import { JSX, memo } from 'react'
|
|
2
|
-
import { clsx } from '../../lib/clsx.ts'
|
|
3
3
|
import { Override } from '../../lib/util.ts'
|
|
4
4
|
import { AlertIcon, EyeIcon } from './icons.tsx'
|
|
5
5
|
|
|
@@ -27,21 +27,21 @@ export const Admonition = memo(function Admonition({
|
|
|
27
27
|
'rounded-lg',
|
|
28
28
|
'border',
|
|
29
29
|
'border-gray-300 dark:border-gray-700',
|
|
30
|
-
role === 'alert' && 'bg-error text-error-
|
|
30
|
+
role === 'alert' && 'bg-error text-error-contrast',
|
|
31
31
|
className,
|
|
32
32
|
)}
|
|
33
33
|
>
|
|
34
34
|
{role === 'info' ? (
|
|
35
35
|
<EyeIcon
|
|
36
36
|
aria-hidden
|
|
37
|
-
className={clsx('
|
|
37
|
+
className={clsx('h-6 w-6 fill-current', 'text-primary')}
|
|
38
38
|
/>
|
|
39
39
|
) : (
|
|
40
40
|
<AlertIcon
|
|
41
41
|
aria-hidden
|
|
42
42
|
className={clsx(
|
|
43
|
-
'
|
|
44
|
-
role === 'alert' ? 'text-inherit' : 'text-
|
|
43
|
+
'h-6 w-6 fill-current',
|
|
44
|
+
role === 'alert' ? 'text-inherit' : 'text-primary',
|
|
45
45
|
)}
|
|
46
46
|
/>
|
|
47
47
|
)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Trans } from '@lingui/react/macro'
|
|
2
|
-
import { JSX } from 'react'
|
|
2
|
+
import { JSX, useMemo } from 'react'
|
|
3
3
|
import type { OAuthClientMetadata } from '@atproto/oauth-types'
|
|
4
4
|
import { Override } from '../../lib/util.ts'
|
|
5
5
|
import { UrlViewer } from './url-viewer.tsx'
|
|
@@ -21,6 +21,14 @@ export function ClientName({
|
|
|
21
21
|
// span
|
|
22
22
|
...attrs
|
|
23
23
|
}: ClientNameProps) {
|
|
24
|
+
const url = useMemo(() => {
|
|
25
|
+
try {
|
|
26
|
+
return new URL(clientId)
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}, [clientId])
|
|
31
|
+
|
|
24
32
|
if (clientTrusted && clientMetadata.client_name) {
|
|
25
33
|
return <span {...attrs}>{clientMetadata.client_name}</span>
|
|
26
34
|
}
|
|
@@ -29,7 +37,7 @@ export function ClientName({
|
|
|
29
37
|
// @atproto/oauth-types here because 1) we don't need to validate here and 2)
|
|
30
38
|
// we prefer not to import un-necessary code to improve bundle size.
|
|
31
39
|
|
|
32
|
-
if (
|
|
40
|
+
if (url?.protocol === 'http:') {
|
|
33
41
|
return (
|
|
34
42
|
<span {...attrs}>
|
|
35
43
|
<Trans>An application on your device</Trans>
|
|
@@ -37,8 +45,26 @@ export function ClientName({
|
|
|
37
45
|
)
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
if (
|
|
41
|
-
|
|
48
|
+
if (url?.protocol === 'https:') {
|
|
49
|
+
// Only display the url details if the client id does not follow our
|
|
50
|
+
// convention.
|
|
51
|
+
const simplifiedView =
|
|
52
|
+
url.protocol === 'https:' &&
|
|
53
|
+
url.pathname === '/oauth-client-metadata.json' &&
|
|
54
|
+
!url.port &&
|
|
55
|
+
!url.search
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<UrlViewer
|
|
59
|
+
{...attrs}
|
|
60
|
+
url={url}
|
|
61
|
+
proto={!simplifiedView}
|
|
62
|
+
host={true}
|
|
63
|
+
path={!simplifiedView}
|
|
64
|
+
query={!simplifiedView}
|
|
65
|
+
hash={false}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
42
68
|
}
|
|
43
69
|
|
|
44
70
|
return <span {...attrs}>{clientId}</span>
|
|
@@ -71,7 +71,7 @@ export const ErrorCard = memo(function ErrorCard({
|
|
|
71
71
|
|
|
72
72
|
<div hidden={!showDetails} id={detailsDivId} aria-hidden={!showDetails}>
|
|
73
73
|
{parsedError instanceof JsonErrorResponse ? (
|
|
74
|
-
<dl className="mt-2 grid grid-cols-[
|
|
74
|
+
<dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-2 text-sm">
|
|
75
75
|
<dt className="font-semibold">
|
|
76
76
|
<Trans>Code</Trans>
|
|
77
77
|
</dt>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Trans } from '@lingui/react/macro'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
2
3
|
import { JSX } from 'react'
|
|
3
4
|
import type { LinkDefinition } from '@atproto/oauth-provider-api'
|
|
4
|
-
import { clsx } from '../../lib/clsx.ts'
|
|
5
5
|
import { Override } from '../../lib/util.ts'
|
|
6
6
|
|
|
7
7
|
export type HelpCardProps = Override<
|
|
@@ -25,7 +25,7 @@ export function HelpCard({
|
|
|
25
25
|
<p
|
|
26
26
|
{...props}
|
|
27
27
|
className={clsx(
|
|
28
|
-
'
|
|
28
|
+
'rounded-md bg-slate-100 p-3 text-sm text-slate-800 dark:bg-slate-800 dark:text-slate-400',
|
|
29
29
|
className,
|
|
30
30
|
)}
|
|
31
31
|
>
|
|
@@ -36,7 +36,7 @@ export function HelpCard({
|
|
|
36
36
|
href={helpLink.href}
|
|
37
37
|
rel={helpLink.rel}
|
|
38
38
|
target="_blank"
|
|
39
|
-
className="text-
|
|
39
|
+
className="text-primary"
|
|
40
40
|
>
|
|
41
41
|
<Trans>Contact support</Trans>
|
|
42
42
|
</a>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useLingui } from '@lingui/react/macro'
|
|
2
|
-
import { ReactNode } from 'react'
|
|
2
|
+
import { ReactNode, useMemo } from 'react'
|
|
3
3
|
import type { LocalizedString } from '@atproto/oauth-provider-api'
|
|
4
4
|
|
|
5
5
|
export type MultiLangStringProps = {
|
|
@@ -11,11 +11,17 @@ export function MultiLangString({
|
|
|
11
11
|
value,
|
|
12
12
|
fallback,
|
|
13
13
|
}: MultiLangStringProps): ReactNode {
|
|
14
|
-
const
|
|
14
|
+
const matchingString = useMatchingString(value)
|
|
15
15
|
return (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
matchingString ?? fallback ?? (typeof value === 'string' ? value : value.en)
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function useMatchingString(value: LocalizedString) {
|
|
21
|
+
const { i18n } = useLingui()
|
|
22
|
+
return useMemo(
|
|
23
|
+
() => findMatchingString(value, i18n.locale),
|
|
24
|
+
[value, i18n.locale],
|
|
19
25
|
)
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -28,8 +34,8 @@ function findMatchingString(
|
|
|
28
34
|
): string | undefined {
|
|
29
35
|
switch (typeof value) {
|
|
30
36
|
case 'string':
|
|
31
|
-
// By convention, string values are in english
|
|
32
|
-
if (locale.startsWith('en')) return value
|
|
37
|
+
// By convention, string values are in english
|
|
38
|
+
if (locale === 'en' || locale.startsWith('en-')) return value
|
|
33
39
|
break
|
|
34
40
|
|
|
35
41
|
case 'object': {
|
|
@@ -37,7 +43,7 @@ function findMatchingString(
|
|
|
37
43
|
const localeMatch = value[locale]
|
|
38
44
|
if (typeof localeMatch === 'string') return localeMatch
|
|
39
45
|
|
|
40
|
-
// Fallback to language match
|
|
46
|
+
// Fallback to language match (e.g. "fr-BE" -> "fr")
|
|
41
47
|
const lang = locale.split('-')[0]
|
|
42
48
|
const langMatch = value[lang]
|
|
43
49
|
if (typeof langMatch === 'string') return langMatch
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useLingui } from '@lingui/react/macro'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
2
3
|
import { JSX } from 'react'
|
|
3
|
-
import { clsx } from '../../lib/clsx.ts'
|
|
4
4
|
import { PasswordStrength, getPasswordStrength } from '../../lib/password.ts'
|
|
5
5
|
import { Override } from '../../lib/util.ts'
|
|
6
6
|
|
|
@@ -40,7 +40,7 @@ export function PasswordStrengthMeter({
|
|
|
40
40
|
return (
|
|
41
41
|
<div
|
|
42
42
|
{...props}
|
|
43
|
-
className={clsx('
|
|
43
|
+
className={clsx('flex h-1 w-full space-x-2', className)}
|
|
44
44
|
role="meter"
|
|
45
45
|
aria-label={t`Password strength indicator`}
|
|
46
46
|
aria-valuemin={0}
|
|
@@ -50,7 +50,7 @@ export function PasswordStrengthMeter({
|
|
|
50
50
|
{Array.from({ length: 4 }, (_, i) => (
|
|
51
51
|
<div
|
|
52
52
|
key={i}
|
|
53
|
-
className={`
|
|
53
|
+
className={`h-1 w-1/4 rounded-sm ${strength > i ? color : colorBg}`}
|
|
54
54
|
/>
|
|
55
55
|
))}
|
|
56
56
|
</div>
|
|
@@ -28,7 +28,7 @@ export function UrlViewer<As extends keyof JSX.IntrinsicElements = 'span'>({
|
|
|
28
28
|
// Element
|
|
29
29
|
...props
|
|
30
30
|
}: Override<JSX.IntrinsicElements[As], UrlRendererProps>) {
|
|
31
|
-
const urlObj = useMemo(() => new URL(url), [url])
|
|
31
|
+
const urlObj = useMemo(() => (url instanceof URL ? url : new URL(url)), [url])
|
|
32
32
|
|
|
33
33
|
return (
|
|
34
34
|
<As {...props}>
|
package/src/error-page.tsx
CHANGED
|
@@ -1,28 +1,22 @@
|
|
|
1
|
-
import './
|
|
1
|
+
import './style.css'
|
|
2
2
|
|
|
3
3
|
import { StrictMode } from 'react'
|
|
4
4
|
import { createRoot } from 'react-dom/client'
|
|
5
|
-
import type {
|
|
6
|
-
AvailableLocales,
|
|
7
|
-
CustomizationData,
|
|
8
|
-
ErrorData,
|
|
9
|
-
} from '@atproto/oauth-provider-api'
|
|
10
|
-
import { readBackendData } from './lib/backend-data.ts'
|
|
5
|
+
import type { HydrationData } from './hydration-data.d.ts'
|
|
11
6
|
import { LocaleProvider } from './locales/locale-provider.tsx'
|
|
12
7
|
import { ErrorView } from './views/error/error-view.tsx'
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export const errorData = readBackendData<ErrorData>('__errorData')
|
|
9
|
+
const {
|
|
10
|
+
//
|
|
11
|
+
__errorData: errorData,
|
|
12
|
+
__customizationData: customizationData,
|
|
13
|
+
} = window as typeof window & HydrationData['error-page']
|
|
20
14
|
|
|
21
15
|
const container = document.getElementById('root')!
|
|
22
16
|
|
|
23
17
|
createRoot(container).render(
|
|
24
18
|
<StrictMode>
|
|
25
|
-
<LocaleProvider
|
|
19
|
+
<LocaleProvider>
|
|
26
20
|
<ErrorView error={errorData} customizationData={customizationData} />
|
|
27
21
|
</LocaleProvider>
|
|
28
22
|
</StrictMode>,
|
package/src/hooks/use-api.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { useLingui } from '@lingui/react/macro'
|
|
2
|
-
import { useCallback,
|
|
2
|
+
import { useCallback, useState } from 'react'
|
|
3
3
|
import { useErrorBoundary } from 'react-error-boundary'
|
|
4
4
|
import type {
|
|
5
5
|
Account,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
ConfirmResetPasswordInput,
|
|
7
|
+
InitiatePasswordResetInput,
|
|
8
8
|
Session,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
SignInInput,
|
|
10
|
+
SignUpInput,
|
|
11
|
+
VerifyHandleAvailabilityInput,
|
|
12
12
|
} from '@atproto/oauth-provider-api'
|
|
13
|
-
import {
|
|
13
|
+
import { Api, UnknownRequestUriError } from '../lib/api.ts'
|
|
14
14
|
import { upsert } from '../lib/util.ts'
|
|
15
|
-
import { useCsrfToken } from './use-csrf-token.ts'
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* Any function wrapped with this helper will automatically show the error
|
|
@@ -38,24 +37,20 @@ function useSafeCallback<F extends (...a: any) => any>(fn: F, deps: unknown[]) {
|
|
|
38
37
|
)
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
export type
|
|
42
|
-
|
|
43
|
-
sessions?: readonly Session[]
|
|
44
|
-
newSessionsRequireConsent?: boolean
|
|
45
|
-
onRedirected?: () => void
|
|
40
|
+
export type SessionWithToken = Session & {
|
|
41
|
+
ephemeralToken?: string
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
export function useApi({
|
|
49
|
-
requestUri,
|
|
50
45
|
sessions: sessionsInit = [],
|
|
51
|
-
newSessionsRequireConsent = true,
|
|
52
46
|
onRedirected,
|
|
53
|
-
}:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const api =
|
|
58
|
-
const [sessions, setSessions] =
|
|
47
|
+
}: {
|
|
48
|
+
sessions?: readonly Session[]
|
|
49
|
+
onRedirected?: () => void
|
|
50
|
+
}) {
|
|
51
|
+
const [api] = useState(() => new Api())
|
|
52
|
+
const [sessions, setSessions] =
|
|
53
|
+
useState<readonly SessionWithToken[]>(sessionsInit)
|
|
59
54
|
|
|
60
55
|
const { i18n } = useLingui()
|
|
61
56
|
const { locale } = i18n
|
|
@@ -74,30 +69,47 @@ export function useApi({
|
|
|
74
69
|
const upsertSession = useCallback(
|
|
75
70
|
({
|
|
76
71
|
account,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
consentRequired
|
|
81
|
-
|
|
82
|
-
|
|
72
|
+
ephemeralToken,
|
|
73
|
+
// The server will tell us if the user needs to consent to the
|
|
74
|
+
// authorization. Defaults to true in case of sign-ups
|
|
75
|
+
consentRequired = true,
|
|
76
|
+
// When a new session is inserted, assume that the user intends to use
|
|
77
|
+
// it, and therefore, it is selected by default.
|
|
78
|
+
selected = true,
|
|
79
|
+
// When a new session is inserted, it is assumed that the user just
|
|
80
|
+
// created the session, and therefore, login is not required.
|
|
81
|
+
loginRequired = false,
|
|
82
|
+
}: { account: Account } & Partial<SessionWithToken>) => {
|
|
83
|
+
const session: SessionWithToken = {
|
|
83
84
|
account,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
ephemeralToken,
|
|
86
|
+
selected,
|
|
87
|
+
loginRequired,
|
|
88
|
+
consentRequired,
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
setSessions((sessions) =>
|
|
90
92
|
upsert(sessions, session, (s) => s.account.sub === account.sub).map(
|
|
91
|
-
// Make sure to de-select any other selected session
|
|
92
|
-
|
|
93
|
+
// Make sure to de-select any other selected session (if selected is
|
|
94
|
+
// true)
|
|
95
|
+
(s) =>
|
|
96
|
+
!selected || s === session || !s.selected
|
|
97
|
+
? s
|
|
98
|
+
: { ...s, selected: false },
|
|
93
99
|
),
|
|
94
100
|
)
|
|
95
101
|
},
|
|
96
|
-
[setSessions
|
|
102
|
+
[setSessions],
|
|
97
103
|
)
|
|
98
104
|
|
|
99
105
|
const performRedirect = useCallback(
|
|
100
|
-
(url: URL) => {
|
|
106
|
+
(url: string | URL) => {
|
|
107
|
+
// @TODO At this point, the request cannot be accepted/rejected anymore.
|
|
108
|
+
// We should probably change the app's state to something that indicates
|
|
109
|
+
// that in order to improve UX in case the user comes back to the app.
|
|
110
|
+
// This is currently ensured by the backend (through back-forward cache
|
|
111
|
+
// busting) but handling it here would provide a better UX.
|
|
112
|
+
|
|
101
113
|
window.location.href = String(url)
|
|
102
114
|
if (onRedirected) setTimeout(onRedirected)
|
|
103
115
|
},
|
|
@@ -105,8 +117,9 @@ export function useApi({
|
|
|
105
117
|
)
|
|
106
118
|
|
|
107
119
|
const doSignIn = useSafeCallback(
|
|
108
|
-
async (data: Omit<
|
|
120
|
+
async (data: Omit<SignInInput, 'locale'>, signal?: AbortSignal) => {
|
|
109
121
|
const response = await api.fetch(
|
|
122
|
+
'POST',
|
|
110
123
|
'/sign-in',
|
|
111
124
|
{ ...data, locale },
|
|
112
125
|
{ signal },
|
|
@@ -118,10 +131,11 @@ export function useApi({
|
|
|
118
131
|
|
|
119
132
|
const doInitiatePasswordReset = useSafeCallback(
|
|
120
133
|
async (
|
|
121
|
-
data: Omit<
|
|
134
|
+
data: Omit<InitiatePasswordResetInput, 'locale'>,
|
|
122
135
|
signal?: AbortSignal,
|
|
123
136
|
) => {
|
|
124
137
|
await api.fetch(
|
|
138
|
+
'POST',
|
|
125
139
|
'/reset-password-request',
|
|
126
140
|
{ ...data, locale },
|
|
127
141
|
{ signal },
|
|
@@ -131,22 +145,23 @@ export function useApi({
|
|
|
131
145
|
)
|
|
132
146
|
|
|
133
147
|
const doConfirmResetPassword = useSafeCallback(
|
|
134
|
-
async (data:
|
|
135
|
-
await api.fetch('/reset-password-confirm', data, { signal })
|
|
148
|
+
async (data: ConfirmResetPasswordInput, signal?: AbortSignal) => {
|
|
149
|
+
await api.fetch('POST', '/reset-password-confirm', data, { signal })
|
|
136
150
|
},
|
|
137
151
|
[api],
|
|
138
152
|
)
|
|
139
153
|
|
|
140
154
|
const doValidateNewHandle = useSafeCallback(
|
|
141
|
-
async (data:
|
|
142
|
-
await api.fetch('/verify-handle-availability', data, { signal })
|
|
155
|
+
async (data: VerifyHandleAvailabilityInput, signal?: AbortSignal) => {
|
|
156
|
+
await api.fetch('POST', '/verify-handle-availability', data, { signal })
|
|
143
157
|
},
|
|
144
158
|
[api],
|
|
145
159
|
)
|
|
146
160
|
|
|
147
161
|
const doSignUp = useSafeCallback(
|
|
148
|
-
async (data: Omit<
|
|
162
|
+
async (data: Omit<SignUpInput, 'locale'>, signal?: AbortSignal) => {
|
|
149
163
|
const response = await api.fetch(
|
|
164
|
+
'POST',
|
|
150
165
|
'/sign-up',
|
|
151
166
|
{ ...data, locale },
|
|
152
167
|
{ signal },
|
|
@@ -157,14 +172,19 @@ export function useApi({
|
|
|
157
172
|
)
|
|
158
173
|
|
|
159
174
|
const doAccept = useSafeCallback(
|
|
160
|
-
async (
|
|
161
|
-
|
|
175
|
+
async (sub: string) => {
|
|
176
|
+
// If "remember me" was unchecked, we need to use the ephemeral token to
|
|
177
|
+
// authenticate the request.
|
|
178
|
+
const bearer = sessions.find((s) => s.account.sub === sub)?.ephemeralToken
|
|
179
|
+
const { url } = await api.fetch('POST', '/accept', { sub }, { bearer })
|
|
180
|
+
performRedirect(url)
|
|
162
181
|
},
|
|
163
|
-
[api, performRedirect],
|
|
182
|
+
[api, sessions, performRedirect],
|
|
164
183
|
)
|
|
165
184
|
|
|
166
185
|
const doReject = useSafeCallback(async () => {
|
|
167
|
-
|
|
186
|
+
const { url } = await api.fetch('POST', '/reject', {})
|
|
187
|
+
performRedirect(url)
|
|
168
188
|
}, [api, performRedirect])
|
|
169
189
|
|
|
170
190
|
return {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CustomizationData,
|
|
3
|
+
ScopeDetail,
|
|
4
|
+
Session,
|
|
5
|
+
} from '@atproto/oauth-provider-api'
|
|
6
|
+
import type { OAuthClientMetadata } from '@atproto/oauth-types'
|
|
7
|
+
|
|
8
|
+
export type AuthorizeData = {
|
|
9
|
+
requestUri: string
|
|
10
|
+
|
|
11
|
+
clientId: string
|
|
12
|
+
clientMetadata: OAuthClientMetadata
|
|
13
|
+
clientTrusted: boolean
|
|
14
|
+
|
|
15
|
+
scopeDetails?: ScopeDetail[]
|
|
16
|
+
|
|
17
|
+
loginHint?: string
|
|
18
|
+
uiLocales?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ErrorData = {
|
|
22
|
+
error: string
|
|
23
|
+
error_description: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type HydrationData = {
|
|
27
|
+
/**
|
|
28
|
+
* Matches the variables needed by `authorization-page.tsx`
|
|
29
|
+
*/
|
|
30
|
+
'authorization-page': {
|
|
31
|
+
__customizationData: CustomizationData
|
|
32
|
+
__authorizeData: AuthorizeData
|
|
33
|
+
__sessions: readonly Session[]
|
|
34
|
+
}
|
|
35
|
+
'error-page': {
|
|
36
|
+
/**
|
|
37
|
+
* Matches the variables needed by `error-page.tsx`
|
|
38
|
+
*/
|
|
39
|
+
__customizationData: CustomizationData
|
|
40
|
+
__errorData: ErrorData
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/lib/api.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
API_ENDPOINT_PREFIX,
|
|
3
|
+
ApiEndpoints,
|
|
4
|
+
CSRF_COOKIE_NAME,
|
|
5
|
+
CSRF_HEADER_NAME,
|
|
6
|
+
} from '@atproto/oauth-provider-api'
|
|
7
|
+
import { readCookie } from './cookies.ts'
|
|
2
8
|
import {
|
|
3
9
|
JsonClient,
|
|
4
10
|
JsonErrorPayload,
|
|
@@ -7,27 +13,12 @@ import {
|
|
|
7
13
|
|
|
8
14
|
export type { Options } from './json-client.ts'
|
|
9
15
|
|
|
10
|
-
export type AcceptData = {
|
|
11
|
-
sub: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
16
|
export class Api extends JsonClient<ApiEndpoints> {
|
|
15
|
-
constructor(
|
|
16
|
-
const baseUrl = new URL(
|
|
17
|
-
super(baseUrl,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
public buildAcceptUrl({ sub }: AcceptData): URL {
|
|
21
|
-
const url = new URL(`${this.baseUrl}/accept`)
|
|
22
|
-
url.searchParams.set('account_sub', sub)
|
|
23
|
-
url.searchParams.set('csrf_token', this.csrfToken)
|
|
24
|
-
return url
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
public buildRejectUrl(): URL {
|
|
28
|
-
const url = new URL(`${this.baseUrl}/reject`)
|
|
29
|
-
url.searchParams.set('csrf_token', this.csrfToken)
|
|
30
|
-
return url
|
|
17
|
+
constructor() {
|
|
18
|
+
const baseUrl = new URL(API_ENDPOINT_PREFIX, window.origin).toString()
|
|
19
|
+
super(baseUrl, () => ({
|
|
20
|
+
[CSRF_HEADER_NAME]: readCookie(CSRF_COOKIE_NAME),
|
|
21
|
+
}))
|
|
31
22
|
}
|
|
32
23
|
|
|
33
24
|
// Override the parent's parseError method to handle expected error responses
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const parseCookieString = (
|
|
2
|
-
cookie: string,
|
|
2
|
+
cookie: string = document.cookie,
|
|
3
3
|
): Record<string, string | undefined> =>
|
|
4
4
|
Object.fromEntries(
|
|
5
5
|
cookie
|
|
@@ -8,4 +8,10 @@ export const parseCookieString = (
|
|
|
8
8
|
.map((str) => str.split('=', 2).map((s) => decodeURIComponent(s.trim()))),
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
export
|
|
11
|
+
export function readCookie(
|
|
12
|
+
name: string,
|
|
13
|
+
cookie: string = document.cookie,
|
|
14
|
+
): string | undefined {
|
|
15
|
+
const cookies = parseCookieString(cookie)
|
|
16
|
+
return cookies[name]
|
|
17
|
+
}
|