imnyang/cinny
Watch
1
0
Fork
You've already forked cinny
0

feat: URL navigation in auth (#1603)

* bump to react 18 and install react-router-dom

* Upgrade to react 18 root

* update vite

* add cs api's

* convert state/auth to ts

* add client config context

* add auto discovery context

* add spec version context

* add auth flow context

* add background dot pattern css

* add promise utils

* init url based routing

* update auth route server path as effect

* add auth server hook

* always use server from discovery info in context

* login - WIP

* upgrade jotai to v2

* add atom with localStorage util

* add multi account sessions atom

* add default IGNORE res to auto discovery

* add error type in async callback hook

* handle password login error

* fix async callback hook

* allow password login

* Show custom server not allowed error in mxId login

* add sso login component

* add token login

* fix hardcoded m.login.password in login func

* update server input on url change

* Improve sso login labels

* update folds

* fix async callback batching state update in safari

* wrap async callback set state in queueMicrotask

* wip

* wip - register

* arrange auth file structure

* add error codes

* extract filed error component form password login

* add register util function

* handle register flow - WIP

* update unsupported auth flow method reasons

* improve password input styles

* Improve UIA flow next stage calculation
complete stages can have any order so we will look for first stage which is not in completed

* process register UIA flow stages

* Extract register UIA stages component

* improve register error messages

* add focus trap & step count in UIA stages

* add reset password path and path utils

* add path with origin hook

* fix sso redirect url

* rename register token query param to token

* restyle auth screen header

* add reset password component - WIP

* add reset password form

* add netlify rewrites

* fix netlify file indentation

* test netlify redirect

* fix vite to include netlify toml

* add more netlify redirects

* add splat to public and assets path

* fix vite base name

* add option to use hash router in config and remove appVersion

* add splash screen component

* add client config loading and error screen

* fix server picker bug

* fix reset password email input type

* make auth page small screen responsive

* fix typo in reset password screen
This commit is contained in:
Ajay Bura 2024-01-21 23:50:56 +11:00 • committed by GitHub
commit 20db27fa7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
103 changed files with 4775 additions and 546 deletions

View file

@ -1,3 +0,0 @@
# Redirects from what the browser requests to what we serve
/login /
/register /

3
build.config.ts Normal file
View file

@ -0,0 +1,3 @@
export default {
base: '/',
};

View file

@ -8,5 +8,10 @@
"mozilla.org", "mozilla.org",
"xmr.se" "xmr.se"
], ],
"allowCustomHomeservers": true "allowCustomHomeservers": true,
"hashRouter": {
"enabled": false,
"basename": "/"
}
} }

View file

@ -96,6 +96,6 @@
<audio id="inviteSound"> <audio id="inviteSound">
<source src="./public/sound/invite.ogg" type="audio/ogg" /> <source src="./public/sound/invite.ogg" type="audio/ogg" />
</audio> </audio>
<script type="module" src="./src/index.jsx"></script> <script type="module" src="./src/index.tsx"></script>
</body> </body>
</html> </html>

34
netlify.toml Normal file
View file

@ -0,0 +1,34 @@
[[redirects]]
from = "/config.json"
to = "/config.json"
status = 200
[[redirects]]
from = "/manifest.json"
to = "/manifest.json"
status = 200
[[redirects]]
from = "/olm.wasm"
to = "/olm.wasm"
status = 200
[[redirects]]
from = "/pdf.worker.min.js"
to = "/pdf.worker.min.js"
status = 200
[[redirects]]
from = "/public/*"
to = "/public/:splat"
status = 200
[[redirects]]
from = "/assets/*"
to = "/assets/:splat"
status = 200
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

