Skip to content

Commit

Permalink
refactor(experience): refactor the verificaiton code flow
Browse files Browse the repository at this point in the history
refactor the verification code flow
  • Loading branch information
simeng-li committed Aug 7, 2024
1 parent 32fdc57 commit 5aead51
Show file tree
Hide file tree
Showing 19 changed files with 495 additions and 241 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { type SsoConnectorMetadata } from '@logto/schemas';
import { type SsoConnectorMetadata, type VerificationType } from '@logto/schemas';
import { noop } from '@silverhand/essentials';
import { createContext } from 'react';

import { type IdentifierInputValue } from '@/components/InputFields/SmartInputField';
import { type VerificationIdsMap } from '@/types/guard';

export type UserInteractionContextType = {
// All the enabled sso connectors
Expand Down Expand Up @@ -32,6 +33,8 @@ export type UserInteractionContextType = {
setForgotPasswordIdentifierInputValue: React.Dispatch<
React.SetStateAction<IdentifierInputValue | undefined>
>;
verificationIdsMap: VerificationIdsMap;
setVerificationId: (type: VerificationType, id: string) => void;
/**
* This method only clear the identifier input values from the session storage.
*
Expand All @@ -55,5 +58,7 @@ export default createContext<UserInteractionContextType>({
setIdentifierInputValue: noop,
forgotPasswordIdentifierInputValue: undefined,
setForgotPasswordIdentifierInputValue: noop,
verificationIdsMap: {},
setVerificationId: noop,
clearInteractionContextSessionStorage: noop,
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type SsoConnectorMetadata } from '@logto/schemas';
import { type ReactNode, useEffect, useMemo, useState, useCallback } from 'react';
import { type SsoConnectorMetadata, type VerificationType } from '@logto/schemas';
import { type ReactNode, useCallback, useEffect, useMemo, useState } from 'react';

import { type IdentifierInputValue } from '@/components/InputFields/SmartInputField';
import useSessionStorage, { StorageKeys } from '@/hooks/use-session-storages';
Expand Down Expand Up @@ -35,6 +35,10 @@ const UserInteractionContextProvider = ({ children }: Props) => {
IdentifierInputValue | undefined
>(get(StorageKeys.ForgotPasswordIdentifierInputValue));

const [verificationIdsMap, setVerificationIdsMap] = useState(
get(StorageKeys.verificationIds) ?? {}
);

useEffect(() => {
if (!ssoEmail) {
remove(StorageKeys.SsoEmail);
Expand Down Expand Up @@ -71,6 +75,15 @@ const UserInteractionContextProvider = ({ children }: Props) => {
set(StorageKeys.ForgotPasswordIdentifierInputValue, forgotPasswordIdentifierInputValue);
}, [forgotPasswordIdentifierInputValue, remove, set]);

useEffect(() => {
if (Object.keys(verificationIdsMap).length === 0) {
remove(StorageKeys.verificationIds);
return;
}

set(StorageKeys.verificationIds, verificationIdsMap);
}, [verificationIdsMap, remove, set]);

const ssoConnectorsMap = useMemo(
() => new Map(ssoConnectors.map((connector) => [connector.id, connector])),
[ssoConnectors]
Expand All @@ -79,8 +92,13 @@ const UserInteractionContextProvider = ({ children }: Props) => {
const clearInteractionContextSessionStorage = useCallback(() => {
remove(StorageKeys.IdentifierInputValue);
remove(StorageKeys.ForgotPasswordIdentifierInputValue);
remove(StorageKeys.verificationIds);
}, [remove]);

const setVerificationId = useCallback((type: VerificationType, id: string) => {
setVerificationIdsMap((previous) => ({ ...previous, [type]: id }));
}, []);

const userInteractionContext = useMemo<UserInteractionContextType>(
() => ({
ssoEmail,
Expand All @@ -92,6 +110,8 @@ const UserInteractionContextProvider = ({ children }: Props) => {
setIdentifierInputValue,
forgotPasswordIdentifierInputValue,
setForgotPasswordIdentifierInputValue,
verificationIdsMap,
setVerificationId,
clearInteractionContextSessionStorage,
}),
[
Expand All @@ -100,6 +120,8 @@ const UserInteractionContextProvider = ({ children }: Props) => {
domainFilteredConnectors,
identifierInputValue,
forgotPasswordIdentifierInputValue,
verificationIdsMap,
setVerificationId,
clearInteractionContextSessionStorage,
]
);
Expand Down
89 changes: 84 additions & 5 deletions packages/experience/src/apis/experience.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type PasswordVerificationPayload,
SignInIdentifier,
type UpdateProfileApiPayload,
type VerificationCodeIdentifier,
} from '@logto/schemas';

import api from './api';
Expand All @@ -26,7 +27,7 @@ type SubmitInteractionResponse = {
redirectTo: string;
};

const initInteraction = async (interactionEvent: InteractionEvent) =>
export const initInteraction = async (interactionEvent: InteractionEvent) =>
api.put(`${experienceRoutes.prefix}`, {
json: {
interactionEvent,
Expand All @@ -43,6 +44,19 @@ const updateProfile = async (payload: UpdateProfileApiPayload) => {
await api.post(experienceRoutes.profile, { json: payload });
};

const updateInteractionEvent = async (interactionEvent: InteractionEvent) =>
api.put(`${experienceRoutes.prefix}/interaction-event`, {
json: {
interactionEvent,
},
});

const identifyAndSubmitInteraction = async (payload?: IdentificationApiPayload) => {
await identifyUser(payload);
return submitInteraction();
};

// Password APIs
export const signInWithPasswordIdentifier = async (payload: PasswordVerificationPayload) => {
await initInteraction(InteractionEvent.SignIn);

Expand All @@ -52,9 +66,7 @@ export const signInWithPasswordIdentifier = async (payload: PasswordVerification
})
.json<VerificationResponse>();

await identifyUser({ verificationId });

return submitInteraction();
return identifyAndSubmitInteraction({ verificationId });
};

export const registerWithUsername = async (username: string) => {
Expand All @@ -66,7 +78,74 @@ export const registerWithUsername = async (username: string) => {
export const continueRegisterWithPassword = async (password: string) => {
await updateProfile({ type: 'password', value: password });

await identifyUser();
return identifyAndSubmitInteraction();
};

// Verification code APIs
type VerificationCodePayload = {
identifier: VerificationCodeIdentifier;
code: string;
verificationId: string;
};

export const sendVerificationCode = async (
interactionEvent: InteractionEvent,
identifier: VerificationCodeIdentifier
) =>
api
.post(`${experienceRoutes.verification}/verification-code`, {
json: {
interactionEvent,
identifier,
},
})
.json<VerificationResponse>();

const verifyVerificationCode = async (json: VerificationCodePayload) =>
api
.post(`${experienceRoutes.verification}/verification-code/verify`, {
json,
})
.json<VerificationResponse>();

export const identifyWithVerificationCode = async (json: VerificationCodePayload) => {
const { verificationId } = await verifyVerificationCode(json);
return identifyAndSubmitInteraction({ verificationId });
};

export const registerWithVerifiedIdentifier = async (verificationId: string) => {
await updateInteractionEvent(InteractionEvent.Register);
return identifyAndSubmitInteraction({ verificationId });
};

export const signInWithVerifiedIdentifier = async (verificationId: string) => {
await updateInteractionEvent(InteractionEvent.SignIn);
return identifyAndSubmitInteraction({ verificationId });
};

// Profile APIs

export const updateProfileWithVerificationCode = async (json: VerificationCodePayload) => {
const { verificationId } = await verifyVerificationCode(json);

const {
identifier: { type },
} = json;

await updateProfile({
type,
verificationId,
});

return submitInteraction();
};

export const resetPassword = async (password: string) => {
await api.put(`${experienceRoutes.profile}/password`, {
json: {
password,
},
});

return submitInteraction();
};
53 changes: 39 additions & 14 deletions packages/experience/src/apis/utils.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
import { InteractionEvent } from '@logto/schemas';
import { InteractionEvent, type VerificationCodeIdentifier } from '@logto/schemas';

import { UserFlow } from '@/types';

import type { SendVerificationCodePayload } from './interaction';
import { putInteraction, sendVerificationCode } from './interaction';
import { initInteraction, sendVerificationCode } from './experience';

/** Move to API */
export const sendVerificationCodeApi = async (
type: UserFlow,
payload: SendVerificationCodePayload
identifier: VerificationCodeIdentifier
) => {
if (type === UserFlow.ForgotPassword) {
await putInteraction(InteractionEvent.ForgotPassword);
}

if (type === UserFlow.SignIn) {
await putInteraction(InteractionEvent.SignIn);
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: {
// Continue flow does not have its own email template, always use sign-in template for now
return sendVerificationCode(InteractionEvent.SignIn, identifier);
}
}
};

if (type === UserFlow.Register) {
await putInteraction(InteractionEvent.Register);
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);
}
}

return sendVerificationCode(payload);
};
Loading

0 comments on commit 5aead51

Please sign in to comment.