Snippets Collections
import { EPatroclusStatus } from "@lendtable/common/dtos/PatroclusDto";
import { ErrorMessage } from "components-v2/validation/ErrorMessage";
import React, { FC, useEffect, useRef, useState } from "react";
import Button from "./Button";
import Header from "./Header";
import LinkIcons from "./LinkIcons";

let currentOtpIndex: number = 0;

const TwoFactorAuth: FC<{
	onContinue: (otp: string) => void;
	onClose: () => void;
	loading: boolean;
	status: EPatroclusStatus.MfaTokenRequired | EPatroclusStatus.MfaTokenInvalid;
	tokenLength: number;
}> = ({ onContinue, onClose, status, loading, tokenLength }) => {
	const [otp, setOtp] = useState<string[]>(new Array(tokenLength).fill(""));
	const [activeOtpIndex, setActiveOtpIndex] = useState<number>(0);
	const [invalid, setInvalid] = useState(false);

	const inputRef = useRef<HTMLInputElement>(null);

	const handleOnChange = ({
		target,
	}: React.ChangeEvent<HTMLInputElement>): void => {
		const { value } = target;
		const newOtp: string[] = [...otp];
		newOtp[currentOtpIndex] = value.substring(value.length - 1);

		if (!value) setActiveOtpIndex(currentOtpIndex - 1);
		else setActiveOtpIndex(currentOtpIndex + 1);

		setOtp(newOtp);
	};

	const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
		e.preventDefault();
		const pastedString = e.clipboardData.getData("text");
		const newOtp: string[] = [];
		for (let i = 0; i < tokenLength; i++) {
			newOtp.push(pastedString[i] || "");
		}
		setActiveOtpIndex(Math.min(pastedString.length, 5));

		setOtp(newOtp);
	};

	const handleOnKeyDown = (
		{ key }: React.KeyboardEvent<HTMLInputElement>,
		index: number
	) => {
		currentOtpIndex = index;
		if (key === "Backspace") {
			setActiveOtpIndex(currentOtpIndex - 1);
		}
	};

	const onSubmit = (e?: React.FormEvent<HTMLFormElement>) => {
		e && e.preventDefault();
		if (loading) {
			return;
		}

		if (otp.join("").length < tokenLength) {
			inputRef.current?.focus();
			setInvalid(true);
			setTimeout(() => {
				setInvalid(false);
			}, 820);
			return;
		}
		onContinue(otp.join(""));
	};

	useEffect(() => {
		inputRef.current?.focus();
	}, [activeOtpIndex]);

	const resetOtp = () => {
		const newOtp: string[] = new Array(tokenLength).fill("");
		setOtp(newOtp);
		setActiveOtpIndex(0);
	};

	const currentNumberOfCharacters = otp.filter(
		(character) => character !== ""
	).length;

	useEffect(() => {
		if (
			status === EPatroclusStatus.MfaTokenInvalid &&
			currentNumberOfCharacters > 0
		) {
			resetOtp();
		}
	}, [status]);

	return (
		<>
			<Header onClose={onClose}>
				<LinkIcons />
			</Header>

			<div>
				<div className="mx-2">
					<div className="mb-2 text-2xl font-semibold">Enter your 2FA Code</div>
					<div className="mb-6 text-xs font-light">
						Enter the 2fa code SMS verification received on your phone or email
						address:
					</div>
				</div>
				<form
					className={
						(invalid ? "animate-shake " : "") +
						"flex items-center justify-center space-x-2"
					}
					id="two-factor-auth"
					onSubmit={(e) => onSubmit(e)}
				>
					{otp.map((_, index) => {
						return (
							<React.Fragment key={index}>
								<input
									ref={index === activeOtpIndex ? inputRef : null}
									type="text"
									className="spin-button-none border-1 h-14 w-10 rounded-lg border-black bg-transparent text-center text-xl font-semibold text-gray-400 outline-none transition focus:border-gray-700 focus:text-gray-700"
									onChange={handleOnChange}
									onKeyDown={(e) => handleOnKeyDown(e, index)}
									onPaste={(e) => handlePaste(e)}
									value={otp[index]}
									disabled={loading}
									inputMode="numeric"
									pattern="[0-9]*"
									autoComplete="one-time-code"
								/>
							</React.Fragment>
						);
					})}
				</form>
				{status === EPatroclusStatus.MfaTokenInvalid && (
					<ErrorMessage
						className="mt-2"
						message="Your 2FA code was incorrect."
					/>
				)}
			</div>
			<Button
				form="two-factor-auth"
				label="Submit"
				submitting={loading}
				disabled={currentNumberOfCharacters < tokenLength}
			></Button>
		</>
	);
};