1018
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -39,13 +39,13 @@
"file-saver": "2.0.5", "file-saver": "2.0.5",
"flux": "4.0.3", "flux": "4.0.3",
"focus-trap-react": "10.0.2", "focus-trap-react": "10.0.2",
"folds": "1.5.0", "folds": "1.5.1",
"formik": "2.2.9", "formik": "2.2.9",
"html-dom-parser": "4.0.0", "html-dom-parser": "4.0.0",
"html-react-parser": "4.2.0", "html-react-parser": "4.2.0",
"immer": "9.0.16", "immer": "9.0.16",
"is-hotkey": "0.2.0", "is-hotkey": "0.2.0",
"jotai": "1.12.0", "jotai": "2.6.0",
"katex": "0.16.4", "katex": "0.16.4",
"linkify-html": "4.0.2", "linkify-html": "4.0.2",
"linkify-react": "4.1.1", "linkify-react": "4.1.1",
@ -55,17 +55,18 @@
"pdfjs-dist": "3.10.111", "pdfjs-dist": "3.10.111",
"prismjs": "1.29.0", "prismjs": "1.29.0",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "17.0.2", "react": "18.2.0",
"react-aria": "3.29.1", "react-aria": "3.29.1",
"react-autosize-textarea": "7.1.0", "react-autosize-textarea": "7.1.0",
"react-blurhash": "0.2.0", "react-blurhash": "0.2.0",
"react-dnd": "15.1.2", "react-dnd": "16.0.1",
"react-dnd-html5-backend": "15.1.3", "react-dnd-html5-backend": "16.0.1",
"react-dom": "17.0.2", "react-dom": "18.2.0",
"react-error-boundary": "4.0.10", "react-error-boundary": "4.0.10",
"react-google-recaptcha": "2.1.0", "react-google-recaptcha": "2.1.0",
"react-modal": "3.16.1", "react-modal": "3.16.1",
"react-range": "1.8.14", "react-range": "1.8.14",
"react-router-dom": "6.20.0",
"sanitize-html": "2.8.0", "sanitize-html": "2.8.0",
"slate": "0.94.1", "slate": "0.94.1",
"slate-history": "0.93.0", "slate-history": "0.93.0",
@ -81,13 +82,14 @@
"@types/file-saver": "2.0.5", "@types/file-saver": "2.0.5",
"@types/node": "18.11.18", "@types/node": "18.11.18",
"@types/prismjs": "1.26.0", "@types/prismjs": "1.26.0",
"@types/react": "18.0.26", "@types/react": "18.2.39",
"@types/react-dom": "18.0.9", "@types/react-dom": "18.2.17",
"@types/react-google-recaptcha": "2.1.8",
"@types/sanitize-html": "2.9.0", "@types/sanitize-html": "2.9.0",
"@types/ua-parser-js": "0.7.36", "@types/ua-parser-js": "0.7.36",
"@typescript-eslint/eslint-plugin": "5.46.1", "@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1", "@typescript-eslint/parser": "5.46.1",
"@vitejs/plugin-react": "3.0.0", "@vitejs/plugin-react": "4.2.0",
"buffer": "6.0.3", "buffer": "6.0.3",
"eslint": "8.29.0", "eslint": "8.29.0",
"eslint-config-airbnb": "19.0.4", "eslint-config-airbnb": "19.0.4",
@ -100,7 +102,7 @@
"prettier": "2.8.1", "prettier": "2.8.1",
"sass": "1.56.2", "sass": "1.56.2",
"typescript": "4.9.4", "typescript": "4.9.4",
"vite": "4.3.9", "vite": "5.0.8",
"vite-plugin-static-copy": "0.13.0" "vite-plugin-static-copy": "0.13.0"
} }
} }

View file

