diff --git a/packages/core/src/routes/interaction/index.test.ts b/packages/core/src/routes/interaction/index.test.ts index f9eb5c41e..12a25b914 100644 --- a/packages/core/src/routes/interaction/index.test.ts +++ b/packages/core/src/routes/interaction/index.test.ts @@ -78,7 +78,7 @@ const { storeInteractionResult, mergeIdentifiers, getInteractionStorage } = awai () => ({ mergeIdentifiers: jest.fn(), storeInteractionResult: jest.fn(), - getInteractionStorage: jest.fn().mockResolvedValue({ + getInteractionStorage: jest.fn().mockReturnValue({ event: InteractionEvent.SignIn, }), }) @@ -262,13 +262,19 @@ describe('session -> interactionRoutes', () => { it('should call send passcode properly', async () => { const body = { - event: InteractionEvent.SignIn, email: 'email@logto.io', }; const response = await sessionRequest.post(path).send(body); expect(getInteractionStorage).toBeCalled(); - expect(sendPasscodeToIdentifier).toBeCalledWith(body, 'jti', createLog); + expect(sendPasscodeToIdentifier).toBeCalledWith( + { + event: InteractionEvent.SignIn, + ...body, + }, + 'jti', + createLog + ); expect(response.status).toEqual(204); }); }); diff --git a/packages/core/src/routes/interaction/index.ts b/packages/core/src/routes/interaction/index.ts index b5a085356..ef438e6fa 100644 --- a/packages/core/src/routes/interaction/index.ts +++ b/packages/core/src/routes/interaction/index.ts @@ -70,11 +70,11 @@ export default function interactionRoutes( verifySignInModeSettings(event, signInExperience); - if (identifier) { + if (identifier && event !== InteractionEvent.ForgotPassword) { verifyIdentifierSettings(identifier, signInExperience); } - if (profile) { + if (profile && event !== InteractionEvent.ForgotPassword) { verifyProfileSettings(profile, signInExperience); } @@ -145,10 +145,12 @@ export default function interactionRoutes( async (ctx, next) => { const identifierPayload = ctx.guard.body; const { signInExperience, interactionDetails } = ctx; - verifyIdentifierSettings(identifierPayload, signInExperience); - const interactionStorage = getInteractionStorage(interactionDetails.result); + if (interactionStorage.event === InteractionEvent.ForgotPassword) { + verifyIdentifierSettings(identifierPayload, signInExperience); + } + const verifiedIdentifier = await verifyIdentifierPayload( ctx, provider, @@ -175,12 +177,14 @@ export default function interactionRoutes( koaInteractionSie(), async (ctx, next) => { const profilePayload = ctx.guard.body; - const { signInExperience, interactionDetails } = ctx; - verifyProfileSettings(profilePayload, signInExperience); // Check interaction exists - getInteractionStorage(interactionDetails.result); + const { event } = getInteractionStorage(interactionDetails.result); + + if (event !== InteractionEvent.ForgotPassword) { + verifyProfileSettings(profilePayload, signInExperience); + } await storeInteractionResult( { @@ -207,10 +211,13 @@ export default function interactionRoutes( async (ctx, next) => { const profilePayload = ctx.guard.body; const { signInExperience, interactionDetails } = ctx; - verifyProfileSettings(profilePayload, signInExperience); const interactionStorage = getInteractionStorage(interactionDetails.result); + if (interactionStorage.event !== InteractionEvent.ForgotPassword) { + verifyProfileSettings(profilePayload, signInExperience); + } + await storeInteractionResult( { profile: { @@ -292,9 +299,9 @@ export default function interactionRoutes( async (ctx, next) => { const { interactionDetails, guard, createLog } = ctx; // Check interaction exists - getInteractionStorage(interactionDetails.result); + const { event } = getInteractionStorage(interactionDetails.result); - await sendPasscodeToIdentifier(guard.body, interactionDetails.jti, createLog); + await sendPasscodeToIdentifier({ event, ...guard.body }, interactionDetails.jti, createLog); ctx.status = 204; diff --git a/packages/core/src/routes/interaction/types/guard.ts b/packages/core/src/routes/interaction/types/guard.ts index 2e9ddca41..bf328af60 100644 --- a/packages/core/src/routes/interaction/types/guard.ts +++ b/packages/core/src/routes/interaction/types/guard.ts @@ -7,11 +7,9 @@ import { socialUserInfoGuard } from '#src/connectors/types.js'; // Passcode Send Route Payload Guard export const sendPasscodePayloadGuard = z.union([ z.object({ - event: eventGuard, email: z.string().regex(emailRegEx), }), z.object({ - event: eventGuard, phone: z.string().regex(phoneRegEx), }), ]); diff --git a/packages/core/src/routes/interaction/utils/passcode-validation.test.ts b/packages/core/src/routes/interaction/utils/passcode-validation.test.ts index 41bbc442e..845e4b68f 100644 --- a/packages/core/src/routes/interaction/utils/passcode-validation.test.ts +++ b/packages/core/src/routes/interaction/utils/passcode-validation.test.ts @@ -4,8 +4,6 @@ import { createMockUtils } from '@logto/shared/esm'; import { createMockLogContext } from '#src/test-utils/koa-audit-log.js'; -import type { SendPasscodePayload } from '../types/index.js'; - const { jest } = import.meta; const { mockEsmWithActual } = createMockUtils(jest); @@ -55,7 +53,7 @@ describe('passcode-validation utils', () => { it.each(sendPasscodeTestCase)( 'send passcode successfully', async ({ payload, createPasscodeParams }) => { - await sendPasscodeToIdentifier(payload as SendPasscodePayload, 'jti', log.createLog); + await sendPasscodeToIdentifier(payload, 'jti', log.createLog); expect(passcode.createPasscode).toBeCalledWith('jti', ...createPasscodeParams); expect(passcode.sendPasscode).toBeCalled(); } diff --git a/packages/core/src/routes/interaction/utils/passcode-validation.ts b/packages/core/src/routes/interaction/utils/passcode-validation.ts index 104c6d1c2..f74e85714 100644 --- a/packages/core/src/routes/interaction/utils/passcode-validation.ts +++ b/packages/core/src/routes/interaction/utils/passcode-validation.ts @@ -20,7 +20,7 @@ const getMessageTypesByEvent = (event: InteractionEvent): MessageTypes => eventToMessageTypesMap[event]; export const sendPasscodeToIdentifier = async ( - payload: SendPasscodePayload, + payload: SendPasscodePayload & { event: InteractionEvent }, jti: string, createLog: LogContext['createLog'] ) => { diff --git a/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts b/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts index dbbb27041..e6f389f7f 100644 --- a/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts +++ b/packages/core/src/routes/interaction/verifications/user-identity-verification.test.ts @@ -153,7 +153,7 @@ describe('verifyUserAccount', () => { }; await expect(verifyUserAccount(interaction)).rejects.toMatchError( - new RequestError({ code: 'user.user_not_exist', status: 404 }, { identifier: 'email' }) + new RequestError({ code: 'user.user_not_exist', status: 404 }, { identity: 'email' }) ); expect(findUserByIdentifierMock).toBeCalledWith({ email: 'email' }); diff --git a/packages/core/src/routes/interaction/verifications/user-identity-verification.ts b/packages/core/src/routes/interaction/verifications/user-identity-verification.ts index f1c31a28b..fc9d514ef 100644 --- a/packages/core/src/routes/interaction/verifications/user-identity-verification.ts +++ b/packages/core/src/routes/interaction/verifications/user-identity-verification.ts @@ -26,7 +26,7 @@ const identifyUserByVerifiedEmailOrPhone = async ( assertThat( user, - new RequestError({ code: 'user.user_not_exist', status: 404 }, { identifier: identifier.value }) + new RequestError({ code: 'user.user_not_exist', status: 404 }, { identity: identifier.value }) ); const { id, isSuspended } = user; diff --git a/packages/integration-tests/src/api/interaction.ts b/packages/integration-tests/src/api/interaction.ts index 2db50c393..e94b4fcda 100644 --- a/packages/integration-tests/src/api/interaction.ts +++ b/packages/integration-tests/src/api/interaction.ts @@ -68,10 +68,9 @@ export const submitInteraction = async (cookie: string) => export type VerificationPasscodePayload = | { - event: InteractionEvent; email: string; } - | { event: InteractionEvent; phone: string }; + | { phone: string }; export const sendVerificationPasscode = async ( cookie: string, diff --git a/packages/integration-tests/src/tests/api/interaction/forgot-password.test.ts b/packages/integration-tests/src/tests/api/interaction/forgot-password.test.ts index 91210122b..3e848f137 100644 --- a/packages/integration-tests/src/tests/api/interaction/forgot-password.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/forgot-password.test.ts @@ -42,7 +42,6 @@ describe('reset password', () => { await client.successSend(putInteraction, { event: InteractionEvent.ForgotPassword }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.ForgotPassword, email: userProfile.primaryEmail, }); @@ -96,7 +95,6 @@ describe('reset password', () => { await client.successSend(putInteraction, { event: InteractionEvent.ForgotPassword }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.ForgotPassword, phone: userProfile.primaryPhone, }); diff --git a/packages/integration-tests/src/tests/api/interaction/register-with-identifier.test.ts b/packages/integration-tests/src/tests/api/interaction/register-with-identifier.test.ts index 9007be638..811f27379 100644 --- a/packages/integration-tests/src/tests/api/interaction/register-with-identifier.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/register-with-identifier.test.ts @@ -73,7 +73,6 @@ describe('Register with passwordless identifier', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.Register, email: primaryEmail, }); @@ -120,7 +119,6 @@ describe('Register with passwordless identifier', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.Register, email: primaryEmail, }); @@ -179,7 +177,6 @@ describe('Register with passwordless identifier', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.Register, phone: primaryPhone, }); @@ -226,7 +223,6 @@ describe('Register with passwordless identifier', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.Register, phone: primaryPhone, }); @@ -288,7 +284,6 @@ describe('Register with passwordless identifier', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.Register, email: primaryEmail, }); @@ -341,7 +336,6 @@ describe('Register with passwordless identifier', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.Register, phone: primaryPhone, }); diff --git a/packages/integration-tests/src/tests/api/interaction/sign-in-with-passcode-identifier.test.ts b/packages/integration-tests/src/tests/api/interaction/sign-in-with-passcode-identifier.test.ts index b705490ae..952402cfe 100644 --- a/packages/integration-tests/src/tests/api/interaction/sign-in-with-passcode-identifier.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/sign-in-with-passcode-identifier.test.ts @@ -37,7 +37,6 @@ describe('Sign-In flow using passcode identifiers', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.SignIn, email: userProfile.primaryEmail, }); @@ -71,7 +70,6 @@ describe('Sign-In flow using passcode identifiers', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.SignIn, phone: userProfile.primaryPhone, }); @@ -111,7 +109,6 @@ describe('Sign-In flow using passcode identifiers', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.SignIn, email: newEmail, }); @@ -151,7 +148,6 @@ describe('Sign-In flow using passcode identifiers', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.SignIn, phone: newPhone, }); @@ -197,7 +193,6 @@ describe('Sign-In flow using passcode identifiers', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.SignIn, email: userProfile.primaryEmail, }); const { code } = await readPasscode(); @@ -257,7 +252,6 @@ describe('Sign-In flow using passcode identifiers', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.SignIn, email: userProfile.primaryEmail, }); const { code } = await readPasscode(); @@ -309,7 +303,6 @@ describe('Sign-In flow using passcode identifiers', () => { }); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.SignIn, email: userProfile.primaryEmail, }); const { code } = await readPasscode(); diff --git a/packages/integration-tests/src/tests/api/interaction/sign-in-with-password-identifier.test.ts b/packages/integration-tests/src/tests/api/interaction/sign-in-with-password-identifier.test.ts index 5216a19e7..1f29fd2da 100644 --- a/packages/integration-tests/src/tests/api/interaction/sign-in-with-password-identifier.test.ts +++ b/packages/integration-tests/src/tests/api/interaction/sign-in-with-password-identifier.test.ts @@ -112,7 +112,6 @@ describe('Sign-In flow using password identifiers', () => { await expectRejects(client.submitInteraction(), 'user.missing_profile'); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.SignIn, email: primaryEmail, }); @@ -172,7 +171,6 @@ describe('Sign-In flow using password identifiers', () => { await expectRejects(client.submitInteraction(), 'user.missing_profile'); await client.successSend(sendVerificationPasscode, { - event: InteractionEvent.SignIn, phone: primaryPhone, }); diff --git a/packages/ui/src/apis/continue.test.ts b/packages/ui/src/apis/continue.test.ts index b4bf7e5c0..be9ba0caa 100644 --- a/packages/ui/src/apis/continue.test.ts +++ b/packages/ui/src/apis/continue.test.ts @@ -1,13 +1,6 @@ -import { MessageTypes } from '@logto/connector-kit'; import ky from 'ky'; -import { - continueApi, - sendContinueSetEmailPasscode, - sendContinueSetPhonePasscode, - verifyContinueSetEmailPasscode, - verifyContinueSetSmsPasscode, -} from './continue'; +import { continueApi } from './continue'; jest.mock('ky', () => ({ extend: () => ky, @@ -68,50 +61,4 @@ describe('continue API', () => { }, }); }); - - it('sendContinueSetEmailPasscode', async () => { - await sendContinueSetEmailPasscode('email'); - - expect(ky.post).toBeCalledWith('/api/session/passwordless/email/send', { - json: { - email: 'email', - flow: MessageTypes.Continue, - }, - }); - }); - - it('sendContinueSetSmsPasscode', async () => { - await sendContinueSetPhonePasscode('111111'); - - expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/send', { - json: { - phone: '111111', - flow: MessageTypes.Continue, - }, - }); - }); - - it('verifyContinueSetEmailPasscode', async () => { - await verifyContinueSetEmailPasscode('email', 'passcode'); - - expect(ky.post).toBeCalledWith('/api/session/passwordless/email/verify', { - json: { - email: 'email', - code: 'passcode', - flow: MessageTypes.Continue, - }, - }); - }); - - it('verifyContinueSetSmsPasscode', async () => { - await verifyContinueSetSmsPasscode('phone', 'passcode'); - - expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/verify', { - json: { - phone: 'phone', - code: 'passcode', - flow: MessageTypes.Continue, - }, - }); - }); }); diff --git a/packages/ui/src/apis/continue.ts b/packages/ui/src/apis/continue.ts index eef440e61..52294532b 100644 --- a/packages/ui/src/apis/continue.ts +++ b/packages/ui/src/apis/continue.ts @@ -1,5 +1,3 @@ -import { MessageTypes } from '@logto/connector-kit'; - import api from './api'; import { bindSocialAccount } from './social'; @@ -7,7 +5,6 @@ type Response = { redirectTo: string; }; -const passwordlessApiPrefix = '/api/session/passwordless'; const continueApiPrefix = '/api/session/sign-in/continue'; type ContinueKey = 'password' | 'username' | 'email' | 'phone'; @@ -25,49 +22,3 @@ export const continueApi = async (key: ContinueKey, value: string, socialToBind? return result; }; - -export const sendContinueSetEmailPasscode = async (email: string) => { - await api - .post(`${passwordlessApiPrefix}/email/send`, { - json: { - email, - flow: MessageTypes.Continue, - }, - }) - .json(); - - return { success: true }; -}; - -export const sendContinueSetPhonePasscode = async (phone: string) => { - await api - .post(`${passwordlessApiPrefix}/sms/send`, { - json: { - phone, - flow: MessageTypes.Continue, - }, - }) - .json(); - - return { success: true }; -}; - -export const verifyContinueSetEmailPasscode = async (email: string, code: string) => { - await api - .post(`${passwordlessApiPrefix}/email/verify`, { - json: { email, code, flow: MessageTypes.Continue }, - }) - .json(); - - return { success: true }; -}; - -export const verifyContinueSetSmsPasscode = async (phone: string, code: string) => { - await api - .post(`${passwordlessApiPrefix}/sms/verify`, { - json: { phone, code, flow: MessageTypes.Continue }, - }) - .json(); - - return { success: true }; -}; diff --git a/packages/ui/src/apis/forgot-password.ts b/packages/ui/src/apis/forgot-password.ts deleted file mode 100644 index 752d8c1eb..000000000 --- a/packages/ui/src/apis/forgot-password.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { MessageTypes } from '@logto/connector-kit'; - -import api from './api'; - -type Response = { - redirectTo: string; -}; - -const forgotPasswordApiPrefix = '/api/session/forgot-password'; - -export const sendForgotPasswordSmsPasscode = async (phone: string) => { - await api - .post('/api/session/passwordless/sms/send', { - json: { - phone, - flow: MessageTypes.ForgotPassword, - }, - }) - .json(); - - return { success: true }; -}; - -export const verifyForgotPasswordSmsPasscode = async (phone: string, code: string) => { - await api - .post('/api/session/passwordless/sms/verify', { - json: { - phone, - code, - flow: MessageTypes.ForgotPassword, - }, - }) - .json(); - - return { success: true }; -}; - -export const sendForgotPasswordEmailPasscode = async (email: string) => { - await api - .post('/api/session/passwordless/email/send', { - json: { - email, - flow: MessageTypes.ForgotPassword, - }, - }) - .json(); - - return { success: true }; -}; - -export const verifyForgotPasswordEmailPasscode = async (email: string, code: string) => { - await api - .post('/api/session/passwordless/email/verify', { - json: { - email, - code, - flow: MessageTypes.ForgotPassword, - }, - }) - .json(); - - return { success: true }; -}; - -export const resetPassword = async (password: string) => { - await api - .post(`${forgotPasswordApiPrefix}/reset`, { - json: { password }, - }) - .json(); - - return { success: true }; -}; diff --git a/packages/ui/src/apis/index.test.ts b/packages/ui/src/apis/index.test.ts index f61d7670e..8ffefd937 100644 --- a/packages/ui/src/apis/index.test.ts +++ b/packages/ui/src/apis/index.test.ts @@ -1,30 +1,6 @@ -import { MessageTypes } from '@logto/connector-kit'; import ky from 'ky'; import { consent } from './consent'; -import { - verifyForgotPasswordEmailPasscode, - verifyForgotPasswordSmsPasscode, - sendForgotPasswordEmailPasscode, - sendForgotPasswordSmsPasscode, - resetPassword, -} from './forgot-password'; -import { - registerWithSms, - registerWithEmail, - sendRegisterEmailPasscode, - sendRegisterSmsPasscode, - verifyRegisterEmailPasscode, - verifyRegisterSmsPasscode, -} from './register'; -import { - signInWithSms, - signInWithEmail, - sendSignInSmsPasscode, - sendSignInEmailPasscode, - verifySignInEmailPasscode, - verifySignInSmsPasscode, -} from './sign-in'; import { invokeSocialSignIn, signInWithSocial, @@ -41,8 +17,6 @@ jest.mock('ky', () => ({ })); describe('api', () => { - const username = 'username'; - const password = 'password'; const phone = '18888888'; const code = '111111'; const email = 'foo@logto.io'; @@ -53,181 +27,11 @@ describe('api', () => { mockKyPost.mockClear(); }); - it('signInWithSms', async () => { - mockKyPost.mockReturnValueOnce({ - json: () => ({ - redirectTo: '/', - }), - }); - await signInWithSms(); - expect(ky.post).toBeCalledWith('/api/session/sign-in/passwordless/sms'); - }); - - it('signInWithEmail', async () => { - mockKyPost.mockReturnValueOnce({ - json: () => ({ - redirectTo: '/', - }), - }); - await signInWithEmail(); - expect(ky.post).toBeCalledWith('/api/session/sign-in/passwordless/email'); - }); - - it('sendSignInSmsPasscode', async () => { - await sendSignInSmsPasscode(phone); - expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/send', { - json: { - phone, - flow: MessageTypes.SignIn, - }, - }); - }); - - it('verifySignInSmsPasscode', async () => { - mockKyPost.mockReturnValueOnce({ - json: () => ({ - redirectTo: '/', - }), - }); - - await verifySignInSmsPasscode(phone, code); - - expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/verify', { - json: { - phone, - code, - flow: MessageTypes.SignIn, - }, - }); - }); - - it('sendSignInEmailPasscode', async () => { - await sendSignInEmailPasscode(email); - expect(ky.post).toBeCalledWith('/api/session/passwordless/email/send', { - json: { - email, - flow: MessageTypes.SignIn, - }, - }); - }); - - it('verifySignInEmailPasscode', async () => { - mockKyPost.mockReturnValueOnce({ - json: () => ({ - redirectTo: '/', - }), - }); - - await verifySignInEmailPasscode(email, code); - - expect(ky.post).toBeCalledWith('/api/session/passwordless/email/verify', { - json: { - email, - code, - flow: MessageTypes.SignIn, - }, - }); - }); - it('consent', async () => { await consent(); expect(ky.post).toBeCalledWith('/api/session/consent'); }); - it('registerWithSms', async () => { - await registerWithSms(); - expect(ky.post).toBeCalledWith('/api/session/register/passwordless/sms'); - }); - - it('registerWithEmail', async () => { - await registerWithEmail(); - expect(ky.post).toBeCalledWith('/api/session/register/passwordless/email'); - }); - - it('sendRegisterSmsPasscode', async () => { - await sendRegisterSmsPasscode(phone); - expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/send', { - json: { - phone, - flow: MessageTypes.Register, - }, - }); - }); - - it('verifyRegisterSmsPasscode', async () => { - await verifyRegisterSmsPasscode(phone, code); - expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/verify', { - json: { - phone, - code, - flow: MessageTypes.Register, - }, - }); - }); - - it('sendRegisterEmailPasscode', async () => { - await sendRegisterEmailPasscode(email); - expect(ky.post).toBeCalledWith('/api/session/passwordless/email/send', { - json: { - email, - flow: MessageTypes.Register, - }, - }); - }); - - it('verifyRegisterEmailPasscode', async () => { - await verifyRegisterEmailPasscode(email, code); - expect(ky.post).toBeCalledWith('/api/session/passwordless/email/verify', { - json: { - email, - code, - flow: MessageTypes.Register, - }, - }); - }); - - it('sendForgotPasswordSmsPasscode', async () => { - await sendForgotPasswordSmsPasscode(phone); - expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/send', { - json: { - phone, - flow: MessageTypes.ForgotPassword, - }, - }); - }); - - it('verifyForgotPasswordSmsPasscode', async () => { - await verifyForgotPasswordSmsPasscode(phone, code); - expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/verify', { - json: { - phone, - code, - flow: MessageTypes.ForgotPassword, - }, - }); - }); - - it('sendForgotPasswordEmailPasscode', async () => { - await sendForgotPasswordEmailPasscode(email); - expect(ky.post).toBeCalledWith('/api/session/passwordless/email/send', { - json: { - email, - flow: MessageTypes.ForgotPassword, - }, - }); - }); - - it('verifyForgotPasswordEmailPasscode', async () => { - await verifyForgotPasswordEmailPasscode(email, code); - expect(ky.post).toBeCalledWith('/api/session/passwordless/email/verify', { - json: { - email, - code, - flow: MessageTypes.ForgotPassword, - }, - }); - }); - it('invokeSocialSignIn', async () => { await invokeSocialSignIn('connectorId', 'state', 'redirectUri'); expect(ky.post).toBeCalledWith('/api/session/sign-in/social', { @@ -279,13 +83,4 @@ describe('api', () => { }, }); }); - - it('resetPassword', async () => { - await resetPassword('password'); - expect(ky.post).toBeCalledWith('/api/session/forgot-password/reset', { - json: { - password: 'password', - }, - }); - }); }); diff --git a/packages/ui/src/apis/interaction.ts b/packages/ui/src/apis/interaction.ts index 70ad547fd..7abba3c5a 100644 --- a/packages/ui/src/apis/interaction.ts +++ b/packages/ui/src/apis/interaction.ts @@ -5,12 +5,15 @@ import type { UsernamePasswordPayload, EmailPasswordPayload, PhonePasswordPayload, + EmailPasscodePayload, + PhonePasscodePayload, } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import api from './api'; const interactionPrefix = '/api/interaction'; +const verificationPath = `verification`; type Response = { redirectTo: string; @@ -60,5 +63,92 @@ export const setUserPassword = async (password: string) => { }, }); + const result = await api.post(`${interactionPrefix}/submit`).json(); + + // Reset password does not have any response body + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return result || { success: true }; +}; + +export type SendPasscodePayload = { email: string } | { phone: string }; + +export const putInteraction = async (event: InteractionEvent) => + api.put(`${interactionPrefix}`, { json: { event } }); + +export const sendPasscode = async (payload: SendPasscodePayload) => { + await api.post(`${interactionPrefix}/${verificationPath}/passcode`, { json: payload }); + + return { success: true }; +}; + +export const signInWithPasscodeIdentifier = async ( + payload: EmailPasscodePayload | PhonePasscodePayload, + socialToBind?: string +) => { + await api.patch(`${interactionPrefix}/identifiers`, { + json: payload, + }); + + if (socialToBind) { + // TODO: bind social account + } + + return api.post(`${interactionPrefix}/submit`).json(); +}; + +export const addProfileWithPasscodeIdentifier = async ( + payload: EmailPasscodePayload | PhonePasscodePayload, + socialToBind?: string +) => { + await api.patch(`${interactionPrefix}/identifiers`, { + json: payload, + }); + + const { passcode, ...identifier } = payload; + + await api.patch(`${interactionPrefix}/profile`, { + json: identifier, + }); + + if (socialToBind) { + // TODO: bind social account + } + + return api.post(`${interactionPrefix}/submit`).json(); +}; + +export const verifyForgotPasswordPasscodeIdentifier = async ( + payload: EmailPasscodePayload | PhonePasscodePayload +) => { + await api.patch(`${interactionPrefix}/identifiers`, { + json: payload, + }); + + return api.post(`${interactionPrefix}/submit`).json(); +}; + +export const signInWithVerifierIdentifier = async () => { + await api.delete(`${interactionPrefix}/profile`); + + await api.put(`${interactionPrefix}/event`, { + json: { + event: InteractionEvent.SignIn, + }, + }); + + return api.post(`${interactionPrefix}/submit`).json(); +}; + +export const registerWithVerifiedIdentifier = async (payload: SendPasscodePayload) => { + await api.put(`${interactionPrefix}/event`, { + json: { + event: InteractionEvent.Register, + }, + }); + + await api.put(`${interactionPrefix}/profile`, { + json: payload, + }); + return api.post(`${interactionPrefix}/submit`).json(); }; diff --git a/packages/ui/src/apis/register.ts b/packages/ui/src/apis/register.ts deleted file mode 100644 index 13849429b..000000000 --- a/packages/ui/src/apis/register.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MessageTypes } from '@logto/connector-kit'; - -import api from './api'; - -const apiPrefix = '/api/session'; - -type Response = { - redirectTo: string; -}; - -export const registerWithSms = async () => - api.post(`${apiPrefix}/register/passwordless/sms`).json(); - -export const registerWithEmail = async () => - api.post(`${apiPrefix}/register/passwordless/email`).json(); - -export const sendRegisterSmsPasscode = async (phone: string) => { - await api - .post(`${apiPrefix}/passwordless/sms/send`, { - json: { - phone, - flow: MessageTypes.Register, - }, - }) - .json(); - - return { success: true }; -}; - -export const verifyRegisterSmsPasscode = async (phone: string, code: string) => - api - .post(`${apiPrefix}/passwordless/sms/verify`, { - json: { - phone, - code, - flow: MessageTypes.Register, - }, - }) - .json(); - -export const sendRegisterEmailPasscode = async (email: string) => { - await api - .post(`${apiPrefix}/passwordless/email/send`, { - json: { - email, - flow: MessageTypes.Register, - }, - }) - .json(); - - return { success: true }; -}; - -export const verifyRegisterEmailPasscode = async (email: string, code: string) => - api - .post(`${apiPrefix}/passwordless/email/verify`, { - json: { - email, - code, - flow: MessageTypes.Register, - }, - }) - .json(); diff --git a/packages/ui/src/apis/sign-in.ts b/packages/ui/src/apis/sign-in.ts deleted file mode 100644 index db5635d00..000000000 --- a/packages/ui/src/apis/sign-in.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { MessageTypes } from '@logto/connector-kit'; - -import api from './api'; -import { bindSocialAccount } from './social'; - -const apiPrefix = '/api/session'; - -type Response = { - redirectTo: string; -}; - -export const signInWithSms = async (socialToBind?: string) => { - const result = await api.post(`${apiPrefix}/sign-in/passwordless/sms`).json(); - - if (result.redirectTo && socialToBind) { - await bindSocialAccount(socialToBind); - } - - return result; -}; - -export const signInWithEmail = async (socialToBind?: string) => { - const result = await api.post(`${apiPrefix}/sign-in/passwordless/email`).json(); - - if (result.redirectTo && socialToBind) { - await bindSocialAccount(socialToBind); - } - - return result; -}; - -export const sendSignInSmsPasscode = async (phone: string) => { - await api - .post(`${apiPrefix}/passwordless/sms/send`, { - json: { - phone, - flow: MessageTypes.SignIn, - }, - }) - .json(); - - return { success: true }; -}; - -export const verifySignInSmsPasscode = async ( - phone: string, - code: string, - socialToBind?: string -) => { - const result = await api - .post(`${apiPrefix}/passwordless/sms/verify`, { - json: { - phone, - code, - flow: MessageTypes.SignIn, - }, - }) - .json(); - - if (result.redirectTo && socialToBind) { - await bindSocialAccount(socialToBind); - } - - return result; -}; - -export const sendSignInEmailPasscode = async (email: string) => { - await api - .post(`${apiPrefix}/passwordless/email/send`, { - json: { - email, - flow: MessageTypes.SignIn, - }, - }) - .json(); - - return { success: true }; -}; - -export const verifySignInEmailPasscode = async ( - email: string, - code: string, - socialToBind?: string -) => { - const result = await api - .post(`${apiPrefix}/passwordless/email/verify`, { - json: { - email, - code, - flow: MessageTypes.SignIn, - }, - }) - .json(); - - if (result.redirectTo && socialToBind) { - await bindSocialAccount(socialToBind); - } - - return result; -}; diff --git a/packages/ui/src/apis/utils.ts b/packages/ui/src/apis/utils.ts index ca091245a..0ecbfe228 100644 --- a/packages/ui/src/apis/utils.ts +++ b/packages/ui/src/apis/utils.ts @@ -1,47 +1,22 @@ -import { SignInIdentifier } from '@logto/schemas'; +import { InteractionEvent } from '@logto/schemas'; import { UserFlow } from '@/types'; -import { sendContinueSetEmailPasscode, sendContinueSetPhonePasscode } from './continue'; -import { sendForgotPasswordEmailPasscode, sendForgotPasswordSmsPasscode } from './forgot-password'; -import { sendRegisterEmailPasscode, sendRegisterSmsPasscode } from './register'; -import { sendSignInEmailPasscode, sendSignInSmsPasscode } from './sign-in'; +import type { SendPasscodePayload } from './interaction'; +import { putInteraction, sendPasscode } from './interaction'; -export type PasscodeChannel = SignInIdentifier.Email | SignInIdentifier.Sms; - -// TODO: @simeng-li merge in to one single api - -export const getSendPasscodeApi = ( - type: UserFlow, - method: PasscodeChannel -): ((_address: string) => Promise<{ success: boolean }>) => { - if (type === UserFlow.forgotPassword && method === SignInIdentifier.Email) { - return sendForgotPasswordEmailPasscode; +export const getSendPasscodeApi = (type: UserFlow) => async (payload: SendPasscodePayload) => { + if (type === UserFlow.forgotPassword) { + await putInteraction(InteractionEvent.ForgotPassword); } - if (type === UserFlow.forgotPassword && method === SignInIdentifier.Sms) { - return sendForgotPasswordSmsPasscode; + if (type === UserFlow.signIn) { + await putInteraction(InteractionEvent.SignIn); } - if (type === UserFlow.signIn && method === SignInIdentifier.Email) { - return sendSignInEmailPasscode; + if (type === UserFlow.register) { + await putInteraction(InteractionEvent.Register); } - if (type === UserFlow.signIn && method === SignInIdentifier.Sms) { - return sendSignInSmsPasscode; - } - - if (type === UserFlow.register && method === SignInIdentifier.Email) { - return sendRegisterEmailPasscode; - } - - if (type === UserFlow.register && method === SignInIdentifier.Sms) { - return sendRegisterSmsPasscode; - } - - if (type === UserFlow.continue && method === SignInIdentifier.Email) { - return sendContinueSetEmailPasscode; - } - - return sendContinueSetPhonePasscode; + return sendPasscode(payload); }; diff --git a/packages/ui/src/containers/EmailForm/EmailContinue.test.tsx b/packages/ui/src/containers/EmailForm/EmailContinue.test.tsx index b803f28e5..4c6fcefdf 100644 --- a/packages/ui/src/containers/EmailForm/EmailContinue.test.tsx +++ b/packages/ui/src/containers/EmailForm/EmailContinue.test.tsx @@ -2,14 +2,15 @@ import { fireEvent, waitFor, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import { sendContinueSetEmailPasscode } from '@/apis/continue'; +import { putInteraction, sendPasscode } from '@/apis/interaction'; import EmailContinue from './EmailContinue'; const mockedNavigate = jest.fn(); -jest.mock('@/apis/continue', () => ({ - sendContinueSetEmailPasscode: jest.fn(() => ({ success: true })), +jest.mock('@/apis/interaction', () => ({ + sendPasscode: jest.fn(() => ({ success: true })), + putInteraction: jest.fn(() => ({ success: true })), })); jest.mock('react-router-dom', () => ({ @@ -39,7 +40,8 @@ describe('EmailContinue', () => { }); await waitFor(() => { - expect(sendContinueSetEmailPasscode).toBeCalledWith(email); + expect(putInteraction).not.toBeCalled(); + expect(sendPasscode).toBeCalledWith({ email }); expect(mockedNavigate).toBeCalledWith( { pathname: '/continue/email/passcode-validation', search: '' }, { state: { email } } diff --git a/packages/ui/src/containers/EmailForm/EmailForm.test.tsx b/packages/ui/src/containers/EmailForm/EmailForm.test.tsx index 6f19a56b1..e926d3038 100644 --- a/packages/ui/src/containers/EmailForm/EmailForm.test.tsx +++ b/packages/ui/src/containers/EmailForm/EmailForm.test.tsx @@ -138,7 +138,7 @@ describe('', () => { }); await waitFor(() => { - expect(onSubmit).toBeCalledWith('foo@logto.io'); + expect(onSubmit).toBeCalledWith({ email: 'foo@logto.io' }); }); }); @@ -166,7 +166,7 @@ describe('', () => { }); await waitFor(() => { - expect(onSubmit).toBeCalledWith('foo@logto.io'); + expect(onSubmit).toBeCalledWith({ email: 'foo@logto.io' }); }); }); }); diff --git a/packages/ui/src/containers/EmailForm/EmailForm.tsx b/packages/ui/src/containers/EmailForm/EmailForm.tsx index d8d7a13ae..7414039b4 100644 --- a/packages/ui/src/containers/EmailForm/EmailForm.tsx +++ b/packages/ui/src/containers/EmailForm/EmailForm.tsx @@ -23,7 +23,7 @@ type Props = { errorMessage?: string; submitButtonText?: TFuncKey; clearErrorMessage?: () => void; - onSubmit: (email: string) => Promise | void; + onSubmit: (payload: { email: string }) => Promise | void; }; type FieldState = { @@ -59,9 +59,9 @@ const EmailForm = ({ return; } - await onSubmit(fieldValue.email); + await onSubmit(fieldValue); }, - [validateForm, hasTerms, termsValidation, onSubmit, fieldValue.email] + [validateForm, hasTerms, termsValidation, onSubmit, fieldValue] ); const { onChange, ...rest } = register('email', emailValidation); diff --git a/packages/ui/src/containers/EmailForm/EmailRegister.test.tsx b/packages/ui/src/containers/EmailForm/EmailRegister.test.tsx index a4fa7d274..fd4356edf 100644 --- a/packages/ui/src/containers/EmailForm/EmailRegister.test.tsx +++ b/packages/ui/src/containers/EmailForm/EmailRegister.test.tsx @@ -1,15 +1,17 @@ +import { InteractionEvent } from '@logto/schemas'; import { fireEvent, waitFor, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import { sendRegisterEmailPasscode } from '@/apis/register'; +import { putInteraction, sendPasscode } from '@/apis/interaction'; import EmailRegister from './EmailRegister'; const mockedNavigate = jest.fn(); -jest.mock('@/apis/register', () => ({ - sendRegisterEmailPasscode: jest.fn(() => ({ success: true })), +jest.mock('@/apis/interaction', () => ({ + sendPasscode: jest.fn(() => ({ success: true })), + putInteraction: jest.fn(() => ({ success: true })), })); jest.mock('react-router-dom', () => ({ @@ -39,7 +41,8 @@ describe('EmailRegister', () => { }); await waitFor(() => { - expect(sendRegisterEmailPasscode).toBeCalledWith(email); + expect(putInteraction).toBeCalledWith(InteractionEvent.Register); + expect(sendPasscode).toBeCalledWith({ email }); expect(mockedNavigate).toBeCalledWith( { pathname: '/register/email/passcode-validation', search: '' }, { state: { email } } diff --git a/packages/ui/src/containers/EmailForm/EmailResetPassword.test.tsx b/packages/ui/src/containers/EmailForm/EmailResetPassword.test.tsx index 78f19827e..dbe70d328 100644 --- a/packages/ui/src/containers/EmailForm/EmailResetPassword.test.tsx +++ b/packages/ui/src/containers/EmailForm/EmailResetPassword.test.tsx @@ -1,17 +1,18 @@ -import { SignInIdentifier } from '@logto/schemas'; +import { InteractionEvent, SignInIdentifier } from '@logto/schemas'; import { fireEvent, waitFor, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import { sendForgotPasswordEmailPasscode } from '@/apis/forgot-password'; +import { putInteraction, sendPasscode } from '@/apis/interaction'; import { UserFlow } from '@/types'; import EmailResetPassword from './EmailResetPassword'; const mockedNavigate = jest.fn(); -jest.mock('@/apis/forgot-password', () => ({ - sendForgotPasswordEmailPasscode: jest.fn(() => ({ success: true })), +jest.mock('@/apis/interaction', () => ({ + sendPasscode: jest.fn(() => ({ success: true })), + putInteraction: jest.fn(() => ({ success: true })), })); jest.mock('react-router-dom', () => ({ @@ -41,7 +42,8 @@ describe('EmailRegister', () => { }); await waitFor(() => { - expect(sendForgotPasswordEmailPasscode).toBeCalledWith(email); + expect(putInteraction).toBeCalledWith(InteractionEvent.ForgotPassword); + expect(sendPasscode).toBeCalledWith({ email }); expect(mockedNavigate).toBeCalledWith( { pathname: `/${UserFlow.forgotPassword}/${SignInIdentifier.Email}/passcode-validation`, diff --git a/packages/ui/src/containers/EmailForm/EmailSignIn.test.tsx b/packages/ui/src/containers/EmailForm/EmailSignIn.test.tsx index 2e191141d..e7875a95f 100644 --- a/packages/ui/src/containers/EmailForm/EmailSignIn.test.tsx +++ b/packages/ui/src/containers/EmailForm/EmailSignIn.test.tsx @@ -1,16 +1,17 @@ -import { SignInIdentifier } from '@logto/schemas'; +import { InteractionEvent, SignInIdentifier } from '@logto/schemas'; import { fireEvent, waitFor, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import { sendSignInEmailPasscode } from '@/apis/sign-in'; +import { sendPasscode, putInteraction } from '@/apis/interaction'; import EmailSignIn from './EmailSignIn'; const mockedNavigate = jest.fn(); -jest.mock('@/apis/sign-in', () => ({ - sendSignInEmailPasscode: jest.fn(() => ({ success: true })), +jest.mock('@/apis/interaction', () => ({ + sendPasscode: jest.fn(() => ({ success: true })), + putInteraction: jest.fn(() => ({ success: true })), })); jest.mock('react-router-dom', () => ({ @@ -51,7 +52,8 @@ describe('EmailSignIn', () => { }); await waitFor(() => { - expect(sendSignInEmailPasscode).not.toBeCalled(); + expect(putInteraction).not.toBeCalled(); + expect(sendPasscode).not.toBeCalled(); expect(mockedNavigate).toBeCalledWith( { pathname: '/sign-in/email/password', search: '' }, { state: { email } } @@ -59,7 +61,7 @@ describe('EmailSignIn', () => { }); }); - test('EmailSignIn form with password true but not primary verification code false', async () => { + test('EmailSignIn form with password true but verification code false', async () => { const { container, getByText } = renderWithPageContext( { }); await waitFor(() => { - expect(sendSignInEmailPasscode).not.toBeCalled(); + expect(putInteraction).not.toBeCalled(); + expect(sendPasscode).not.toBeCalled(); expect(mockedNavigate).toBeCalledWith( { pathname: '/sign-in/email/password', search: '' }, { state: { email } } @@ -93,7 +96,7 @@ describe('EmailSignIn', () => { }); }); - test('EmailSignIn form with password true but not primary verification code true', async () => { + test('EmailSignIn form with password true but not primary, verification code true', async () => { const { container, getByText } = renderWithPageContext( { }); await waitFor(() => { - expect(sendSignInEmailPasscode).toBeCalledWith(email); + expect(putInteraction).toBeCalledWith(InteractionEvent.SignIn); + expect(sendPasscode).toBeCalledWith({ email }); expect(mockedNavigate).toBeCalledWith( { pathname: '/sign-in/email/passcode-validation', search: '' }, { state: { email } } @@ -155,7 +159,8 @@ describe('EmailSignIn', () => { }); await waitFor(() => { - expect(sendSignInEmailPasscode).toBeCalledWith(email); + expect(putInteraction).toBeCalledWith(InteractionEvent.SignIn); + expect(sendPasscode).toBeCalledWith({ email }); expect(mockedNavigate).toBeCalledWith( { pathname: '/sign-in/email/passcode-validation', search: '' }, { state: { email } } diff --git a/packages/ui/src/containers/PasscodeValidation/index.test.tsx b/packages/ui/src/containers/PasscodeValidation/index.test.tsx index 7903ecf5e..afde4105a 100644 --- a/packages/ui/src/containers/PasscodeValidation/index.test.tsx +++ b/packages/ui/src/containers/PasscodeValidation/index.test.tsx @@ -3,16 +3,10 @@ import { act, fireEvent, waitFor } from '@testing-library/react'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; import { - verifyContinueSetEmailPasscode, - continueApi, - verifyContinueSetSmsPasscode, -} from '@/apis/continue'; -import { - verifyForgotPasswordEmailPasscode, - verifyForgotPasswordSmsPasscode, -} from '@/apis/forgot-password'; -import { verifyRegisterEmailPasscode, verifyRegisterSmsPasscode } from '@/apis/register'; -import { verifySignInEmailPasscode, verifySignInSmsPasscode } from '@/apis/sign-in'; + verifyForgotPasswordPasscodeIdentifier, + signInWithPasscodeIdentifier, + addProfileWithPasscodeIdentifier, +} from '@/apis/interaction'; import { UserFlow } from '@/types'; import PasscodeValidation from '.'; @@ -32,25 +26,10 @@ jest.mock('@/apis/utils', () => ({ getSendPasscodeApi: () => sendPasscodeApi, })); -jest.mock('@/apis/sign-in', () => ({ - verifySignInEmailPasscode: jest.fn(), - verifySignInSmsPasscode: jest.fn(), -})); - -jest.mock('@/apis/register', () => ({ - verifyRegisterEmailPasscode: jest.fn(), - verifyRegisterSmsPasscode: jest.fn(), -})); - -jest.mock('@/apis/forgot-password', () => ({ - verifyForgotPasswordEmailPasscode: jest.fn(), - verifyForgotPasswordSmsPasscode: jest.fn(), -})); - -jest.mock('@/apis/continue', () => ({ - verifyContinueSetEmailPasscode: jest.fn(), - verifyContinueSetSmsPasscode: jest.fn(), - continueApi: jest.fn(), +jest.mock('@/apis/interaction', () => ({ + verifyForgotPasswordPasscodeIdentifier: jest.fn(), + signInWithPasscodeIdentifier: jest.fn(), + addProfileWithPasscodeIdentifier: jest.fn(), })); describe('', () => { @@ -103,12 +82,12 @@ describe('', () => { fireEvent.click(resendButton); }); - expect(sendPasscodeApi).toBeCalledWith(email); + expect(sendPasscodeApi).toBeCalledWith({ email }); }); describe('sign-in', () => { it('fire email sign-in validate passcode event', async () => { - (verifySignInEmailPasscode as jest.Mock).mockImplementationOnce(() => ({ + (signInWithPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({ redirectTo: 'foo.com', })); @@ -124,7 +103,10 @@ describe('', () => { } await waitFor(() => { - expect(verifySignInEmailPasscode).toBeCalledWith(email, '111111', undefined); + expect(signInWithPasscodeIdentifier).toBeCalledWith( + { email, passcode: '111111' }, + undefined + ); }); await waitFor(() => { @@ -133,7 +115,7 @@ describe('', () => { }); it('fire sms sign-in validate passcode event', async () => { - (verifySignInSmsPasscode as jest.Mock).mockImplementationOnce(() => ({ + (signInWithPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({ redirectTo: 'foo.com', })); @@ -149,7 +131,13 @@ describe('', () => { } await waitFor(() => { - expect(verifySignInSmsPasscode).toBeCalledWith(phone, '111111', undefined); + expect(signInWithPasscodeIdentifier).toBeCalledWith( + { + phone, + passcode: '111111', + }, + undefined + ); }); await waitFor(() => { @@ -160,7 +148,7 @@ describe('', () => { describe('register', () => { it('fire email register validate passcode event', async () => { - (verifyRegisterEmailPasscode as jest.Mock).mockImplementationOnce(() => ({ + (addProfileWithPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({ redirectTo: 'foo.com', })); @@ -180,7 +168,10 @@ describe('', () => { } await waitFor(() => { - expect(verifyRegisterEmailPasscode).toBeCalledWith(email, '111111'); + expect(addProfileWithPasscodeIdentifier).toBeCalledWith({ + email, + passcode: '111111', + }); }); await waitFor(() => { @@ -189,7 +180,7 @@ describe('', () => { }); it('fire sms register validate passcode event', async () => { - (verifyRegisterSmsPasscode as jest.Mock).mockImplementationOnce(() => ({ + (addProfileWithPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({ redirectTo: 'foo.com', })); @@ -205,7 +196,7 @@ describe('', () => { } await waitFor(() => { - expect(verifyRegisterSmsPasscode).toBeCalledWith(phone, '111111'); + expect(addProfileWithPasscodeIdentifier).toBeCalledWith({ phone, passcode: '111111' }); }); await waitFor(() => { @@ -216,7 +207,7 @@ describe('', () => { describe('forgot password', () => { it('fire email forgot-password validate passcode event', async () => { - (verifyForgotPasswordEmailPasscode as jest.Mock).mockImplementationOnce(() => ({ + (verifyForgotPasswordPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({ success: true, })); @@ -237,17 +228,17 @@ describe('', () => { } await waitFor(() => { - expect(verifyForgotPasswordEmailPasscode).toBeCalledWith(email, '111111'); + expect(verifyForgotPasswordPasscodeIdentifier).toBeCalledWith({ + email, + passcode: '111111', + }); }); - await waitFor(() => { - expect(window.location.replace).not.toBeCalled(); - expect(mockedNavigate).toBeCalledWith('/forgot-password/reset', { replace: true }); - }); + // TODO: @simeng test exception flow to fulfill the password }); it('fire sms forgot-password validate passcode event', async () => { - (verifyForgotPasswordSmsPasscode as jest.Mock).mockImplementationOnce(() => ({ + (verifyForgotPasswordPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({ success: true, })); @@ -268,22 +259,21 @@ describe('', () => { } await waitFor(() => { - expect(verifyForgotPasswordSmsPasscode).toBeCalledWith(phone, '111111'); + expect(verifyForgotPasswordPasscodeIdentifier).toBeCalledWith({ + phone, + passcode: '111111', + }); }); - await waitFor(() => { - expect(window.location.replace).not.toBeCalled(); - expect(mockedNavigate).toBeCalledWith('/forgot-password/reset', { replace: true }); - }); + // TODO: @simeng test exception flow to fulfill the password }); }); describe('continue flow', () => { it('set email', async () => { - (verifyContinueSetEmailPasscode as jest.Mock).mockImplementationOnce(() => ({ - success: true, + (addProfileWithPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({ + redirectTo: '/redirect', })); - (continueApi as jest.Mock).mockImplementationOnce(() => ({ redirectTo: '/redirect' })); const { container } = renderWithPageContext( ', () => { } await waitFor(() => { - expect(verifyContinueSetEmailPasscode).toBeCalledWith(email, '111111'); + expect(addProfileWithPasscodeIdentifier).toBeCalledWith( + { + email, + passcode: '111111', + }, + undefined + ); }); await waitFor(() => { - expect(continueApi).toBeCalledWith('email', email, undefined); expect(window.location.replace).toBeCalledWith('/redirect'); }); }); it('set Phone', async () => { - (verifyContinueSetSmsPasscode as jest.Mock).mockImplementationOnce(() => ({ - success: true, + (addProfileWithPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({ + redirectTo: '/redirect', })); - (continueApi as jest.Mock).mockImplementationOnce(() => ({ redirectTo: '/redirect' })); const { container } = renderWithPageContext( @@ -330,11 +324,16 @@ describe('', () => { } await waitFor(() => { - expect(verifyContinueSetSmsPasscode).toBeCalledWith(phone, '111111'); + expect(addProfileWithPasscodeIdentifier).toBeCalledWith( + { + phone, + passcode: '111111', + }, + undefined + ); }); await waitFor(() => { - expect(continueApi).toBeCalledWith('phone', phone, undefined); expect(window.location.replace).toBeCalledWith('/redirect'); }); }); diff --git a/packages/ui/src/containers/PasscodeValidation/use-continue-set-email-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-continue-set-email-passcode-validation.ts index 9cc5c47fa..c4a00fc2e 100644 --- a/packages/ui/src/containers/PasscodeValidation/use-continue-set-email-passcode-validation.ts +++ b/packages/ui/src/containers/PasscodeValidation/use-continue-set-email-passcode-validation.ts @@ -1,7 +1,7 @@ import { SignInIdentifier } from '@logto/schemas'; import { useMemo, useCallback } from 'react'; -import { verifyContinueSetEmailPasscode, continueApi } from '@/apis/continue'; +import { addProfileWithPasscodeIdentifier } from '@/apis/interaction'; import type { ErrorHandlers } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler'; @@ -24,45 +24,34 @@ const useContinueSetEmailPasscodeValidation = (email: string, errorCallback?: () const verifyPasscodeErrorHandlers: ErrorHandlers = useMemo( () => ({ + 'user.email_not_exist': identifierNotExistErrorHandler, + ...requiredProfileErrorHandler, ...sharedErrorHandlers, callback: errorCallback, }), - [errorCallback, sharedErrorHandlers] + [ + errorCallback, + identifierNotExistErrorHandler, + requiredProfileErrorHandler, + sharedErrorHandlers, + ] ); const { run: verifyPasscode } = useApi( - verifyContinueSetEmailPasscode, + addProfileWithPasscodeIdentifier, verifyPasscodeErrorHandlers ); - const setEmailErrorHandlers: ErrorHandlers = useMemo( - () => ({ - 'user.email_not_exist': identifierNotExistErrorHandler, - ...requiredProfileErrorHandler, - callback: errorCallback, - }), - [errorCallback, identifierNotExistErrorHandler, requiredProfileErrorHandler] - ); - - const { run: setEmail } = useApi(continueApi, setEmailErrorHandlers); - const onSubmit = useCallback( - async (code: string) => { - const verified = await verifyPasscode(email, code); - - if (!verified) { - return; - } - + async (passcode: string) => { const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial); - - const result = await setEmail('email', email, socialToBind); + const result = await verifyPasscode({ email, passcode }, socialToBind); if (result?.redirectTo) { window.location.replace(result.redirectTo); } }, - [email, setEmail, verifyPasscode] + [email, verifyPasscode] ); return { diff --git a/packages/ui/src/containers/PasscodeValidation/use-continue-set-sms-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-continue-set-sms-passcode-validation.ts index 748127309..c5848a7f8 100644 --- a/packages/ui/src/containers/PasscodeValidation/use-continue-set-sms-passcode-validation.ts +++ b/packages/ui/src/containers/PasscodeValidation/use-continue-set-sms-passcode-validation.ts @@ -1,7 +1,7 @@ import { SignInIdentifier } from '@logto/schemas'; import { useMemo, useCallback } from 'react'; -import { verifyContinueSetSmsPasscode, continueApi } from '@/apis/continue'; +import { addProfileWithPasscodeIdentifier } from '@/apis/interaction'; import type { ErrorHandlers } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler'; @@ -24,42 +24,34 @@ const useContinueSetSmsPasscodeValidation = (phone: string, errorCallback?: () = const verifyPasscodeErrorHandlers: ErrorHandlers = useMemo( () => ({ + 'user.phone_not_exist': identifierNotExistErrorHandler, + ...requiredProfileErrorHandler, ...sharedErrorHandlers, callback: errorCallback, }), - [errorCallback, sharedErrorHandlers] + [ + errorCallback, + identifierNotExistErrorHandler, + requiredProfileErrorHandler, + sharedErrorHandlers, + ] ); - const { run: verifyPasscode } = useApi(verifyContinueSetSmsPasscode, verifyPasscodeErrorHandlers); - - const setPhoneErrorHandlers: ErrorHandlers = useMemo( - () => ({ - 'user.phone_not_exist': identifierNotExistErrorHandler, - ...requiredProfileErrorHandler, - callback: errorCallback, - }), - [errorCallback, identifierNotExistErrorHandler, requiredProfileErrorHandler] + const { run: verifyPasscode } = useApi( + addProfileWithPasscodeIdentifier, + verifyPasscodeErrorHandlers ); - const { run: setPhone } = useApi(continueApi, setPhoneErrorHandlers); - const onSubmit = useCallback( - async (code: string) => { - const verified = await verifyPasscode(phone, code); - - if (!verified) { - return; - } - + async (passcode: string) => { const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial); - - const result = await setPhone('phone', phone, socialToBind); + const result = await verifyPasscode({ phone, passcode }, socialToBind); if (result?.redirectTo) { window.location.replace(result.redirectTo); } }, - [phone, setPhone, verifyPasscode] + [phone, verifyPasscode] ); return { diff --git a/packages/ui/src/containers/PasscodeValidation/use-forgot-password-email-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-forgot-password-email-passcode-validation.ts index 3d3ec9dd3..b51da3254 100644 --- a/packages/ui/src/containers/PasscodeValidation/use-forgot-password-email-passcode-validation.ts +++ b/packages/ui/src/containers/PasscodeValidation/use-forgot-password-email-passcode-validation.ts @@ -2,7 +2,7 @@ import { SignInIdentifier } from '@logto/schemas'; import { useMemo, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { verifyForgotPasswordEmailPasscode } from '@/apis/forgot-password'; +import { verifyForgotPasswordPasscodeIdentifier } from '@/apis/interaction'; import type { ErrorHandlers } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; import { UserFlow } from '@/types'; @@ -23,24 +23,30 @@ const useForgotPasswordEmailPasscodeValidation = (email: string, errorCallback?: const errorHandlers: ErrorHandlers = useMemo( () => ({ 'user.email_not_exist': identifierNotExistErrorHandler, + 'user.new_password_required_in_profile': () => { + navigate(`/${UserFlow.forgotPassword}/reset`, { replace: true }); + }, ...sharedErrorHandlers, callback: errorCallback, }), - [identifierNotExistErrorHandler, sharedErrorHandlers, errorCallback] + [identifierNotExistErrorHandler, sharedErrorHandlers, errorCallback, navigate] ); - const { result, run: verifyPasscode } = useApi(verifyForgotPasswordEmailPasscode, errorHandlers); + const { result, run: verifyPasscode } = useApi( + verifyForgotPasswordPasscodeIdentifier, + errorHandlers + ); const onSubmit = useCallback( - async (code: string) => { - return verifyPasscode(email, code); + async (passcode: string) => { + return verifyPasscode({ email, passcode }); }, [email, verifyPasscode] ); useEffect(() => { if (result) { - navigate(`/${UserFlow.forgotPassword}/reset`, { replace: true }); + navigate(`/${UserFlow.signIn}`, { replace: true }); } }, [navigate, result]); diff --git a/packages/ui/src/containers/PasscodeValidation/use-forgot-password-sms-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-forgot-password-sms-passcode-validation.ts index d46c6fd8e..ad8a064ee 100644 --- a/packages/ui/src/containers/PasscodeValidation/use-forgot-password-sms-passcode-validation.ts +++ b/packages/ui/src/containers/PasscodeValidation/use-forgot-password-sms-passcode-validation.ts @@ -2,7 +2,7 @@ import { SignInIdentifier } from '@logto/schemas'; import { useMemo, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { verifyForgotPasswordSmsPasscode } from '@/apis/forgot-password'; +import { verifyForgotPasswordPasscodeIdentifier } from '@/apis/interaction'; import type { ErrorHandlers } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; import { UserFlow } from '@/types'; @@ -13,6 +13,7 @@ import useSharedErrorHandler from './use-shared-error-handler'; const useForgotPasswordSmsPasscodeValidation = (phone: string, errorCallback?: () => void) => { const navigate = useNavigate(); const { sharedErrorHandlers, errorMessage, clearErrorMessage } = useSharedErrorHandler(); + const identifierNotExistErrorHandler = useIdentifierErrorAlert( UserFlow.forgotPassword, SignInIdentifier.Sms, @@ -22,24 +23,30 @@ const useForgotPasswordSmsPasscodeValidation = (phone: string, errorCallback?: ( const errorHandlers: ErrorHandlers = useMemo( () => ({ 'user.phone_not_exist': identifierNotExistErrorHandler, + 'user.new_password_required_in_profile': () => { + navigate(`/${UserFlow.forgotPassword}/reset`, { replace: true }); + }, ...sharedErrorHandlers, callback: errorCallback, }), - [sharedErrorHandlers, errorCallback, identifierNotExistErrorHandler] + [identifierNotExistErrorHandler, sharedErrorHandlers, errorCallback, navigate] ); - const { result, run: verifyPasscode } = useApi(verifyForgotPasswordSmsPasscode, errorHandlers); + const { result, run: verifyPasscode } = useApi( + verifyForgotPasswordPasscodeIdentifier, + errorHandlers + ); const onSubmit = useCallback( - async (code: string) => { - return verifyPasscode(phone, code); + async (passcode: string) => { + return verifyPasscode({ phone, passcode }); }, [phone, verifyPasscode] ); useEffect(() => { if (result) { - navigate(`/${UserFlow.forgotPassword}/reset`, { replace: true }); + navigate(`/${UserFlow.signIn}`, { replace: true }); } }, [navigate, result]); diff --git a/packages/ui/src/containers/PasscodeValidation/use-register-with-email-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-register-with-email-passcode-validation.ts index 3bb3614d7..f8fdd4d02 100644 --- a/packages/ui/src/containers/PasscodeValidation/use-register-with-email-passcode-validation.ts +++ b/packages/ui/src/containers/PasscodeValidation/use-register-with-email-passcode-validation.ts @@ -3,8 +3,7 @@ import { useMemo, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { verifyRegisterEmailPasscode } from '@/apis/register'; -import { signInWithEmail } from '@/apis/sign-in'; +import { addProfileWithPasscodeIdentifier, signInWithVerifierIdentifier } from '@/apis/interaction'; import type { ErrorHandlers } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; import { useConfirmModal } from '@/hooks/use-confirm-modal'; @@ -25,7 +24,10 @@ const useRegisterWithEmailPasscodeValidation = (email: string, errorCallback?: ( const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true); - const { run: signInWithEmailAsync } = useApi(signInWithEmail, requiredProfileErrorHandlers); + const { run: signInWithEmailAsync } = useApi( + signInWithVerifierIdentifier, + requiredProfileErrorHandlers + ); const identifierExistErrorHandler = useIdentifierErrorAlert( UserFlow.register, @@ -75,11 +77,11 @@ const useRegisterWithEmailPasscodeValidation = (email: string, errorCallback?: ( ] ); - const { result, run: verifyPasscode } = useApi(verifyRegisterEmailPasscode, errorHandlers); + const { result, run: verifyPasscode } = useApi(addProfileWithPasscodeIdentifier, errorHandlers); const onSubmit = useCallback( - async (code: string) => { - return verifyPasscode(email, code); + async (passcode: string) => { + return verifyPasscode({ email, passcode }); }, [email, verifyPasscode] ); diff --git a/packages/ui/src/containers/PasscodeValidation/use-register-with-sms-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-register-with-sms-passcode-validation.ts index 2a52726a3..1336704e8 100644 --- a/packages/ui/src/containers/PasscodeValidation/use-register-with-sms-passcode-validation.ts +++ b/packages/ui/src/containers/PasscodeValidation/use-register-with-sms-passcode-validation.ts @@ -3,8 +3,7 @@ import { useMemo, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { verifyRegisterSmsPasscode } from '@/apis/register'; -import { signInWithSms } from '@/apis/sign-in'; +import { addProfileWithPasscodeIdentifier, signInWithVerifierIdentifier } from '@/apis/interaction'; import type { ErrorHandlers } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; import { useConfirmModal } from '@/hooks/use-confirm-modal'; @@ -25,7 +24,10 @@ const useRegisterWithSmsPasscodeValidation = (phone: string, errorCallback?: () const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true); - const { run: signInWithSmsAsync } = useApi(signInWithSms, requiredProfileErrorHandlers); + const { run: signInWithSmsAsync } = useApi( + signInWithVerifierIdentifier, + requiredProfileErrorHandlers + ); const identifierExistErrorHandler = useIdentifierErrorAlert( UserFlow.register, @@ -75,7 +77,7 @@ const useRegisterWithSmsPasscodeValidation = (phone: string, errorCallback?: () ] ); - const { result, run: verifyPasscode } = useApi(verifyRegisterSmsPasscode, errorHandlers); + const { result, run: verifyPasscode } = useApi(addProfileWithPasscodeIdentifier, errorHandlers); useEffect(() => { if (result?.redirectTo) { @@ -84,8 +86,11 @@ const useRegisterWithSmsPasscodeValidation = (phone: string, errorCallback?: () }, [result]); const onSubmit = useCallback( - async (code: string) => { - return verifyPasscode(phone, code); + async (passcode: string) => { + return verifyPasscode({ + phone, + passcode, + }); }, [phone, verifyPasscode] ); diff --git a/packages/ui/src/containers/PasscodeValidation/use-resend-passcode.ts b/packages/ui/src/containers/PasscodeValidation/use-resend-passcode.ts index d39330915..0837e4410 100644 --- a/packages/ui/src/containers/PasscodeValidation/use-resend-passcode.ts +++ b/packages/ui/src/containers/PasscodeValidation/use-resend-passcode.ts @@ -1,4 +1,4 @@ -import type { SignInIdentifier } from '@logto/schemas'; +import { SignInIdentifier } from '@logto/schemas'; import { t } from 'i18next'; import { useCallback, useContext } from 'react'; import { useTimer } from 'react-timer-hook'; @@ -29,16 +29,17 @@ const useResendPasscode = ( expiryTimestamp: getTimeout(), }); - const { run: sendPassCode } = useApi(getSendPasscodeApi(type, method)); + const { run: sendPassCode } = useApi(getSendPasscodeApi(type)); const onResendPasscode = useCallback(async () => { - const result = await sendPassCode(target); + const payload = method === SignInIdentifier.Email ? { email: target } : { phone: target }; + const result = await sendPassCode(payload); if (result) { setToast(t('description.passcode_sent')); restart(getTimeout(), true); } - }, [restart, sendPassCode, setToast, target]); + }, [method, restart, sendPassCode, setToast, target]); return { seconds, diff --git a/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-email-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-email-passcode-validation.ts index 0844b3bb9..a3229a7fb 100644 --- a/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-email-passcode-validation.ts +++ b/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-email-passcode-validation.ts @@ -3,8 +3,7 @@ import { useMemo, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { registerWithEmail } from '@/apis/register'; -import { verifySignInEmailPasscode } from '@/apis/sign-in'; +import { signInWithPasscodeIdentifier, registerWithVerifiedIdentifier } from '@/apis/interaction'; import type { ErrorHandlers } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; import { useConfirmModal } from '@/hooks/use-confirm-modal'; @@ -26,7 +25,10 @@ const useSignInWithEmailPasscodeValidation = (email: string, errorCallback?: () const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true); - const { run: registerWithEmailAsync } = useApi(registerWithEmail, requiredProfileErrorHandlers); + const { run: registerWithEmailAsync } = useApi( + registerWithVerifiedIdentifier, + requiredProfileErrorHandlers + ); const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial); @@ -51,7 +53,7 @@ const useSignInWithEmailPasscodeValidation = (email: string, errorCallback?: () return; } - const result = await registerWithEmailAsync(); + const result = await registerWithEmailAsync({ email }); if (result?.redirectTo) { window.location.replace(result.redirectTo); @@ -80,7 +82,10 @@ const useSignInWithEmailPasscodeValidation = (email: string, errorCallback?: () ] ); - const { result, run: verifyPasscode } = useApi(verifySignInEmailPasscode, errorHandlers); + const { result, run: asyncSignInWithPasscodeIdentifier } = useApi( + signInWithPasscodeIdentifier, + errorHandlers + ); useEffect(() => { if (result?.redirectTo) { @@ -89,10 +94,16 @@ const useSignInWithEmailPasscodeValidation = (email: string, errorCallback?: () }, [result]); const onSubmit = useCallback( - async (code: string) => { - return verifyPasscode(email, code, socialToBind); + async (passcode: string) => { + return asyncSignInWithPasscodeIdentifier( + { + email, + passcode, + }, + socialToBind + ); }, - [email, socialToBind, verifyPasscode] + [asyncSignInWithPasscodeIdentifier, email, socialToBind] ); return { diff --git a/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-sms-passcode-validation.ts b/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-sms-passcode-validation.ts index a63f2c3f2..8e8c3abbb 100644 --- a/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-sms-passcode-validation.ts +++ b/packages/ui/src/containers/PasscodeValidation/use-sign-in-with-sms-passcode-validation.ts @@ -3,8 +3,7 @@ import { useMemo, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { registerWithSms } from '@/apis/register'; -import { verifySignInSmsPasscode } from '@/apis/sign-in'; +import { signInWithPasscodeIdentifier, registerWithVerifiedIdentifier } from '@/apis/interaction'; import type { ErrorHandlers } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; import { useConfirmModal } from '@/hooks/use-confirm-modal'; @@ -26,7 +25,10 @@ const useSignInWithSmsPasscodeValidation = (phone: string, errorCallback?: () => const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true); - const { run: registerWithSmsAsync } = useApi(registerWithSms, requiredProfileErrorHandlers); + const { run: registerWithSmsAsync } = useApi( + registerWithVerifiedIdentifier, + requiredProfileErrorHandlers + ); const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial); @@ -51,7 +53,7 @@ const useSignInWithSmsPasscodeValidation = (phone: string, errorCallback?: () => return; } - const result = await registerWithSmsAsync(); + const result = await registerWithSmsAsync({ phone }); if (result?.redirectTo) { window.location.replace(result.redirectTo); @@ -80,7 +82,10 @@ const useSignInWithSmsPasscodeValidation = (phone: string, errorCallback?: () => ] ); - const { result, run: verifyPasscode } = useApi(verifySignInSmsPasscode, errorHandlers); + const { result, run: asyncSignInWithPasscodeIdentifier } = useApi( + signInWithPasscodeIdentifier, + errorHandlers + ); useEffect(() => { if (result?.redirectTo) { @@ -90,9 +95,15 @@ const useSignInWithSmsPasscodeValidation = (phone: string, errorCallback?: () => const onSubmit = useCallback( async (code: string) => { - return verifyPasscode(phone, code, socialToBind); + return asyncSignInWithPasscodeIdentifier( + { + phone, + passcode: code, + }, + socialToBind + ); }, - [phone, socialToBind, verifyPasscode] + [phone, socialToBind, asyncSignInWithPasscodeIdentifier] ); return { diff --git a/packages/ui/src/containers/PasswordSignInForm/PasswordlessSignInLink.tsx b/packages/ui/src/containers/PasswordSignInForm/PasswordlessSignInLink.tsx index 76b5adf02..fe1995e05 100644 --- a/packages/ui/src/containers/PasswordSignInForm/PasswordlessSignInLink.tsx +++ b/packages/ui/src/containers/PasswordSignInForm/PasswordlessSignInLink.tsx @@ -1,4 +1,4 @@ -import type { SignInIdentifier } from '@logto/schemas'; +import { SignInIdentifier } from '@logto/schemas'; import { useContext, useEffect } from 'react'; import TextLink from '@/components/TextLink'; @@ -33,7 +33,7 @@ const PasswordlessSignInLink = ({ className, method, value }: Props) => { text="action.sign_in_via_passcode" onClick={() => { clearErrorMessage(); - void onSubmit(value); + void onSubmit(method === SignInIdentifier.Email ? { email: value } : { phone: value }); }} /> ); diff --git a/packages/ui/src/containers/PasswordSignInForm/index.test.tsx b/packages/ui/src/containers/PasswordSignInForm/index.test.tsx index 7fe22dae7..8e9f0916e 100644 --- a/packages/ui/src/containers/PasswordSignInForm/index.test.tsx +++ b/packages/ui/src/containers/PasswordSignInForm/index.test.tsx @@ -1,20 +1,16 @@ -import { SignInIdentifier } from '@logto/schemas'; +import { InteractionEvent, SignInIdentifier } from '@logto/schemas'; import { fireEvent, waitFor, act } from '@testing-library/react'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import { signInWithPasswordIdentifier } from '@/apis/interaction'; -import { sendSignInEmailPasscode, sendSignInSmsPasscode } from '@/apis/sign-in'; +import { signInWithPasswordIdentifier, putInteraction, sendPasscode } from '@/apis/interaction'; import { UserFlow } from '@/types'; import PasswordSignInForm from '.'; -jest.mock('@/apis/sign-in', () => ({ - sendSignInEmailPasscode: jest.fn(() => ({ success: true })), - sendSignInSmsPasscode: jest.fn(() => ({ success: true })), -})); - jest.mock('@/apis/interaction', () => ({ signInWithPasswordIdentifier: jest.fn(() => ({ redirectTo: '/' })), + sendPasscode: jest.fn(() => ({ success: true })), + putInteraction: jest.fn(() => ({ success: true })), })); const mockedNavigate = jest.fn(); @@ -80,7 +76,8 @@ describe('PasswordSignInForm', () => { }); await waitFor(() => { - expect(sendSignInEmailPasscode).toBeCalledWith(email); + expect(putInteraction).toBeCalledWith(InteractionEvent.SignIn); + expect(sendPasscode).toBeCalledWith({ email }); }); expect(mockedNavigate).toBeCalledWith( @@ -125,7 +122,8 @@ describe('PasswordSignInForm', () => { }); await waitFor(() => { - expect(sendSignInSmsPasscode).toBeCalledWith(phone); + expect(putInteraction).toBeCalledWith(InteractionEvent.SignIn); + expect(sendPasscode).toBeCalledWith({ phone }); }); expect(mockedNavigate).toBeCalledWith( diff --git a/packages/ui/src/containers/PhoneForm/PhoneForm.test.tsx b/packages/ui/src/containers/PhoneForm/PhoneForm.test.tsx index c73faed9c..5388ce55f 100644 --- a/packages/ui/src/containers/PhoneForm/PhoneForm.test.tsx +++ b/packages/ui/src/containers/PhoneForm/PhoneForm.test.tsx @@ -145,7 +145,7 @@ describe('', () => { }); await waitFor(() => { - expect(onSubmit).toBeCalledWith(`${defaultCountryCallingCode}${phoneNumber}`); + expect(onSubmit).toBeCalledWith({ phone: `${defaultCountryCallingCode}${phoneNumber}` }); }); }); @@ -173,7 +173,7 @@ describe('', () => { }); await waitFor(() => { - expect(onSubmit).toBeCalledWith(`${defaultCountryCallingCode}${phoneNumber}`); + expect(onSubmit).toBeCalledWith({ phone: `${defaultCountryCallingCode}${phoneNumber}` }); }); }); }); diff --git a/packages/ui/src/containers/PhoneForm/PhoneForm.tsx b/packages/ui/src/containers/PhoneForm/PhoneForm.tsx index c07d0e9bc..ab03ba34f 100644 --- a/packages/ui/src/containers/PhoneForm/PhoneForm.tsx +++ b/packages/ui/src/containers/PhoneForm/PhoneForm.tsx @@ -23,7 +23,7 @@ type Props = { errorMessage?: string; submitButtonText?: TFuncKey; clearErrorMessage?: () => void; - onSubmit: (phone: string) => Promise | void; + onSubmit: (payload: { phone: string }) => Promise | void; }; type FieldState = { @@ -79,9 +79,9 @@ const PhoneForm = ({ return; } - await onSubmit(fieldValue.phone); + await onSubmit(fieldValue); }, - [validateForm, hasTerms, termsValidation, onSubmit, fieldValue.phone] + [validateForm, hasTerms, termsValidation, onSubmit, fieldValue] ); return ( diff --git a/packages/ui/src/containers/PhoneForm/SmsContinue.test.tsx b/packages/ui/src/containers/PhoneForm/SmsContinue.test.tsx index 01dafa97e..3f52b3cad 100644 --- a/packages/ui/src/containers/PhoneForm/SmsContinue.test.tsx +++ b/packages/ui/src/containers/PhoneForm/SmsContinue.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, waitFor, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import { sendContinueSetPhonePasscode } from '@/apis/continue'; +import { putInteraction, sendPasscode } from '@/apis/interaction'; import { getDefaultCountryCallingCode } from '@/utils/country-code'; import SmsContinue from './SmsContinue'; @@ -14,8 +14,9 @@ jest.mock('i18next', () => ({ language: 'en', })); -jest.mock('@/apis/continue', () => ({ - sendContinueSetPhonePasscode: jest.fn(() => ({ success: true })), +jest.mock('@/apis/interaction', () => ({ + sendPasscode: jest.fn(() => ({ success: true })), + putInteraction: jest.fn(() => ({ success: true })), })); jest.mock('react-router-dom', () => ({ @@ -47,7 +48,8 @@ describe('SmsContinue', () => { }); await waitFor(() => { - expect(sendContinueSetPhonePasscode).toBeCalledWith(fullPhoneNumber); + expect(putInteraction).not.toBeCalled(); + expect(sendPasscode).toBeCalledWith({ phone: fullPhoneNumber }); expect(mockedNavigate).toBeCalledWith( { pathname: '/continue/sms/passcode-validation', search: '' }, { state: { phone: fullPhoneNumber } } diff --git a/packages/ui/src/containers/PhoneForm/SmsRegister.test.tsx b/packages/ui/src/containers/PhoneForm/SmsRegister.test.tsx index 375262312..30adc07e3 100644 --- a/packages/ui/src/containers/PhoneForm/SmsRegister.test.tsx +++ b/packages/ui/src/containers/PhoneForm/SmsRegister.test.tsx @@ -1,8 +1,9 @@ +import { InteractionEvent } from '@logto/schemas'; import { fireEvent, waitFor, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import { sendRegisterSmsPasscode } from '@/apis/register'; +import { putInteraction, sendPasscode } from '@/apis/interaction'; import { getDefaultCountryCallingCode } from '@/utils/country-code'; import SmsRegister from './SmsRegister'; @@ -14,8 +15,9 @@ jest.mock('i18next', () => ({ language: 'en', })); -jest.mock('@/apis/register', () => ({ - sendRegisterSmsPasscode: jest.fn(() => ({ success: true })), +jest.mock('@/apis/interaction', () => ({ + sendPasscode: jest.fn(() => ({ success: true })), + putInteraction: jest.fn(() => ({ success: true })), })); jest.mock('react-router-dom', () => ({ @@ -47,7 +49,8 @@ describe('SmsRegister', () => { }); await waitFor(() => { - expect(sendRegisterSmsPasscode).toBeCalledWith(fullPhoneNumber); + expect(putInteraction).toBeCalledWith(InteractionEvent.Register); + expect(sendPasscode).toBeCalledWith({ phone: fullPhoneNumber }); expect(mockedNavigate).toBeCalledWith( { pathname: '/register/sms/passcode-validation', search: '' }, { state: { phone: fullPhoneNumber } } diff --git a/packages/ui/src/containers/PhoneForm/SmsResetPassword.test.tsx b/packages/ui/src/containers/PhoneForm/SmsResetPassword.test.tsx index efefd7cbc..773176c9b 100644 --- a/packages/ui/src/containers/PhoneForm/SmsResetPassword.test.tsx +++ b/packages/ui/src/containers/PhoneForm/SmsResetPassword.test.tsx @@ -1,9 +1,9 @@ -import { SignInIdentifier } from '@logto/schemas'; +import { SignInIdentifier, InteractionEvent } from '@logto/schemas'; import { fireEvent, waitFor, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import { sendForgotPasswordSmsPasscode } from '@/apis/forgot-password'; +import { putInteraction, sendPasscode } from '@/apis/interaction'; import { UserFlow } from '@/types'; import { getDefaultCountryCallingCode } from '@/utils/country-code'; @@ -16,8 +16,9 @@ jest.mock('i18next', () => ({ language: 'en', })); -jest.mock('@/apis/forgot-password', () => ({ - sendForgotPasswordSmsPasscode: jest.fn(() => ({ success: true })), +jest.mock('@/apis/interaction', () => ({ + sendPasscode: jest.fn(() => ({ success: true })), + putInteraction: jest.fn(() => ({ success: true })), })); jest.mock('react-router-dom', () => ({ @@ -49,7 +50,8 @@ describe('SmsRegister', () => { }); await waitFor(() => { - expect(sendForgotPasswordSmsPasscode).toBeCalledWith(fullPhoneNumber); + expect(putInteraction).toBeCalledWith(InteractionEvent.ForgotPassword); + expect(sendPasscode).toBeCalledWith({ phone: fullPhoneNumber }); expect(mockedNavigate).toBeCalledWith( { pathname: `/${UserFlow.forgotPassword}/${SignInIdentifier.Sms}/passcode-validation`, diff --git a/packages/ui/src/containers/PhoneForm/SmsSignIn.test.tsx b/packages/ui/src/containers/PhoneForm/SmsSignIn.test.tsx index 5b6d8baf0..2e984db09 100644 --- a/packages/ui/src/containers/PhoneForm/SmsSignIn.test.tsx +++ b/packages/ui/src/containers/PhoneForm/SmsSignIn.test.tsx @@ -1,9 +1,9 @@ -import { SignInIdentifier } from '@logto/schemas'; +import { SignInIdentifier, InteractionEvent } from '@logto/schemas'; import { fireEvent, waitFor, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import { sendSignInSmsPasscode } from '@/apis/sign-in'; +import { sendPasscode, putInteraction } from '@/apis/interaction'; import { getDefaultCountryCallingCode } from '@/utils/country-code'; import SmsSignIn from './SmsSignIn'; @@ -15,8 +15,9 @@ jest.mock('i18next', () => ({ language: 'en', })); -jest.mock('@/apis/sign-in', () => ({ - sendSignInSmsPasscode: jest.fn(() => ({ success: true })), +jest.mock('@/apis/interaction', () => ({ + sendPasscode: jest.fn(() => ({ success: true })), + putInteraction: jest.fn(() => ({ success: true })), })); jest.mock('react-router-dom', () => ({ @@ -59,7 +60,8 @@ describe('SmsSignIn', () => { }); await waitFor(() => { - expect(sendSignInSmsPasscode).not.toBeCalled(); + expect(putInteraction).not.toBeCalled(); + expect(sendPasscode).not.toBeCalled(); expect(mockedNavigate).toBeCalledWith( { pathname: '/sign-in/sms/password', search: '' }, { state: { phone: fullPhoneNumber } } @@ -93,7 +95,8 @@ describe('SmsSignIn', () => { }); await waitFor(() => { - expect(sendSignInSmsPasscode).not.toBeCalled(); + expect(putInteraction).not.toBeCalled(); + expect(sendPasscode).not.toBeCalled(); expect(mockedNavigate).toBeCalledWith( { pathname: '/sign-in/sms/password', search: '' }, { state: { phone: fullPhoneNumber } } @@ -128,7 +131,8 @@ describe('SmsSignIn', () => { }); await waitFor(() => { - expect(sendSignInSmsPasscode).toBeCalledWith(fullPhoneNumber); + expect(putInteraction).toBeCalledWith(InteractionEvent.SignIn); + expect(sendPasscode).toBeCalledWith({ phone: fullPhoneNumber }); expect(mockedNavigate).toBeCalledWith( { pathname: '/sign-in/sms/passcode-validation', search: '' }, { state: { phone: fullPhoneNumber } } @@ -163,7 +167,8 @@ describe('SmsSignIn', () => { }); await waitFor(() => { - expect(sendSignInSmsPasscode).toBeCalledWith(fullPhoneNumber); + expect(putInteraction).toBeCalledWith(InteractionEvent.SignIn); + expect(sendPasscode).toBeCalledWith({ phone: fullPhoneNumber }); expect(mockedNavigate).toBeCalledWith( { pathname: '/sign-in/sms/passcode-validation', search: '' }, { state: { phone: fullPhoneNumber } } diff --git a/packages/ui/src/hooks/use-continue-sign-in-with-password.ts b/packages/ui/src/hooks/use-continue-sign-in-with-password.ts index 87c869155..fa84f19c6 100644 --- a/packages/ui/src/hooks/use-continue-sign-in-with-password.ts +++ b/packages/ui/src/hooks/use-continue-sign-in-with-password.ts @@ -1,18 +1,22 @@ -import { SignInIdentifier } from '@logto/schemas'; +import type { SignInIdentifier } from '@logto/schemas'; import { useNavigate } from 'react-router-dom'; import { UserFlow } from '@/types'; -const useContinueSignInWithPassword = (method: SignInIdentifier.Email | SignInIdentifier.Sms) => { +const useContinueSignInWithPassword = ( + method: T +) => { const navigate = useNavigate(); - return (value: string) => { + type Payload = T extends SignInIdentifier.Email ? { email: string } : { phone: string }; + + return (payload: Payload) => { navigate( { pathname: `/${UserFlow.signIn}/${method}/password`, search: location.search, }, - { state: method === SignInIdentifier.Email ? { email: value } : { phone: value } } + { state: payload } ); }; }; diff --git a/packages/ui/src/hooks/use-passwordless-send-code.ts b/packages/ui/src/hooks/use-passwordless-send-code.ts index 085fdd5fa..64840c90e 100644 --- a/packages/ui/src/hooks/use-passwordless-send-code.ts +++ b/packages/ui/src/hooks/use-passwordless-send-code.ts @@ -7,9 +7,9 @@ import type { ErrorHandlers } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; import type { UserFlow } from '@/types'; -const usePasswordlessSendCode = ( +const usePasswordlessSendCode = ( flow: UserFlow, - method: SignInIdentifier.Email | SignInIdentifier.Sms, + method: T, replaceCurrentPage?: boolean ) => { const [errorMessage, setErrorMessage] = useState(); @@ -28,13 +28,15 @@ const usePasswordlessSendCode = ( setErrorMessage(''); }, []); - const api = getSendPasscodeApi(flow, method); + const api = getSendPasscodeApi(flow); const { run: asyncSendPasscode } = useApi(api, errorHandlers); + type Payload = T extends SignInIdentifier.Email ? { email: string } : { phone: string }; + const onSubmit = useCallback( - async (value: string) => { - const result = await asyncSendPasscode(value); + async (payload: Payload) => { + const result = await asyncSendPasscode(payload); if (!result) { return; @@ -46,7 +48,7 @@ const usePasswordlessSendCode = ( search: location.search, }, { - state: method === SignInIdentifier.Email ? { email: value } : { phone: value }, + state: payload, replace: replaceCurrentPage, } ); diff --git a/packages/ui/src/pages/PasswordRegisterWithUsername/use-username-password-register.ts b/packages/ui/src/pages/PasswordRegisterWithUsername/use-username-password-register.ts index 51b7d5fce..edd59e9ca 100644 --- a/packages/ui/src/pages/PasswordRegisterWithUsername/use-username-password-register.ts +++ b/packages/ui/src/pages/PasswordRegisterWithUsername/use-username-password-register.ts @@ -27,7 +27,7 @@ const useUsernamePasswordRegister = () => { const { result, run: asyncSetPassword } = useApi(setUserPassword, resetPasswordErrorHandlers); useEffect(() => { - if (result?.redirectTo) { + if (result && 'redirectTo' in result) { window.location.replace(result.redirectTo); } }, [result, setToast, t]); diff --git a/packages/ui/src/pages/ResetPassword/index.test.tsx b/packages/ui/src/pages/ResetPassword/index.test.tsx index ef4d81275..7ec23a704 100644 --- a/packages/ui/src/pages/ResetPassword/index.test.tsx +++ b/packages/ui/src/pages/ResetPassword/index.test.tsx @@ -2,7 +2,7 @@ import { act, waitFor, fireEvent } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import { resetPassword } from '@/apis/forgot-password'; +import { setUserPassword } from '@/apis/interaction'; import ResetPassword from '.'; @@ -13,8 +13,8 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockedNavigate, })); -jest.mock('@/apis/forgot-password', () => ({ - resetPassword: jest.fn(async () => ({ redirectTo: '/' })), +jest.mock('@/apis/interaction', () => ({ + setUserPassword: jest.fn(async () => ({ redirectTo: '/' })), })); describe('ForgotPassword', () => { @@ -51,7 +51,7 @@ describe('ForgotPassword', () => { }); await waitFor(() => { - expect(resetPassword).toBeCalledWith('123456'); + expect(setUserPassword).toBeCalledWith('123456'); }); }); }); diff --git a/packages/ui/src/pages/ResetPassword/use-reset-password.ts b/packages/ui/src/pages/ResetPassword/use-reset-password.ts index f09246805..cf33e267d 100644 --- a/packages/ui/src/pages/ResetPassword/use-reset-password.ts +++ b/packages/ui/src/pages/ResetPassword/use-reset-password.ts @@ -2,7 +2,7 @@ import { useMemo, useState, useContext, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { resetPassword } from '@/apis/forgot-password'; +import { setUserPassword } from '@/apis/interaction'; import type { ErrorHandlers } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; import { useConfirmModal } from '@/hooks/use-confirm-modal'; @@ -24,11 +24,7 @@ const useResetPassword = () => { () => ({ 'session.verification_session_not_found': async (error) => { await show({ type: 'alert', ModalContent: error.message, cancelText: 'action.got_it' }); - navigate(-1); - }, - 'session.verification_expired': async (error) => { - await show({ type: 'alert', ModalContent: error.message, cancelText: 'action.got_it' }); - navigate(-1); + navigate(-2); }, 'user.same_password': (error) => { setErrorMessage(error.message); @@ -37,7 +33,7 @@ const useResetPassword = () => { [navigate, setErrorMessage, show] ); - const { result, run: asyncResetPassword } = useApi(resetPassword, resetPasswordErrorHandlers); + const { result, run: asyncResetPassword } = useApi(setUserPassword, resetPasswordErrorHandlers); useEffect(() => { if (result) { diff --git a/packages/ui/src/pages/SecondaryRegister/index.test.tsx b/packages/ui/src/pages/SecondaryRegister/index.test.tsx index cffc044ac..0ffe85502 100644 --- a/packages/ui/src/pages/SecondaryRegister/index.test.tsx +++ b/packages/ui/src/pages/SecondaryRegister/index.test.tsx @@ -6,7 +6,6 @@ import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider import { mockSignInExperienceSettings } from '@/__mocks__/logto'; import SecondaryRegister from '@/pages/SecondaryRegister'; -jest.mock('@/apis/register', () => ({ register: jest.fn(async () => 0) })); jest.mock('i18next', () => ({ language: 'en', })); diff --git a/packages/ui/src/pages/SecondarySignIn/index.test.tsx b/packages/ui/src/pages/SecondarySignIn/index.test.tsx index 51be21535..44f0908de 100644 --- a/packages/ui/src/pages/SecondarySignIn/index.test.tsx +++ b/packages/ui/src/pages/SecondarySignIn/index.test.tsx @@ -6,7 +6,6 @@ import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider import { mockSignInExperienceSettings } from '@/__mocks__/logto'; import SecondarySignIn from '@/pages/SecondarySignIn'; -jest.mock('@/apis/register', () => ({ register: jest.fn(async () => 0) })); jest.mock('i18next', () => ({ language: 'en', }));