0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-17 22:31:28 -05:00

refactor(core): remove the event from sendPasscode API payload (#2742)

This commit is contained in:
simeng-li 2022-12-28 11:53:13 +08:00 committed by GitHub
parent 1f7efd3680
commit d641e201c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 418 additions and 860 deletions

View file

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

View file

@ -70,11 +70,11 @@ export default function interactionRoutes<T extends AnonymousRouter>(
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<T extends AnonymousRouter>(
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<T extends AnonymousRouter>(
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<T extends AnonymousRouter>(
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<T extends AnonymousRouter>(
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;

View file

@ -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),
}),
]);

View file

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

View file

@ -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']
) => {

View file

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

View file

@ -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;

View file

@ -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,

View file

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

View file

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

View file

@ -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();

View file

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

View file

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

View file

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

View file

@ -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<Response>();
return { success: true };
};

View file

@ -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',
},
});
});
});

View file

@ -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<Response | undefined>();
// 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<Response>();
};
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<Response>();
};
export const verifyForgotPasswordPasscodeIdentifier = async (
payload: EmailPasscodePayload | PhonePasscodePayload
) => {
await api.patch(`${interactionPrefix}/identifiers`, {
json: payload,
});
return api.post(`${interactionPrefix}/submit`).json<Response>();
};
export const signInWithVerifierIdentifier = async () => {
await api.delete(`${interactionPrefix}/profile`);
await api.put(`${interactionPrefix}/event`, {
json: {
event: InteractionEvent.SignIn,
},
});
return api.post(`${interactionPrefix}/submit`).json<Response>();
};
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<Response>();
};

View file

@ -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<Response>();
export const registerWithEmail = async () =>
api.post(`${apiPrefix}/register/passwordless/email`).json<Response>();
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<Response>();
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<Response>();

View file

@ -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<Response>();
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<Response>();
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<Response>();
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<Response>();
if (result.redirectTo && socialToBind) {
await bindSocialAccount(socialToBind);
}
return result;
};

View file

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

View file

@ -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 } }

View file

@ -138,7 +138,7 @@ describe('<EmailForm/>', () => {
});
await waitFor(() => {
expect(onSubmit).toBeCalledWith('foo@logto.io');
expect(onSubmit).toBeCalledWith({ email: 'foo@logto.io' });
});
});
@ -166,7 +166,7 @@ describe('<EmailForm/>', () => {
});
await waitFor(() => {
expect(onSubmit).toBeCalledWith('foo@logto.io');
expect(onSubmit).toBeCalledWith({ email: 'foo@logto.io' });
});
});
});

View file

@ -23,7 +23,7 @@ type Props = {
errorMessage?: string;
submitButtonText?: TFuncKey;
clearErrorMessage?: () => void;
onSubmit: (email: string) => Promise<void> | void;
onSubmit: (payload: { email: string }) => Promise<void> | 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);

View file

@ -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 } }

View file

@ -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`,

View file

@ -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(
<MemoryRouter>
<EmailSignIn
@ -85,7 +87,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 } }
@ -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(
<MemoryRouter>
<EmailSignIn
@ -120,7 +123,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 } }
@ -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 } }

View file

@ -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('<PasscodeValidation />', () => {
@ -103,12 +82,12 @@ describe('<PasscodeValidation />', () => {
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('<PasscodeValidation />', () => {
}
await waitFor(() => {
expect(verifySignInEmailPasscode).toBeCalledWith(email, '111111', undefined);
expect(signInWithPasscodeIdentifier).toBeCalledWith(
{ email, passcode: '111111' },
undefined
);
});
await waitFor(() => {
@ -133,7 +115,7 @@ describe('<PasscodeValidation />', () => {
});
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('<PasscodeValidation />', () => {
}
await waitFor(() => {
expect(verifySignInSmsPasscode).toBeCalledWith(phone, '111111', undefined);
expect(signInWithPasscodeIdentifier).toBeCalledWith(
{
phone,
passcode: '111111',
},
undefined
);
});
await waitFor(() => {
@ -160,7 +148,7 @@ describe('<PasscodeValidation />', () => {
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('<PasscodeValidation />', () => {
}
await waitFor(() => {
expect(verifyRegisterEmailPasscode).toBeCalledWith(email, '111111');
expect(addProfileWithPasscodeIdentifier).toBeCalledWith({
email,
passcode: '111111',
});
});
await waitFor(() => {
@ -189,7 +180,7 @@ describe('<PasscodeValidation />', () => {
});
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('<PasscodeValidation />', () => {
}
await waitFor(() => {
expect(verifyRegisterSmsPasscode).toBeCalledWith(phone, '111111');
expect(addProfileWithPasscodeIdentifier).toBeCalledWith({ phone, passcode: '111111' });
});
await waitFor(() => {
@ -216,7 +207,7 @@ describe('<PasscodeValidation />', () => {
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('<PasscodeValidation />', () => {
}
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('<PasscodeValidation />', () => {
}
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(
<PasscodeValidation
@ -302,20 +292,24 @@ describe('<PasscodeValidation />', () => {
}
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(
<PasscodeValidation type={UserFlow.continue} method={SignInIdentifier.Sms} target={phone} />
@ -330,11 +324,16 @@ describe('<PasscodeValidation />', () => {
}
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');
});
});

View file

@ -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 {

View file

@ -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 {

View file

@ -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]);

View file

@ -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]);

View file

@ -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]
);

View file

@ -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]
);

View file

@ -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,

View file

@ -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 {

View file

@ -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 {

View file

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

View file

@ -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(

View file

@ -145,7 +145,7 @@ describe('<PhonePasswordless/>', () => {
});
await waitFor(() => {
expect(onSubmit).toBeCalledWith(`${defaultCountryCallingCode}${phoneNumber}`);
expect(onSubmit).toBeCalledWith({ phone: `${defaultCountryCallingCode}${phoneNumber}` });
});
});
@ -173,7 +173,7 @@ describe('<PhonePasswordless/>', () => {
});
await waitFor(() => {
expect(onSubmit).toBeCalledWith(`${defaultCountryCallingCode}${phoneNumber}`);
expect(onSubmit).toBeCalledWith({ phone: `${defaultCountryCallingCode}${phoneNumber}` });
});
});
});

View file

@ -23,7 +23,7 @@ type Props = {
errorMessage?: string;
submitButtonText?: TFuncKey;
clearErrorMessage?: () => void;
onSubmit: (phone: string) => Promise<void> | void;
onSubmit: (payload: { phone: string }) => Promise<void> | 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 (

View file

@ -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 } }

View file

@ -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 } }

View file

@ -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`,

View file

@ -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 } }

View file

@ -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 = <T extends SignInIdentifier.Email | SignInIdentifier.Sms>(
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 }
);
};
};

View file

@ -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 = <T extends SignInIdentifier.Email | SignInIdentifier.Sms>(
flow: UserFlow,
method: SignInIdentifier.Email | SignInIdentifier.Sms,
method: T,
replaceCurrentPage?: boolean
) => {
const [errorMessage, setErrorMessage] = useState<string>();
@ -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,
}
);

View file

@ -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]);

View file

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

View file

@ -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) {

View file

@ -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',
}));

View file

@ -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',
}));