@ -0,0 +1,64 @@
import { ReactNode, useCallback, useEffect, useMemo } from 'react';
import { MatrixError, createClient } from 'matrix-js-sdk';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo';
import { promiseFulfilledResult, promiseRejectedResult } from '../utils/common';
import {
AuthFlows,
RegisterFlowStatus,
RegisterFlowsResponse,
parseRegisterErrResp,
} from '../hooks/useAuthFlows';
type AuthFlowsLoaderProps = {
fallback?: () => ReactNode;
error?: (err: unknown) => ReactNode;
children: (authFlows: AuthFlows) => ReactNode;
};
export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderProps) {
const autoDiscoveryInfo = useAutoDiscoveryInfo();
const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url;
const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]);
const [state, load] = useAsyncCallback(
useCallback(async () => {
const result = await Promise.allSettled([mx.loginFlows(), mx.registerRequest({})]);
const loginFlows = promiseFulfilledResult(result[0]);
const registerResp = promiseRejectedResult(result[1]) as MatrixError | undefined;
let registerFlows: RegisterFlowsResponse = { status: RegisterFlowStatus.InvalidRequest };
if (typeof registerResp === 'object' && registerResp.httpStatus) {
registerFlows = parseRegisterErrResp(registerResp);
}
if (!loginFlows) {
throw new Error('Missing auth flow!');
}
if ('errcode' in loginFlows) {
throw new Error('Failed to load auth flow!');
}
const authFlows: AuthFlows = {
loginFlows,
registerFlows,
};
return authFlows;
}, [mx])
);
useEffect(() => {
load();
}, [load]);
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
return fallback?.();
}
if (state.status === AsyncStatus.Error) {
return error?.(state.error);
}
return children(state.data);
}

View file

@ -0,0 +1,38 @@
import { ReactNode, useCallback, useEffect, useState } from 'react';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { ClientConfig } from '../hooks/useClientConfig';
import { trimTrailingSlash } from '../utils/common';
const getClientConfig = async (): Promise<ClientConfig> => {
const url = `${trimTrailingSlash(import.meta.env.BASE_URL)}/config.json`;
const config = await fetch(url, { method: 'GET' });
return config.json();
};
type ClientConfigLoaderProps = {
fallback?: () => ReactNode;
error?: (err: unknown, retry: () => void, ignore: () => void) => ReactNode;
children: (config: ClientConfig) => ReactNode;
};
export function ClientConfigLoader({ fallback, error, children }: ClientConfigLoaderProps) {
const [state, load] = useAsyncCallback(getClientConfig);
const [ignoreError, setIgnoreError] = useState(false);
const ignoreCallback = useCallback(() => setIgnoreError(true), []);
useEffect(() => {
load();
}, [load]);
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
return fallback?.();
}
if (!ignoreError && state.status === AsyncStatus.Error) {
return error?.(state.error, load, ignoreCallback);
}
const config: ClientConfig = state.status === AsyncStatus.Success ? state.data : {};
return children(config);
}

View file

@ -0,0 +1,35 @@
import { ReactNode, RefObject, useCallback, useRef, useState } from 'react';
import { useDebounce } from '../hooks/useDebounce';
type ConfirmPasswordMatchProps = {
initialValue: boolean;
children: (
match: boolean,
doMatch: () => void,
passRef: RefObject<HTMLInputElement>,
confPassRef: RefObject<HTMLInputElement>
) => ReactNode;
};
export function ConfirmPasswordMatch({ initialValue, children }: ConfirmPasswordMatchProps) {
const [match, setMatch] = useState(initialValue);
const passRef = useRef<HTMLInputElement>(null);
const confPassRef = useRef<HTMLInputElement>(null);
const doMatch = useDebounce(
useCallback(() => {
const pass = passRef.current?.value;
const confPass = confPassRef.current?.value;
if (!confPass) {
setMatch(initialValue);
return;
}
setMatch(pass === confPass);
}, [initialValue]),
{
wait: 500,
immediate: false,
}
);
return children(match, doMatch, passRef, confPassRef);
}

View file

@ -0,0 +1,32 @@
import { ReactNode, useCallback, useEffect } from 'react';
import { AsyncStatus, useAsyncCallback } from '../hooks/useAsyncCallback';
import { SpecVersions, specVersions } from '../cs-api';
import { useAutoDiscoveryInfo } from '../hooks/useAutoDiscoveryInfo';
type SpecVersionsLoaderProps = {
fallback?: () => ReactNode;
error?: (err: unknown) => ReactNode;
children: (versions: SpecVersions) => ReactNode;
};
export function SpecVersionsLoader({ fallback, error, children }: SpecVersionsLoaderProps) {
const autoDiscoveryInfo = useAutoDiscoveryInfo();
const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url;
const [state, load] = useAsyncCallback(
useCallback(() => specVersions(fetch, baseUrl), [baseUrl])
);
useEffect(() => {
load();
}, [load]);
if (state.status === AsyncStatus.Idle || state.status === AsyncStatus.Loading) {
return fallback?.();
}
if (state.status === AsyncStatus.Error) {
return error?.(state.error);
}
return children(state.data);
}

