Skip to content

Commit

Permalink
refactor(experience): refactor the code verificatin api
Browse files Browse the repository at this point in the history
refactor the code verification api
  • Loading branch information
simeng-li committed Sep 4, 2024
1 parent 5d0eb80 commit ede343a
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 53 deletions.
69 changes: 31 additions & 38 deletions packages/experience/src/apis/utils.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,44 @@
import { InteractionEvent, type VerificationCodeIdentifier } from '@logto/schemas';
import { validate } from 'superstruct';

import { type ContinueFlowInteractionEvent, UserFlow } from '@/types';
import { continueFlowStateGuard } from '@/types/guard';

import { initInteraction, sendVerificationCode } from './experience';

/** Move to API */
// Consider deprecate this, remove the `UserFlow.Continue` case
// Align the flow definition with the interaction event
export const userFlowToInteractionEventMap = Object.freeze({
[UserFlow.SignIn]: InteractionEvent.SignIn,
[UserFlow.Register]: InteractionEvent.Register,
[UserFlow.ForgotPassword]: InteractionEvent.ForgotPassword,
});

/**
* This method is used to get the interaction event from the location state
* For continue flow, the interaction event is stored in the location state,
* we need to retrieve it from the state in order to send the verification code with the correct interaction event template
*/
export const getInteractionEventFromState = (state: unknown) => {
if (!state) {
return;
}

const [, continueFlowState] = validate(state, continueFlowStateGuard);

return continueFlowState?.interactionEvent;
};

