Skip to content

Commit

Permalink
refactor(experience): refactor the verification code flow (migration-…
Browse files Browse the repository at this point in the history
…2) (#6408)

* refactor(experience): refactor the verificaiton code flow

refactor the verification code flow

* refactor(experience): migrate the social and sso flow (migration-3) (#6406)

* refactor(experience): migrate the social and sso flow

migrate the social and sso flow

* refactor(experience): migrate profile fulfillment flow  (migration-4) (#6414)

* refactor(experience): migrate profile fulfillment flow

migrate the profile fulfillment flow

* refactor(experience): remove unused hook

remove unused hook

* fix(experience): fix password policy checker

fix password policy checker error display

* fix(experience): fix the api name

fix the api name

* refactor(experience): migrate mfa flow (migration-5) (#6417)

* refactor(experience): migrate mfa binding flow

migrate mfa binding flow

* test(experience): update unit tests (migration-6) (#6420)

* test(experience): update unit tests

update unit tests

* chore(experience): remove legacy APIs

remove legacy APIs

* refactor(experience): revert api prefix

revert api prefix

* fix(experience): update the sso connectors endpoint

update the sso connectors endpoint
  • Loading branch information
simeng-li committed Sep 4, 2024
1 parent 7409076 commit 9be1f6a
Show file tree
Hide file tree
Showing 76 changed files with 1,732 additions and 1,129 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 @@ -31,6 +32,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 @@ -54,5 +57,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
1 change: 1 addition & 0 deletions packages/experience/src/apis/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const kyPrefixUrl = '/';
73 changes: 0 additions & 73 deletions packages/experience/src/apis/experience.ts

This file was deleted.

14 changes: 14 additions & 0 deletions packages/experience/src/apis/experience/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const prefix = '/api/experience';

export const experienceApiRoutes = Object.freeze({
prefix,
identification: `${prefix}/identification`,
submit: `${prefix}/submit`,
verification: `${prefix}/verification`,
profile: `${prefix}/profile`,
mfa: `${prefix}/profile/mfa`,
});

export type VerificationResponse = {
verificationId: string;
};
149 changes: 149 additions & 0 deletions packages/experience/src/apis/experience/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import {
InteractionEvent,
type PasswordVerificationPayload,
SignInIdentifier,
type VerificationCodeIdentifier,
} from '@logto/schemas';

import { type ContinueFlowInteractionEvent } from '@/types';

import api from '../api';

import { experienceApiRoutes, type VerificationResponse } from './const';
import {
initInteraction,
identifyUser,
submitInteraction,
updateInteractionEvent,
_updateProfile,
identifyAndSubmitInteraction,
} from './interaction';

export {
initInteraction,
submitInteraction,
identifyUser,
identifyAndSubmitInteraction,
} from './interaction';

export * from './mfa';
export * from './social';

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 });
};

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

const { verificationId } = await api
.post(`${experienceApiRoutes.verification}/password`, {
json: payload,
})
.json<VerificationResponse>();

return identifyAndSubmitInteraction({ verificationId });
};

export const registerWithUsername = async (username: string) => {
await initInteraction(InteractionEvent.Register);

return _updateProfile({ type: SignInIdentifier.Username, value: username });
};

export const continueRegisterWithPassword = async (password: string) => {
await _updateProfile({ type: 'password', value: password });

return identifyAndSubmitInteraction();
};

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

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

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

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

// Profile APIs

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

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

await _updateProfile({
type,
verificationId,
});

if (interactionEvent === InteractionEvent.Register) {
await identifyUser();
}

return submitInteraction();
};

type UpdateProfilePayload = {
type: SignInIdentifier.Username | 'password';
value: string;
};

export const updateProfile = async (
payload: UpdateProfilePayload,
interactionEvent: ContinueFlowInteractionEvent
) => {
await _updateProfile(payload);

if (interactionEvent === InteractionEvent.Register) {
await identifyUser();
}

return submitInteraction();
};

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

return submitInteraction();
};
41 changes: 41 additions & 0 deletions packages/experience/src/apis/experience/interaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
type InteractionEvent,
type IdentificationApiPayload,
type UpdateProfileApiPayload,
} from '@logto/schemas';

import api from '../api';

import { experienceApiRoutes } from './const';

type SubmitInteractionResponse = {
redirectTo: string;
};

export const initInteraction = async (interactionEvent: InteractionEvent) =>
api.put(`${experienceApiRoutes.prefix}`, {
json: {
interactionEvent,
},
});

export const identifyUser = async (payload: IdentificationApiPayload = {}) =>
api.post(experienceApiRoutes.identification, { json: payload });

export const submitInteraction = async () =>
api.post(`${experienceApiRoutes.submit}`).json<SubmitInteractionResponse>();

export const _updateProfile = async (payload: UpdateProfileApiPayload) =>
api.post(experienceApiRoutes.profile, { json: payload });

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

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

0 comments on commit 9be1f6a

Please sign in to comment.