View file

@ -0,0 +1,17 @@
import { ReactNode } from 'react';
import { UIAFlow } from 'matrix-js-sdk';
import { useSupportedUIAFlows } from '../hooks/useUIAFlows';
export function SupportedUIAFlowsLoader({
flows,
supportedStages,
children,
}: {
supportedStages: string[];
flows: UIAFlow[];
children: (supportedFlows: UIAFlow[]) => ReactNode;
}) {
const supportedFlows = useSupportedUIAFlows(flows, supportedStages);
return children(supportedFlows);
}

View file

@ -0,0 +1,72 @@
import React, { ReactNode } from 'react';
import {
Overlay,
OverlayBackdrop,
Box,
config,
Text,
TooltipProvider,
Tooltip,
Icons,
Icon,
Chip,
IconButton,
} from 'folds';
import FocusTrap from 'focus-trap-react';
export type UIAFlowOverlayProps = {
currentStep: number;
stepCount: number;
children: ReactNode;
onCancel: () => void;
};
export function UIAFlowOverlay({
currentStep,
stepCount,
children,
onCancel,
}: UIAFlowOverlayProps) {
return (
<Overlay open backdrop={<OverlayBackdrop />}>
<FocusTrap focusTrapOptions={{ initialFocus: false }}>
<Box style={{ height: '100%' }} direction="Column" grow="Yes" gap="400">
<Box grow="Yes" direction="Column" alignItems="Center" justifyContent="Center">
{children}
</Box>
<Box
style={{ padding: config.space.S200 }}
shrink="No"
justifyContent="Center"
alignItems="Center"
gap="200"
>
<Chip as="div" radii="Pill" outlined>
<Text as="span" size="T300">{`Step ${currentStep}/${stepCount}`}</Text>
</Chip>
<TooltipProvider
tooltip={
<Tooltip variant="Critical">
<Text>Exit</Text>
</Tooltip>
}
position="Top"
>
{(anchorRef) => (
<IconButton
ref={anchorRef}
variant="Critical"
size="300"
onClick={onCancel}
radii="Pill"
outlined
>
<Icon size="50" src={Icons.Cross} />
</IconButton>
)}
</TooltipProvider>
</Box>
</Box>
</FocusTrap>
</Overlay>
);
}

View file

@ -0,0 +1,45 @@
import React, { ComponentProps, forwardRef } from 'react';
import { Icon, IconButton, Input, config, Icons } from 'folds';
import { UseStateProvider } from '../UseStateProvider';
type PasswordInputProps = Omit<ComponentProps<typeof Input>, 'type' | 'size'> & {
size: '400' | '500';
};
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
({ variant, size, style, after, ...props }, ref) => {
const paddingRight: string = size === '500' ? config.space.S300 : config.space.S200;
return (
<UseStateProvider initial={false}>
{(visible, setVisible) => (
<Input
{...props}
ref={ref}
style={{ paddingRight, ...style }}
type={visible ? 'text' : 'password'}
size={size}
variant={variant}
after={
<>
{after}
<IconButton
onClick={() => setVisible(!visible)}
type="button"
variant={visible ? 'Warning' : variant}
size="300"
radii="300"
>
<Icon
style={{ opacity: config.opacity.P300 }}
size="100"
src={visible ? Icons.Eye : Icons.EyeBlind}
/>
</IconButton>
</>
}
/>
)}
</UseStateProvider>
);
}
);

View file

@ -0,0 +1,12 @@
import { style } from '@vanilla-extract/css';
import { color, config } from 'folds';
export const SplashScreen = style({
minHeight: '100%',
backgroundColor: color.Background.Container,
color: color.Background.OnContainer,
});
export const SplashScreenFooter = style({
padding: config.space.S400,
});

View file

