Skip to content

Commit

Permalink
add reset password form
Browse files Browse the repository at this point in the history
  • Loading branch information
ajbura committed Jan 19, 2024
1 parent 66ae076 commit 3372df6
Show file tree
Hide file tree
Showing 14 changed files with 569 additions and 192 deletions.
35 changes: 35 additions & 0 deletions src/app/components/ConfirmPasswordMatch.tsx
Original file line number Diff line number Diff line change
@@ -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);
}
72 changes: 72 additions & 0 deletions src/app/components/UIAFlowOverlay.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
41 changes: 19 additions & 22 deletions src/app/components/uia-stages/EmailStage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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 { RegisterEmailCallback, RegisteredEmailResponse } from '../../hooks/useRegisterEmail';
import { RequestEmailTokenCallback, RequestEmailTokenResponse } from '../../hooks/types';

function EmailErrorDialog({
title,
Expand Down Expand Up @@ -70,15 +70,15 @@ export function EmailStageDialog({
email,
clientSecret,
stageData,
registerEmailState,
registerEmail,
emailTokenState,
requestEmailToken,
submitAuthDict,
onCancel,
}: StageComponentProps & {
email?: string;
clientSecret: string;
registerEmailState: AsyncState<RegisteredEmailResponse, MatrixError>;
registerEmail: RegisterEmailCallback;
emailTokenState: AsyncState<RequestEmailTokenResponse, MatrixError>;
requestEmailToken: RequestEmailTokenCallback;
}) {
const { errorCode, error, session } = stageData;

Expand All @@ -100,18 +100,18 @@ export function EmailStageDialog({

const handleEmailSubmit = useCallback(
(userEmail: string) => {
registerEmail(userEmail, clientSecret);
requestEmailToken(userEmail, clientSecret);
},
[clientSecret, registerEmail]
[clientSecret, requestEmailToken]
);

useEffect(() => {
if (email && !errorCode && registerEmailState.status === AsyncStatus.Idle) {
registerEmail(email, clientSecret);
if (email && !errorCode && emailTokenState.status === AsyncStatus.Idle) {
requestEmailToken(email, clientSecret);
}
}, [email, errorCode, clientSecret, registerEmailState, registerEmail]);
}, [email, errorCode, clientSecret, emailTokenState, requestEmailToken]);

if (registerEmailState.status === AsyncStatus.Loading) {
if (emailTokenState.status === AsyncStatus.Loading) {
return (
<Box direction="Column" alignItems="Center" gap="400">
<Spinner variant="Secondary" size="600" />
Expand All @@ -120,37 +120,34 @@ export function EmailStageDialog({
);
}

if (registerEmailState.status === AsyncStatus.Error) {
if (emailTokenState.status === AsyncStatus.Error) {
return (
<EmailErrorDialog
title={registerEmailState.error.errcode ?? 'Verify Email'}
title={emailTokenState.error.errcode ?? 'Verify Email'}
message={
registerEmailState.error?.data?.error ??
registerEmailState.error.message ??
'Failed to send Email verification request.'
emailTokenState.error?.data?.error ??
emailTokenState.error.message ??
'Failed to send verification Email request.'
}
onRetry={handleEmailSubmit}
onCancel={onCancel}
/>
);
}

if (registerEmailState.status === AsyncStatus.Success) {
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 "${registerEmailState.data.email}" and validate before continuing further.`}</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(registerEmailState.data.result.sid)}
>
<Button variant="Primary" onClick={() => handleSubmit(emailTokenState.data.result.sid)}>
<Text as="span" size="B400">
Continue
</Text>
Expand Down
12 changes: 12 additions & 0 deletions src/app/hooks/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IRequestTokenResponse } from 'matrix-js-sdk';

export type RequestEmailTokenResponse = {
email: string;
clientSecret: string;
result: IRequestTokenResponse;
};
export type RequestEmailTokenCallback = (
email: string,
clientSecret: string,
nextLink?: string
) => Promise<RequestEmailTokenResponse>;
32 changes: 32 additions & 0 deletions src/app/hooks/usePasswordEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { MatrixClient, MatrixError } from 'matrix-js-sdk';
import { useCallback, useRef } from 'react';
import { AsyncState, useAsyncCallback } from './useAsyncCallback';
import { RequestEmailTokenCallback, RequestEmailTokenResponse } from './types';

export const usePasswordEmail = (
mx: MatrixClient
): [AsyncState<RequestEmailTokenResponse, MatrixError>, RequestEmailTokenCallback] => {
const sendAttemptRef = useRef(1);

const passwordEmailCallback: RequestEmailTokenCallback = useCallback(
async (email, clientSecret, nextLink) => {
const sendAttempt = sendAttemptRef.current;
sendAttemptRef.current += 1;
const result = await mx.requestPasswordEmailToken(email, clientSecret, sendAttempt, nextLink);
return {
email,
clientSecret,
result,
};
},
[mx]
);

const [passwordEmailState, passwordEmail] = useAsyncCallback<
RequestEmailTokenResponse,
MatrixError,
Parameters<RequestEmailTokenCallback>
>(passwordEmailCallback);

return [passwordEmailState, passwordEmail];
};
21 changes: 7 additions & 14 deletions src/app/hooks/useRegisterEmail.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,31 @@
import { IRequestTokenResponse, MatrixClient, MatrixError } from 'matrix-js-sdk';
import { MatrixClient, MatrixError } from 'matrix-js-sdk';
import { useCallback, useRef } from 'react';
import { AsyncState, useAsyncCallback } from './useAsyncCallback';
import { RequestEmailTokenCallback, RequestEmailTokenResponse } from './types';

export type RegisteredEmailResponse = {
email: string;
result: IRequestTokenResponse;
};
export type RegisterEmailCallback = (
email: string,
clientSecret: string,
nextLink?: string
) => Promise<RegisteredEmailResponse>;
export const useRegisterEmail = (
mx: MatrixClient
): [AsyncState<RegisteredEmailResponse, MatrixError>, RegisterEmailCallback] => {
): [AsyncState<RequestEmailTokenResponse, MatrixError>, RequestEmailTokenCallback] => {
const sendAttemptRef = useRef(1);

const registerEmailCallback: RegisterEmailCallback = useCallback(
const registerEmailCallback: RequestEmailTokenCallback = useCallback(
async (email, clientSecret, nextLink) => {
const sendAttempt = sendAttemptRef.current;
sendAttemptRef.current += 1;
const result = await mx.requestRegisterEmailToken(email, clientSecret, sendAttempt, nextLink);
return {
email,
clientSecret,
result,
};
},
[mx]
);

const [registerEmailState, registerEmail] = useAsyncCallback<
RegisteredEmailResponse,
RequestEmailTokenResponse,
MatrixError,
Parameters<RegisterEmailCallback>
Parameters<RequestEmailTokenCallback>
>(registerEmailCallback);

return [registerEmailState, registerEmail];
Expand Down
9 changes: 2 additions & 7 deletions src/app/pages/auth/login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,9 @@ import { TokenLogin } from './TokenLogin';
import { OrDivider } from '../OrDivider';
import { getLoginPath, getRegisterPath } from '../../pathUtils';
import { usePathWithOrigin } from '../../../hooks/usePathWithOrigin';
import { LoginPathSearchParams } from '../../paths';

export type LoginSearchParams = {
username?: string;
email?: string;
loginToken?: string;
};

const getLoginSearchParams = (searchParams: URLSearchParams): LoginSearchParams => ({
const getLoginSearchParams = (searchParams: URLSearchParams): LoginPathSearchParams => ({
username: searchParams.get('username') ?? undefined,
email: searchParams.get('email') ?? undefined,
loginToken: searchParams.get('loginToken') ?? undefined,
Expand Down
Loading

0 comments on commit 3372df6

Please sign in to comment.