@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,46 @@
|
|
|
1
|
+
import { Trans } from '@lingui/react/macro'
|
|
2
|
+
import { JSX } from 'react'
|
|
3
|
+
import type { LinkDefinition } from '@atproto/oauth-provider-api'
|
|
4
|
+
import { clsx } from '../../lib/clsx.ts'
|
|
5
|
+
import { Override } from '../../lib/util.ts'
|
|
6
|
+
|
|
7
|
+
export type HelpCardProps = Override<
|
|
8
|
+
Omit<JSX.IntrinsicElements['p'], 'children'>,
|
|
9
|
+
{
|
|
10
|
+
links?: readonly LinkDefinition[]
|
|
11
|
+
}
|
|
12
|
+
>
|
|
13
|
+
|
|
14
|
+
export function HelpCard({
|
|
15
|
+
links,
|
|
16
|
+
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: HelpCardProps) {
|
|
20
|
+
const helpLink = links?.find((l) => l.rel === 'help')
|
|
21
|
+
|
|
22
|
+
if (!helpLink) return null
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<p
|
|
26
|
+
{...props}
|
|
27
|
+
className={clsx(
|
|
28
|
+
'text-sm rounded-md bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-400 p-3',
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
<Trans>
|
|
33
|
+
Having trouble?{' '}
|
|
34
|
+
<a
|
|
35
|
+
role="link"
|
|
36
|
+
href={helpLink.href}
|
|
37
|
+
rel={helpLink.rel}
|
|
38
|
+
target="_blank"
|
|
39
|
+
className="text-brand"
|
|
40
|
+
>
|
|
41
|
+
<Trans>Contact support</Trans>
|
|
42
|
+
</a>
|
|
43
|
+
</Trans>
|
|
44
|
+
</p>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { FunctionComponent, JSX } from 'react'
|
|
2
|
+
import { Override } from '../../lib/util.ts'
|
|
3
|
+
|
|
4
|
+
export type IconProps = Override<
|
|
5
|
+
Omit<JSX.IntrinsicElements['svg'], 'viewBox' | 'children' | 'xmlns'>,
|
|
6
|
+
{
|
|
7
|
+
/**
|
|
8
|
+
* The title of the icon, used for accessibility.
|
|
9
|
+
*/
|
|
10
|
+
title?: string
|
|
11
|
+
}
|
|
12
|
+
>
|
|
13
|
+
|
|
14
|
+
const makeSvgComponent = (path: string, displayName: string) => {
|
|
15
|
+
const SvgComponent: FunctionComponent<IconProps> = ({ title, ...props }) => (
|
|
16
|
+
<svg
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
viewBox="0 0 24 24"
|
|
19
|
+
{...props}
|
|
20
|
+
aria-hidden={!title}
|
|
21
|
+
>
|
|
22
|
+
{title && <title>{title}</title>}
|
|
23
|
+
<path
|
|
24
|
+
fill="currentColor"
|
|
25
|
+
fillRule="evenodd"
|
|
26
|
+
clipRule="evenodd"
|
|
27
|
+
d={path}
|
|
28
|
+
></path>
|
|
29
|
+
</svg>
|
|
30
|
+
)
|
|
31
|
+
SvgComponent.displayName = displayName
|
|
32
|
+
return SvgComponent
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const AccountIcon = makeSvgComponent(
|
|
36
|
+
'M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z',
|
|
37
|
+
'AccountIcon',
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
export const AlertIcon = makeSvgComponent(
|
|
41
|
+
'M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z',
|
|
42
|
+
'AlertIcon',
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
export const AtSymbolIcon = makeSvgComponent(
|
|
46
|
+
'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.958 9.958 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.207 4.207 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458.358-2.547 2.514-4.586 5.044-4.23.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.222 2.222 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529-.24 1.71.784 3.03 1.98 3.198 1.195.168 2.543-.819 2.784-2.529.24-1.71-.784-3.03-1.98-3.198Z',
|
|
47
|
+
'AtSymbolIcon',
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
export const CaretRightIcon = makeSvgComponent(
|
|
51
|
+
'M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z',
|
|
52
|
+
'CaretRightIcon',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
export const CheckMarkIcon = makeSvgComponent(
|
|
56
|
+
'M21.59 3.193a1 1 0 0 1 .217 1.397l-11.706 16a1 1 0 0 1-1.429.193l-6.294-5a1 1 0 1 1 1.244-1.566l5.48 4.353 11.09-15.16a1 1 0 0 1 1.398-.217Z',
|
|
57
|
+
'CheckMarkIcon',
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
export const EmailIcon = makeSvgComponent(
|
|
61
|
+
'M4.568 4h14.864c.252 0 .498 0 .706.017.229.019.499.063.77.201a2 2 0 0 1 .874.874c.138.271.182.541.201.77.017.208.017.454.017.706v10.864c0 .252 0 .498-.017.706a2.022 2.022 0 0 1-.201.77 2 2 0 0 1-.874.874 2.022 2.022 0 0 1-.77.201c-.208.017-.454.017-.706.017H4.568c-.252 0-.498 0-.706-.017a2.022 2.022 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C2 17.93 2 17.684 2 17.432V6.568c0-.252 0-.498.017-.706.019-.229.063-.499.201-.77a2 2 0 0 1 .874-.874c.271-.138.541-.182.77-.201C4.07 4 4.316 4 4.568 4Zm.456 2L12 11.708 18.976 6H5.024ZM20 7.747l-6.733 5.509a2 2 0 0 1-2.534 0L4 7.746V17.4a8.187 8.187 0 0 0 .011.589h.014c.116.01.278.011.575.011h14.8a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V7.747Z',
|
|
62
|
+
'EmailIcon',
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
export const EyeIcon = makeSvgComponent(
|
|
66
|
+
'M12 6.5c3.79 0 7.17 2.13 8.82 5.5-1.65 3.37-5.02 5.5-8.82 5.5S4.83 15.37 3.18 12C4.83 8.63 8.21 6.5 12 6.5m0-2C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5m0 5c1.38 0 2.5 1.12 2.5 2.5s-1.12 2.5-2.5 2.5-2.5-1.12-2.5-2.5 1.12-2.5 2.5-2.5m0-2c-2.48 0-4.5 2.02-4.5 4.5s2.02 4.5 4.5 4.5 4.5-2.02 4.5-4.5-2.02-4.5-4.5-4.5',
|
|
67
|
+
'EyeIcon',
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
export const EyeSlashIcon = makeSvgComponent(
|
|
71
|
+
'M12 6c3.79 0 7.17 2.13 8.82 5.5-.59 1.22-1.42 2.27-2.41 3.12l1.41 1.41c1.39-1.23 2.49-2.77 3.18-4.53C21.27 7.11 17 4 12 4c-1.27 0-2.49.2-3.64.57l1.65 1.65C10.66 6.09 11.32 6 12 6m-1.07 1.14L13 9.21c.57.25 1.03.71 1.28 1.28l2.07 2.07c.08-.34.14-.7.14-1.07C16.5 9.01 14.48 7 12 7c-.37 0-.72.05-1.07.14M2.01 3.87l2.68 2.68C3.06 7.83 1.77 9.53 1 11.5 2.73 15.89 7 19 12 19c1.52 0 2.98-.29 4.32-.82l3.42 3.42 1.41-1.41L3.42 2.45zm7.5 7.5 2.61 2.61c-.04.01-.08.02-.12.02-1.38 0-2.5-1.12-2.5-2.5 0-.05.01-.08.01-.13m-3.4-3.4 1.75 1.75c-.23.55-.36 1.15-.36 1.78 0 2.48 2.02 4.5 4.5 4.5.63 0 1.23-.13 1.77-.36l.98.98c-.88.24-1.8.38-2.75.38-3.79 0-7.17-2.13-8.82-5.5.7-1.43 1.72-2.61 2.93-3.53',
|
|
72
|
+
'EyeSlashIcon',
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
export const LockIcon = makeSvgComponent(
|
|
76
|
+
'M7 7a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h1V7Zm-1 4v9h12v-9H6Zm9-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z',
|
|
77
|
+
'LockIcon',
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
export const TokenIcon = makeSvgComponent(
|
|
81
|
+
'M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z',
|
|
82
|
+
'TokenIcon',
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
export const XMarkIcon = makeSvgComponent(
|
|
86
|
+
'M4.293 4.293a1 1 0 0 1 1.414 0L12 10.586l6.293-6.293a1 1 0 1 1 1.414 1.414L13.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414L12 13.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L10.586 12 4.293 5.707a1 1 0 0 1 0-1.414Z',
|
|
87
|
+
'XMarkIcon',
|
|
88
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { JSX } from 'react'
|
|
2
|
+
import type { LinkDefinition } from '@atproto/oauth-provider-api'
|
|
3
|
+
import { Override } from '../../lib/util.ts'
|
|
4
|
+
import { LinkTitle } from './link-title.tsx'
|
|
5
|
+
|
|
6
|
+
export type LinkAnchorProps = Override<
|
|
7
|
+
JSX.IntrinsicElements['a'],
|
|
8
|
+
{
|
|
9
|
+
link: LinkDefinition
|
|
10
|
+
}
|
|
11
|
+
>
|
|
12
|
+
export function LinkAnchor({
|
|
13
|
+
link,
|
|
14
|
+
|
|
15
|
+
// a
|
|
16
|
+
children = <LinkTitle link={link} />,
|
|
17
|
+
role = 'link',
|
|
18
|
+
target = '_blank',
|
|
19
|
+
href = link.href,
|
|
20
|
+
rel = link.rel,
|
|
21
|
+
...props
|
|
22
|
+
}: LinkAnchorProps) {
|
|
23
|
+
return (
|
|
24
|
+
<a {...props} role={role} target={target} href={href} rel={rel}>
|
|
25
|
+
{children}
|
|
26
|
+
</a>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Trans } from '@lingui/react/macro'
|
|
2
|
+
import type { LinkDefinition } from '@atproto/oauth-provider-api'
|
|
3
|
+
import { MultiLangString } from './multi-lang-string.tsx'
|
|
4
|
+
|
|
5
|
+
export type LinkNameProps = {
|
|
6
|
+
link: LinkDefinition
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function LinkTitle({ link }: LinkNameProps) {
|
|
10
|
+
return (
|
|
11
|
+
<MultiLangString
|
|
12
|
+
value={link.title}
|
|
13
|
+
fallback={
|
|
14
|
+
link.rel === 'canonical' ? (
|
|
15
|
+
<Trans>Home</Trans>
|
|
16
|
+
) : link.rel === 'privacy-policy' ? (
|
|
17
|
+
<Trans>Privacy Policy</Trans>
|
|
18
|
+
) : link.rel === 'terms-of-service' ? (
|
|
19
|
+
<Trans>Terms of Service</Trans>
|
|
20
|
+
) : link.rel === 'help' ? (
|
|
21
|
+
<Trans>Support</Trans>
|
|
22
|
+
) : undefined
|
|
23
|
+
}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useLingui } from '@lingui/react/macro'
|
|
2
|
+
import { ReactNode } from 'react'
|
|
3
|
+
import type { LocalizedString } from '@atproto/oauth-provider-api'
|
|
4
|
+
|
|
5
|
+
export type MultiLangStringProps = {
|
|
6
|
+
value: LocalizedString
|
|
7
|
+
fallback?: ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function MultiLangString({
|
|
11
|
+
value,
|
|
12
|
+
fallback,
|
|
13
|
+
}: MultiLangStringProps): ReactNode {
|
|
14
|
+
const { i18n } = useLingui()
|
|
15
|
+
return (
|
|
16
|
+
findMatchingString(value, i18n.locale) ??
|
|
17
|
+
fallback ??
|
|
18
|
+
(typeof value === 'string' ? value : value.en)
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Only returns a string if it matches the desired locale.
|
|
24
|
+
*/
|
|
25
|
+
function findMatchingString(
|
|
26
|
+
value: LocalizedString,
|
|
27
|
+
locale: string,
|
|
28
|
+
): string | undefined {
|
|
29
|
+
switch (typeof value) {
|
|
30
|
+
case 'string':
|
|
31
|
+
// By convention, string values are in english ("en")
|
|
32
|
+
if (locale.startsWith('en')) return value
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
case 'object': {
|
|
36
|
+
// Exact match
|
|
37
|
+
const localeMatch = value[locale]
|
|
38
|
+
if (typeof localeMatch === 'string') return localeMatch
|
|
39
|
+
|
|
40
|
+
// Fallback to language match
|
|
41
|
+
const lang = locale.split('-')[0]
|
|
42
|
+
const langMatch = value[lang]
|
|
43
|
+
if (typeof langMatch === 'string') return langMatch
|
|
44
|
+
|
|
45
|
+
// Fallback to any locale from same language (e.g. "pt-PT" -> "pt-BR")
|
|
46
|
+
for (const k in value) {
|
|
47
|
+
if (k.startsWith(`${lang}-`)) {
|
|
48
|
+
const fallbackMatch = value[k]
|
|
49
|
+
if (typeof fallbackMatch === 'string') return fallbackMatch
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Trans, useLingui } from '@lingui/react/macro'
|
|
2
|
+
import { JSX } from 'react'
|
|
3
|
+
import { PasswordStrength, getPasswordStrength } from '../../lib/password.ts'
|
|
4
|
+
import { Override } from '../../lib/util.ts'
|
|
5
|
+
|
|
6
|
+
export type PasswordStrengthLabelProps = Override<
|
|
7
|
+
Omit<JSX.IntrinsicElements['span'], 'children' | 'aria-label'>,
|
|
8
|
+
{
|
|
9
|
+
password: string
|
|
10
|
+
}
|
|
11
|
+
>
|
|
12
|
+
|
|
13
|
+
export function PasswordStrengthLabel({
|
|
14
|
+
password,
|
|
15
|
+
|
|
16
|
+
// span
|
|
17
|
+
...props
|
|
18
|
+
}: PasswordStrengthLabelProps) {
|
|
19
|
+
const { t } = useLingui()
|
|
20
|
+
const strength = getPasswordStrength(password)
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<span {...props} aria-label={t`Password strength`}>
|
|
24
|
+
{strength === PasswordStrength.extra ? (
|
|
25
|
+
<Trans>Extra</Trans>
|
|
26
|
+
) : strength === PasswordStrength.strong ? (
|
|
27
|
+
<Trans>Strong</Trans>
|
|
28
|
+
) : strength === PasswordStrength.moderate ? (
|
|
29
|
+
<Trans>Moderate</Trans>
|
|
30
|
+
) : password ? (
|
|
31
|
+
<Trans>Weak</Trans>
|
|
32
|
+
) : (
|
|
33
|
+
<Trans>Missing</Trans>
|
|
34
|
+
)}
|
|
35
|
+
</span>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useLingui } from '@lingui/react/macro'
|
|
2
|
+
import { JSX } from 'react'
|
|
3
|
+
import { clsx } from '../../lib/clsx.ts'
|
|
4
|
+
import { PasswordStrength, getPasswordStrength } from '../../lib/password.ts'
|
|
5
|
+
import { Override } from '../../lib/util.ts'
|
|
6
|
+
|
|
7
|
+
export type PasswordStrengthMeterProps = Override<
|
|
8
|
+
Omit<
|
|
9
|
+
JSX.IntrinsicElements['div'],
|
|
10
|
+
| 'children'
|
|
11
|
+
| 'role'
|
|
12
|
+
| 'aria-label'
|
|
13
|
+
| 'aria-valuemin'
|
|
14
|
+
| 'aria-valuemax'
|
|
15
|
+
| 'aria-valuenow'
|
|
16
|
+
>,
|
|
17
|
+
{
|
|
18
|
+
password: string
|
|
19
|
+
}
|
|
20
|
+
>
|
|
21
|
+
|
|
22
|
+
export function PasswordStrengthMeter({
|
|
23
|
+
password,
|
|
24
|
+
|
|
25
|
+
// div
|
|
26
|
+
className,
|
|
27
|
+
...props
|
|
28
|
+
}: PasswordStrengthMeterProps) {
|
|
29
|
+
const { t } = useLingui()
|
|
30
|
+
const strength = password ? getPasswordStrength(password) : 0
|
|
31
|
+
|
|
32
|
+
const colorBg = 'bg-gray-300 dark:bg-slate-500'
|
|
33
|
+
const color =
|
|
34
|
+
strength === PasswordStrength.extra || strength === PasswordStrength.strong
|
|
35
|
+
? 'bg-success'
|
|
36
|
+
: strength === PasswordStrength.moderate
|
|
37
|
+
? 'bg-warning'
|
|
38
|
+
: 'bg-error'
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
{...props}
|
|
43
|
+
className={clsx('w-full h-1 flex space-x-2', className)}
|
|
44
|
+
role="meter"
|
|
45
|
+
aria-label={t`Password strength indicator`}
|
|
46
|
+
aria-valuemin={0}
|
|
47
|
+
aria-valuemax={PasswordStrength.extra}
|
|
48
|
+
aria-valuenow={strength}
|
|
49
|
+
>
|
|
50
|
+
{Array.from({ length: 4 }, (_, i) => (
|
|
51
|
+
<div
|
|
52
|
+
key={i}
|
|
53
|
+
className={`rounded h-1 w-1/4 ${strength > i ? color : colorBg}`}
|
|
54
|
+
/>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { JSX, useMemo } from 'react'
|
|
2
|
+
import { Override } from '../../lib/util.ts'
|
|
3
|
+
|
|
4
|
+
export type UrlPartRenderingOptions = {
|
|
5
|
+
faded?: boolean
|
|
6
|
+
bold?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type UrlRendererProps = {
|
|
10
|
+
url: string | URL
|
|
11
|
+
proto?: boolean | UrlPartRenderingOptions
|
|
12
|
+
host?: boolean | UrlPartRenderingOptions
|
|
13
|
+
path?: boolean | UrlPartRenderingOptions
|
|
14
|
+
query?: boolean | UrlPartRenderingOptions
|
|
15
|
+
hash?: boolean | UrlPartRenderingOptions
|
|
16
|
+
as?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function UrlViewer<As extends keyof JSX.IntrinsicElements = 'span'>({
|
|
20
|
+
url,
|
|
21
|
+
proto = false,
|
|
22
|
+
host = true,
|
|
23
|
+
path = false,
|
|
24
|
+
query = false,
|
|
25
|
+
hash = false,
|
|
26
|
+
as: As = 'span',
|
|
27
|
+
|
|
28
|
+
// Element
|
|
29
|
+
...props
|
|
30
|
+
}: Override<JSX.IntrinsicElements[As], UrlRendererProps>) {
|
|
31
|
+
const urlObj = useMemo(() => new URL(url), [url])
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<As {...props}>
|
|
35
|
+
{proto && (
|
|
36
|
+
<UrlPartViewer
|
|
37
|
+
value={`${urlObj.protocol}//`}
|
|
38
|
+
{...(proto === true ? null : proto)}
|
|
39
|
+
/>
|
|
40
|
+
)}
|
|
41
|
+
{host && (
|
|
42
|
+
<UrlPartViewer
|
|
43
|
+
value={urlObj.host}
|
|
44
|
+
{...(host === true ? { faded: false, bold: true } : host)}
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
{path && (
|
|
48
|
+
<UrlPartViewer
|
|
49
|
+
value={urlObj.pathname}
|
|
50
|
+
{...(path === true ? null : path)}
|
|
51
|
+
/>
|
|
52
|
+
)}
|
|
53
|
+
{query && (
|
|
54
|
+
<UrlPartViewer
|
|
55
|
+
value={urlObj.search}
|
|
56
|
+
{...(query === true ? null : query)}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
{hash && (
|
|
60
|
+
<UrlPartViewer value={urlObj.hash} {...(hash === true ? null : hash)} />
|
|
61
|
+
)}
|
|
62
|
+
</As>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function UrlPartViewer({
|
|
67
|
+
value,
|
|
68
|
+
faded = true,
|
|
69
|
+
bold = false,
|
|
70
|
+
}: { value: string } & UrlPartRenderingOptions) {
|
|
71
|
+
const Comp = bold ? 'b' : 'span'
|
|
72
|
+
return <Comp className={faded ? 'opacity-50' : ''}>{value}</Comp>
|
|
73
|
+
}
|
package/src/cookies.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const parseCookieString = (
|
|
2
|
+
cookie: string,
|
|
3
|
+
): Record<string, string | undefined> =>
|
|
4
|
+
Object.fromEntries(
|
|
5
|
+
cookie
|
|
6
|
+
.split(';')
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.map((str) => str.split('=', 2).map((s) => decodeURIComponent(s.trim()))),
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
export const cookies = parseCookieString(document.cookie)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Mock - OAuth Provider</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script>
|
|
11
|
+
/*
|
|
12
|
+
* This file's purpose is to provide a way to develop the UI without
|
|
13
|
+
* running a full featured OAuth server. It mocks the server responses and
|
|
14
|
+
* provides configuration data to the UI.
|
|
15
|
+
*
|
|
16
|
+
* This file is not part of the production build.
|
|
17
|
+
*
|
|
18
|
+
* Start the development server with the following command from the
|
|
19
|
+
* oauth-provider root:
|
|
20
|
+
*
|
|
21
|
+
* ```sh
|
|
22
|
+
* pnpm run start:ui
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Then open the browser at http://localhost:5173/
|
|
26
|
+
*/
|
|
27
|
+
</script>
|
|
28
|
+
<style>
|
|
29
|
+
/*
|
|
30
|
+
* PDS branding configuration (colors), in R G B format.
|
|
31
|
+
*
|
|
32
|
+
* The variables here are meant to override the default values defined in
|
|
33
|
+
* main.css. These values are typically generated by the backend and
|
|
34
|
+
* injected into the HTML. The colors suffixed with "-c" denote the
|
|
35
|
+
* "contrast" color of the corresponding color name. These are also
|
|
36
|
+
* automatically generated by the backend from the branding colors.
|
|
37
|
+
*
|
|
38
|
+
* The default colors can be seen by commenting out a color name (and
|
|
39
|
+
* corresponding "-c" contrast color) below:
|
|
40
|
+
*/
|
|
41
|
+
:root {
|
|
42
|
+
--color-brand: 10 122 255;
|
|
43
|
+
--color-brand-c: 255 255 255;
|
|
44
|
+
--color-error: 244 11 66;
|
|
45
|
+
--color-error-c: 255 255 255;
|
|
46
|
+
--color-warning: 251 86 7;
|
|
47
|
+
--color-warning-c: 255 255 255;
|
|
48
|
+
--color-success: 2 195 154;
|
|
49
|
+
--color-success-c: 0 0 0;
|
|
50
|
+
}
|
|
51
|
+
</style>
|
|
52
|
+
<script type="module">
|
|
53
|
+
/*
|
|
54
|
+
* PDS branding configuration
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
const name = 'Bluesky'
|
|
58
|
+
const links = [
|
|
59
|
+
{
|
|
60
|
+
title: { en: 'Home' },
|
|
61
|
+
href: 'https://bsky.social/',
|
|
62
|
+
rel: 'canonical', // prevents the login page from being indexed by search engines
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
title: { en: 'Terms of Service' },
|
|
66
|
+
href: 'https://bsky.social/about/support/tos',
|
|
67
|
+
rel: 'terms-of-service',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
title: { en: 'Privacy Policy' },
|
|
71
|
+
href: 'https://bsky.social/about/support/privacy-policy',
|
|
72
|
+
rel: 'privacy-policy',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
title: { en: 'Support' },
|
|
76
|
+
href: 'https://blueskyweb.zendesk.com/hc/en-us',
|
|
77
|
+
rel: 'help',
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
const logo = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 320 286"><path fill="rgb(10,122,255)" d="M69.364 19.146c36.687 27.806 76.147 84.186 90.636 114.439 14.489-30.253 53.948-86.633 90.636-114.439C277.107-.917 320-16.44 320 32.957c0 9.865-5.603 82.875-8.889 94.729-11.423 41.208-53.045 51.719-90.071 45.357 64.719 11.12 81.182 47.953 45.627 84.785-80 82.874-106.667-44.333-106.667-44.333s-26.667 127.207-106.667 44.333c-35.555-36.832-19.092-73.665 45.627-84.785-37.026 6.362-78.648-4.149-90.071-45.357C5.603 115.832 0 42.822 0 32.957 0-16.44 42.893-.917 69.364 19.147Z" /></svg>')}`
|
|
81
|
+
|
|
82
|
+
// Provide a value here to test the "sing-in only" flow
|
|
83
|
+
const loginHint = undefined // 'alice.test'
|
|
84
|
+
|
|
85
|
+
// Use empty array to disable the "sing-up" flow, use a single value to
|
|
86
|
+
// disable the domain selector.
|
|
87
|
+
const availableUserDomains = ['.bsky.social', '.bsky.team']
|
|
88
|
+
|
|
89
|
+
// Use non empty string to enable hCaptcha during "sing-up" flow
|
|
90
|
+
const hcaptchaSiteKey = undefined
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
* Client branding configuration
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
// Use an "http://" URL to test the "an app on your device" flow
|
|
97
|
+
const clientId = 'https://example.com/client.json'
|
|
98
|
+
const clientName = 'My App'
|
|
99
|
+
const clientPolicyUri = 'https://bsky.app'
|
|
100
|
+
const clientTosUri = 'https://bsky.app'
|
|
101
|
+
const clientLogoUri = 'https://bsky.app'
|
|
102
|
+
|
|
103
|
+
// Mock data
|
|
104
|
+
|
|
105
|
+
const requestUri = 'foo-bar'
|
|
106
|
+
|
|
107
|
+
window.__availableLocales = ['en', 'fr']
|
|
108
|
+
|
|
109
|
+
window.__customizationData = {
|
|
110
|
+
availableUserDomains,
|
|
111
|
+
inviteCodeRequired: false,
|
|
112
|
+
hcaptchaSiteKey,
|
|
113
|
+
name,
|
|
114
|
+
links,
|
|
115
|
+
logo,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
window.__errorData = {
|
|
119
|
+
error: 'foo',
|
|
120
|
+
error_description: 'bar',
|
|
121
|
+
}
|
|
122
|
+
</script>
|
|
123
|
+
<script src="./error-page.tsx" type="module"></script>
|
|
124
|
+
</body>
|
|
125
|
+
</html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import './styles.css'
|
|
2
|
+
|
|
3
|
+
import { StrictMode } from 'react'
|
|
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'
|
|
11
|
+
import { LocaleProvider } from './locales/locale-provider.tsx'
|
|
12
|
+
import { ErrorView } from './views/error/error-view.tsx'
|
|
13
|
+
|
|
14
|
+
export const availableLocales =
|
|
15
|
+
readBackendData<AvailableLocales>('__availableLocales')
|
|
16
|
+
export const customizationData = readBackendData<CustomizationData>(
|
|
17
|
+
'__customizationData',
|
|
18
|
+
)
|
|
19
|
+
export const errorData = readBackendData<ErrorData>('__errorData')
|
|
20
|
+
|
|
21
|
+
const container = document.getElementById('root')!
|
|
22
|
+
|
|
23
|
+
createRoot(container).render(
|
|
24
|
+
<StrictMode>
|
|
25
|
+
<LocaleProvider availableLocales={availableLocales}>
|
|
26
|
+
<ErrorView error={errorData} customizationData={customizationData} />
|
|
27
|
+
</LocaleProvider>
|
|
28
|
+
</StrictMode>,
|
|
29
|
+
)
|