@ -0,0 +1,29 @@
import { Box, Text } from 'folds';
import React, { ReactNode } from 'react';
import classNames from 'classnames';
import * as patternsCSS from '../../styles/Patterns.css';
import * as css from './SplashScreen.css';
type SplashScreenProps = {
children: ReactNode;
};
export function SplashScreen({ children }: SplashScreenProps) {
return (
<Box
className={classNames(css.SplashScreen, patternsCSS.BackgroundDotPattern)}
direction="Column"
>
{children}
<Box
className={css.SplashScreenFooter}
shrink="No"
alignItems="Center"
justifyContent="Center"
>
<Text size="H2" align="Center">
Cinny
</Text>
</Box>
</Box>
);
}

View file

@ -0,0 +1 @@
export * from './SplashScreen';

View file

@ -0,0 +1,65 @@
import React, { useEffect, useCallback } from 'react';
import { Dialog, Text, Box, Button, config } from 'folds';
import { AuthType } from 'matrix-js-sdk';
import { StageComponentProps } from './types';
function DummyErrorDialog({
title,
message,
onRetry,
onCancel,
}: {
title: string;
message: string;
onRetry: () => void;
onCancel: () => void;
}) {
return (
<Dialog>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="100">
<Text size="H4">{title}</Text>
<Text>{message}</Text>
</Box>
<Button variant="Critical" onClick={onRetry}>
<Text as="span" size="B400">
Retry
</Text>
</Button>
<Button variant="Critical" fill="None" outlined onClick={onCancel}>
<Text as="span" size="B400">
Cancel
</Text>
</Button>
</Box>
</Dialog>
);
}
export function AutoDummyStageDialog({ stageData, submitAuthDict, onCancel }: StageComponentProps) {
const { errorCode, error, session } = stageData;
const handleSubmit = useCallback(() => {
submitAuthDict({
type: AuthType.Dummy,
session,
});
}, [session, submitAuthDict]);
useEffect(() => {
if (!errorCode) handleSubmit();
}, [handleSubmit, errorCode]);
if (errorCode) {
return (
<DummyErrorDialog
title={errorCode}
message={error ?? 'Failed to register.'}
onRetry={handleSubmit}
onCancel={onCancel}
/>
);
}
return null;
}

View file