export default TwoFactorAuth;
import React from 'react';
import { UseFormMethods } from 'react-hook-form';
import { useStepMachine } from 'ui/step-machine';
import { ClipLoader } from 'react-spinners';
import { AuthError, Values } from './types';
import machine, { Step } from './stepMachine';
import Confirm from './Pages/Confirm';
import Change from './Pages/Change';
import Reset from './Pages/Reset';

interface Props {
  formProps: UseFormMethods<Values>;
  onForgotPassword: (email: string) => Promise<any>;
  onForgotPasswordSubmit: (values: any) => Promise<unknown>;
  error?: AuthError;
  submitError?: AuthError;
  submitting: boolean;
}

const Recovery = ({
  formProps: { errors, control, handleSubmit },
  onForgotPassword,
  onForgotPasswordSubmit,
  error,
  submitError,
  submitting,
}: Props) => {
  const { step, next, previous, handleSubmitStep } = useStepMachine(machine, {
    onSubit: onForgotPasswordSubmit,
  });
  return (
    <form
      noValidate
      onSubmit={handleSubmit(async (data) => {
        if (step === Step.RESET) {
          const { error } = await onForgotPassword(data.emailAddress);
          if (!error) {
            handleSubmitStep(data);
          }
          return null;
        }
        return handleSubmitStep(data);
      })}
    >
      <Choose>
        <When condition={step === Step.RESET}>
          <Reset
            error={error}
            errors={errors}
            control={control}
            submitting={submitting}
          />
        </When>
        <When condition={step === Step.CONFIRM}>
          <Confirm errors={errors} control={control} onPrevious={previous} />
        </When>
        <When condition={step === Step.CHANGE}>
          <Change
            error={submitError}
            onForgotPasswordSubmit={onForgotPasswordSubmit}
            submitting={submitting}
            errors={errors}
            control={control}
            onNext={next}
            onPrevious={previous}
          />
        </When>
        <When condition={step === Step.SUBMIT}>
          <ClipLoader />
        </When>
      </Choose>
    </form>
  );
};

export default Recovery;
import React from 'react';
import { render, screen, act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useForm } from 'react-hook-form';
import { createWrapper } from '__tests__/wrapper';
import { en } from 'ui/messages';
import { ForgotPasswordSubmit } from 'ports/auth/forgotPasswordSubmit';
import { ForgotPassword } from 'ports/auth/forgotPassword';
import Recovery from '../Recovery';
import { Values } from '../types';

const setup = () => {
  const onForgotPassword = jest.fn();
  const onForgotPasswordSubmit = jest.fn();

  const Provider: React.FC = () => {
    const formProps = useForm<Values>();
    return (
      <Recovery
        formProps={formProps}
        submitting={false}
        onForgotPassword={onForgotPassword}
        onForgotPasswordSubmit={onForgotPasswordSubmit}
      />
    );
  };

  const wrapper = createWrapper({
    messages: en,
    inject(jpex) {
      jpex.constant<ForgotPassword>(onForgotPassword);
      jpex.constant<ForgotPasswordSubmit>(onForgotPasswordSubmit);
    },
    render: () => {
      return <Provider />;
    },
  });
  return { wrapper, onForgotPassword, onForgotPasswordSubmit };
};

describe('testing password recovery page', () => {
  it('should run useForgotPassword on submit of RESET step', async () => {
    const { wrapper, onForgotPassword } = setup();
    await act(async () => {
      render(<div />, { wrapper });
      await userEvent.type(
        screen.getByLabelText('Email Address'),
        'user@email.com'
      );
      await userEvent.click(screen.getByText('Next'));
    });

    // expect(onForgotPassword).toBeCalledWith('user@email.com');
    expect(onForgotPassword).toHaveBeenCalled();
  });
});
star

Tue Apr 16 2024 05:21:12 GMT+0000 (Coordinated Universal Time)

#typescriptreact
star

Wed Feb 08 2023 01:27:47 GMT+0000 (Coordinated Universal Time)

#typescriptreact
star

Thu Feb 04 2021 16:59:16 GMT+0000 (Coordinated Universal Time)

#typescriptreact
star

Thu Feb 04 2021 16:43:16 GMT+0000 (Coordinated Universal Time)

#typescriptreact

Save snippets that work with our extensions

Available in the Chrome Web Store Get Firefox Add-on Get VS Code extension