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

refactor(core,ui,schemas,test)!: replace passcode with verification code (#2833)

This commit is contained in:
simeng-li 2023-01-09 09:55:34 +08:00 committed by GitHub
parent 364e51d755
commit 94ed1852b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 586 additions and 529 deletions

View file

@ -84,10 +84,10 @@ const { storeInteractionResult, mergeIdentifiers, getInteractionStorage } = awai
})
);
const { sendPasscodeToIdentifier } = await mockEsmWithActual(
'./utils/passcode-validation.js',
const { sendVerificationCodeToIdentifier } = await mockEsmWithActual(
'./utils/verification-code-validation.js',
() => ({
sendPasscodeToIdentifier: jest.fn(),
sendVerificationCodeToIdentifier: jest.fn(),
})
);
@ -208,7 +208,7 @@ describe('session -> interactionRoutes', () => {
it('should update identifiers properly', async () => {
const body = {
email: 'email@logto.io',
passcode: 'passcode',
verificationCode: 'verificationCode',
};
const response = await sessionRequest.patch(path).send(body);
expect(getInteractionStorage).toBeCalled();
@ -257,17 +257,17 @@ describe('session -> interactionRoutes', () => {
});
});
describe('POST /interaction/verification/passcode', () => {
const path = `${interactionPrefix}/${verificationPath}/passcode`;
describe('POST /interaction/verification/verification-code', () => {
const path = `${interactionPrefix}/${verificationPath}/verification-code`;
it('should call send passcode properly', async () => {
it('should call send verificationCode properly', async () => {
const body = {
email: 'email@logto.io',
};
const response = await sessionRequest.post(path).send(body);
expect(getInteractionStorage).toBeCalled();
expect(sendPasscodeToIdentifier).toBeCalledWith(
expect(sendVerificationCodeToIdentifier).toBeCalledWith(
{
event: InteractionEvent.SignIn,
...body,

View file

@ -16,19 +16,22 @@ import koaInteractionDetails from './middleware/koa-interaction-details.js';
import type { WithInteractionDetailsContext } from './middleware/koa-interaction-details.js';
import koaInteractionHooks from './middleware/koa-interaction-hooks.js';
import koaInteractionSie from './middleware/koa-interaction-sie.js';
import { sendPasscodePayloadGuard, socialAuthorizationUrlPayloadGuard } from './types/guard.js';
import {
sendVerificationCodePayloadGuard,
socialAuthorizationUrlPayloadGuard,
} from './types/guard.js';
import {
getInteractionStorage,
storeInteractionResult,
mergeIdentifiers,
} from './utils/interaction.js';
import { sendPasscodeToIdentifier } from './utils/passcode-validation.js';
import {
verifySignInModeSettings,
verifyIdentifierSettings,
verifyProfileSettings,
} from './utils/sign-in-experience-validation.js';
import { createSocialAuthorizationUrl } from './utils/social-verification.js';
import { sendVerificationCodeToIdentifier } from './utils/verification-code-validation.js';
import {
verifyIdentifierPayload,
verifyIdentifier,
@ -327,18 +330,22 @@ export default function interactionRoutes<T extends AnonymousRouter>(
}
);
// Create passwordless interaction passcode
// Create passwordless interaction verification-code
router.post(
`${interactionPrefix}/${verificationPath}/passcode`,
`${interactionPrefix}/${verificationPath}/verification-code`,
koaGuard({
body: sendPasscodePayloadGuard,
body: sendVerificationCodePayloadGuard,
}),
async (ctx, next) => {
const { interactionDetails, guard, createLog } = ctx;
// Check interaction exists
const { event } = getInteractionStorage(interactionDetails.result);
await sendPasscodeToIdentifier({ event, ...guard.body }, interactionDetails.jti, createLog);
await sendVerificationCodeToIdentifier(
{ event, ...guard.body },
interactionDetails.jti,
createLog
);
ctx.status = 204;

View file

@ -3,8 +3,8 @@ import { emailRegEx, phoneRegEx, validateRedirectUrl } from '@logto/core-kit';
import { eventGuard, profileGuard, InteractionEvent } from '@logto/schemas';
import { z } from 'zod';
// Passcode Send Route Payload Guard
export const sendPasscodePayloadGuard = z.union([
// Verification Send Route Payload Guard
export const sendVerificationCodePayloadGuard = z.union([
z.object({
email: z.string().regex(emailRegEx),
}),

View file

@ -2,15 +2,15 @@ import type { SocialUserInfo } from '@logto/connector-kit';
import type {
UsernamePasswordPayload,
EmailPasswordPayload,
EmailPasscodePayload,
EmailVerificationCodePayload,
PhonePasswordPayload,
PhonePasscodePayload,
PhoneVerificationCodePayload,
InteractionEvent,
} from '@logto/schemas';
import type { z } from 'zod';
import type {
sendPasscodePayloadGuard,
sendVerificationCodePayloadGuard,
socialAuthorizationUrlPayloadGuard,
accountIdIdentifierGuard,
verifiedEmailIdentifierGuard,
@ -30,9 +30,11 @@ export type PasswordIdentifierPayload =
| EmailPasswordPayload
| PhonePasswordPayload;
export type PasscodeIdentifierPayload = EmailPasscodePayload | PhonePasscodePayload;
export type VerificationCodeIdentifierPayload =
| EmailVerificationCodePayload
| PhoneVerificationCodePayload;
export type SendPasscodePayload = z.infer<typeof sendPasscodePayloadGuard>;
export type SendVerificationCodePayload = z.infer<typeof sendVerificationCodePayloadGuard>;
export type SocialAuthorizationUrlPayload = z.infer<typeof socialAuthorizationUrlPayloadGuard>;

View file

@ -1,14 +1,17 @@
import type { SocialConnectorPayload, User, IdentifierPayload } from '@logto/schemas';
import type { PasscodeIdentifierPayload, PasswordIdentifierPayload } from '../types/index.js';
import type {
VerificationCodeIdentifierPayload,
PasswordIdentifierPayload,
} from '../types/index.js';
export const isPasswordIdentifier = (
identifier: IdentifierPayload
): identifier is PasswordIdentifierPayload => 'password' in identifier;
export const isPasscodeIdentifier = (
export const isVerificationCodeIdentifier = (
identifier: IdentifierPayload
): identifier is PasscodeIdentifierPayload => 'passcode' in identifier;
): identifier is VerificationCodeIdentifierPayload => 'verificationCode' in identifier;
export const isSocialIdentifier = (
identifier: IdentifierPayload

View file

@ -120,8 +120,8 @@ describe('identifier validation', () => {
}).toThrow();
});
it('email passcode', () => {
const identifier = { email: 'email', passcode: 'passcode' };
it('email verificationCode', () => {
const identifier = { email: 'email', verificationCode: 'verificationCode' };
expect(() => {
verifyIdentifierSettings(identifier, mockSignInExperience);
@ -211,8 +211,8 @@ describe('identifier validation', () => {
}).toThrow();
});
it('phone passcode', () => {
const identifier = { phone: '123456', passcode: 'passcode' };
it('phone verificationCode', () => {
const identifier = { phone: '123456', verificationCode: 'verificationCode' };
expect(() => {
verifyIdentifierSettings(identifier, mockSignInExperience);

View file

@ -55,9 +55,9 @@ export const verifyIdentifierSettings = (
return false;
}
// Email Passcode Verification: SignIn verificationCode enabled or SignUp Email verify enabled
// Email verificationCode Verification: SignIn verificationCode enabled or SignUp Email verify enabled
if (
'passcode' in identifier &&
'verificationCode' in identifier &&
!verificationCode &&
!signUp.identifiers.includes(SignInIdentifier.Email) &&
!signUp.verify
@ -86,9 +86,9 @@ export const verifyIdentifierSettings = (
return false;
}
// Phone Passcode Verification: SignIn verificationCode enabled or SignUp Email verify enabled
// Phone verificationCode Verification: SignIn verificationCode enabled or SignUp Email verify enabled
if (
'passcode' in identifier &&
'verificationCode' in identifier &&
!verificationCode &&
!signUp.identifiers.includes(SignInIdentifier.Phone) &&
!signUp.verify

View file

@ -14,47 +14,47 @@ const passcode = {
await mockEsmWithActual('#src/libraries/passcode.js', () => passcode);
const { sendPasscodeToIdentifier } = await import('./passcode-validation.js');
const { sendVerificationCodeToIdentifier } = await import('./verification-code-validation.js');
const sendPasscodeTestCase = [
const sendVerificationCodeTestCase = [
{
payload: { email: 'email', event: InteractionEvent.SignIn },
createPasscodeParams: [VerificationCodeType.SignIn, { email: 'email' }],
createVerificationCodeParams: [VerificationCodeType.SignIn, { email: 'email' }],
},
{
payload: { email: 'email', event: InteractionEvent.Register },
createPasscodeParams: [VerificationCodeType.Register, { email: 'email' }],
createVerificationCodeParams: [VerificationCodeType.Register, { email: 'email' }],
},
{
payload: { email: 'email', event: InteractionEvent.ForgotPassword },
createPasscodeParams: [VerificationCodeType.ForgotPassword, { email: 'email' }],
createVerificationCodeParams: [VerificationCodeType.ForgotPassword, { email: 'email' }],
},
{
payload: { phone: 'phone', event: InteractionEvent.SignIn },
createPasscodeParams: [VerificationCodeType.SignIn, { phone: 'phone' }],
createVerificationCodeParams: [VerificationCodeType.SignIn, { phone: 'phone' }],
},
{
payload: { phone: 'phone', event: InteractionEvent.Register },
createPasscodeParams: [VerificationCodeType.Register, { phone: 'phone' }],
createVerificationCodeParams: [VerificationCodeType.Register, { phone: 'phone' }],
},
{
payload: { phone: 'phone', event: InteractionEvent.ForgotPassword },
createPasscodeParams: [VerificationCodeType.ForgotPassword, { phone: 'phone' }],
createVerificationCodeParams: [VerificationCodeType.ForgotPassword, { phone: 'phone' }],
},
];
describe('passcode-validation utils', () => {
describe('verification-code-validation utils', () => {
const log = createMockLogContext();
afterEach(() => {
jest.clearAllMocks();
});
it.each(sendPasscodeTestCase)(
'send passcode successfully',
async ({ payload, createPasscodeParams }) => {
await sendPasscodeToIdentifier(payload, 'jti', log.createLog);
expect(passcode.createPasscode).toBeCalledWith('jti', ...createPasscodeParams);
it.each(sendVerificationCodeTestCase)(
'send verification code successfully',
async ({ payload, createVerificationCodeParams }) => {
await sendVerificationCodeToIdentifier(payload, 'jti', log.createLog);
expect(passcode.createPasscode).toBeCalledWith('jti', ...createVerificationCodeParams);
expect(passcode.sendPasscode).toBeCalled();
}
);

View file

@ -4,7 +4,10 @@ import type { InteractionEvent } from '@logto/schemas';
import { createPasscode, sendPasscode, verifyPasscode } from '#src/libraries/passcode.js';
import type { LogContext } from '#src/middleware/koa-audit-log.js';
import type { SendPasscodePayload, PasscodeIdentifierPayload } from '../types/index.js';
import type {
SendVerificationCodePayload,
VerificationCodeIdentifierPayload,
} from '../types/index.js';
/**
* Refactor Needed:
@ -19,8 +22,8 @@ const eventToVerificationCodeTypeMap: Record<InteractionEvent, VerificationCodeT
const getVerificationCodeTypeByEvent = (event: InteractionEvent): VerificationCodeType =>
eventToVerificationCodeTypeMap[event];
export const sendPasscodeToIdentifier = async (
payload: SendPasscodePayload & { event: InteractionEvent },
export const sendVerificationCodeToIdentifier = async (
payload: SendVerificationCodePayload & { event: InteractionEvent },
jti: string,
createLog: LogContext['createLog']
) => {
@ -30,22 +33,22 @@ export const sendPasscodeToIdentifier = async (
const log = createLog(`Interaction.${event}.Identifier.VerificationCode.Create`);
log.append(identifier);
const passcode = await createPasscode(jti, messageType, identifier);
const { dbEntry } = await sendPasscode(passcode);
const verificationCode = await createPasscode(jti, messageType, identifier);
const { dbEntry } = await sendPasscode(verificationCode);
log.append({ connectorId: dbEntry.id });
};
export const verifyIdentifierByPasscode = async (
payload: PasscodeIdentifierPayload & { event: InteractionEvent },
export const verifyIdentifierByVerificationCode = async (
payload: VerificationCodeIdentifierPayload & { event: InteractionEvent },
jti: string,
createLog: LogContext['createLog']
) => {
const { event, passcode, ...identifier } = payload;
const { event, verificationCode, ...identifier } = payload;
const messageType = getVerificationCodeTypeByEvent(event);
const log = createLog(`Interaction.${event}.Identifier.VerificationCode.Submit`);
log.append(identifier);
await verifyPasscode(jti, messageType, passcode, identifier);
await verifyPasscode(jti, messageType, verificationCode, identifier);
};

View file

@ -21,9 +21,12 @@ await mockEsmWithActual('../utils/interaction.js', () => ({
storeInteractionResult: jest.fn(),
}));
const { verifyIdentifierByPasscode } = mockEsm('../utils/passcode-validation.js', () => ({
verifyIdentifierByPasscode: jest.fn(),
}));
const { verifyIdentifierByVerificationCode } = mockEsm(
'../utils/verification-code-validation.js',
() => ({
verifyIdentifierByVerificationCode: jest.fn(),
})
);
const { verifySocialIdentity } = mockEsm('../utils/social-verification.js', () => ({
verifySocialIdentity: jest.fn().mockResolvedValue({ id: 'foo' }),
@ -115,8 +118,8 @@ describe('identifier verification', () => {
expect(result).toEqual({ key: 'accountId', value: 'foo' });
});
it('email passcode', async () => {
const identifier = { email: 'email', passcode: 'passcode' };
it('email verificationCode', async () => {
const identifier = { email: 'email', verificationCode: 'verificationCode' };
const result = await identifierPayloadVerification(
baseCtx,
@ -124,7 +127,7 @@ describe('identifier verification', () => {
identifier,
interactionStorage
);
expect(verifyIdentifierByPasscode).toBeCalledWith(
expect(verifyIdentifierByVerificationCode).toBeCalledWith(
{ ...identifier, event: interactionStorage.event },
'jti',
logContext.createLog
@ -133,8 +136,8 @@ describe('identifier verification', () => {
expect(result).toEqual({ key: 'emailVerified', value: identifier.email });
});
it('phone passcode', async () => {
const identifier = { phone: 'phone', passcode: 'passcode' };
it('phone verificationCode', async () => {
const identifier = { phone: 'phone', verificationCode: 'verificationCode' };
const result = await identifierPayloadVerification(
baseCtx,
@ -143,7 +146,7 @@ describe('identifier verification', () => {
interactionStorage
);
expect(verifyIdentifierByPasscode).toBeCalledWith(
expect(verifyIdentifierByVerificationCode).toBeCalledWith(
{ ...identifier, event: interactionStorage.event },
'jti',
logContext.createLog

View file

@ -13,7 +13,7 @@ import assertThat from '#src/utils/assert-that.js';
import type {
PasswordIdentifierPayload,
PasscodeIdentifierPayload,
VerificationCodeIdentifierPayload,
SocialIdentifier,
VerifiedEmailIdentifier,
VerifiedPhoneIdentifier,
@ -22,9 +22,13 @@ import type {
AccountIdIdentifier,
} from '../types/index.js';
import findUserByIdentifier from '../utils/find-user-by-identifier.js';
import { isPasscodeIdentifier, isPasswordIdentifier, isSocialIdentifier } from '../utils/index.js';
import { verifyIdentifierByPasscode } from '../utils/passcode-validation.js';
import {
isVerificationCodeIdentifier,
isPasswordIdentifier,
isSocialIdentifier,
} from '../utils/index.js';
import { verifySocialIdentity } from '../utils/social-verification.js';
import { verifyIdentifierByVerificationCode } from '../utils/verification-code-validation.js';
const verifyPasswordIdentifier = async (
event: InteractionEvent,
@ -46,15 +50,15 @@ const verifyPasswordIdentifier = async (
return { key: 'accountId', value: id };
};
const verifyPasscodeIdentifier = async (
const verifyVerificationCodeIdentifier = async (
event: InteractionEvent,
identifier: PasscodeIdentifierPayload,
identifier: VerificationCodeIdentifierPayload,
ctx: WithLogContext,
provider: Provider
): Promise<VerifiedEmailIdentifier | VerifiedPhoneIdentifier> => {
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
await verifyIdentifierByPasscode({ ...identifier, event }, jti, ctx.createLog);
await verifyIdentifierByVerificationCode({ ...identifier, event }, jti, ctx.createLog);
return 'email' in identifier
? { key: 'emailVerified', value: identifier.email }
@ -107,8 +111,8 @@ export default async function identifierPayloadVerification(
return verifyPasswordIdentifier(event, identifierPayload, ctx);
}
if (isPasscodeIdentifier(identifierPayload)) {
return verifyPasscodeIdentifier(event, identifierPayload, ctx, provider);
if (isVerificationCodeIdentifier(identifierPayload)) {
return verifyVerificationCodeIdentifier(event, identifierPayload, ctx, provider);
}
if (isSocialIdentifier(identifierPayload)) {

View file

@ -66,17 +66,14 @@ export const submitInteraction = async (cookie: string) =>
.post('interaction/submit', { headers: { cookie }, followRedirect: false })
.json<RedirectResponse>();
export type VerificationPasscodePayload =
export type SendVerificationCodePayload =
| {
email: string;
}
| { phone: string };
export const sendVerificationPasscode = async (
cookie: string,
payload: VerificationPasscodePayload
) =>
api.post('interaction/verification/passcode', {
export const sendVerificationCode = async (cookie: string, payload: SendVerificationCodePayload) =>
api.post('interaction/verification/verification-code', {
headers: { cookie },
json: payload,
followRedirect: false,

View file

@ -2,7 +2,7 @@ import { InteractionEvent, ConnectorType, SignInIdentifier } from '@logto/schema
import {
putInteraction,
sendVerificationPasscode,
sendVerificationCode,
deleteUser,
patchInteractionIdentifiers,
putInteractionProfile,
@ -13,7 +13,7 @@ import { generatePassword } from '#src/utils.js';
import { initClient, processSession, logoutClient } from './utils/client.js';
import { clearConnectorsByTypes, setEmailConnector, setSmsConnector } from './utils/connector.js';
import { enableAllPasscodeSignInMethods } from './utils/sign-in-experience.js';
import { enableAllVerificationCodeSignInMethods } from './utils/sign-in-experience.js';
import { generateNewUser } from './utils/user.js';
describe('reset password', () => {
@ -21,7 +21,7 @@ describe('reset password', () => {
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
await setEmailConnector();
await setSmsConnector();
await enableAllPasscodeSignInMethods({
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone],
password: true,
verify: true,
@ -41,22 +41,22 @@ describe('reset password', () => {
const client = await initClient();
await client.successSend(putInteraction, { event: InteractionEvent.ForgotPassword });
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
email: userProfile.primaryEmail,
});
const passcodeRecord = await readPasscode();
const verificationCodeRecord = await readPasscode();
expect(passcodeRecord).toMatchObject({
expect(verificationCodeRecord).toMatchObject({
address: userProfile.primaryEmail,
type: InteractionEvent.ForgotPassword,
});
const { code } = passcodeRecord;
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
email: userProfile.primaryEmail,
passcode: code,
verificationCode: code,
});
await expectRejects(client.submitInteraction(), 'user.new_password_required_in_profile');
@ -65,9 +65,9 @@ describe('reset password', () => {
await expectRejects(client.submitInteraction(), 'user.same_password');
const newPasscodeRecord = generatePassword();
const newPasswordRecord = generatePassword();
await client.successSend(patchInteractionProfile, { password: newPasscodeRecord });
await client.successSend(patchInteractionProfile, { password: newPasswordRecord });
await client.submitInteraction();
@ -75,7 +75,7 @@ describe('reset password', () => {
event: InteractionEvent.SignIn,
identifier: {
email: userProfile.primaryEmail,
password: newPasscodeRecord,
password: newPasswordRecord,
},
});
@ -94,22 +94,22 @@ describe('reset password', () => {
const client = await initClient();
await client.successSend(putInteraction, { event: InteractionEvent.ForgotPassword });
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
phone: userProfile.primaryPhone,
});
const passcodeRecord = await readPasscode();
const verificationCodeRecord = await readPasscode();
expect(passcodeRecord).toMatchObject({
expect(verificationCodeRecord).toMatchObject({
phone: userProfile.primaryPhone,
type: InteractionEvent.ForgotPassword,
});
const { code } = passcodeRecord;
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
phone: userProfile.primaryPhone,
passcode: code,
verificationCode: code,
});
await expectRejects(client.submitInteraction(), 'user.new_password_required_in_profile');
@ -118,9 +118,9 @@ describe('reset password', () => {
await expectRejects(client.submitInteraction(), 'user.same_password');
const newPasscodeRecord = generatePassword();
const newPasswordRecord = generatePassword();
await client.successSend(patchInteractionProfile, { password: newPasscodeRecord });
await client.successSend(patchInteractionProfile, { password: newPasswordRecord });
await client.submitInteraction();
@ -128,7 +128,7 @@ describe('reset password', () => {
event: InteractionEvent.SignIn,
identifier: {
phone: userProfile.primaryPhone,
password: newPasscodeRecord,
password: newPasswordRecord,
},
});

View file

@ -2,7 +2,7 @@ import { ConnectorType, InteractionEvent, SignInIdentifier } from '@logto/schema
import { assert } from '@silverhand/essentials';
import {
sendVerificationPasscode,
sendVerificationCode,
putInteraction,
deleteUser,
patchInteractionIdentifiers,
@ -16,7 +16,7 @@ import { readPasscode, expectRejects } from '#src/helpers.js';
import { initClient, processSession, logoutClient } from './utils/client.js';
import { clearConnectorsByTypes, setEmailConnector, setSmsConnector } from './utils/connector.js';
import {
enableAllPasscodeSignInMethods,
enableAllVerificationCodeSignInMethods,
enableAllPasswordSignInMethods,
} from './utils/sign-in-experience.js';
import { generateNewUserProfile, generateNewUser } from './utils/user.js';
@ -59,7 +59,7 @@ describe('Register with passwordless identifier', () => {
});
it('register with email', async () => {
await enableAllPasscodeSignInMethods({
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Email],
password: false,
verify: true,
@ -72,22 +72,22 @@ describe('Register with passwordless identifier', () => {
event: InteractionEvent.Register,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
email: primaryEmail,
});
const passcodeRecord = await readPasscode();
const verificationCodeRecord = await readPasscode();
expect(passcodeRecord).toMatchObject({
expect(verificationCodeRecord).toMatchObject({
address: primaryEmail,
type: InteractionEvent.Register,
});
const { code } = passcodeRecord;
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
email: primaryEmail,
passcode: code,
verificationCode: code,
});
await client.successSend(putInteractionProfile, {
@ -102,7 +102,7 @@ describe('Register with passwordless identifier', () => {
});
it('register with email and fulfill password', async () => {
await enableAllPasscodeSignInMethods({
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Email],
password: true,
verify: true,
@ -118,17 +118,17 @@ describe('Register with passwordless identifier', () => {
event: InteractionEvent.Register,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
email: primaryEmail,
});
const passcodeRecord = await readPasscode();
const verificationCodeRecord = await readPasscode();
const { code } = passcodeRecord;
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
email: primaryEmail,
passcode: code,
verificationCode: code,
});
await client.successSend(putInteractionProfile, {
@ -163,7 +163,7 @@ describe('Register with passwordless identifier', () => {
});
it('register with phone', async () => {
await enableAllPasscodeSignInMethods({
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Phone],
password: false,
verify: true,
@ -176,22 +176,22 @@ describe('Register with passwordless identifier', () => {
event: InteractionEvent.Register,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
phone: primaryPhone,
});
const passcodeRecord = await readPasscode();
const verificationCodeRecord = await readPasscode();
expect(passcodeRecord).toMatchObject({
expect(verificationCodeRecord).toMatchObject({
phone: primaryPhone,
type: InteractionEvent.Register,
});
const { code } = passcodeRecord;
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
phone: primaryPhone,
passcode: code,
verificationCode: code,
});
await client.successSend(putInteractionProfile, {
@ -206,7 +206,7 @@ describe('Register with passwordless identifier', () => {
});
it('register with phone and fulfill password', async () => {
await enableAllPasscodeSignInMethods({
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Phone],
password: true,
verify: true,
@ -222,7 +222,7 @@ describe('Register with passwordless identifier', () => {
event: InteractionEvent.Register,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
phone: primaryPhone,
});
@ -230,7 +230,7 @@ describe('Register with passwordless identifier', () => {
await client.successSend(patchInteractionIdentifiers, {
phone: primaryPhone,
passcode: code,
verificationCode: code,
});
await client.successSend(putInteractionProfile, {
@ -271,7 +271,7 @@ describe('Register with passwordless identifier', () => {
userProfile: { primaryEmail },
} = await generateNewUser({ primaryEmail: true });
await enableAllPasscodeSignInMethods({
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Email],
password: false,
verify: true,
@ -283,22 +283,22 @@ describe('Register with passwordless identifier', () => {
event: InteractionEvent.Register,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
email: primaryEmail,
});
const passcodeRecord = await readPasscode();
const verificationCodeRecord = await readPasscode();
expect(passcodeRecord).toMatchObject({
expect(verificationCodeRecord).toMatchObject({
address: primaryEmail,
type: InteractionEvent.Register,
});
const { code } = passcodeRecord;
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
email: primaryEmail,
passcode: code,
verificationCode: code,
});
await client.successSend(putInteractionProfile, {
@ -322,7 +322,7 @@ describe('Register with passwordless identifier', () => {
userProfile: { primaryPhone },
} = await generateNewUser({ primaryPhone: true });
await enableAllPasscodeSignInMethods({
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Phone],
password: false,
verify: true,
@ -335,22 +335,22 @@ describe('Register with passwordless identifier', () => {
event: InteractionEvent.Register,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
phone: primaryPhone,
});
const passcodeRecord = await readPasscode();
const verificationCodeRecord = await readPasscode();
expect(passcodeRecord).toMatchObject({
expect(verificationCodeRecord).toMatchObject({
phone: primaryPhone,
type: InteractionEvent.Register,
});
const { code } = passcodeRecord;
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
phone: primaryPhone,
passcode: code,
verificationCode: code,
});
await client.successSend(putInteractionProfile, {

View file

@ -1,7 +1,7 @@
import { ConnectorType, InteractionEvent, SignInIdentifier } from '@logto/schemas';
import {
sendVerificationPasscode,
sendVerificationCode,
putInteraction,
putInteractionEvent,
putInteractionProfile,
@ -14,21 +14,21 @@ import { generateEmail, generatePhone } from '#src/utils.js';
import { initClient, processSession, logoutClient } from './utils/client.js';
import { clearConnectorsByTypes, setEmailConnector, setSmsConnector } from './utils/connector.js';
import { enableAllPasscodeSignInMethods } from './utils/sign-in-experience.js';
import { enableAllVerificationCodeSignInMethods } from './utils/sign-in-experience.js';
import { generateNewUser, generateNewUserProfile } from './utils/user.js';
describe('Sign-In flow using passcode identifiers', () => {
describe('Sign-In flow using verification-code identifiers', () => {
beforeAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
await setEmailConnector();
await setSmsConnector();
await enableAllPasscodeSignInMethods();
await enableAllVerificationCodeSignInMethods();
});
afterAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
});
it('sign-in with email and passcode', async () => {
it('sign-in with email and verification-code', async () => {
const { userProfile, user } = await generateNewUser({ primaryEmail: true });
const client = await initClient();
@ -36,22 +36,22 @@ describe('Sign-In flow using passcode identifiers', () => {
event: InteractionEvent.SignIn,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
email: userProfile.primaryEmail,
});
const passcodeRecord = await readPasscode();
const verificationCodeRecord = await readPasscode();
expect(passcodeRecord).toMatchObject({
expect(verificationCodeRecord).toMatchObject({
address: userProfile.primaryEmail,
type: InteractionEvent.SignIn,
});
const { code } = passcodeRecord;
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
email: userProfile.primaryEmail,
passcode: code,
verificationCode: code,
});
const { redirectTo } = await client.submitInteraction();
@ -61,7 +61,7 @@ describe('Sign-In flow using passcode identifiers', () => {
await deleteUser(user.id);
});
it('sign-in with phone and passcode', async () => {
it('sign-in with phone and verification-code', async () => {
const { userProfile, user } = await generateNewUser({ primaryPhone: true });
const client = await initClient();
@ -69,22 +69,22 @@ describe('Sign-In flow using passcode identifiers', () => {
event: InteractionEvent.SignIn,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
phone: userProfile.primaryPhone,
});
const passcodeRecord = await readPasscode();
const verificationCodeRecord = await readPasscode();
expect(passcodeRecord).toMatchObject({
expect(verificationCodeRecord).toMatchObject({
phone: userProfile.primaryPhone,
type: InteractionEvent.SignIn,
});
const { code } = passcodeRecord;
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
phone: userProfile.primaryPhone,
passcode: code,
verificationCode: code,
});
const { redirectTo } = await client.submitInteraction();
@ -94,7 +94,7 @@ describe('Sign-In flow using passcode identifiers', () => {
await deleteUser(user.id);
});
it('sign-in with non-exist email account with passcode', async () => {
it('sign-in with non-exist email account with verification-code', async () => {
const newEmail = generateEmail();
// Enable email sign-up
@ -108,17 +108,17 @@ describe('Sign-In flow using passcode identifiers', () => {
event: InteractionEvent.SignIn,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
email: newEmail,
});
const passcodeRecord = await readPasscode();
const verificationCodeRecord = await readPasscode();
const { code } = passcodeRecord;
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
email: newEmail,
passcode: code,
verificationCode: code,
});
await expectRejects(client.submitInteraction(), 'user.user_not_exist');
@ -133,7 +133,7 @@ describe('Sign-In flow using passcode identifiers', () => {
await deleteUser(id);
});
it('sign-in with non-exist phone account with passcode', async () => {
it('sign-in with non-exist phone account with verification-code', async () => {
const newPhone = generatePhone();
// Enable phone sign-up
@ -147,17 +147,17 @@ describe('Sign-In flow using passcode identifiers', () => {
event: InteractionEvent.SignIn,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
phone: newPhone,
});
const passcodeRecord = await readPasscode();
const verificationCodeRecord = await readPasscode();
const { code } = passcodeRecord;
const { code } = verificationCodeRecord;
await client.successSend(patchInteractionIdentifiers, {
phone: newPhone,
passcode: code,
verificationCode: code,
});
await expectRejects(client.submitInteraction(), 'user.user_not_exist');
@ -173,7 +173,7 @@ describe('Sign-In flow using passcode identifiers', () => {
});
// Fulfill the username and password
it('email passcode sign-in', async () => {
it('email verification-code sign-in', async () => {
await updateSignInExperience({
signUp: {
identifiers: [SignInIdentifier.Username],
@ -192,14 +192,14 @@ describe('Sign-In flow using passcode identifiers', () => {
event: InteractionEvent.SignIn,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
email: userProfile.primaryEmail,
});
const { code } = await readPasscode();
await client.successSend(patchInteractionIdentifiers, {
email: userProfile.primaryEmail,
passcode: code,
verificationCode: code,
});
await expectRejects(client.submitInteraction(), 'user.missing_profile');
@ -232,7 +232,7 @@ describe('Sign-In flow using passcode identifiers', () => {
await deleteUser(user.id);
});
it('email passcode sign-in with existing password', async () => {
it('email verification-code sign-in with existing password', async () => {
await updateSignInExperience({
signUp: {
identifiers: [SignInIdentifier.Username],
@ -251,14 +251,14 @@ describe('Sign-In flow using passcode identifiers', () => {
event: InteractionEvent.SignIn,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
email: userProfile.primaryEmail,
});
const { code } = await readPasscode();
await client.successSend(patchInteractionIdentifiers, {
email: userProfile.primaryEmail,
passcode: code,
verificationCode: code,
});
await expectRejects(client.submitInteraction(), 'user.missing_profile');
@ -282,7 +282,7 @@ describe('Sign-In flow using passcode identifiers', () => {
await deleteUser(user.id);
});
it('email passcode sign-in with registered username', async () => {
it('email verification-code sign-in with registered username', async () => {
await updateSignInExperience({
signUp: {
identifiers: [SignInIdentifier.Username],
@ -302,14 +302,14 @@ describe('Sign-In flow using passcode identifiers', () => {
event: InteractionEvent.SignIn,
});
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
email: userProfile.primaryEmail,
});
const { code } = await readPasscode();
await client.successSend(patchInteractionIdentifiers, {
email: userProfile.primaryEmail,
passcode: code,
verificationCode: code,
});
await expectRejects(client.submitInteraction(), 'user.missing_profile');

View file

@ -2,7 +2,7 @@ import { InteractionEvent, ConnectorType, SignInIdentifier } from '@logto/schema
import {
putInteraction,
sendVerificationPasscode,
sendVerificationCode,
patchInteractionIdentifiers,
putInteractionProfile,
deleteUser,
@ -13,7 +13,7 @@ import { initClient, processSession, logoutClient } from './utils/client.js';
import { clearConnectorsByTypes, setSmsConnector, setEmailConnector } from './utils/connector.js';
import {
enableAllPasswordSignInMethods,
enableAllPasscodeSignInMethods,
enableAllVerificationCodeSignInMethods,
} from './utils/sign-in-experience.js';
import { generateNewUser, generateNewUserProfile } from './utils/user.js';
@ -91,7 +91,7 @@ describe('Sign-In flow using password identifiers', () => {
// Fulfill the email address
it('sign-in with username and password and fulfill the email', async () => {
await enableAllPasscodeSignInMethods({
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Email],
password: true,
verify: true,
@ -111,7 +111,7 @@ describe('Sign-In flow using password identifiers', () => {
await expectRejects(client.submitInteraction(), 'user.missing_profile');
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
email: primaryEmail,
});
@ -119,7 +119,7 @@ describe('Sign-In flow using password identifiers', () => {
await client.successSend(patchInteractionIdentifiers, {
email: primaryEmail,
passcode: code,
verificationCode: code,
});
await client.successSend(putInteractionProfile, {
@ -150,7 +150,7 @@ describe('Sign-In flow using password identifiers', () => {
// Fulfill the phone number
it('sign-in with username and password and fulfill the phone number', async () => {
await enableAllPasscodeSignInMethods({
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Phone, SignInIdentifier.Email],
password: true,
verify: true,
@ -170,7 +170,7 @@ describe('Sign-In flow using password identifiers', () => {
await expectRejects(client.submitInteraction(), 'user.missing_profile');
await client.successSend(sendVerificationPasscode, {
await client.successSend(sendVerificationCode, {
phone: primaryPhone,
});
@ -178,7 +178,7 @@ describe('Sign-In flow using password identifiers', () => {
await client.successSend(patchInteractionIdentifiers, {
phone: primaryPhone,
passcode: code,
verificationCode: code,
});
await client.successSend(putInteractionProfile, {

View file

@ -30,7 +30,7 @@ const defaultPasswordSignInMethods = [
},
];
const defaultPasscodeSignInMethods = [
const defaultVerificationCodeSignInMethods = [
{
identifier: SignInIdentifier.Username,
password: true,
@ -62,13 +62,13 @@ export const enableAllPasswordSignInMethods = async (
},
});
export const enableAllPasscodeSignInMethods = async (
export const enableAllVerificationCodeSignInMethods = async (
signUp: SignInExperience['signUp'] = defaultSignUpMethod
) =>
updateSignInExperience({
signInMode: SignInMode.SignInAndRegister,
signUp,
signIn: {
methods: defaultPasscodeSignInMethods,
methods: defaultVerificationCodeSignInMethods,
},
});

View file

@ -25,17 +25,17 @@ export const phonePasswordPayloadGuard = z.object({
});
export type PhonePasswordPayload = z.infer<typeof phonePasswordPayloadGuard>;
export const emailPasscodePayloadGuard = z.object({
export const emailVerificationCodePayloadGuard = z.object({
email: z.string().regex(emailRegEx),
passcode: z.string().min(1),
verificationCode: z.string().min(1),
});
export type EmailPasscodePayload = z.infer<typeof emailPasscodePayloadGuard>;
export type EmailVerificationCodePayload = z.infer<typeof emailVerificationCodePayloadGuard>;
export const phonePasscodePayloadGuard = z.object({
export const phoneVerificationCodePayloadGuard = z.object({
phone: z.string().regex(phoneRegEx),
passcode: z.string().min(1),
verificationCode: z.string().min(1),
});
export type PhonePasscodePayload = z.infer<typeof phonePasscodePayloadGuard>;
export type PhoneVerificationCodePayload = z.infer<typeof phoneVerificationCodePayloadGuard>;
export const socialConnectorPayloadGuard = z.object({
connectorId: z.string(),
@ -64,8 +64,8 @@ export const identifierPayloadGuard = z.union([
usernamePasswordPayloadGuard,
emailPasswordPayloadGuard,
phonePasswordPayloadGuard,
emailPasscodePayloadGuard,
phonePasscodePayloadGuard,
emailVerificationCodePayloadGuard,
phoneVerificationCodePayloadGuard,
socialConnectorPayloadGuard,
socialIdentityPayloadGuard,
]);
@ -74,8 +74,8 @@ export type IdentifierPayload =
| UsernamePasswordPayload
| EmailPasswordPayload
| PhonePasswordPayload
| EmailPasscodePayload
| PhonePasscodePayload
| EmailVerificationCodePayload
| PhoneVerificationCodePayload
| SocialConnectorPayload
| SocialIdentityPayload;

View file

@ -14,7 +14,6 @@ import Continue from './pages/Continue';
import ContinueWithEmailOrPhone from './pages/Continue/EmailOrPhone';
import ErrorPage from './pages/ErrorPage';
import ForgotPassword from './pages/ForgotPassword';
import Passcode from './pages/Passcode';
import PasswordRegisterWithUsername from './pages/PasswordRegisterWithUsername';
import Profile from './pages/Profile';
import Register from './pages/Register';
@ -26,6 +25,7 @@ import SignInPassword from './pages/SignInPassword';
import SocialLanding from './pages/SocialLanding';
import SocialRegister from './pages/SocialRegister';
import SocialSignIn from './pages/SocialSignInCallback';
import VerificationCode from './pages/VerificationCode';
import { getSignInExperienceSettings } from './utils/sign-in-experience';
import './scss/normalized.scss';
@ -110,7 +110,7 @@ const App = () => {
<Route path="/social/landing/:connector" element={<SocialLanding />} />
{/* Always keep route path with param as the last one */}
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
<Route path="/:type/:method/verification-code" element={<VerificationCode />} />
</Route>
<Route path="*" element={<ErrorPage />} />

View file

@ -5,8 +5,8 @@ import type {
UsernamePasswordPayload,
EmailPasswordPayload,
PhonePasswordPayload,
EmailPasscodePayload,
PhonePasscodePayload,
EmailVerificationCodePayload,
PhoneVerificationCodePayload,
SocialConnectorPayload,
SocialIdentityPayload,
} from '@logto/schemas';
@ -77,19 +77,19 @@ export const setUserPassword = async (password: string) => {
return result || { success: true };
};
export type SendPasscodePayload = { email: string } | { phone: string };
export type SendVerificationCodePayload = { 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 });
export const sendVerificationCode = async (payload: SendVerificationCodePayload) => {
await api.post(`${interactionPrefix}/${verificationPath}/verification-code`, { json: payload });
return { success: true };
};
export const signInWithPasscodeIdentifier = async (
payload: EmailPasscodePayload | PhonePasscodePayload,
export const signInWithVerificationCodeIdentifier = async (
payload: EmailVerificationCodePayload | PhoneVerificationCodePayload,
socialToBind?: string
) => {
await api.patch(`${interactionPrefix}/identifiers`, {
@ -103,15 +103,15 @@ export const signInWithPasscodeIdentifier = async (
return api.post(`${interactionPrefix}/submit`).json<Response>();
};
export const addProfileWithPasscodeIdentifier = async (
payload: EmailPasscodePayload | PhonePasscodePayload,
export const addProfileWithVerificationCodeIdentifier = async (
payload: EmailVerificationCodePayload | PhoneVerificationCodePayload,
socialToBind?: string
) => {
await api.patch(`${interactionPrefix}/identifiers`, {
json: payload,
});
const { passcode, ...identifier } = payload;
const { verificationCode, ...identifier } = payload;
await api.patch(`${interactionPrefix}/profile`, {
json: identifier,
@ -124,8 +124,8 @@ export const addProfileWithPasscodeIdentifier = async (
return api.post(`${interactionPrefix}/submit`).json<Response>();
};
export const verifyForgotPasswordPasscodeIdentifier = async (
payload: EmailPasscodePayload | PhonePasscodePayload
export const verifyForgotPasswordVerificationCodeIdentifier = async (
payload: EmailVerificationCodePayload | PhoneVerificationCodePayload
) => {
await api.patch(`${interactionPrefix}/identifiers`, {
json: payload,
@ -146,7 +146,7 @@ export const signInWithVerifierIdentifier = async () => {
return api.post(`${interactionPrefix}/submit`).json<Response>();
};
export const registerWithVerifiedIdentifier = async (payload: SendPasscodePayload) => {
export const registerWithVerifiedIdentifier = async (payload: SendVerificationCodePayload) => {
await api.put(`${interactionPrefix}/event`, {
json: {
event: InteractionEvent.Register,

View file

@ -3,24 +3,25 @@ import { InteractionEvent } from '@logto/schemas';
import { UserFlow, SearchParameters } from '@/types';
import { getSearchParameters } from '@/utils';
import type { SendPasscodePayload } from './interaction';
import { putInteraction, sendPasscode } from './interaction';
import type { SendVerificationCodePayload } from './interaction';
import { putInteraction, sendVerificationCode } from './interaction';
export const getSendPasscodeApi = (type: UserFlow) => async (payload: SendPasscodePayload) => {
if (type === UserFlow.forgotPassword) {
await putInteraction(InteractionEvent.ForgotPassword);
}
export const getSendVerificationCodeApi =
(type: UserFlow) => async (payload: SendVerificationCodePayload) => {
if (type === UserFlow.forgotPassword) {
await putInteraction(InteractionEvent.ForgotPassword);
}
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
// Init a new interaction only if the user is not binding with a social
if (type === UserFlow.signIn && !socialToBind) {
await putInteraction(InteractionEvent.SignIn);
}
// Init a new interaction only if the user is not binding with a social
if (type === UserFlow.signIn && !socialToBind) {
await putInteraction(InteractionEvent.SignIn);
}
if (type === UserFlow.register) {
await putInteraction(InteractionEvent.Register);
}
if (type === UserFlow.register) {
await putInteraction(InteractionEvent.Register);
}
return sendPasscode(payload);
};
return sendVerificationCode(payload);
};

View file

@ -1,8 +1,8 @@
import { render, fireEvent } from '@testing-library/react';
import Passcode, { defaultLength } from '.';
import VerificationCode, { defaultLength } from '.';
describe('Passcode Component', () => {
describe('VerificationCode Component', () => {
const onChange = jest.fn();
beforeEach(() => {
@ -12,7 +12,9 @@ describe('Passcode Component', () => {
it('render with value', () => {
const input = ['1', '2', '3', '4', '5', '6'];
const { container } = render(<Passcode name="passcode" value={input} onChange={onChange} />);
const { container } = render(
<VerificationCode name="passcode" value={input} onChange={onChange} />
);
const inputElements = container.querySelectorAll('input');
expect(inputElements).toHaveLength(defaultLength);
@ -25,7 +27,9 @@ describe('Passcode Component', () => {
it('render with short value', () => {
const input = ['1', '2', '3'];
const { container } = render(<Passcode name="passcode" value={input} onChange={onChange} />);
const { container } = render(
<VerificationCode name="passcode" value={input} onChange={onChange} />
);
const inputElements = container.querySelectorAll('input');
expect(inputElements).toHaveLength(defaultLength);
@ -42,7 +46,9 @@ describe('Passcode Component', () => {
it('render with long value', () => {
const input = ['1', '2', '3', '4', '5', '6', '7'];
const { container } = render(<Passcode name="passcode" value={input} onChange={onChange} />);
const { container } = render(
<VerificationCode name="passcode" value={input} onChange={onChange} />
);
const inputElements = container.querySelectorAll('input');
expect(inputElements).toHaveLength(defaultLength);
@ -54,7 +60,9 @@ describe('Passcode Component', () => {
it('on manual input', () => {
const input = ['1', '2', '3', '4', '5', '6'];
const { container } = render(<Passcode name="passcode" value={input} onChange={onChange} />);
const { container } = render(
<VerificationCode name="passcode" value={input} onChange={onChange} />
);
const inputElements = container.querySelectorAll('input');
if (inputElements[2]) {
@ -66,7 +74,9 @@ describe('Passcode Component', () => {
it('on manual input with non-numric input', () => {
const input = ['1', '2', '3', '4', '5', '6'];
const { container } = render(<Passcode name="passcode" value={input} onChange={onChange} />);
const { container } = render(
<VerificationCode name="passcode" value={input} onChange={onChange} />
);
const inputElements = container.querySelectorAll('input');
if (inputElements[2]) {
@ -77,7 +87,9 @@ describe('Passcode Component', () => {
it('replace old value with new input char', () => {
const input = ['1', '2', '3', '4', '5', '6'];
const { container } = render(<Passcode name="passcode" value={input} onChange={onChange} />);
const { container } = render(
<VerificationCode name="passcode" value={input} onChange={onChange} />
);
const inputElements = container.querySelectorAll('input');
if (inputElements[2]) {
@ -88,7 +100,9 @@ describe('Passcode Component', () => {
it('onKeyDown handler', () => {
const input = ['1', '2', '3', '4', '5', ''];
const { container } = render(<Passcode name="passcode" value={input} onChange={onChange} />);
const { container } = render(
<VerificationCode name="passcode" value={input} onChange={onChange} />
);
const inputElements = container.querySelectorAll('input');
// Backspace on empty input
@ -112,7 +126,9 @@ describe('Passcode Component', () => {
it('onPasteHandler', () => {
const input = ['1', '2', '3', '4', '5', '6'];
const { container } = render(<Passcode name="passcode" value={input} onChange={onChange} />);
const { container } = render(
<VerificationCode name="passcode" value={input} onChange={onChange} />
);
const inputElements = container.querySelectorAll('input');
// Full update

View file

@ -31,7 +31,14 @@ const normalize = (value: string[], length: number): string[] => {
return value;
};
const Passcode = ({ name, className, value, length = defaultLength, error, onChange }: Props) => {
const VerificationCode = ({
name,
className,
value,
length = defaultLength,
error,
onChange,
}: Props) => {
/* eslint-disable @typescript-eslint/ban-types */
const inputReferences = useRef<Array<HTMLInputElement | null>>(
Array.from<null>({ length }).fill(null)
@ -199,4 +206,4 @@ const Passcode = ({ name, className, value, length = defaultLength, error, onCha
);
};
export default Passcode;
export default VerificationCode;

View file

@ -2,14 +2,14 @@ import { fireEvent, waitFor, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { putInteraction, sendPasscode } from '@/apis/interaction';
import { putInteraction, sendVerificationCode } from '@/apis/interaction';
import EmailContinue from './EmailContinue';
const mockedNavigate = jest.fn();
jest.mock('@/apis/interaction', () => ({
sendPasscode: jest.fn(() => ({ success: true })),
sendVerificationCode: jest.fn(() => ({ success: true })),
putInteraction: jest.fn(() => ({ success: true })),
}));
@ -41,9 +41,9 @@ describe('EmailContinue', () => {
await waitFor(() => {
expect(putInteraction).not.toBeCalled();
expect(sendPasscode).toBeCalledWith({ email });
expect(sendVerificationCode).toBeCalledWith({ email });
expect(mockedNavigate).toBeCalledWith(
{ pathname: '/continue/email/passcode-validation', search: '' },
{ pathname: '/continue/email/verification-code', search: '' },
{ state: { email } }
);
});

View file

@ -1,6 +1,6 @@
import { SignInIdentifier } from '@logto/schemas';
import usePasswordlessSendCode from '@/hooks/use-passwordless-send-code';
import useSendVerificationCode from '@/hooks/use-send-verification-code';
import { UserFlow } from '@/types';
import EmailForm from './EmailForm';
@ -13,7 +13,7 @@ type Props = {
};
const EmailContinue = (props: Props) => {
const { onSubmit, errorMessage, clearErrorMessage } = usePasswordlessSendCode(
const { onSubmit, errorMessage, clearErrorMessage } = useSendVerificationCode(
UserFlow.continue,
SignInIdentifier.Email
);

View file

@ -3,14 +3,14 @@ import { fireEvent, waitFor, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { putInteraction, sendPasscode } from '@/apis/interaction';
import { putInteraction, sendVerificationCode } from '@/apis/interaction';
import EmailRegister from './EmailRegister';
const mockedNavigate = jest.fn();
jest.mock('@/apis/interaction', () => ({
sendPasscode: jest.fn(() => ({ success: true })),
sendVerificationCode: jest.fn(() => ({ success: true })),
putInteraction: jest.fn(() => ({ success: true })),
}));
@ -42,9 +42,9 @@ describe('EmailRegister', () => {
await waitFor(() => {
expect(putInteraction).toBeCalledWith(InteractionEvent.Register);
expect(sendPasscode).toBeCalledWith({ email });
expect(sendVerificationCode).toBeCalledWith({ email });
expect(mockedNavigate).toBeCalledWith(
{ pathname: '/register/email/passcode-validation', search: '' },
{ pathname: '/register/email/verification-code', search: '' },
{ state: { email } }
);
});

View file

@ -1,6 +1,6 @@
import { SignInIdentifier } from '@logto/schemas';
import usePasswordlessSendCode from '@/hooks/use-passwordless-send-code';
import useSendVerificationCode from '@/hooks/use-send-verification-code';
import { UserFlow } from '@/types';
import EmailForm from './EmailForm';
@ -12,7 +12,7 @@ type Props = {
};
const EmailRegister = (props: Props) => {
const { onSubmit, errorMessage, clearErrorMessage } = usePasswordlessSendCode(
const { onSubmit, errorMessage, clearErrorMessage } = useSendVerificationCode(
UserFlow.register,
SignInIdentifier.Email
);

View file

@ -3,7 +3,7 @@ import { fireEvent, waitFor, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { putInteraction, sendPasscode } from '@/apis/interaction';
import { putInteraction, sendVerificationCode } from '@/apis/interaction';
import { UserFlow } from '@/types';
import EmailResetPassword from './EmailResetPassword';
@ -11,7 +11,7 @@ import EmailResetPassword from './EmailResetPassword';
const mockedNavigate = jest.fn();
jest.mock('@/apis/interaction', () => ({
sendPasscode: jest.fn(() => ({ success: true })),
sendVerificationCode: jest.fn(() => ({ success: true })),
putInteraction: jest.fn(() => ({ success: true })),
}));
@ -43,10 +43,10 @@ describe('EmailRegister', () => {
await waitFor(() => {
expect(putInteraction).toBeCalledWith(InteractionEvent.ForgotPassword);
expect(sendPasscode).toBeCalledWith({ email });
expect(sendVerificationCode).toBeCalledWith({ email });
expect(mockedNavigate).toBeCalledWith(
{
pathname: `/${UserFlow.forgotPassword}/${SignInIdentifier.Email}/passcode-validation`,
pathname: `/${UserFlow.forgotPassword}/${SignInIdentifier.Email}/verification-code`,
search: '',
},
{ state: { email } }

View file

@ -1,6 +1,6 @@
import { SignInIdentifier } from '@logto/schemas';
import usePasswordlessSendCode from '@/hooks/use-passwordless-send-code';
import useSendVerificationCode from '@/hooks/use-send-verification-code';
import { UserFlow } from '@/types';
import EmailForm from './EmailForm';
@ -13,7 +13,7 @@ type Props = {
};
const EmailResetPassword = (props: Props) => {
const { onSubmit, errorMessage, clearErrorMessage } = usePasswordlessSendCode(
const { onSubmit, errorMessage, clearErrorMessage } = useSendVerificationCode(
UserFlow.forgotPassword,
SignInIdentifier.Email
);

View file

@ -3,14 +3,14 @@ import { fireEvent, waitFor, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { sendPasscode, putInteraction } from '@/apis/interaction';
import { sendVerificationCode, putInteraction } from '@/apis/interaction';
import EmailSignIn from './EmailSignIn';
const mockedNavigate = jest.fn();
jest.mock('@/apis/interaction', () => ({
sendPasscode: jest.fn(() => ({ success: true })),
sendVerificationCode: jest.fn(() => ({ success: true })),
putInteraction: jest.fn(() => ({ success: true })),
}));
@ -53,7 +53,7 @@ describe('EmailSignIn', () => {
await waitFor(() => {
expect(putInteraction).not.toBeCalled();
expect(sendPasscode).not.toBeCalled();
expect(sendVerificationCode).not.toBeCalled();
expect(mockedNavigate).toBeCalledWith(
{ pathname: '/sign-in/email/password', search: '' },
{ state: { email } }
@ -88,7 +88,7 @@ describe('EmailSignIn', () => {
await waitFor(() => {
expect(putInteraction).not.toBeCalled();
expect(sendPasscode).not.toBeCalled();
expect(sendVerificationCode).not.toBeCalled();
expect(mockedNavigate).toBeCalledWith(
{ pathname: '/sign-in/email/password', search: '' },
{ state: { email } }
@ -124,9 +124,9 @@ describe('EmailSignIn', () => {
await waitFor(() => {
expect(putInteraction).toBeCalledWith(InteractionEvent.SignIn);
expect(sendPasscode).toBeCalledWith({ email });
expect(sendVerificationCode).toBeCalledWith({ email });
expect(mockedNavigate).toBeCalledWith(
{ pathname: '/sign-in/email/passcode-validation', search: '' },
{ pathname: '/sign-in/email/verification-code', search: '' },
{ state: { email } }
);
});
@ -160,9 +160,9 @@ describe('EmailSignIn', () => {
await waitFor(() => {
expect(putInteraction).toBeCalledWith(InteractionEvent.SignIn);
expect(sendPasscode).toBeCalledWith({ email });
expect(sendVerificationCode).toBeCalledWith({ email });
expect(mockedNavigate).toBeCalledWith(
{ pathname: '/sign-in/email/passcode-validation', search: '' },
{ pathname: '/sign-in/email/verification-code', search: '' },
{ state: { email } }
);
});

View file

@ -2,7 +2,7 @@ import type { SignIn } from '@logto/schemas';
import { SignInIdentifier } from '@logto/schemas';
import useContinueSignInWithPassword from '@/hooks/use-continue-sign-in-with-password';
import usePasswordlessSendCode from '@/hooks/use-passwordless-send-code';
import useSendVerificationCode from '@/hooks/use-send-verification-code';
import type { ArrayElement } from '@/types';
import { UserFlow } from '@/types';
@ -18,8 +18,8 @@ type Props = FormProps & {
signInMethod: ArrayElement<SignIn['methods']>;
};
const EmailSignInWithPasscode = (props: FormProps) => {
const { onSubmit, errorMessage, clearErrorMessage } = usePasswordlessSendCode(
const EmailSignInWithVerificationCode = (props: FormProps) => {
const { onSubmit, errorMessage, clearErrorMessage } = useSendVerificationCode(
UserFlow.signIn,
SignInIdentifier.Email
);
@ -49,9 +49,9 @@ const EmailSignIn = ({ signInMethod, ...props }: Props) => {
return <EmailSignInWithPassword {...props} />;
}
// Send passcode
// Send verification code
if (verificationCode) {
return <EmailSignInWithPasscode {...props} />;
return <EmailSignInWithVerificationCode {...props} />;
}
return null;

View file

@ -1,36 +0,0 @@
import { SignInIdentifier } from '@logto/schemas';
import { UserFlow } from '@/types';
import useContinueSetEmailPasscodeValidation from './use-continue-set-email-passcode-validation';
import useContinueSetPhonePasscodeValidation from './use-continue-set-phone-passcode-validation';
import useForgotPasswordEmailPasscodeValidation from './use-forgot-password-email-passcode-validation';
import useForgotPasswordPhonePasscodeValidation from './use-forgot-password-phone-passcode-validation';
import useRegisterWithEmailPasscodeValidation from './use-register-with-email-passcode-validation';
import useRegisterWithPhonePasscodeValidation from './use-register-with-phone-passcode-validation';
import useSignInWithEmailPasscodeValidation from './use-sign-in-with-email-passcode-validation';
import useSignInWithPhonePasscodeValidation from './use-sign-in-with-phone-passcode-validation';
export const getPasscodeValidationHook = (
type: UserFlow,
method: SignInIdentifier.Email | SignInIdentifier.Phone
) => {
switch (type) {
case UserFlow.signIn:
return method === SignInIdentifier.Email
? useSignInWithEmailPasscodeValidation
: useSignInWithPhonePasscodeValidation;
case UserFlow.register:
return method === SignInIdentifier.Email
? useRegisterWithEmailPasscodeValidation
: useRegisterWithPhonePasscodeValidation;
case UserFlow.forgotPassword:
return method === SignInIdentifier.Email
? useForgotPasswordEmailPasscodeValidation
: useForgotPasswordPhonePasscodeValidation;
default:
return method === SignInIdentifier.Email
? useContinueSetEmailPasscodeValidation
: useContinueSetPhonePasscodeValidation;
}
};

View file

@ -3,7 +3,7 @@ import { useContext, useEffect } from 'react';
import TextLink from '@/components/TextLink';
import { PageContext } from '@/hooks/use-page-context';
import usePasswordlessSendCode from '@/hooks/use-passwordless-send-code';
import useSendVerificationCode from '@/hooks/use-send-verification-code';
import { UserFlow } from '@/types';
type Props = {
@ -15,7 +15,7 @@ type Props = {
const PasswordlessSignInLink = ({ className, method, value }: Props) => {
const { setToast } = useContext(PageContext);
const { errorMessage, clearErrorMessage, onSubmit } = usePasswordlessSendCode(
const { errorMessage, clearErrorMessage, onSubmit } = useSendVerificationCode(
UserFlow.signIn,
method,
true

View file

@ -2,14 +2,18 @@ import { InteractionEvent, SignInIdentifier } from '@logto/schemas';
import { fireEvent, waitFor, act } from '@testing-library/react';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { signInWithPasswordIdentifier, putInteraction, sendPasscode } from '@/apis/interaction';
import {
signInWithPasswordIdentifier,
putInteraction,
sendVerificationCode,
} from '@/apis/interaction';
import { UserFlow } from '@/types';
import PasswordSignInForm from '.';
jest.mock('@/apis/interaction', () => ({
signInWithPasswordIdentifier: jest.fn(() => ({ redirectTo: '/' })),
sendPasscode: jest.fn(() => ({ success: true })),
sendVerificationCode: jest.fn(() => ({ success: true })),
putInteraction: jest.fn(() => ({ success: true })),
}));
@ -67,22 +71,22 @@ describe('PasswordSignInForm', () => {
expect(signInWithPasswordIdentifier).toBeCalledWith({ email, password }, undefined);
});
const sendPasscodeLink = getByText('action.sign_in_via_passcode');
const sendVerificationCodeLink = getByText('action.sign_in_via_passcode');
expect(sendPasscodeLink).not.toBeNull();
expect(sendVerificationCodeLink).not.toBeNull();
act(() => {
fireEvent.click(sendPasscodeLink);
fireEvent.click(sendVerificationCodeLink);
});
await waitFor(() => {
expect(putInteraction).toBeCalledWith(InteractionEvent.SignIn);
expect(sendPasscode).toBeCalledWith({ email });
expect(sendVerificationCode).toBeCalledWith({ email });
});
expect(mockedNavigate).toBeCalledWith(
{
pathname: `/${UserFlow.signIn}/${SignInIdentifier.Email}/passcode-validation`,
pathname: `/${UserFlow.signIn}/${SignInIdentifier.Email}/verification-code`,
search: '',
},
{
@ -113,22 +117,22 @@ describe('PasswordSignInForm', () => {
expect(signInWithPasswordIdentifier).toBeCalledWith({ phone, password }, undefined);
});
const sendPasscodeLink = getByText('action.sign_in_via_passcode');
const sendVerificationCodeLink = getByText('action.sign_in_via_passcode');
expect(sendPasscodeLink).not.toBeNull();
expect(sendVerificationCodeLink).not.toBeNull();
act(() => {
fireEvent.click(sendPasscodeLink);
fireEvent.click(sendVerificationCodeLink);
});
await waitFor(() => {
expect(putInteraction).toBeCalledWith(InteractionEvent.SignIn);
expect(sendPasscode).toBeCalledWith({ phone });
expect(sendVerificationCode).toBeCalledWith({ phone });
});
expect(mockedNavigate).toBeCalledWith(
{
pathname: `/${UserFlow.signIn}/${SignInIdentifier.Phone}/passcode-validation`,
pathname: `/${UserFlow.signIn}/${SignInIdentifier.Phone}/verification-code`,
search: '',
},
{

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 { putInteraction, sendPasscode } from '@/apis/interaction';
import { putInteraction, sendVerificationCode } from '@/apis/interaction';
import { getDefaultCountryCallingCode } from '@/utils/country-code';
import PhoneContinue from './PhoneContinue';
@ -15,7 +15,7 @@ jest.mock('i18next', () => ({
}));
jest.mock('@/apis/interaction', () => ({
sendPasscode: jest.fn(() => ({ success: true })),
sendVerificationCode: jest.fn(() => ({ success: true })),
putInteraction: jest.fn(() => ({ success: true })),
}));
@ -49,9 +49,9 @@ describe('PhoneContinue', () => {
await waitFor(() => {
expect(putInteraction).not.toBeCalled();
expect(sendPasscode).toBeCalledWith({ phone: fullPhoneNumber });
expect(sendVerificationCode).toBeCalledWith({ phone: fullPhoneNumber });
expect(mockedNavigate).toBeCalledWith(
{ pathname: '/continue/phone/passcode-validation', search: '' },
{ pathname: '/continue/phone/verification-code', search: '' },
{ state: { phone: fullPhoneNumber } }
);
});

View file

@ -1,6 +1,6 @@
import { SignInIdentifier } from '@logto/schemas';
import usePasswordlessSendCode from '@/hooks/use-passwordless-send-code';
import useSendVerificationCode from '@/hooks/use-send-verification-code';
import { UserFlow } from '@/types';
import PhoneForm from './PhoneForm';
@ -13,7 +13,7 @@ type Props = {
};
const PhoneContinue = (props: Props) => {
const { onSubmit, errorMessage, clearErrorMessage } = usePasswordlessSendCode(
const { onSubmit, errorMessage, clearErrorMessage } = useSendVerificationCode(
UserFlow.continue,
SignInIdentifier.Phone
);

View file

@ -3,7 +3,7 @@ import { fireEvent, waitFor, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { putInteraction, sendPasscode } from '@/apis/interaction';
import { putInteraction, sendVerificationCode } from '@/apis/interaction';
import { getDefaultCountryCallingCode } from '@/utils/country-code';
import PhoneRegister from './PhoneRegister';
@ -16,7 +16,7 @@ jest.mock('i18next', () => ({
}));
jest.mock('@/apis/interaction', () => ({
sendPasscode: jest.fn(() => ({ success: true })),
sendVerificationCode: jest.fn(() => ({ success: true })),
putInteraction: jest.fn(() => ({ success: true })),
}));
@ -50,9 +50,9 @@ describe('PhoneRegister', () => {
await waitFor(() => {
expect(putInteraction).toBeCalledWith(InteractionEvent.Register);
expect(sendPasscode).toBeCalledWith({ phone: fullPhoneNumber });
expect(sendVerificationCode).toBeCalledWith({ phone: fullPhoneNumber });
expect(mockedNavigate).toBeCalledWith(
{ pathname: '/register/phone/passcode-validation', search: '' },
{ pathname: '/register/phone/verification-code', search: '' },
{ state: { phone: fullPhoneNumber } }
);
});

View file

@ -1,6 +1,6 @@
import { SignInIdentifier } from '@logto/schemas';
import usePasswordlessSendCode from '@/hooks/use-passwordless-send-code';
import useSendVerificationCode from '@/hooks/use-send-verification-code';
import { UserFlow } from '@/types';
import PhoneForm from './PhoneForm';
@ -12,7 +12,7 @@ type Props = {
};
const PhoneRegister = (props: Props) => {
const { onSubmit, errorMessage, clearErrorMessage } = usePasswordlessSendCode(
const { onSubmit, errorMessage, clearErrorMessage } = useSendVerificationCode(
UserFlow.register,
SignInIdentifier.Phone
);

View file

@ -3,7 +3,7 @@ import { fireEvent, waitFor, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { putInteraction, sendPasscode } from '@/apis/interaction';
import { putInteraction, sendVerificationCode } from '@/apis/interaction';
import { UserFlow } from '@/types';
import { getDefaultCountryCallingCode } from '@/utils/country-code';
@ -17,7 +17,7 @@ jest.mock('i18next', () => ({
}));
jest.mock('@/apis/interaction', () => ({
sendPasscode: jest.fn(() => ({ success: true })),
sendVerificationCode: jest.fn(() => ({ success: true })),
putInteraction: jest.fn(() => ({ success: true })),
}));
@ -51,10 +51,10 @@ describe('PhoneRegister', () => {
await waitFor(() => {
expect(putInteraction).toBeCalledWith(InteractionEvent.ForgotPassword);
expect(sendPasscode).toBeCalledWith({ phone: fullPhoneNumber });
expect(sendVerificationCode).toBeCalledWith({ phone: fullPhoneNumber });
expect(mockedNavigate).toBeCalledWith(
{
pathname: `/${UserFlow.forgotPassword}/${SignInIdentifier.Phone}/passcode-validation`,
pathname: `/${UserFlow.forgotPassword}/${SignInIdentifier.Phone}/verification-code`,
search: '',
},
{ state: { phone: fullPhoneNumber } }

View file

@ -1,6 +1,6 @@
import { SignInIdentifier } from '@logto/schemas';
import usePasswordlessSendCode from '@/hooks/use-passwordless-send-code';
import useSendVerificationCode from '@/hooks/use-send-verification-code';
import { UserFlow } from '@/types';
import PhoneForm from './PhoneForm';
@ -13,7 +13,7 @@ type Props = {
};
const PhoneResetPassword = (props: Props) => {
const { onSubmit, errorMessage, clearErrorMessage } = usePasswordlessSendCode(
const { onSubmit, errorMessage, clearErrorMessage } = useSendVerificationCode(
UserFlow.forgotPassword,
SignInIdentifier.Phone
);

View file

@ -3,7 +3,7 @@ import { fireEvent, waitFor, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { sendPasscode, putInteraction } from '@/apis/interaction';
import { sendVerificationCode, putInteraction } from '@/apis/interaction';
import { getDefaultCountryCallingCode } from '@/utils/country-code';
import PhoneSignIn from './PhoneSignIn';
@ -16,7 +16,7 @@ jest.mock('i18next', () => ({
}));
jest.mock('@/apis/interaction', () => ({
sendPasscode: jest.fn(() => ({ success: true })),
sendVerificationCode: jest.fn(() => ({ success: true })),
putInteraction: jest.fn(() => ({ success: true })),
}));
@ -61,7 +61,7 @@ describe('PhoneSignIn', () => {
await waitFor(() => {
expect(putInteraction).not.toBeCalled();
expect(sendPasscode).not.toBeCalled();
expect(sendVerificationCode).not.toBeCalled();
expect(mockedNavigate).toBeCalledWith(
{ pathname: '/sign-in/phone/password', search: '' },
{ state: { phone: fullPhoneNumber } }
@ -96,7 +96,7 @@ describe('PhoneSignIn', () => {
await waitFor(() => {
expect(putInteraction).not.toBeCalled();
expect(sendPasscode).not.toBeCalled();
expect(sendVerificationCode).not.toBeCalled();
expect(mockedNavigate).toBeCalledWith(
{ pathname: '/sign-in/phone/password', search: '' },
{ state: { phone: fullPhoneNumber } }
@ -132,9 +132,9 @@ describe('PhoneSignIn', () => {
await waitFor(() => {
expect(putInteraction).toBeCalledWith(InteractionEvent.SignIn);
expect(sendPasscode).toBeCalledWith({ phone: fullPhoneNumber });
expect(sendVerificationCode).toBeCalledWith({ phone: fullPhoneNumber });
expect(mockedNavigate).toBeCalledWith(
{ pathname: '/sign-in/phone/passcode-validation', search: '' },
{ pathname: '/sign-in/phone/verification-code', search: '' },
{ state: { phone: fullPhoneNumber } }
);
});
@ -168,9 +168,9 @@ describe('PhoneSignIn', () => {
await waitFor(() => {
expect(putInteraction).toBeCalledWith(InteractionEvent.SignIn);
expect(sendPasscode).toBeCalledWith({ phone: fullPhoneNumber });
expect(sendVerificationCode).toBeCalledWith({ phone: fullPhoneNumber });
expect(mockedNavigate).toBeCalledWith(
{ pathname: '/sign-in/phone/passcode-validation', search: '' },
{ pathname: '/sign-in/phone/verification-code', search: '' },
{ state: { phone: fullPhoneNumber } }
);
});

View file

@ -2,7 +2,7 @@ import type { SignIn } from '@logto/schemas';
import { SignInIdentifier } from '@logto/schemas';
import useContinueSignInWithPassword from '@/hooks/use-continue-sign-in-with-password';
import usePasswordlessSendCode from '@/hooks/use-passwordless-send-code';
import useSendVerificationCode from '@/hooks/use-send-verification-code';
import type { ArrayElement } from '@/types';
import { UserFlow } from '@/types';
@ -18,8 +18,8 @@ type Props = FormProps & {
signInMethod: ArrayElement<SignIn['methods']>;
};
const PhoneSignInWithPasscode = (props: FormProps) => {
const { onSubmit, errorMessage, clearErrorMessage } = usePasswordlessSendCode(
const PhoneSignInWithVerificationCode = (props: FormProps) => {
const { onSubmit, errorMessage, clearErrorMessage } = useSendVerificationCode(
UserFlow.signIn,
SignInIdentifier.Phone
);
@ -49,9 +49,9 @@ const PhoneSignIn = ({ signInMethod, ...props }: Props) => {
return <PhoneSignInWithPassword {...props} />;
}
// Send passcode
// Send verification code
if (verificationCode) {
return <PhoneSignInWithPasscode {...props} />;
return <PhoneSignInWithVerificationCode {...props} />;
}
return null;

View file

@ -3,17 +3,17 @@ import { act, fireEvent, waitFor } from '@testing-library/react';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import {
verifyForgotPasswordPasscodeIdentifier,
signInWithPasscodeIdentifier,
addProfileWithPasscodeIdentifier,
verifyForgotPasswordVerificationCodeIdentifier,
signInWithVerificationCodeIdentifier,
addProfileWithVerificationCodeIdentifier,
} from '@/apis/interaction';
import { UserFlow } from '@/types';
import PasscodeValidation from '.';
import VerificationCode from '.';
jest.useFakeTimers();
const sendPasscodeApi = jest.fn();
const sendVerificationCodeApi = jest.fn();
const mockedNavigate = jest.fn();
@ -23,16 +23,16 @@ jest.mock('react-router-dom', () => ({
}));
jest.mock('@/apis/utils', () => ({
getSendPasscodeApi: () => sendPasscodeApi,
getSendVerificationCodeApi: () => sendVerificationCodeApi,
}));
jest.mock('@/apis/interaction', () => ({
verifyForgotPasswordPasscodeIdentifier: jest.fn(),
signInWithPasscodeIdentifier: jest.fn(),
addProfileWithPasscodeIdentifier: jest.fn(),
verifyForgotPasswordVerificationCodeIdentifier: jest.fn(),
signInWithVerificationCodeIdentifier: jest.fn(),
addProfileWithVerificationCodeIdentifier: jest.fn(),
}));
describe('<PasscodeValidation />', () => {
describe('<VerificationCode />', () => {
const email = 'foo@logto.io';
const phone = '18573333333';
const originalLocation = window.location;
@ -57,7 +57,7 @@ describe('<PasscodeValidation />', () => {
it('render counter', () => {
const { queryByText } = renderWithPageContext(
<PasscodeValidation type={UserFlow.signIn} method={SignInIdentifier.Email} target={email} />
<VerificationCode type={UserFlow.signIn} method={SignInIdentifier.Email} target={email} />
);
expect(queryByText('description.resend_after_seconds')).not.toBeNull();
@ -71,7 +71,7 @@ describe('<PasscodeValidation />', () => {
it('fire resend event', async () => {
const { getByText } = renderWithPageContext(
<PasscodeValidation type={UserFlow.signIn} method={SignInIdentifier.Email} target={email} />
<VerificationCode type={UserFlow.signIn} method={SignInIdentifier.Email} target={email} />
);
act(() => {
jest.advanceTimersByTime(1e3 * 60);
@ -82,17 +82,17 @@ describe('<PasscodeValidation />', () => {
fireEvent.click(resendButton);
});
expect(sendPasscodeApi).toBeCalledWith({ email });
expect(sendVerificationCodeApi).toBeCalledWith({ email });
});
describe('sign-in', () => {
it('fire email sign-in validate passcode event', async () => {
(signInWithPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
it('fire email sign-in validate verification code event', async () => {
(signInWithVerificationCodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
redirectTo: 'foo.com',
}));
const { container } = renderWithPageContext(
<PasscodeValidation type={UserFlow.signIn} method={SignInIdentifier.Email} target={email} />
<VerificationCode type={UserFlow.signIn} method={SignInIdentifier.Email} target={email} />
);
const inputs = container.querySelectorAll('input');
@ -103,8 +103,8 @@ describe('<PasscodeValidation />', () => {
}
await waitFor(() => {
expect(signInWithPasscodeIdentifier).toBeCalledWith(
{ email, passcode: '111111' },
expect(signInWithVerificationCodeIdentifier).toBeCalledWith(
{ email, verificationCode: '111111' },
undefined
);
});
@ -114,13 +114,13 @@ describe('<PasscodeValidation />', () => {
});
});
it('fire phone sign-in validate passcode event', async () => {
(signInWithPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
it('fire phone sign-in validate verification code event', async () => {
(signInWithVerificationCodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
redirectTo: 'foo.com',
}));
const { container } = renderWithPageContext(
<PasscodeValidation type={UserFlow.signIn} method={SignInIdentifier.Phone} target={phone} />
<VerificationCode type={UserFlow.signIn} method={SignInIdentifier.Phone} target={phone} />
);
const inputs = container.querySelectorAll('input');
@ -131,10 +131,10 @@ describe('<PasscodeValidation />', () => {
}
await waitFor(() => {
expect(signInWithPasscodeIdentifier).toBeCalledWith(
expect(signInWithVerificationCodeIdentifier).toBeCalledWith(
{
phone,
passcode: '111111',
verificationCode: '111111',
},
undefined
);
@ -147,17 +147,13 @@ describe('<PasscodeValidation />', () => {
});
describe('register', () => {
it('fire email register validate passcode event', async () => {
(addProfileWithPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
it('fire email register validate verification code event', async () => {
(addProfileWithVerificationCodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
redirectTo: 'foo.com',
}));
const { container } = renderWithPageContext(
<PasscodeValidation
type={UserFlow.register}
method={SignInIdentifier.Email}
target={email}
/>
<VerificationCode type={UserFlow.register} method={SignInIdentifier.Email} target={email} />
);
const inputs = container.querySelectorAll('input');
@ -168,9 +164,9 @@ describe('<PasscodeValidation />', () => {
}
await waitFor(() => {
expect(addProfileWithPasscodeIdentifier).toBeCalledWith({
expect(addProfileWithVerificationCodeIdentifier).toBeCalledWith({
email,
passcode: '111111',
verificationCode: '111111',
});
});
@ -179,17 +175,13 @@ describe('<PasscodeValidation />', () => {
});
});
it('fire phone register validate passcode event', async () => {
(addProfileWithPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
it('fire phone register validate verification code event', async () => {
(addProfileWithVerificationCodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
redirectTo: 'foo.com',
}));
const { container } = renderWithPageContext(
<PasscodeValidation
type={UserFlow.register}
method={SignInIdentifier.Phone}
target={phone}
/>
<VerificationCode type={UserFlow.register} method={SignInIdentifier.Phone} target={phone} />
);
const inputs = container.querySelectorAll('input');
@ -200,7 +192,10 @@ describe('<PasscodeValidation />', () => {
}
await waitFor(() => {
expect(addProfileWithPasscodeIdentifier).toBeCalledWith({ phone, passcode: '111111' });
expect(addProfileWithVerificationCodeIdentifier).toBeCalledWith({
phone,
verificationCode: '111111',
});
});
await waitFor(() => {
@ -210,13 +205,13 @@ describe('<PasscodeValidation />', () => {
});
describe('forgot password', () => {
it('fire email forgot-password validate passcode event', async () => {
(verifyForgotPasswordPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
it('fire email forgot-password validate verification code event', async () => {
(verifyForgotPasswordVerificationCodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
success: true,
}));
const { container } = renderWithPageContext(
<PasscodeValidation
<VerificationCode
type={UserFlow.forgotPassword}
method={SignInIdentifier.Email}
target={email}
@ -232,22 +227,22 @@ describe('<PasscodeValidation />', () => {
}
await waitFor(() => {
expect(verifyForgotPasswordPasscodeIdentifier).toBeCalledWith({
expect(verifyForgotPasswordVerificationCodeIdentifier).toBeCalledWith({
email,
passcode: '111111',
verificationCode: '111111',
});
});
// TODO: @simeng test exception flow to fulfill the password
});
it('fire phone forgot-password validate passcode event', async () => {
(verifyForgotPasswordPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
it('fire phone forgot-password validate verification code event', async () => {
(verifyForgotPasswordVerificationCodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
success: true,
}));
const { container } = renderWithPageContext(
<PasscodeValidation
<VerificationCode
type={UserFlow.forgotPassword}
method={SignInIdentifier.Phone}
target={phone}
@ -263,9 +258,9 @@ describe('<PasscodeValidation />', () => {
}
await waitFor(() => {
expect(verifyForgotPasswordPasscodeIdentifier).toBeCalledWith({
expect(verifyForgotPasswordVerificationCodeIdentifier).toBeCalledWith({
phone,
passcode: '111111',
verificationCode: '111111',
});
});
@ -275,16 +270,12 @@ describe('<PasscodeValidation />', () => {
describe('continue flow', () => {
it('set email', async () => {
(addProfileWithPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
(addProfileWithVerificationCodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
redirectTo: '/redirect',
}));
const { container } = renderWithPageContext(
<PasscodeValidation
type={UserFlow.continue}
method={SignInIdentifier.Email}
target={email}
/>
<VerificationCode type={UserFlow.continue} method={SignInIdentifier.Email} target={email} />
);
const inputs = container.querySelectorAll('input');
@ -296,10 +287,10 @@ describe('<PasscodeValidation />', () => {
}
await waitFor(() => {
expect(addProfileWithPasscodeIdentifier).toBeCalledWith(
expect(addProfileWithVerificationCodeIdentifier).toBeCalledWith(
{
email,
passcode: '111111',
verificationCode: '111111',
},
undefined
);
@ -311,16 +302,12 @@ describe('<PasscodeValidation />', () => {
});
it('set Phone', async () => {
(addProfileWithPasscodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
(addProfileWithVerificationCodeIdentifier as jest.Mock).mockImplementationOnce(() => ({
redirectTo: '/redirect',
}));
const { container } = renderWithPageContext(
<PasscodeValidation
type={UserFlow.continue}
method={SignInIdentifier.Phone}
target={phone}
/>
<VerificationCode type={UserFlow.continue} method={SignInIdentifier.Phone} target={phone} />
);
const inputs = container.querySelectorAll('input');
@ -332,10 +319,10 @@ describe('<PasscodeValidation />', () => {
}
await waitFor(() => {
expect(addProfileWithPasscodeIdentifier).toBeCalledWith(
expect(addProfileWithVerificationCodeIdentifier).toBeCalledWith(
{
phone,
passcode: '111111',
verificationCode: '111111',
},
undefined
);

View file

@ -3,14 +3,14 @@ import classNames from 'classnames';
import { useState, useEffect, useCallback } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import Passcode, { defaultLength } from '@/components/Passcode';
import TextLink from '@/components/TextLink';
import VerificationCodeInput, { defaultLength } from '@/components/VerificationCode';
import { UserFlow } from '@/types';
import PasswordSignInLink from './PasswordSignInLink';
import * as styles from './index.module.scss';
import useResendPasscode from './use-resend-passcode';
import { getPasscodeValidationHook } from './utils';
import useResendVerificationCode from './use-resend-verification-code';
import { getVerificationCodeHook } from './utils';
type Props = {
type: UserFlow;
@ -20,21 +20,22 @@ type Props = {
className?: string;
};
const PasscodeValidation = ({ type, method, className, hasPasswordButton, target }: Props) => {
const VerificationCode = ({ type, method, className, hasPasswordButton, target }: Props) => {
const [code, setCode] = useState<string[]>([]);
const { t } = useTranslation();
const usePasscodeValidation = getPasscodeValidationHook(type, method);
const useVerificationCode = getVerificationCodeHook(type, method);
const errorCallback = useCallback(() => {
setCode([]);
}, []);
const { errorMessage, clearErrorMessage, onSubmit } = usePasscodeValidation(
target,
errorCallback
);
const { errorMessage, clearErrorMessage, onSubmit } = useVerificationCode(target, errorCallback);
const { seconds, isRunning, onResendPasscode } = useResendPasscode(type, method, target);
const { seconds, isRunning, onResendVerificationCode } = useResendVerificationCode(
type,
method,
target
);
useEffect(() => {
if (code.length === defaultLength && code.every(Boolean)) {
@ -44,7 +45,7 @@ const PasscodeValidation = ({ type, method, className, hasPasswordButton, target
return (
<form className={classNames(styles.form, className)}>
<Passcode
<VerificationCodeInput
name="passcode"
className={classNames(styles.inputField, errorMessage && styles.withError)}
value={code}
@ -63,7 +64,7 @@ const PasscodeValidation = ({ type, method, className, hasPasswordButton, target
text="description.resend_passcode"
onClick={() => {
clearErrorMessage();
void onResendPasscode();
void onResendVerificationCode();
}}
/>
)}
@ -74,4 +75,4 @@ const PasscodeValidation = ({ type, method, className, hasPasswordButton, target
);
};
export default PasscodeValidation;
export default VerificationCode;

View file

@ -1,7 +1,7 @@
import { SignInIdentifier } from '@logto/schemas';
import { useMemo, useCallback } from 'react';
import { addProfileWithPasscodeIdentifier } from '@/apis/interaction';
import { addProfileWithVerificationCodeIdentifier } 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';
@ -11,7 +11,7 @@ import { getSearchParameters } from '@/utils';
import useIdentifierErrorAlert from './use-identifier-error-alert';
import useSharedErrorHandler from './use-shared-error-handler';
const useContinueSetEmailPasscodeValidation = (email: string, errorCallback?: () => void) => {
const useContinueSetEmailVerificationCode = (email: string, errorCallback?: () => void) => {
const { sharedErrorHandlers, errorMessage, clearErrorMessage } = useSharedErrorHandler();
const requiredProfileErrorHandler = useRequiredProfileErrorHandler(true);
@ -22,7 +22,7 @@ const useContinueSetEmailPasscodeValidation = (email: string, errorCallback?: ()
email
);
const verifyPasscodeErrorHandlers: ErrorHandlers = useMemo(
const verifyVerificationCodeErrorHandlers: ErrorHandlers = useMemo(
() => ({
'user.email_already_in_use': identifierNotExistErrorHandler,
...requiredProfileErrorHandler,
@ -37,21 +37,21 @@ const useContinueSetEmailPasscodeValidation = (email: string, errorCallback?: ()
]
);
const { run: verifyPasscode } = useApi(
addProfileWithPasscodeIdentifier,
verifyPasscodeErrorHandlers
const { run: verifyVerificationCode } = useApi(
addProfileWithVerificationCodeIdentifier,
verifyVerificationCodeErrorHandlers
);
const onSubmit = useCallback(
async (passcode: string) => {
async (verificationCode: string) => {
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
const result = await verifyPasscode({ email, passcode }, socialToBind);
const result = await verifyVerificationCode({ email, verificationCode }, socialToBind);
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
},
[email, verifyPasscode]
[email, verifyVerificationCode]
);
return {
@ -61,4 +61,4 @@ const useContinueSetEmailPasscodeValidation = (email: string, errorCallback?: ()
};
};
export default useContinueSetEmailPasscodeValidation;
export default useContinueSetEmailVerificationCode;

View file

@ -1,7 +1,7 @@
import { SignInIdentifier } from '@logto/schemas';
import { useMemo, useCallback } from 'react';
import { addProfileWithPasscodeIdentifier } from '@/apis/interaction';
import { addProfileWithVerificationCodeIdentifier } 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';
@ -11,7 +11,7 @@ import { getSearchParameters } from '@/utils';
import useIdentifierErrorAlert from './use-identifier-error-alert';
import useSharedErrorHandler from './use-shared-error-handler';
const useContinueSetPhonePasscodeValidation = (phone: string, errorCallback?: () => void) => {
const useContinueSetPhoneVerificationCode = (phone: string, errorCallback?: () => void) => {
const { sharedErrorHandlers, errorMessage, clearErrorMessage } = useSharedErrorHandler();
const requiredProfileErrorHandler = useRequiredProfileErrorHandler(true);
@ -22,7 +22,7 @@ const useContinueSetPhonePasscodeValidation = (phone: string, errorCallback?: ()
phone
);
const verifyPasscodeErrorHandlers: ErrorHandlers = useMemo(
const verifyVerificationCodeErrorHandlers: ErrorHandlers = useMemo(
() => ({
'user.phone_already_in_use': identifierExistErrorHandler,
...requiredProfileErrorHandler,
@ -32,21 +32,21 @@ const useContinueSetPhonePasscodeValidation = (phone: string, errorCallback?: ()
[errorCallback, identifierExistErrorHandler, requiredProfileErrorHandler, sharedErrorHandlers]
);
const { run: verifyPasscode } = useApi(
addProfileWithPasscodeIdentifier,
verifyPasscodeErrorHandlers
const { run: verifyVerificationCode } = useApi(
addProfileWithVerificationCodeIdentifier,
verifyVerificationCodeErrorHandlers
);
const onSubmit = useCallback(
async (passcode: string) => {
async (verificationCode: string) => {
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
const result = await verifyPasscode({ phone, passcode }, socialToBind);
const result = await verifyVerificationCode({ phone, verificationCode }, socialToBind);
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
},
[phone, verifyPasscode]
[phone, verifyVerificationCode]
);
return {
@ -56,4 +56,4 @@ const useContinueSetPhonePasscodeValidation = (phone: string, errorCallback?: ()
};
};
export default useContinueSetPhonePasscodeValidation;
export default useContinueSetPhoneVerificationCode;

View file

@ -2,7 +2,7 @@ import { SignInIdentifier } from '@logto/schemas';
import { useMemo, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { verifyForgotPasswordPasscodeIdentifier } from '@/apis/interaction';
import { verifyForgotPasswordVerificationCodeIdentifier } from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import { UserFlow } from '@/types';
@ -10,7 +10,7 @@ import { UserFlow } from '@/types';
import useIdentifierErrorAlert from './use-identifier-error-alert';
import useSharedErrorHandler from './use-shared-error-handler';
const useForgotPasswordEmailPasscodeValidation = (email: string, errorCallback?: () => void) => {
const useForgotPasswordEmailVerificationCode = (email: string, errorCallback?: () => void) => {
const navigate = useNavigate();
const { sharedErrorHandlers, errorMessage, clearErrorMessage } = useSharedErrorHandler();
@ -32,16 +32,16 @@ const useForgotPasswordEmailPasscodeValidation = (email: string, errorCallback?:
[identifierNotExistErrorHandler, sharedErrorHandlers, errorCallback, navigate]
);
const { result, run: verifyPasscode } = useApi(
verifyForgotPasswordPasscodeIdentifier,
const { result, run: verifyVerificationCode } = useApi(
verifyForgotPasswordVerificationCodeIdentifier,
errorHandlers
);
const onSubmit = useCallback(
async (passcode: string) => {
return verifyPasscode({ email, passcode });
async (verificationCode: string) => {
return verifyVerificationCode({ email, verificationCode });
},
[email, verifyPasscode]
[email, verifyVerificationCode]
);
useEffect(() => {
@ -57,4 +57,4 @@ const useForgotPasswordEmailPasscodeValidation = (email: string, errorCallback?:
};
};
export default useForgotPasswordEmailPasscodeValidation;
export default useForgotPasswordEmailVerificationCode;

View file

@ -2,7 +2,7 @@ import { SignInIdentifier } from '@logto/schemas';
import { useMemo, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { verifyForgotPasswordPasscodeIdentifier } from '@/apis/interaction';
import { verifyForgotPasswordVerificationCodeIdentifier } from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import { UserFlow } from '@/types';
@ -10,7 +10,7 @@ import { UserFlow } from '@/types';
import useIdentifierErrorAlert from './use-identifier-error-alert';
import useSharedErrorHandler from './use-shared-error-handler';
const useForgotPasswordPhonePasscodeValidation = (phone: string, errorCallback?: () => void) => {
const useForgotPasswordPhoneVerificationCode = (phone: string, errorCallback?: () => void) => {
const navigate = useNavigate();
const { sharedErrorHandlers, errorMessage, clearErrorMessage } = useSharedErrorHandler();
@ -32,16 +32,16 @@ const useForgotPasswordPhonePasscodeValidation = (phone: string, errorCallback?:
[identifierNotExistErrorHandler, sharedErrorHandlers, errorCallback, navigate]
);
const { result, run: verifyPasscode } = useApi(
verifyForgotPasswordPasscodeIdentifier,
const { result, run: verifyVerificationCode } = useApi(
verifyForgotPasswordVerificationCodeIdentifier,
errorHandlers
);
const onSubmit = useCallback(
async (passcode: string) => {
return verifyPasscode({ phone, passcode });
async (verificationCode: string) => {
return verifyVerificationCode({ phone, verificationCode });
},
[phone, verifyPasscode]
[phone, verifyVerificationCode]
);
useEffect(() => {
@ -57,4 +57,4 @@ const useForgotPasswordPhonePasscodeValidation = (phone: string, errorCallback?:
};
};
export default useForgotPasswordPhonePasscodeValidation;
export default useForgotPasswordPhoneVerificationCode;

View file

@ -3,7 +3,10 @@ import { useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { addProfileWithPasscodeIdentifier, signInWithVerifierIdentifier } from '@/apis/interaction';
import {
addProfileWithVerificationCodeIdentifier,
signInWithVerifierIdentifier,
} from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
@ -14,7 +17,7 @@ import { UserFlow } from '@/types';
import useIdentifierErrorAlert from './use-identifier-error-alert';
import useSharedErrorHandler from './use-shared-error-handler';
const useRegisterWithEmailPasscodeValidation = (email: string, errorCallback?: () => void) => {
const useRegisterWithEmailVerificationCode = (email: string, errorCallback?: () => void) => {
const { t } = useTranslation();
const { show } = useConfirmModal();
const navigate = useNavigate();
@ -77,13 +80,16 @@ const useRegisterWithEmailPasscodeValidation = (email: string, errorCallback?: (
]
);
const { result, run: verifyPasscode } = useApi(addProfileWithPasscodeIdentifier, errorHandlers);
const { result, run: verifyVerificationCode } = useApi(
addProfileWithVerificationCodeIdentifier,
errorHandlers
);
const onSubmit = useCallback(
async (passcode: string) => {
return verifyPasscode({ email, passcode });
async (verificationCode: string) => {
return verifyVerificationCode({ email, verificationCode });
},
[email, verifyPasscode]
[email, verifyVerificationCode]
);
useEffect(() => {
@ -99,4 +105,4 @@ const useRegisterWithEmailPasscodeValidation = (email: string, errorCallback?: (
};
};
export default useRegisterWithEmailPasscodeValidation;
export default useRegisterWithEmailVerificationCode;

View file

@ -3,7 +3,10 @@ import { useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { addProfileWithPasscodeIdentifier, signInWithVerifierIdentifier } from '@/apis/interaction';
import {
addProfileWithVerificationCodeIdentifier,
signInWithVerifierIdentifier,
} from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
@ -15,7 +18,7 @@ import { formatPhoneNumberWithCountryCallingCode } from '@/utils/country-code';
import useIdentifierErrorAlert from './use-identifier-error-alert';
import useSharedErrorHandler from './use-shared-error-handler';
const useRegisterWithPhonePasscodeValidation = (phone: string, errorCallback?: () => void) => {
const useRegisterWithPhoneVerificationCode = (phone: string, errorCallback?: () => void) => {
const { t } = useTranslation();
const { show } = useConfirmModal();
const navigate = useNavigate();
@ -77,7 +80,10 @@ const useRegisterWithPhonePasscodeValidation = (phone: string, errorCallback?: (
]
);
const { result, run: verifyPasscode } = useApi(addProfileWithPasscodeIdentifier, errorHandlers);
const { result, run: verifyVerificationCode } = useApi(
addProfileWithVerificationCodeIdentifier,
errorHandlers
);
useEffect(() => {
if (result?.redirectTo) {
@ -86,13 +92,13 @@ const useRegisterWithPhonePasscodeValidation = (phone: string, errorCallback?: (
}, [result]);
const onSubmit = useCallback(
async (passcode: string) => {
return verifyPasscode({
async (verificationCode: string) => {
return verifyVerificationCode({
phone,
passcode,
verificationCode,
});
},
[phone, verifyPasscode]
[phone, verifyVerificationCode]
);
return {
@ -102,4 +108,4 @@ const useRegisterWithPhonePasscodeValidation = (phone: string, errorCallback?: (
};
};
export default useRegisterWithPhonePasscodeValidation;
export default useRegisterWithPhoneVerificationCode;

View file

@ -3,7 +3,7 @@ import { t } from 'i18next';
import { useCallback, useContext } from 'react';
import { useTimer } from 'react-timer-hook';
import { getSendPasscodeApi } from '@/apis/utils';
import { getSendVerificationCodeApi } from '@/apis/utils';
import useApi from '@/hooks/use-api';
import { PageContext } from '@/hooks/use-page-context';
import type { UserFlow } from '@/types';
@ -17,7 +17,7 @@ const getTimeout = () => {
return now;
};
const useResendPasscode = (
const useResendVerificationCode = (
type: UserFlow,
method: SignInIdentifier.Email | SignInIdentifier.Phone,
target: string
@ -29,23 +29,23 @@ const useResendPasscode = (
expiryTimestamp: getTimeout(),
});
const { run: sendPassCode } = useApi(getSendPasscodeApi(type));
const { run: sendVerificationCode } = useApi(getSendVerificationCodeApi(type));
const onResendPasscode = useCallback(async () => {
const onResendVerificationCode = useCallback(async () => {
const payload = method === SignInIdentifier.Email ? { email: target } : { phone: target };
const result = await sendPassCode(payload);
const result = await sendVerificationCode(payload);
if (result) {
setToast(t('description.passcode_sent'));
restart(getTimeout(), true);
}
}, [method, restart, sendPassCode, setToast, target]);
}, [method, restart, sendVerificationCode, setToast, target]);
return {
seconds,
isRunning,
onResendPasscode,
onResendVerificationCode,
};
};
export default useResendPasscode;
export default useResendVerificationCode;

View file

@ -3,7 +3,10 @@ import { useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { signInWithPasscodeIdentifier, registerWithVerifiedIdentifier } from '@/apis/interaction';
import {
signInWithVerificationCodeIdentifier,
registerWithVerifiedIdentifier,
} from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
@ -15,7 +18,7 @@ import { getSearchParameters } from '@/utils';
import useIdentifierErrorAlert from './use-identifier-error-alert';
import useSharedErrorHandler from './use-shared-error-handler';
const useSignInWithEmailPasscodeValidation = (email: string, errorCallback?: () => void) => {
const useSignInWithEmailVerificationCode = (email: string, errorCallback?: () => void) => {
const { t } = useTranslation();
const { show } = useConfirmModal();
const navigate = useNavigate();
@ -82,8 +85,8 @@ const useSignInWithEmailPasscodeValidation = (email: string, errorCallback?: ()
]
);
const { result, run: asyncSignInWithPasscodeIdentifier } = useApi(
signInWithPasscodeIdentifier,
const { result, run: asyncSignInWithVerificationCodeIdentifier } = useApi(
signInWithVerificationCodeIdentifier,
errorHandlers
);
@ -94,16 +97,16 @@ const useSignInWithEmailPasscodeValidation = (email: string, errorCallback?: ()
}, [result]);
const onSubmit = useCallback(
async (passcode: string) => {
return asyncSignInWithPasscodeIdentifier(
async (verificationCode: string) => {
return asyncSignInWithVerificationCodeIdentifier(
{
email,
passcode,
verificationCode,
},
socialToBind
);
},
[asyncSignInWithPasscodeIdentifier, email, socialToBind]
[asyncSignInWithVerificationCodeIdentifier, email, socialToBind]
);
return {
@ -113,4 +116,4 @@ const useSignInWithEmailPasscodeValidation = (email: string, errorCallback?: ()
};
};
export default useSignInWithEmailPasscodeValidation;
export default useSignInWithEmailVerificationCode;

View file

@ -3,7 +3,10 @@ import { useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { signInWithPasscodeIdentifier, registerWithVerifiedIdentifier } from '@/apis/interaction';
import {
signInWithVerificationCodeIdentifier,
registerWithVerifiedIdentifier,
} from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
@ -15,7 +18,7 @@ import { getSearchParameters } from '@/utils';
import useIdentifierErrorAlert from './use-identifier-error-alert';
import useSharedErrorHandler from './use-shared-error-handler';
const useSignInWithPhonePasscodeValidation = (phone: string, errorCallback?: () => void) => {
const useSignInWithPhoneVerificationCode = (phone: string, errorCallback?: () => void) => {
const { t } = useTranslation();
const { show } = useConfirmModal();
const navigate = useNavigate();
@ -82,8 +85,8 @@ const useSignInWithPhonePasscodeValidation = (phone: string, errorCallback?: ()
]
);
const { result, run: asyncSignInWithPasscodeIdentifier } = useApi(
signInWithPasscodeIdentifier,
const { result, run: asyncSignInWithVerificationCodeIdentifier } = useApi(
signInWithVerificationCodeIdentifier,
errorHandlers
);
@ -94,16 +97,16 @@ const useSignInWithPhonePasscodeValidation = (phone: string, errorCallback?: ()
}, [result]);
const onSubmit = useCallback(
async (code: string) => {
return asyncSignInWithPasscodeIdentifier(
async (verificationCode: string) => {
return asyncSignInWithVerificationCodeIdentifier(
{
phone,
passcode: code,
verificationCode,
},
socialToBind
);
},
[phone, socialToBind, asyncSignInWithPasscodeIdentifier]
[phone, socialToBind, asyncSignInWithVerificationCodeIdentifier]
);
return {
@ -113,4 +116,4 @@ const useSignInWithPhonePasscodeValidation = (phone: string, errorCallback?: ()
};
};
export default useSignInWithPhonePasscodeValidation;
export default useSignInWithPhoneVerificationCode;

View file

@ -0,0 +1,36 @@
import { SignInIdentifier } from '@logto/schemas';
import { UserFlow } from '@/types';
import useContinueSetEmailVerificationCode from './use-continue-set-email-verification-code-validation';
import useContinueSetPhoneVerificationCode from './use-continue-set-phone-verification-code-validation';
import useForgotPasswordEmailVerificationCode from './use-forgot-password-email-verification-code-validation';
import useForgotPasswordPhoneVerificationCode from './use-forgot-password-phone-verification-code-validation';
import useRegisterWithEmailVerificationCode from './use-register-with-email-verification-code-validation';
import useRegisterWithPhoneVerificationCode from './use-register-with-phone-verification-code-validation';
import useSignInWithEmailVerificationCode from './use-sign-in-with-email-verification-code-validation';
import useSignInWithPhoneVerificationCode from './use-sign-in-with-phone-verification-code-validation';
export const getVerificationCodeHook = (
type: UserFlow,
method: SignInIdentifier.Email | SignInIdentifier.Phone
) => {
switch (type) {
case UserFlow.signIn:
return method === SignInIdentifier.Email
? useSignInWithEmailVerificationCode
: useSignInWithPhoneVerificationCode;
case UserFlow.register:
return method === SignInIdentifier.Email
? useRegisterWithEmailVerificationCode
: useRegisterWithPhoneVerificationCode;
case UserFlow.forgotPassword:
return method === SignInIdentifier.Email
? useForgotPasswordEmailVerificationCode
: useForgotPasswordPhoneVerificationCode;
default:
return method === SignInIdentifier.Email
? useContinueSetEmailVerificationCode
: useContinueSetPhoneVerificationCode;
}
};

View file

@ -2,12 +2,12 @@ import { SignInIdentifier } from '@logto/schemas';
import { useState, useMemo, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { getSendPasscodeApi } from '@/apis/utils';
import { getSendVerificationCodeApi } from '@/apis/utils';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import type { UserFlow } from '@/types';
const usePasswordlessSendCode = <T extends SignInIdentifier.Email | SignInIdentifier.Phone>(
const useSendVerificationCode = <T extends SignInIdentifier.Email | SignInIdentifier.Phone>(
flow: UserFlow,
method: T,
replaceCurrentPage?: boolean
@ -28,15 +28,15 @@ const usePasswordlessSendCode = <T extends SignInIdentifier.Email | SignInIdenti
setErrorMessage('');
}, []);
const api = getSendPasscodeApi(flow);
const api = getSendVerificationCodeApi(flow);
const { run: asyncSendPasscode } = useApi(api, errorHandlers);
const { run: asyncSendVerificationCode } = useApi(api, errorHandlers);
type Payload = T extends SignInIdentifier.Email ? { email: string } : { phone: string };
const onSubmit = useCallback(
async (payload: Payload) => {
const result = await asyncSendPasscode(payload);
const result = await asyncSendVerificationCode(payload);
if (!result) {
return;
@ -44,7 +44,7 @@ const usePasswordlessSendCode = <T extends SignInIdentifier.Email | SignInIdenti
navigate(
{
pathname: `/${flow}/${method}/passcode-validation`,
pathname: `/${flow}/${method}/verification-code`,
search: location.search,
},
{
@ -53,7 +53,7 @@ const usePasswordlessSendCode = <T extends SignInIdentifier.Email | SignInIdenti
}
);
},
[asyncSendPasscode, flow, method, navigate, replaceCurrentPage]
[asyncSendVerificationCode, flow, method, navigate, replaceCurrentPage]
);
return {
@ -63,4 +63,4 @@ const usePasswordlessSendCode = <T extends SignInIdentifier.Email | SignInIdenti
};
};
export default usePasswordlessSendCode;
export default useSendVerificationCode;

View file

@ -7,7 +7,7 @@ import { EmailResetPassword } from '@/containers/EmailForm';
import { PhoneResetPassword } from '@/containers/PhoneForm';
import { useForgotPasswordSettings } from '@/hooks/use-sie';
import ErrorPage from '@/pages/ErrorPage';
import { passcodeMethodGuard } from '@/types/guard';
import { verificationCodeMethodGuard } from '@/types/guard';
type Props = {
method?: string;
@ -17,7 +17,7 @@ const ForgotPassword = () => {
const { method = '' } = useParams<Props>();
const forgotPassword = useForgotPasswordSettings();
if (!is(method, passcodeMethodGuard)) {
if (!is(method, verificationCodeMethodGuard)) {
return <ErrorPage />;
}

View file

@ -8,7 +8,7 @@ import { EmailRegister } from '@/containers/EmailForm';
import { PhoneRegister } from '@/containers/PhoneForm';
import { useSieMethods } from '@/hooks/use-sie';
import ErrorPage from '@/pages/ErrorPage';
import { SignInMethodGuard, passcodeMethodGuard } from '@/types/guard';
import { SignInMethodGuard, verificationCodeMethodGuard } from '@/types/guard';
type Parameters = {
method?: string;
@ -28,7 +28,7 @@ const SecondaryRegister = () => {
}
// Validate the verify settings
if (is(method, passcodeMethodGuard) && !signUpSettings.verify) {
if (is(method, verificationCodeMethodGuard) && !signUpSettings.verify) {
return <ErrorPage />;
}

View file

@ -7,7 +7,7 @@ import SecondaryPageWrapper from '@/components/SecondaryPageWrapper';
import PasswordSignInForm from '@/containers/PasswordSignInForm';
import { useSieMethods } from '@/hooks/use-sie';
import ErrorPage from '@/pages/ErrorPage';
import { passcodeStateGuard } from '@/types/guard';
import { verificationCodeStateGuard } from '@/types/guard';
import { formatPhoneNumberWithCountryCallingCode } from '@/utils/country-code';
import { isEmailOrPhoneMethod } from '@/utils/sign-in-experience';
@ -31,7 +31,7 @@ const SignInPassword = () => {
return <ErrorPage />;
}
const invalidState = !is(state, passcodeStateGuard);
const invalidState = !is(state, verificationCodeStateGuard);
const value = !invalidState && state[methodSetting.identifier];
if (!value) {

View file

@ -3,7 +3,7 @@ import { Routes, Route, MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import Passcode from '.';
import VerificationCode from '.';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@ -12,13 +12,13 @@ jest.mock('react-router-dom', () => ({
}),
}));
describe('Passcode Page', () => {
describe('VerificationCode Page', () => {
it('render properly', () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter initialEntries={['/sign-in/email/passcode-validation']}>
<MemoryRouter initialEntries={['/sign-in/email/verification-code']}>
<SettingsProvider>
<Routes>
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
<Route path="/:type/:method/verification-code" element={<VerificationCode />} />
</Routes>
</SettingsProvider>
</MemoryRouter>
@ -30,9 +30,9 @@ describe('Passcode Page', () => {
it('render with invalid method', () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter initialEntries={['/sign-in/username/passcode-validation']}>
<MemoryRouter initialEntries={['/sign-in/username/verification-code']}>
<Routes>
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
<Route path="/:type/:method/verification-code" element={<VerificationCode />} />
</Routes>
</MemoryRouter>
);
@ -43,9 +43,9 @@ describe('Passcode Page', () => {
it('render with invalid type', () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter initialEntries={['/social/email/passcode-validation']}>
<MemoryRouter initialEntries={['/social/email/verification-code']}>
<Routes>
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
<Route path="/:type/:method/verification-code" element={<VerificationCode />} />
</Routes>
</MemoryRouter>
);

View file

@ -3,11 +3,15 @@ import { useParams, useLocation } from 'react-router-dom';
import { is } from 'superstruct';
import SecondaryPageWrapper from '@/components/SecondaryPageWrapper';
import PasscodeValidation from '@/containers/PasscodeValidation';
import VerificationCodeContainer from '@/containers/VerificationCode';
import { useSieMethods } from '@/hooks/use-sie';
import ErrorPage from '@/pages/ErrorPage';
import { UserFlow } from '@/types';
import { passcodeStateGuard, passcodeMethodGuard, userFlowGuard } from '@/types/guard';
import {
verificationCodeStateGuard,
verificationCodeMethodGuard,
userFlowGuard,
} from '@/types/guard';
import { formatPhoneNumberWithCountryCallingCode } from '@/utils/country-code';
type Parameters = {
@ -15,14 +19,14 @@ type Parameters = {
method: string;
};
const Passcode = () => {
const VerificationCode = () => {
const { method, type = '' } = useParams<Parameters>();
const { signInMethods } = useSieMethods();
const { state } = useLocation();
const invalidType = !is(type, userFlowGuard);
const invalidMethod = !is(method, passcodeMethodGuard);
const invalidState = !is(state, passcodeStateGuard);
const invalidMethod = !is(method, verificationCodeMethodGuard);
const invalidState = !is(state, verificationCodeStateGuard);
if (invalidType || invalidMethod) {
return <ErrorPage />;
@ -50,7 +54,7 @@ const Passcode = () => {
target: method === 'email' ? target : formatPhoneNumberWithCountryCallingCode(target),
}}
>
<PasscodeValidation
<VerificationCodeContainer
type={type}
method={method}
target={target}
@ -60,4 +64,4 @@ const Passcode = () => {
);
};
export default Passcode;
export default VerificationCode;

View file

@ -8,12 +8,12 @@ export const bindSocialStateGuard = s.object({
}),
});
export const passcodeStateGuard = s.object({
export const verificationCodeStateGuard = s.object({
email: s.optional(s.string()),
phone: s.optional(s.string()),
});
export const passcodeMethodGuard = s.union([
export const verificationCodeMethodGuard = s.union([
s.literal(SignInIdentifier.Email),
s.literal(SignInIdentifier.Phone),
]);