@ -0,0 +1,172 @@
import React, { useEffect, useCallback, FormEventHandler } from 'react';
import { Dialog, Text, Box, Button, config, Input, color, Spinner } from 'folds';
import { AuthType, MatrixError } from 'matrix-js-sdk';
import { StageComponentProps } from './types';
import { AsyncState, AsyncStatus } from '../../hooks/useAsyncCallback';
import { RequestEmailTokenCallback, RequestEmailTokenResponse } from '../../hooks/types';
function EmailErrorDialog({
title,
message,
defaultEmail,
onRetry,
onCancel,
}: {
title: string;
message: string;
defaultEmail?: string;
onRetry: (email: string) => void;
onCancel: () => void;
}) {
const handleFormSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
const { retryEmailInput } = evt.target as HTMLFormElement & {
retryEmailInput: HTMLInputElement;
};
const t = retryEmailInput.value;
onRetry(t);
};
return (
<Dialog>
<Box
as="form"
onSubmit={handleFormSubmit}
style={{ padding: config.space.S400 }}
direction="Column"
gap="400"
>
<Box direction="Column" gap="100">
<Text size="H4">{title}</Text>
<Text>{message}</Text>
<Text as="label" size="L400" style={{ paddingTop: config.space.S400 }}>
Email
</Text>
<Input
name="retryEmailInput"
variant="Background"
size="500"
outlined
defaultValue={defaultEmail}
required
/>
</Box>
<Button variant="Primary" type="submit">
<Text as="span" size="B400">
Send Verification Email
</Text>
</Button>
<Button variant="Critical" fill="None" outlined type="button" onClick={onCancel}>
<Text as="span" size="B400">
Cancel
</Text>
</Button>
</Box>
</Dialog>
);
}
export function EmailStageDialog({
email,
clientSecret,
stageData,
emailTokenState,
requestEmailToken,
submitAuthDict,
onCancel,
}: StageComponentProps & {
email?: string;
clientSecret: string;
emailTokenState: AsyncState<RequestEmailTokenResponse, MatrixError>;
requestEmailToken: RequestEmailTokenCallback;
}) {
const { errorCode, error, session } = stageData;
const handleSubmit = useCallback(
(sessionId: string) => {
const threepIDCreds = {
sid: sessionId,
client_secret: clientSecret,
};
submitAuthDict({
type: AuthType.Email,
threepid_creds: threepIDCreds,
threepidCreds: threepIDCreds,
session,
});
},
[submitAuthDict, session, clientSecret]
);
const handleEmailSubmit = useCallback(
(userEmail: string) => {
requestEmailToken(userEmail, clientSecret);
},
[clientSecret, requestEmailToken]
);
useEffect(() => {
if (email && !errorCode && emailTokenState.status === AsyncStatus.Idle) {
requestEmailToken(email, clientSecret);
}
}, [email, errorCode, clientSecret, emailTokenState, requestEmailToken]);
if (emailTokenState.status === AsyncStatus.Loading) {
return (
<Box direction="Column" alignItems="Center" gap="400">
<Spinner variant="Secondary" size="600" />
<Text style={{ color: color.Secondary.Main }}>Sending verification email...</Text>
</Box>
);
}
if (emailTokenState.status === AsyncStatus.Error) {
return (
<EmailErrorDialog
title={emailTokenState.error.errcode ?? 'Verify Email'}
message={
emailTokenState.error?.data?.error ??
emailTokenState.error.message ??
'Failed to send verification Email request.'
}
onRetry={handleEmailSubmit}
onCancel={onCancel}
/>
);
}
if (emailTokenState.status === AsyncStatus.Success) {
return (
<Dialog>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="100">
<Text size="H4">Verification Request Sent</Text>
<Text>{`Please check your email "${emailTokenState.data.email}" and validate before continuing further.`}</Text>
{errorCode && (
<Text style={{ color: color.Critical.Main }}>{`${errorCode}: ${error}`}</Text>
)}
</Box>
<Button variant="Primary" onClick={() => handleSubmit(emailTokenState.data.result.sid)}>
<Text as="span" size="B400">
Continue
</Text>
</Button>
</Box>
</Dialog>
);
}
if (!email) {
return (
<EmailErrorDialog
title="Provide Email"
message="Please provide email to send verification request."
onRetry={handleEmailSubmit}
onCancel={onCancel}
/>
);
}
return null;
}

View file

@ -0,0 +1,64 @@
import React from 'react';
import { Dialog, Text, Box, Button, config } from 'folds';
import { AuthType } from 'matrix-js-sdk';
import ReCAPTCHA from 'react-google-recaptcha';
import { StageComponentProps } from './types';
function ReCaptchaErrorDialog({
title,
message,
onCancel,
}: {
title: string;
message: string;
onCancel: () => void;
}) {
return (
<Dialog>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="100">
<Text size="H4">{title}</Text>
<Text>{message}</Text>
</Box>
<Button variant="Critical" fill="None" outlined onClick={onCancel}>
<Text as="span" size="B400">
Cancel
</Text>
</Button>
</Box>
</Dialog>
);
}
export function ReCaptchaStageDialog({ stageData, submitAuthDict, onCancel }: StageComponentProps) {
const { info, session } = stageData;
const publicKey = info?.public_key;
const handleChange = (token: string | null) => {
submitAuthDict({
type: AuthType.Recaptcha,
response: token,
session,
});
};
if (typeof publicKey !== 'string' || !session) {
return (
<ReCaptchaErrorDialog
title="Invalid Data"
message="No valid data found to proceed with ReCAPTCHA."
onCancel={onCancel}
/>
);
}
return (
<Dialog>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Text>Please check the box below to proceed.</Text>
<ReCAPTCHA sitekey={publicKey} onChange={handleChange} />
</Box>
</Dialog>
);
}

Some files were not shown because too many files have changed in this diff Show more