export const sendVerificationCodeApi = async (
type: UserFlow,
flow: UserFlow,
identifier: VerificationCodeIdentifier,
interactionEvent?: ContinueFlowInteractionEvent
) => {
switch (type) {
case UserFlow.SignIn: {
await initInteraction(InteractionEvent.SignIn);
return sendVerificationCode(InteractionEvent.SignIn, identifier);
}
case UserFlow.Register: {
await initInteraction(InteractionEvent.Register);
return sendVerificationCode(InteractionEvent.Register, identifier);
}
case UserFlow.ForgotPassword: {
await initInteraction(InteractionEvent.ForgotPassword);
return sendVerificationCode(InteractionEvent.ForgotPassword, identifier);
}
case UserFlow.Continue: {
return sendVerificationCode(interactionEvent ?? InteractionEvent.SignIn, identifier);
}
if (flow === UserFlow.Continue) {
return sendVerificationCode(interactionEvent ?? InteractionEvent.SignIn, identifier);
}
};

export const resendVerificationCodeApi = async (
type: UserFlow,
identifier: VerificationCodeIdentifier
) => {
switch (type) {
case UserFlow.SignIn: {
return sendVerificationCode(InteractionEvent.SignIn, identifier);
}
case UserFlow.Register: {
return sendVerificationCode(InteractionEvent.Register, identifier);
}
case UserFlow.ForgotPassword: {
return sendVerificationCode(InteractionEvent.ForgotPassword, identifier);
}
case UserFlow.Continue: {
// Continue flow does not have its own email template, always use sign-in template for now
return sendVerificationCode(InteractionEvent.SignIn, identifier);
}
}
const event = userFlowToInteractionEventMap[flow];
await initInteraction(event);
return sendVerificationCode(event, identifier);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import {
import { act, fireEvent, waitFor } from '@testing-library/react';

import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { identifyWithVerificationCode, updateProfileWithVerificationCode } from '@/apis/experience';
import { resendVerificationCodeApi } from '@/apis/utils';
import {
identifyWithVerificationCode,
updateProfileWithVerificationCode,
sendVerificationCode,
} from '@/apis/experience';
import { setupI18nForTesting } from '@/jest.setup';
import { UserFlow } from '@/types';

Expand All @@ -29,11 +32,12 @@ jest.mock('react-router-dom', () => ({
}));

jest.mock('@/apis/utils', () => ({
...jest.requireActual('@/apis/utils'),
sendVerificationCodeApi: jest.fn(),
resendVerificationCodeApi: jest.fn(),
}));

jest.mock('@/apis/experience', () => ({
sendVerificationCode: jest.fn(),
identifyWithVerificationCode: jest.fn().mockResolvedValue({ redirectTo: '/redirect' }),
updateProfileWithVerificationCode: jest.fn().mockResolvedValue({ redirectTo: '/redirect' }),
}));
Expand Down Expand Up @@ -123,7 +127,7 @@ describe('<VerificationCode />', () => {
fireEvent.click(resendButton);
});

expect(resendVerificationCodeApi).toBeCalledWith(UserFlow.SignIn, emailIdentifier);
expect(sendVerificationCode).toBeCalledWith(InteractionEvent.SignIn, emailIdentifier);

// Reset i18n
await setupI18nForTesting();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import type { VerificationCodeIdentifier } from '@logto/schemas';
import { VerificationType } from '@logto/schemas';
import { useCallback, useContext, useMemo } from 'react';
import { useLocation, useSearchParams } from 'react-router-dom';
import { validate } from 'superstruct';

import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import { updateProfileWithVerificationCode } from '@/apis/experience';
import { getInteractionEventFromState } from '@/apis/utils';
import useApi from '@/hooks/use-api';
import type { ErrorHandlers } from '@/hooks/use-error-handler';
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
import { SearchParameters } from '@/types';
import { continueFlowStateGuard } from '@/types/guard';

import useGeneralVerificationCodeErrorHandler from './use-general-verification-code-error-handler';
import useIdentifierErrorAlert, { IdentifierErrorType } from './use-identifier-error-alert';
Expand All @@ -27,9 +26,8 @@ const useContinueFlowCodeVerification = (
const redirectTo = useGlobalRedirectTo();

const { state } = useLocation();
const [, continueFlowState] = validate(state, continueFlowStateGuard);
const { verificationIdsMap } = useContext(UserInteractionContext);
const interactionEvent = continueFlowState?.interactionEvent;
const interactionEvent = getInteractionEventFromState(state);

const handleError = useErrorHandler();
const verifyVerificationCode = useApi(updateProfileWithVerificationCode);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { type VerificationCodeIdentifier } from '@logto/schemas';
import { InteractionEvent, type VerificationCodeIdentifier } from '@logto/schemas';
import { t } from 'i18next';
import { useCallback, useContext } from 'react';
import { useCallback, useContext, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { useTimer } from 'react-timer-hook';

import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
import { resendVerificationCodeApi } from '@/apis/utils';
import { sendVerificationCode } from '@/apis/experience';
import { getInteractionEventFromState, userFlowToInteractionEventMap } from '@/apis/utils';
import useApi from '@/hooks/use-api';
import useErrorHandler from '@/hooks/use-error-handler';
import useToast from '@/hooks/use-toast';
import type { UserFlow } from '@/types';
import { UserFlow } from '@/types';
import { codeVerificationTypeMap } from '@/utils/sign-in-experience';

export const timeRange = 59;
Expand All @@ -22,18 +24,29 @@ const getTimeout = () => {

const useResendVerificationCode = (flow: UserFlow, identifier: VerificationCodeIdentifier) => {
const { setToast } = useToast();
const { state } = useLocation();

const interactionEvent = useMemo<InteractionEvent>(() => {
if (flow === UserFlow.Continue) {
const interactionEvent = getInteractionEventFromState(state);
console.log('interactionEvent', interactionEvent);
return interactionEvent ?? InteractionEvent.SignIn;
}

return userFlowToInteractionEventMap[flow];
}, [flow, state]);

const { seconds, isRunning, restart } = useTimer({
autoStart: true,
expiryTimestamp: getTimeout(),
});

const handleError = useErrorHandler();
const sendVerificationCode = useApi(resendVerificationCodeApi);
const resendVerificationCode = useApi(sendVerificationCode);
const { setVerificationId } = useContext(UserInteractionContext);

const onResendVerificationCode = useCallback(async () => {
const [error, result] = await sendVerificationCode(flow, identifier);
const [error, result] = await resendVerificationCode(interactionEvent, identifier);

if (error) {
await handleError(error);
Expand All @@ -47,7 +60,15 @@ const useResendVerificationCode = (flow: UserFlow, identifier: VerificationCodeI
setToast(t('description.passcode_sent'));
restart(getTimeout(), true);
}
}, [flow, handleError, identifier, restart, sendVerificationCode, setToast, setVerificationId]);
}, [
resendVerificationCode,
interactionEvent,
identifier,
handleError,
setVerificationId,
setToast,
restart,
]);

return {
seconds,
Expand Down

0 comments on commit ede343a

Please sign in to comment.