mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
chore: merge branch master into feature/suspend
This commit is contained in:
commit
955812c7ed
86 changed files with 1852 additions and 727 deletions
8
.changeset/thin-oranges-act.md
Normal file
8
.changeset/thin-oranges-act.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
"@logto/core": minor
|
||||
"@logto/integration-tests": minor
|
||||
---
|
||||
|
||||
## 💥 Breaking change 💥
|
||||
|
||||
Use case-insensitive strategy for searching emails
|
|
@ -30,6 +30,7 @@
|
|||
top: 0;
|
||||
margin: 0;
|
||||
opacity: 0%;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
input:checked:not(:disabled) ~ .icon > svg:nth-child(1),
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
margin-right: _.unit(2);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: cover;
|
||||
margin-left: _.unit(1);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
import { ConnectorType } from '@logto/schemas';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Alert from '@/components/Alert';
|
||||
import Transfer from '@/components/Transfer';
|
||||
import UnnamedTrans from '@/components/UnnamedTrans';
|
||||
import useConnectorGroups from '@/hooks/use-connector-groups';
|
||||
import ConnectorPlatformIcon from '@/icons/ConnectorPlatformIcon';
|
||||
|
||||
import * as styles from './ConnectorsTransfer.module.scss';
|
||||
|
||||
type Props = {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
};
|
||||
|
||||
const ConnectorsTransfer = ({ value, onChange }: Props) => {
|
||||
const { data, error } = useConnectorGroups();
|
||||
const isLoading = !data && !error;
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
if (isLoading) {
|
||||
return <div>loading</div>;
|
||||
}
|
||||
|
||||
if (!data && error) {
|
||||
<div>{`error occurred: ${error.body?.message ?? error.message}`}</div>;
|
||||
}
|
||||
|
||||
const datasource = data
|
||||
? data
|
||||
.filter(({ type }) => type === ConnectorType.Social)
|
||||
.filter(({ enabled }) => enabled)
|
||||
.map(({ target, name, connectors, logo }) => ({
|
||||
value: target,
|
||||
title: (
|
||||
<div className={styles.title}>
|
||||
<div className={styles.logo}>
|
||||
<img src={logo} alt={target} />
|
||||
</div>
|
||||
<UnnamedTrans resource={name} />
|
||||
{connectors.length > 1 &&
|
||||
connectors
|
||||
.filter(({ enabled }) => enabled)
|
||||
.map(({ platform }) => (
|
||||
<div key={platform} className={styles.icon}>
|
||||
{platform && <ConnectorPlatformIcon platform={platform} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{datasource.length > 0 && value.length === 0 && (
|
||||
<Alert>{t('sign_in_exp.setup_warning.no_added_social_connector')}</Alert>
|
||||
)}
|
||||
<Transfer
|
||||
value={value}
|
||||
datasource={datasource}
|
||||
title={t('sign_in_exp.sign_in_methods.transfer.title')}
|
||||
footer={
|
||||
<div>
|
||||
{t('sign_in_exp.sign_in_methods.transfer.footer.not_in_list')}{' '}
|
||||
<Link to="/connectors/social" target="_blank">
|
||||
{t('sign_in_exp.sign_in_methods.transfer.footer.set_up_more')}
|
||||
</Link>{' '}
|
||||
{t('sign_in_exp.sign_in_methods.transfer.footer.go_to')}
|
||||
</div>
|
||||
}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectorsTransfer;
|
|
@ -50,6 +50,9 @@ const SignUpForm = () => {
|
|||
<>
|
||||
<div className={styles.title}>{t('sign_in_exp.sign_up_and_sign_in.sign_up.title')}</div>
|
||||
<FormField title="sign_in_exp.sign_up_and_sign_in.sign_up.sign_up_identifier">
|
||||
<div className={styles.formFieldDescription}>
|
||||
{t('sign_in_exp.sign_up_and_sign_in.sign_up.identifier_description')}
|
||||
</div>
|
||||
<Controller
|
||||
name="signUp.identifier"
|
||||
control={control}
|
||||
|
@ -92,6 +95,9 @@ const SignUpForm = () => {
|
|||
title="sign_in_exp.sign_up_and_sign_in.sign_up.sign_up_authentication"
|
||||
className={styles.signUpAuthentication}
|
||||
>
|
||||
<div className={styles.formFieldDescription}>
|
||||
{t('sign_in_exp.sign_up_and_sign_in.sign_up.authentication_description')}
|
||||
</div>
|
||||
<Controller
|
||||
name="signUp.password"
|
||||
control={control}
|
||||
|
|
|
@ -156,9 +156,17 @@ const SignInMethodEditBox = ({
|
|||
</DragDropProvider>
|
||||
<ConnectorSetupWarning
|
||||
requiredConnectors={value
|
||||
.reduce<ConnectorType[]>((connectors, { identifier: signInIdentifier }) => {
|
||||
return [...connectors, ...signInIdentifierToRequiredConnectorMapping[signInIdentifier]];
|
||||
}, [])
|
||||
.reduce<ConnectorType[]>(
|
||||
(connectors, { identifier: signInIdentifier, verificationCode }) => {
|
||||
return [
|
||||
...connectors,
|
||||
...(verificationCode
|
||||
? signInIdentifierToRequiredConnectorMapping[signInIdentifier]
|
||||
: []),
|
||||
];
|
||||
},
|
||||
[]
|
||||
)
|
||||
.filter((connector) => !ignoredWarningConnectors.includes(connector))}
|
||||
/>
|
||||
<AddButton
|
||||
|
|
|
@ -66,7 +66,7 @@ describe('validate sign-in', () => {
|
|||
});
|
||||
|
||||
describe('There must be at least one enabled connector for the specific identifier.', () => {
|
||||
it('throws when there is no enabled email connector and identifiers includes email', () => {
|
||||
it('throws when there is no enabled email connector and identifiers includes email with verification code checked', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
|
@ -74,6 +74,7 @@ describe('validate sign-in', () => {
|
|||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Email,
|
||||
verificationCode: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -88,7 +89,7 @@ describe('validate sign-in', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('throws when there is no enabled sms connector and identifiers includes phone', () => {
|
||||
it('throws when there is no enabled sms connector and identifiers includes phone with verification code checked', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
|
@ -96,6 +97,7 @@ describe('validate sign-in', () => {
|
|||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Sms,
|
||||
verificationCode: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -11,7 +11,12 @@ export const validateSignIn = (
|
|||
signUp: SignUp,
|
||||
enabledConnectors: LogtoConnector[]
|
||||
) => {
|
||||
if (signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Email)) {
|
||||
if (
|
||||
signIn.methods.some(
|
||||
({ identifier, verificationCode }) =>
|
||||
verificationCode && identifier === SignInIdentifier.Email
|
||||
)
|
||||
) {
|
||||
assertThat(
|
||||
enabledConnectors.some((item) => item.type === ConnectorType.Email),
|
||||
new RequestError({
|
||||
|
@ -21,7 +26,11 @@ export const validateSignIn = (
|
|||
);
|
||||
}
|
||||
|
||||
if (signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Sms)) {
|
||||
if (
|
||||
signIn.methods.some(
|
||||
({ identifier, verificationCode }) => verificationCode && identifier === SignInIdentifier.Sms
|
||||
)
|
||||
) {
|
||||
assertThat(
|
||||
enabledConnectors.some((item) => item.type === ConnectorType.Sms),
|
||||
new RequestError({
|
||||
|
|
|
@ -8,12 +8,7 @@ import { getLogtoConnectorById } from '@/connectors';
|
|||
import type { SocialUserInfo } from '@/connectors/types';
|
||||
import { socialUserInfoGuard } from '@/connectors/types';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import {
|
||||
findUserByEmail,
|
||||
findUserByPhone,
|
||||
hasUserWithEmail,
|
||||
hasUserWithPhone,
|
||||
} from '@/queries/user';
|
||||
import { findUserByEmail, findUserByPhone } from '@/queries/user';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
export type SocialUserInfoSession = {
|
||||
|
@ -88,16 +83,20 @@ export const getUserInfoFromInteractionResult = async (
|
|||
export const findSocialRelatedUser = async (
|
||||
info: SocialUserInfo
|
||||
): Promise<Nullable<[{ type: 'email' | 'phone'; value: string }, User]>> => {
|
||||
if (info.phone && (await hasUserWithPhone(info.phone))) {
|
||||
if (info.phone) {
|
||||
const user = await findUserByPhone(info.phone);
|
||||
|
||||
return [{ type: 'phone', value: info.phone }, user];
|
||||
if (user) {
|
||||
return [{ type: 'phone', value: info.phone }, user];
|
||||
}
|
||||
}
|
||||
|
||||
if (info.email && (await hasUserWithEmail(info.email))) {
|
||||
if (info.email) {
|
||||
const user = await findUserByEmail(info.email);
|
||||
|
||||
return [{ type: 'email', value: info.email }, user];
|
||||
if (user) {
|
||||
return [{ type: 'email', value: info.email }, user];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -18,14 +18,14 @@ export const findUserByUsername = async (username: string) =>
|
|||
`);
|
||||
|
||||
export const findUserByEmail = async (email: string) =>
|
||||
envSet.pool.one<User>(sql`
|
||||
envSet.pool.maybeOne<User>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
from ${table}
|
||||
where lower(${fields.primaryEmail})=lower(${email})
|
||||
`);
|
||||
|
||||
export const findUserByPhone = async (phone: string) =>
|
||||
envSet.pool.one<User>(sql`
|
||||
envSet.pool.maybeOne<User>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`,`)}
|
||||
from ${table}
|
||||
where ${fields.primaryPhone}=${phone}
|
||||
|
@ -47,11 +47,12 @@ export const findUserByIdentity = async (target: string, userId: string) =>
|
|||
`
|
||||
);
|
||||
|
||||
export const hasUser = async (username: string) =>
|
||||
export const hasUser = async (username: string, excludeUserId?: string) =>
|
||||
envSet.pool.exists(sql`
|
||||
select ${fields.id}
|
||||
from ${table}
|
||||
where ${fields.username}=${username}
|
||||
${conditionalSql(excludeUserId, (id) => sql`and ${fields.id}<>${id}`)}
|
||||
`);
|
||||
|
||||
export const hasUserWithId = async (id: string) =>
|
||||
|
@ -61,18 +62,20 @@ export const hasUserWithId = async (id: string) =>
|
|||
where ${fields.id}=${id}
|
||||
`);
|
||||
|
||||
export const hasUserWithEmail = async (email: string) =>
|
||||
export const hasUserWithEmail = async (email: string, excludeUserId?: string) =>
|
||||
envSet.pool.exists(sql`
|
||||
select ${fields.primaryEmail}
|
||||
from ${table}
|
||||
where lower(${fields.primaryEmail})=lower(${email})
|
||||
${conditionalSql(excludeUserId, (id) => sql`and ${fields.id}<>${id}`)}
|
||||
`);
|
||||
|
||||
export const hasUserWithPhone = async (phone: string) =>
|
||||
export const hasUserWithPhone = async (phone: string, excludeUserId?: string) =>
|
||||
envSet.pool.exists(sql`
|
||||
select ${fields.primaryPhone}
|
||||
from ${table}
|
||||
where ${fields.primaryPhone}=${phone}
|
||||
${conditionalSql(excludeUserId, (id) => sql`and ${fields.id}<>${id}`)}
|
||||
`);
|
||||
|
||||
export const hasUserWithIdentity = async (target: string, userId: string) =>
|
||||
|
|
|
@ -183,16 +183,29 @@ describe('adminUserRoutes', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('PATCH /users/:userId should allow empty avatar URL', async () => {
|
||||
const name = 'Michael';
|
||||
const avatar = '';
|
||||
|
||||
const response = await userRequest.patch('/users/foo').send({ name, avatar });
|
||||
it('PATCH /users/:userId should allow empty string for clearable fields', async () => {
|
||||
const response = await userRequest
|
||||
.patch('/users/foo')
|
||||
.send({ name: '', avatar: '', primaryEmail: '' });
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
...mockUserResponse,
|
||||
name,
|
||||
avatar,
|
||||
name: '',
|
||||
avatar: '',
|
||||
primaryEmail: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('PATCH /users/:userId should allow null values for clearable fields', async () => {
|
||||
const response = await userRequest
|
||||
.patch('/users/foo')
|
||||
.send({ name: null, username: null, primaryPhone: null });
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
...mockUserResponse,
|
||||
name: null,
|
||||
username: null,
|
||||
primaryPhone: null,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -171,10 +171,10 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
|
|||
koaGuard({
|
||||
params: object({ userId: string() }),
|
||||
body: object({
|
||||
username: string().regex(usernameRegEx).optional(),
|
||||
primaryEmail: string().regex(emailRegEx).optional(),
|
||||
primaryPhone: string().regex(phoneRegEx).optional(),
|
||||
name: string().nullable().optional(),
|
||||
username: string().regex(usernameRegEx).or(literal('')).nullable().optional(),
|
||||
primaryEmail: string().regex(emailRegEx).or(literal('')).nullable().optional(),
|
||||
primaryPhone: string().regex(phoneRegEx).or(literal('')).nullable().optional(),
|
||||
name: string().or(literal('')).nullable().optional(),
|
||||
avatar: string().url().or(literal('')).nullable().optional(),
|
||||
customData: arbitraryObjectGuard.optional(),
|
||||
roleNames: string().array().optional(),
|
||||
|
@ -187,7 +187,7 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
|
|||
} = ctx.guard;
|
||||
|
||||
await findUserById(userId);
|
||||
await checkExistingSignUpIdentifiers(body);
|
||||
await checkExistingSignUpIdentifiers(body, userId);
|
||||
|
||||
// Temp solution to validate the existence of input roleNames
|
||||
if (body.roleNames?.length) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
getContinueSignInResult,
|
||||
getRoutePrefix,
|
||||
getVerificationStorageFromInteraction,
|
||||
isUserPasswordSet,
|
||||
} from './utils';
|
||||
|
||||
export const continueRoute = getRoutePrefix('sign-in', 'continue');
|
||||
|
@ -42,7 +43,7 @@ export default function continueRoutes<T extends AnonymousRouter>(router: T, pro
|
|||
|
||||
// Social identities can take place the role of password
|
||||
assertThat(
|
||||
!user.passwordEncrypted && Object.keys(user.identities).length === 0,
|
||||
!isUserPasswordSet(user),
|
||||
new RequestError({
|
||||
code: 'user.password_exists',
|
||||
})
|
||||
|
|
|
@ -53,12 +53,8 @@ export const smsSignInAction = <StateT, ContextT extends WithLogContext, Respons
|
|||
|
||||
checkValidateExpiration(expiresAt);
|
||||
|
||||
assertThat(
|
||||
await hasUserWithPhone(phone),
|
||||
new RequestError({ code: 'user.phone_not_exists', status: 404 })
|
||||
);
|
||||
|
||||
const user = await findUserByPhone(phone);
|
||||
assertThat(user, new RequestError({ code: 'user.phone_not_exists', status: 404 }));
|
||||
const { id, isSuspended } = user;
|
||||
assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
ctx.log(type, { userId: id });
|
||||
|
@ -100,12 +96,8 @@ export const emailSignInAction = <StateT, ContextT extends WithLogContext, Respo
|
|||
|
||||
checkValidateExpiration(expiresAt);
|
||||
|
||||
assertThat(
|
||||
await hasUserWithEmail(email),
|
||||
new RequestError({ code: 'user.email_not_exists', status: 404 })
|
||||
);
|
||||
|
||||
const user = await findUserByEmail(email);
|
||||
assertThat(user, new RequestError({ code: 'user.email_not_exists', status: 404 }));
|
||||
const { id, isSuspended } = user;
|
||||
assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
ctx.log(type, { userId: id });
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* eslint-disable max-lines */
|
||||
import type { User } from '@logto/schemas';
|
||||
import { PasscodeType, SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
|
||||
import type { Nullable } from '@silverhand/essentials';
|
||||
import { addDays, addSeconds, subDays } from 'date-fns';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
|
@ -14,8 +15,8 @@ import passwordlessRoutes, { registerRoute, signInRoute } from './passwordless';
|
|||
|
||||
const insertUser = jest.fn(async (..._args: unknown[]) => mockUser);
|
||||
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||
const findUserByEmail = jest.fn(async (): Promise<User> => mockUser);
|
||||
const findUserByPhone = jest.fn(async (): Promise<User> => mockUser);
|
||||
const findUserByEmail = jest.fn(async (): Promise<Nullable<User>> => mockUser);
|
||||
const findUserByPhone = jest.fn(async (): Promise<Nullable<User>> => mockUser);
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => mockUser);
|
||||
const findDefaultSignInExperience = jest.fn(async () => ({
|
||||
...mockSignInExperience,
|
||||
|
@ -261,6 +262,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
});
|
||||
|
||||
it('throw 404 (with flow `forgot-password`)', async () => {
|
||||
findUserByPhone.mockResolvedValueOnce(null);
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/sms/verify')
|
||||
.send({ phone: '13000000001', code: '1234', flow: PasscodeType.ForgotPassword });
|
||||
|
@ -359,6 +361,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
it('throw 404 (with flow `forgot-password`)', async () => {
|
||||
const fakeTime = new Date();
|
||||
jest.useFakeTimers().setSystemTime(fakeTime);
|
||||
findUserByEmail.mockResolvedValueOnce(null);
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/email/verify')
|
||||
.send({ email: 'b@a.com', code: '1234', flow: PasscodeType.ForgotPassword });
|
||||
|
@ -502,6 +505,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
findUserByPhone.mockResolvedValueOnce(null);
|
||||
const response = await sessionRequest.post(`${signInRoute}/sms`);
|
||||
expect(response.statusCode).toEqual(404);
|
||||
});
|
||||
|
@ -658,6 +662,7 @@ describe('session -> passwordlessRoutes', () => {
|
|||
},
|
||||
},
|
||||
});
|
||||
findUserByEmail.mockResolvedValueOnce(null);
|
||||
const response = await sessionRequest.post(`${signInRoute}/email`);
|
||||
expect(response.statusCode).toEqual(404);
|
||||
});
|
||||
|
|
|
@ -6,12 +6,7 @@ import { object, string } from 'zod';
|
|||
import RequestError from '@/errors/RequestError';
|
||||
import { createPasscode, sendPasscode, verifyPasscode } from '@/lib/passcode';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import {
|
||||
findUserByEmail,
|
||||
findUserByPhone,
|
||||
hasUserWithEmail,
|
||||
hasUserWithPhone,
|
||||
} from '@/queries/user';
|
||||
import { findUserByEmail, findUserByPhone } from '@/queries/user';
|
||||
import { passcodeTypeGuard } from '@/routes/session/types';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
|
@ -105,13 +100,10 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
await verifyPasscode(jti, flow, code, { phone });
|
||||
|
||||
if (flow === PasscodeType.ForgotPassword) {
|
||||
assertThat(
|
||||
await hasUserWithPhone(phone),
|
||||
new RequestError({ code: 'user.phone_not_exists', status: 404 })
|
||||
);
|
||||
const user = await findUserByPhone(phone);
|
||||
assertThat(user, new RequestError({ code: 'user.phone_not_exists', status: 404 }));
|
||||
|
||||
const { id } = await findUserByPhone(phone);
|
||||
await assignVerificationResult(ctx, provider, { flow, userId: id });
|
||||
await assignVerificationResult(ctx, provider, { flow, userId: user.id });
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
|
@ -156,14 +148,11 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
await verifyPasscode(jti, flow, code, { email });
|
||||
|
||||
if (flow === PasscodeType.ForgotPassword) {
|
||||
assertThat(
|
||||
await hasUserWithEmail(email),
|
||||
new RequestError({ code: 'user.email_not_exists', status: 404 })
|
||||
);
|
||||
const user = await findUserByEmail(email);
|
||||
|
||||
const { id } = await findUserByEmail(email);
|
||||
assertThat(user, new RequestError({ code: 'user.email_not_exists', status: 404 }));
|
||||
|
||||
await assignVerificationResult(ctx, provider, { flow, userId: id });
|
||||
await assignVerificationResult(ctx, provider, { flow, userId: user.id });
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
|
|
|
@ -152,6 +152,13 @@ export const getContinueSignInResult = async (
|
|||
return rest;
|
||||
};
|
||||
|
||||
export const isUserPasswordSet = ({
|
||||
passwordEncrypted,
|
||||
identities,
|
||||
}: Pick<User, 'passwordEncrypted' | 'identities'>): boolean => {
|
||||
return Boolean(passwordEncrypted) || Object.keys(identities).length > 0;
|
||||
};
|
||||
|
||||
/* eslint-disable complexity */
|
||||
export const checkRequiredProfile = async (
|
||||
ctx: Context,
|
||||
|
@ -160,11 +167,11 @@ export const checkRequiredProfile = async (
|
|||
signInExperience: SignInExperience
|
||||
) => {
|
||||
const { signUp } = signInExperience;
|
||||
const { passwordEncrypted, id, username, primaryEmail, primaryPhone } = user;
|
||||
const { id, username, primaryEmail, primaryPhone } = user;
|
||||
|
||||
// If check failed, save the sign in result, the user can continue after requirements are meet
|
||||
|
||||
if (signUp.password && !passwordEncrypted) {
|
||||
if (signUp.password && !isUserPasswordSet(user)) {
|
||||
await assignContinueSignInResult(ctx, provider, { userId: id });
|
||||
throw new RequestError({ code: 'user.require_password', status: 422 });
|
||||
}
|
||||
|
@ -191,9 +198,9 @@ export const checkRequiredProfile = async (
|
|||
};
|
||||
|
||||
export const checkRequiredSignUpIdentifiers = async (identifiers: {
|
||||
username?: string;
|
||||
primaryEmail?: string;
|
||||
primaryPhone?: string;
|
||||
username?: Nullable<string>;
|
||||
primaryEmail?: Nullable<string>;
|
||||
primaryPhone?: Nullable<string>;
|
||||
}) => {
|
||||
const { username, primaryEmail, primaryPhone } = identifiers;
|
||||
|
||||
|
@ -217,22 +224,25 @@ export const checkRequiredSignUpIdentifiers = async (identifiers: {
|
|||
};
|
||||
/* eslint-enable complexity */
|
||||
|
||||
export const checkExistingSignUpIdentifiers = async (identifiers: {
|
||||
username?: string;
|
||||
primaryEmail?: string;
|
||||
primaryPhone?: string;
|
||||
}) => {
|
||||
export const checkExistingSignUpIdentifiers = async (
|
||||
identifiers: {
|
||||
username?: Nullable<string>;
|
||||
primaryEmail?: Nullable<string>;
|
||||
primaryPhone?: Nullable<string>;
|
||||
},
|
||||
excludeUserId: string
|
||||
) => {
|
||||
const { username, primaryEmail, primaryPhone } = identifiers;
|
||||
|
||||
if (username && (await hasUser(username))) {
|
||||
if (username && (await hasUser(username, excludeUserId))) {
|
||||
throw new RequestError({ code: 'user.username_exists', status: 422 });
|
||||
}
|
||||
|
||||
if (primaryEmail && (await hasUserWithEmail(primaryEmail))) {
|
||||
if (primaryEmail && (await hasUserWithEmail(primaryEmail, excludeUserId))) {
|
||||
throw new RequestError({ code: 'user.email_exists', status: 422 });
|
||||
}
|
||||
|
||||
if (primaryPhone && (await hasUserWithPhone(primaryPhone))) {
|
||||
if (primaryPhone && (await hasUserWithPhone(primaryPhone, excludeUserId))) {
|
||||
throw new RequestError({ code: 'user.sms_exists', status: 422 });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -67,11 +67,22 @@ const translation = {
|
|||
reset_password_description_sms:
|
||||
'Gib die Telefonnummer deines Kontos ein und wir senden dir einen Bestätigungscode um dein Passwort zurückzusetzen.',
|
||||
new_password: 'Neues Passwort',
|
||||
set_password: 'Set password', // UNTRANSLATED
|
||||
password_changed: 'Passwort geändert',
|
||||
no_account: "Don't have an account?", // UNTRANSLATED
|
||||
have_account: 'Already have an account?', // UNTRANSLATED
|
||||
enter_password: 'Enter Password', // UNTRANSLATED
|
||||
enter_password_for: 'Enter the password of {{method}} {{value}}', // UNTRANSLATED
|
||||
enter_username: 'Enter username', // UNTRANSLATED
|
||||
enter_username_description:
|
||||
'Username is an alternative for sign-in. Username must contain only letters, numbers, and underscores.', // UNTRANSLATED
|
||||
link_email: 'Link email', // UNTRANSLATED
|
||||
link_phone: 'Link phone', // UNTRANSLATED
|
||||
link_email_or_phone: 'Link email or phone', // UNTRANSLATED
|
||||
link_email_description: 'Link your email to sign in or help with account recovery.', // UNTRANSLATED
|
||||
link_phone_description: 'Link your phone number to sign in or help with account recovery.', // UNTRANSLATED
|
||||
link_email_or_phone_description:
|
||||
'Link your email or phone number to sign in or help with account recovery.', // UNTRANSLATED
|
||||
},
|
||||
error: {
|
||||
username_password_mismatch: 'Benutzername oder Passwort ist falsch',
|
||||
|
|
|
@ -63,11 +63,22 @@ const translation = {
|
|||
reset_password_description_sms:
|
||||
'Enter the phone number associated with your account, and we’ll message you the verification code to reset your password.',
|
||||
new_password: 'New password',
|
||||
set_password: 'Set password',
|
||||
password_changed: 'Password Changed',
|
||||
no_account: "Don't have an account?",
|
||||
have_account: 'Already have an account?',
|
||||
enter_password: 'Enter Password',
|
||||
enter_password_for: 'Enter the password of {{method}} {{value}}',
|
||||
enter_username: 'Enter username',
|
||||
enter_username_description:
|
||||
'Username is an alternative for sign-in. Username must contain only letters, numbers, and underscores.',
|
||||
link_email: 'Link email',
|
||||
link_phone: 'Link phone',
|
||||
link_email_or_phone: 'Link email or phone',
|
||||
link_email_description: 'Link your email to sign in or help with account recovery.',
|
||||
link_phone_description: 'Link your phone number to sign in or help with account recovery.',
|
||||
link_email_or_phone_description:
|
||||
'Link your email or phone number to sign in or help with account recovery.',
|
||||
},
|
||||
error: {
|
||||
username_password_mismatch: 'Username and password do not match',
|
||||
|
|
|
@ -67,11 +67,22 @@ const translation = {
|
|||
reset_password_description_sms:
|
||||
'Entrez le numéro de téléphone associé à votre compte et nous vous enverrons le code de vérification par SMS pour réinitialiser votre mot de passe.',
|
||||
new_password: 'Nouveau mot de passe',
|
||||
set_password: 'Set password', // UNTRANSLATED
|
||||
password_changed: 'Password Changed', // UNTRANSLATED
|
||||
no_account: "Don't have an account?", // UNTRANSLATED
|
||||
have_account: 'Already have an account?', // UNTRANSLATED
|
||||
enter_password: 'Enter Password', // UNTRANSLATED
|
||||
enter_password_for: 'Enter the password of {{method}} {{value}}', // UNTRANSLATED
|
||||
enter_username: 'Enter username', // UNTRANSLATED
|
||||
enter_username_description:
|
||||
'Username is an alternative for sign-in. Username must contain only letters, numbers, and underscores.', // UNTRANSLATED
|
||||
link_email: 'Link email', // UNTRANSLATED
|
||||
link_phone: 'Link phone', // UNTRANSLATED
|
||||
link_email_or_phone: 'Link email or phone', // UNTRANSLATED
|
||||
link_email_description: 'Link your email to sign in or help with account recovery.', // UNTRANSLATED
|
||||
link_phone_description: 'Link your phone number to sign in or help with account recovery.', // UNTRANSLATED
|
||||
link_email_or_phone_description:
|
||||
'Link your email or phone number to sign in or help with account recovery.', // UNTRANSLATED
|
||||
},
|
||||
error: {
|
||||
username_password_mismatch: "Le nom d'utilisateur et le mot de passe ne correspondent pas",
|
||||
|
|
|
@ -63,11 +63,22 @@ const translation = {
|
|||
reset_password_description_sms:
|
||||
'계정과 연결된 전화번호를 입력하면 비밀번호 재설정을 위한 인증 코드를 문자로 보내드립니다.',
|
||||
new_password: '새 비밀번호',
|
||||
set_password: 'Set password', // UNTRANSLATED
|
||||
password_changed: 'Password Changed', // UNTRANSLATED
|
||||
no_account: "Don't have an account?", // UNTRANSLATED
|
||||
have_account: 'Already have an account?', // UNTRANSLATED
|
||||
enter_password: 'Enter Password', // UNTRANSLATED
|
||||
enter_password_for: 'Enter the password of {{method}} {{value}}', // UNTRANSLATED
|
||||
enter_username: 'Enter username', // UNTRANSLATED
|
||||
enter_username_description:
|
||||
'Username is an alternative for sign-in. Username must contain only letters, numbers, and underscores.', // UNTRANSLATED
|
||||
link_email: 'Link email', // UNTRANSLATED
|
||||
link_phone: 'Link phone', // UNTRANSLATED
|
||||
link_email_or_phone: 'Link email or phone', // UNTRANSLATED
|
||||
link_email_description: 'Link your email to sign in or help with account recovery.', // UNTRANSLATED
|
||||
link_phone_description: 'Link your phone number to sign in or help with account recovery.', // UNTRANSLATED
|
||||
link_email_or_phone_description:
|
||||
'Link your email or phone number to sign in or help with account recovery.', // UNTRANSLATED
|
||||
},
|
||||
error: {
|
||||
username_password_mismatch: '사용자 이름 또는 비밀번호가 일치하지 않아요.',
|
||||
|
|
|
@ -63,11 +63,22 @@ const translation = {
|
|||
reset_password_description_sms:
|
||||
'Digite o número de telefone associado à sua conta e enviaremos uma mensagem de texto com o código de verificação para redefinir sua senha.',
|
||||
new_password: 'Nova Senha',
|
||||
set_password: 'Set password', // UNTRANSLATED
|
||||
password_changed: 'Password Changed', // UNTRANSLATED
|
||||
no_account: "Don't have an account?", // UNTRANSLATED
|
||||
have_account: 'Already have an account?', // UNTRANSLATED
|
||||
enter_password: 'Enter Password', // UNTRANSLATED
|
||||
enter_password_for: 'Enter the password of {{method}} {{value}}', // UNTRANSLATED
|
||||
enter_username: 'Enter username', // UNTRANSLATED
|
||||
enter_username_description:
|
||||
'Username is an alternative for sign-in. Username must contain only letters, numbers, and underscores.', // UNTRANSLATED
|
||||
link_email: 'Link email', // UNTRANSLATED
|
||||
link_phone: 'Link phone', // UNTRANSLATED
|
||||
link_email_or_phone: 'Link email or phone', // UNTRANSLATED
|
||||
link_email_description: 'Link your email to sign in or help with account recovery.', // UNTRANSLATED
|
||||
link_phone_description: 'Link your phone number to sign in or help with account recovery.', // UNTRANSLATED
|
||||
link_email_or_phone_description:
|
||||
'Link your email or phone number to sign in or help with account recovery.', // UNTRANSLATED
|
||||
},
|
||||
error: {
|
||||
username_password_mismatch: 'O Utilizador e a password não correspondem',
|
||||
|
|
|
@ -64,11 +64,22 @@ const translation = {
|
|||
reset_password_description_sms:
|
||||
'Hesabınızla ilişkili telefon numarasını girin, şifrenizi sıfırlamak için size doğrulama kodunu kısa mesajla gönderelim.',
|
||||
new_password: 'Yeni Şifre',
|
||||
set_password: 'Set password', // UNTRANSLATED
|
||||
password_changed: 'Password Changed', // UNTRANSLATED
|
||||
no_account: "Don't have an account?", // UNTRANSLATED
|
||||
have_account: 'Already have an account?', // UNTRANSLATED
|
||||
enter_password: 'Enter Password', // UNTRANSLATED
|
||||
enter_password_for: 'Enter the password of {{method}} {{value}}', // UNTRANSLATED
|
||||
enter_username: 'Enter username', // UNTRANSLATED
|
||||
enter_username_description:
|
||||
'Username is an alternative for sign-in. Username must contain only letters, numbers, and underscores.', // UNTRANSLATED
|
||||
link_email: 'Link email', // UNTRANSLATED
|
||||
link_phone: 'Link phone', // UNTRANSLATED
|
||||
link_email_or_phone: 'Link email or phone', // UNTRANSLATED
|
||||
link_email_description: 'Link your email to sign in or help with account recovery.', // UNTRANSLATED
|
||||
link_phone_description: 'Link your phone number to sign in or help with account recovery.', // UNTRANSLATED
|
||||
link_email_or_phone_description:
|
||||
'Link your email or phone number to sign in or help with account recovery.', // UNTRANSLATED
|
||||
},
|
||||
error: {
|
||||
username_password_mismatch: 'Kullanıcı adı ve şifre eşleşmiyor.',
|
||||
|
|
|
@ -61,11 +61,22 @@ const translation = {
|
|||
reset_password_description_email: '输入邮件地址,领取验证码以重设密码。',
|
||||
reset_password_description_sms: '输入手机号,领取验证码以重设密码。',
|
||||
new_password: '新密码',
|
||||
set_password: 'Set password', // UNTRANSLATED
|
||||
password_changed: '已重置密码!',
|
||||
no_account: "Don't have an account?", // UNTRANSLATED
|
||||
have_account: 'Already have an account?', // UNTRANSLATED
|
||||
enter_password: 'Enter Password', // UNTRANSLATED
|
||||
enter_password_for: 'Enter the password of {{method}} {{value}}', // UNTRANSLATED
|
||||
enter_username: 'Enter username', // UNTRANSLATED
|
||||
enter_username_description:
|
||||
'Username is an alternative for sign-in. Username must contain only letters, numbers, and underscores.', // UNTRANSLATED
|
||||
link_email: 'Link email', // UNTRANSLATED
|
||||
link_phone: 'Link phone', // UNTRANSLATED
|
||||
link_email_or_phone: 'Link email or phone', // UNTRANSLATED
|
||||
link_email_description: 'Link your email to sign in or help with account recovery.', // UNTRANSLATED
|
||||
link_phone_description: 'Link your phone number to sign in or help with account recovery.', // UNTRANSLATED
|
||||
link_email_or_phone_description:
|
||||
'Link your email or phone number to sign in or help with account recovery.', // UNTRANSLATED
|
||||
},
|
||||
error: {
|
||||
username_password_mismatch: '用户名和密码不匹配',
|
||||
|
|
|
@ -17,48 +17,52 @@ const sign_in_exp = {
|
|||
got_it: 'Alles klar',
|
||||
},
|
||||
sign_up_and_sign_in: {
|
||||
identifiers: 'Sign up identifiers', // UNTRANSLATED
|
||||
identifiers: 'Sign-up identifiers', // UNTRANSLATED
|
||||
identifiers_email: 'Email address', // UNTRANSLATED
|
||||
identifiers_sms: 'Phone number', // UNTRANSLATED
|
||||
identifiers_username: 'Username', // UNTRANSLATED
|
||||
identifiers_email_or_sms: 'Email address or phone number', // UNTRANSLATED
|
||||
identifiers_none: 'None', // UNTRANSLATED
|
||||
identifiers_none: 'Not applicable', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
sign_up: {
|
||||
title: 'SIGN UP', // UNTRANSLATED
|
||||
sign_up_identifier: 'Sign up identifier', // UNTRANSLATED
|
||||
sign_up_authentication: 'Sign up authentication', // UNTRANSLATED
|
||||
set_a_password_option: 'Set a password', // UNTRANSLATED
|
||||
verify_at_sign_up_option: 'Verify at sign up', // UNTRANSLATED
|
||||
sign_up_identifier: 'Sign-up identifier', // UNTRANSLATED
|
||||
identifier_description:
|
||||
'The sign-up identifier is required for account creation and must be included in your sign-in screen.', // UNTRANSLATED
|
||||
sign_up_authentication: 'Authentication setting for sign-up', // UNTRANSLATED
|
||||
authentication_description:
|
||||
'All selected actions will be obligatory for users to complete the flow.', // UNTRANSLATED
|
||||
set_a_password_option: 'Create your password', // UNTRANSLATED
|
||||
verify_at_sign_up_option: 'Verify at sign-up', // UNTRANSLATED
|
||||
social_only_creation_description: '(This apply to social only account creation)', // UNTRANSLATED
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Identifier and authentication settings for sign-in', // UNTRANSLATED
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.', // UNTRANSLATED
|
||||
'Users can sign in using any of the options available. Adjust the layout by drag and dropping below options.', // UNTRANSLATED
|
||||
add_sign_in_method: 'Add Sign-in Method', // UNTRANSLATED
|
||||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.', // UNTRANSLATED
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN IN', // UNTRANSLATED
|
||||
social_sign_in: 'Social sign in', // UNTRANSLATED
|
||||
title: 'SOCIAL SIGN-IN', // UNTRANSLATED
|
||||
social_sign_in: 'Social sign-in', // UNTRANSLATED
|
||||
description:
|
||||
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.', // UNTRANSLATED
|
||||
add_social_connector: 'Add Social Connector', // UNTRANSLATED
|
||||
'Depending on the mandatory identifier you set up, your user may be asked to provide an identifier when signing up via social connector.', // UNTRANSLATED
|
||||
add_social_connector: 'Link Social Connector', // UNTRANSLATED
|
||||
set_up_hint: {
|
||||
not_in_list: 'Not in the list?', // UNTRANSLATED
|
||||
set_up_more: 'Set up more', // UNTRANSLATED
|
||||
go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED
|
||||
set_up_more: 'Set up', // UNTRANSLATED
|
||||
go_to: 'other social connectors now.', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
tip: {
|
||||
set_a_password: 'A unique set of a password to your username is a must.', // UNTRANSLATED
|
||||
verify_at_sign_up:
|
||||
'Right now we only support email verified at sign up but soon to open this capability!', // UNTRANSLATED
|
||||
'We currently only support verified email. Your user base may contain a large number of poor-quality email addresses if no validation.', // UNTRANSLATED
|
||||
password_auth:
|
||||
'This is essential as you have enabled the option to set a password during the sign-up process.', // UNTRANSLATED
|
||||
verification_code_auth:
|
||||
|
@ -91,28 +95,6 @@ const sign_in_exp = {
|
|||
slogan: 'Slogan',
|
||||
slogan_placeholder: 'Entfessle deine Kreativität',
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: 'ANMELDEMETHODEN',
|
||||
primary: 'Primäre Anmeldemethode',
|
||||
enable_secondary: 'Aktiviere sekundäre Anmeldemethoden',
|
||||
enable_secondary_description:
|
||||
'Sobald sie aktiviert ist, unterstützt deine App neben der primären Anmeldemethode noch weitere Anmeldemethoden. ',
|
||||
methods: 'Anmeldemethode',
|
||||
methods_sms: 'SMS Anmeldung',
|
||||
methods_email: 'E-Mail Anmeldung',
|
||||
methods_social: 'Social Anmeldung',
|
||||
methods_username: 'Benutzername-und-Passwort Anmeldung',
|
||||
methods_primary_tag: '(Primär)',
|
||||
define_social_methods: 'Definiere die unterstützten Social Anmeldemethoden',
|
||||
transfer: {
|
||||
title: 'Social Connectoren',
|
||||
footer: {
|
||||
not_in_list: 'Nicht in der Liste?',
|
||||
set_up_more: 'Mehr Social Connectoren einrichten',
|
||||
go_to: 'oder "Connectoren" aufrufen.',
|
||||
},
|
||||
},
|
||||
},
|
||||
others: {
|
||||
terms_of_use: {
|
||||
title: 'NUTZUNGSBEDINGUNGEN',
|
||||
|
@ -168,21 +150,21 @@ const sign_in_exp = {
|
|||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_sms:
|
||||
'Du hast noch keinen SMS Connector eingerichtet. Deine Anmeldung wird erst freigeschaltet, wenn du die Einstellungen abgeschlossen hast. ',
|
||||
'No SMS connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_connector_email:
|
||||
'Du hast noch keinen E-Mail Connector eingerichtet. Deine Anmeldung wird erst freigeschaltet, wenn du die Einstellungen abgeschlossen hast. ',
|
||||
'No email connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_connector_social:
|
||||
'Du hast noch keinen Social Connector eingerichtet. Deine Anmeldung wird erst freigeschaltet, wenn du die Einstellungen abgeschlossen hast. ',
|
||||
'No social connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_added_social_connector:
|
||||
'Du hast jetzt ein paar Social Connectoren eingerichtet. Füge jetzt einige zu deinem Anmeldeerlebnis hinzu.',
|
||||
},
|
||||
save_alert: {
|
||||
description:
|
||||
'Du änderst die Anmeldemethoden. Das wird sich auf einige deiner Benutzer auswirken. Bist du sicher, dass du das tun willst?',
|
||||
'You are implementing new sign-in and sign-up procedures. All of your users may be affected by the new set-up. Are you sure to commit to the change?', // UNTRANSLATED
|
||||
before: 'Vorher',
|
||||
after: 'Nachher',
|
||||
sign_up: 'Sign up', // UNTRANSLATED
|
||||
sign_in: 'Sign in', // UNTRANSLATED
|
||||
sign_up: 'Sign-up', // UNTRANSLATED
|
||||
sign_in: 'Sign-in', // UNTRANSLATED
|
||||
social: 'Social', // UNTRANSLATED
|
||||
},
|
||||
preview: {
|
||||
|
|
|
@ -39,48 +39,52 @@ const sign_in_exp = {
|
|||
slogan_placeholder: 'Unleash your creativity',
|
||||
},
|
||||
sign_up_and_sign_in: {
|
||||
identifiers: 'Sign up identifiers',
|
||||
identifiers: 'Sign-up identifiers',
|
||||
identifiers_email: 'Email address',
|
||||
identifiers_sms: 'Phone number',
|
||||
identifiers_username: 'Username',
|
||||
identifiers_email_or_sms: 'Email address or phone number',
|
||||
identifiers_none: 'None',
|
||||
identifiers_none: 'Not applicable',
|
||||
and: 'and',
|
||||
or: 'or',
|
||||
sign_up: {
|
||||
title: 'SIGN UP',
|
||||
sign_up_identifier: 'Sign up identifier',
|
||||
sign_up_authentication: 'Sign up authentication',
|
||||
set_a_password_option: 'Set a password',
|
||||
verify_at_sign_up_option: 'Verify at sign up',
|
||||
sign_up_identifier: 'Sign-up identifier',
|
||||
identifier_description:
|
||||
'The sign-up identifier is required for account creation and must be included in your sign-in screen.',
|
||||
sign_up_authentication: 'Authentication setting for sign-up',
|
||||
authentication_description:
|
||||
'All selected actions will be obligatory for users to complete the flow.',
|
||||
set_a_password_option: 'Create your password',
|
||||
verify_at_sign_up_option: 'Verify at sign-up',
|
||||
social_only_creation_description: '(This apply to social only account creation)',
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN',
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication',
|
||||
sign_in_identifier_and_auth: 'Identifier and authentication settings for sign-in',
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.',
|
||||
'Users can sign in using any of the options available. Adjust the layout by drag and dropping below options.',
|
||||
add_sign_in_method: 'Add Sign-in Method',
|
||||
password_auth: 'Password',
|
||||
verification_code_auth: 'Verification code',
|
||||
auth_swap_tip: 'Swap to change the priority',
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.',
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN IN',
|
||||
social_sign_in: 'Social sign in',
|
||||
title: 'SOCIAL SIGN-IN',
|
||||
social_sign_in: 'Social sign-in',
|
||||
description:
|
||||
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.',
|
||||
add_social_connector: 'Add Social Connector',
|
||||
'Depending on the mandatory identifier you set up, your user may be asked to provide an identifier when signing up via social connector.',
|
||||
add_social_connector: 'Link Social Connector',
|
||||
set_up_hint: {
|
||||
not_in_list: 'Not in the list?',
|
||||
set_up_more: 'Set up more',
|
||||
go_to: 'social connectors or go to “Connectors” section.',
|
||||
set_up_more: 'Set up',
|
||||
go_to: 'other social connectors now.',
|
||||
},
|
||||
},
|
||||
tip: {
|
||||
set_a_password: 'A unique set of a password to your username is a must.',
|
||||
verify_at_sign_up:
|
||||
'Right now we only support email verified at sign up but soon to open this capability!',
|
||||
'We currently only support verified email. Your user base may contain a large number of poor-quality email addresses if no validation.',
|
||||
password_auth:
|
||||
'This is essential as you have enabled the option to set a password during the sign-up process.',
|
||||
verification_code_auth:
|
||||
|
@ -89,28 +93,6 @@ const sign_in_exp = {
|
|||
'This is essential as you have selected {{identifier}} as a required identifier.',
|
||||
},
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: 'SIGN-IN METHODS',
|
||||
primary: 'Primary sign-in method',
|
||||
enable_secondary: 'Enable secondary sign in',
|
||||
enable_secondary_description:
|
||||
"Once it's turned on, you app will support more sign-in method(s) besides the primary one. ",
|
||||
methods: 'Sign-in method',
|
||||
methods_sms: 'Phone number sign in',
|
||||
methods_email: 'Email sign in',
|
||||
methods_social: 'Social sign in',
|
||||
methods_username: 'Username-with-password sign in',
|
||||
methods_primary_tag: '(Primary)',
|
||||
define_social_methods: 'Define social sign-in methods',
|
||||
transfer: {
|
||||
title: 'Social connectors',
|
||||
footer: {
|
||||
not_in_list: 'Not in the list?',
|
||||
set_up_more: 'Set up more',
|
||||
go_to: 'social connectors or go to “Connectors” section.',
|
||||
},
|
||||
},
|
||||
},
|
||||
others: {
|
||||
terms_of_use: {
|
||||
title: 'TERMS OF USE',
|
||||
|
@ -162,21 +144,21 @@ const sign_in_exp = {
|
|||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_sms:
|
||||
'You haven’t set up a SMS connector yet. Your sign in experience won’t go live until you finish the settings first. ',
|
||||
'No SMS connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.',
|
||||
no_connector_email:
|
||||
'You haven’t set up an Email connector yet. Your sign in experience won’t go live until you finish the settings first. ',
|
||||
'No email connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.',
|
||||
no_connector_social:
|
||||
'You haven’t set up any social connectors yet. Your sign in experience won’t go live until you finish the settings first. ',
|
||||
'No social connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.',
|
||||
no_added_social_connector:
|
||||
'You’ve set up a few social connectors now. Make sure to add some to your sign in experience.',
|
||||
},
|
||||
save_alert: {
|
||||
description:
|
||||
'You are changing sign-in methods. This will impact some of your users. Are you sure you want to do that?',
|
||||
'You are implementing new sign-in and sign-up procedures. All of your users may be affected by the new set-up. Are you sure to commit to the change?',
|
||||
before: 'Before',
|
||||
after: 'After',
|
||||
sign_up: 'Sign up',
|
||||
sign_in: 'Sign in',
|
||||
sign_up: 'Sign-up',
|
||||
sign_in: 'Sign-in',
|
||||
social: 'Social',
|
||||
},
|
||||
preview: {
|
||||
|
|
|
@ -41,48 +41,52 @@ const sign_in_exp = {
|
|||
slogan_placeholder: 'Libérez votre créativité',
|
||||
},
|
||||
sign_up_and_sign_in: {
|
||||
identifiers: 'Sign up identifiers', // UNTRANSLATED
|
||||
identifiers: 'Sign-up identifiers', // UNTRANSLATED
|
||||
identifiers_email: 'Email address', // UNTRANSLATED
|
||||
identifiers_sms: 'Phone number', // UNTRANSLATED
|
||||
identifiers_username: 'Username', // UNTRANSLATED
|
||||
identifiers_email_or_sms: 'Email address or phone number', // UNTRANSLATED
|
||||
identifiers_none: 'None', // UNTRANSLATED
|
||||
identifiers_none: 'Not applicable', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
sign_up: {
|
||||
title: 'SIGN UP', // UNTRANSLATED
|
||||
sign_up_identifier: 'Sign up identifier', // UNTRANSLATED
|
||||
sign_up_authentication: 'Sign up authentication', // UNTRANSLATED
|
||||
set_a_password_option: 'Set a password', // UNTRANSLATED
|
||||
verify_at_sign_up_option: 'Verify at sign up', // UNTRANSLATED
|
||||
sign_up_identifier: 'Sign-up identifier', // UNTRANSLATED
|
||||
identifier_description:
|
||||
'The sign-up identifier is required for account creation and must be included in your sign-in screen.', // UNTRANSLATED
|
||||
sign_up_authentication: 'Authentication setting for sign-up', // UNTRANSLATED
|
||||
authentication_description:
|
||||
'All selected actions will be obligatory for users to complete the flow.', // UNTRANSLATED
|
||||
set_a_password_option: 'Create your password', // UNTRANSLATED
|
||||
verify_at_sign_up_option: 'Verify at sign-up', // UNTRANSLATED
|
||||
social_only_creation_description: '(This apply to social only account creation)', // UNTRANSLATED
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Identifier and authentication settings for sign-in', // UNTRANSLATED
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.', // UNTRANSLATED
|
||||
'Users can sign in using any of the options available. Adjust the layout by drag and dropping below options.', // UNTRANSLATED
|
||||
add_sign_in_method: 'Add Sign-in Method', // UNTRANSLATED
|
||||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.', // UNTRANSLATED
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN IN', // UNTRANSLATED
|
||||
social_sign_in: 'Social sign in', // UNTRANSLATED
|
||||
title: 'SOCIAL SIGN-IN', // UNTRANSLATED
|
||||
social_sign_in: 'Social sign-in', // UNTRANSLATED
|
||||
description:
|
||||
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.', // UNTRANSLATED
|
||||
add_social_connector: 'Add Social Connector', // UNTRANSLATED
|
||||
'Depending on the mandatory identifier you set up, your user may be asked to provide an identifier when signing up via social connector.', // UNTRANSLATED
|
||||
add_social_connector: 'Link Social Connector', // UNTRANSLATED
|
||||
set_up_hint: {
|
||||
not_in_list: 'Not in the list?', // UNTRANSLATED
|
||||
set_up_more: 'Set up more', // UNTRANSLATED
|
||||
go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED
|
||||
set_up_more: 'Set up', // UNTRANSLATED
|
||||
go_to: 'other social connectors now.', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
tip: {
|
||||
set_a_password: 'A unique set of a password to your username is a must.', // UNTRANSLATED
|
||||
verify_at_sign_up:
|
||||
'Right now we only support email verified at sign up but soon to open this capability!', // UNTRANSLATED
|
||||
'We currently only support verified email. Your user base may contain a large number of poor-quality email addresses if no validation.', // UNTRANSLATED
|
||||
password_auth:
|
||||
'This is essential as you have enabled the option to set a password during the sign-up process.', // UNTRANSLATED
|
||||
verification_code_auth:
|
||||
|
@ -91,28 +95,6 @@ const sign_in_exp = {
|
|||
'This is essential as you have selected {{identifier}} as a required identifier.', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: 'METHODES DE CONNEXION',
|
||||
primary: 'Méthode de connexion principale',
|
||||
enable_secondary: 'Activer une seconde méthode de connexion',
|
||||
enable_secondary_description:
|
||||
"Une fois qu'elle est activée, votre application prend en charge d'autres méthodes de connexion que la méthode principale. ",
|
||||
methods: 'Méthode de connexion',
|
||||
methods_sms: 'Connexion avec numéro de téléphone',
|
||||
methods_email: 'Connexion avec email',
|
||||
methods_social: 'Connexion avec social',
|
||||
methods_username: "Connexion avec nom d'utilisateur et mot de passe",
|
||||
methods_primary_tag: '(Principale)',
|
||||
define_social_methods: "Définir les méthodes d'authentification sociale",
|
||||
transfer: {
|
||||
title: 'Connecteurs sociaux',
|
||||
footer: {
|
||||
not_in_list: 'Pas dans la liste ?',
|
||||
set_up_more: 'Configurer plus',
|
||||
go_to: 'connecteurs sociaux ou allez à la section "Connecteurs".',
|
||||
},
|
||||
},
|
||||
},
|
||||
others: {
|
||||
terms_of_use: {
|
||||
title: "CONDITIONS D'UTILISATION",
|
||||
|
@ -164,21 +146,21 @@ const sign_in_exp = {
|
|||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_sms:
|
||||
"Vous n'avez pas encore configuré de connecteur SMS. Votre expérience de connexion ne sera pas disponible tant que vous n'aurez pas terminé les paramètres. ",
|
||||
'No SMS connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_connector_email:
|
||||
"Vous n'avez pas encore configuré de connecteur Email. Votre expérience de connexion ne sera pas disponible tant que vous n'aurez pas terminé les paramètres. ",
|
||||
'No email connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_connector_social:
|
||||
"Vous n'avez pas encore configuré de connecteurs sociaux. Votre expérience de connexion ne sera pas disponible tant que vous n'aurez pas terminé les paramètres. ",
|
||||
'No social connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_added_social_connector:
|
||||
"Vous avez maintenant configuré quelques connecteurs sociaux. Assurez-vous d'en ajouter quelques-uns à votre expérience de connexion.",
|
||||
},
|
||||
save_alert: {
|
||||
description:
|
||||
'Vous changez de méthode de connexion. Cela aura un impact sur certains de vos utilisateurs. Êtes-vous sûr de vouloir faire cela ?',
|
||||
'You are implementing new sign-in and sign-up procedures. All of your users may be affected by the new set-up. Are you sure to commit to the change?', // UNTRANSLATED
|
||||
before: 'Avant',
|
||||
after: 'Après',
|
||||
sign_up: 'Sign up', // UNTRANSLATED
|
||||
sign_in: 'Sign in', // UNTRANSLATED
|
||||
sign_up: 'Sign-up', // UNTRANSLATED
|
||||
sign_in: 'Sign-in', // UNTRANSLATED
|
||||
social: 'Social', // UNTRANSLATED
|
||||
},
|
||||
preview: {
|
||||
|
|
|
@ -36,48 +36,52 @@ const sign_in_exp = {
|
|||
slogan_placeholder: 'Unleash your creativity',
|
||||
},
|
||||
sign_up_and_sign_in: {
|
||||
identifiers: 'Sign up identifiers', // UNTRANSLATED
|
||||
identifiers: 'Sign-up identifiers', // UNTRANSLATED
|
||||
identifiers_email: 'Email address', // UNTRANSLATED
|
||||
identifiers_sms: 'Phone number', // UNTRANSLATED
|
||||
identifiers_username: 'Username', // UNTRANSLATED
|
||||
identifiers_email_or_sms: 'Email address or phone number', // UNTRANSLATED
|
||||
identifiers_none: 'None', // UNTRANSLATED
|
||||
identifiers_none: 'Not applicable', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
sign_up: {
|
||||
title: 'SIGN UP', // UNTRANSLATED
|
||||
sign_up_identifier: 'Sign up identifier', // UNTRANSLATED
|
||||
sign_up_authentication: 'Sign up authentication', // UNTRANSLATED
|
||||
set_a_password_option: 'Set a password', // UNTRANSLATED
|
||||
verify_at_sign_up_option: 'Verify at sign up', // UNTRANSLATED
|
||||
sign_up_identifier: 'Sign-up identifier', // UNTRANSLATED
|
||||
identifier_description:
|
||||
'The sign-up identifier is required for account creation and must be included in your sign-in screen.', // UNTRANSLATED
|
||||
sign_up_authentication: 'Authentication setting for sign-up', // UNTRANSLATED
|
||||
authentication_description:
|
||||
'All selected actions will be obligatory for users to complete the flow.', // UNTRANSLATED
|
||||
set_a_password_option: 'Create your password', // UNTRANSLATED
|
||||
verify_at_sign_up_option: 'Verify at sign-up', // UNTRANSLATED
|
||||
social_only_creation_description: '(This apply to social only account creation)', // UNTRANSLATED
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Identifier and authentication settings for sign-in', // UNTRANSLATED
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.', // UNTRANSLATED
|
||||
'Users can sign in using any of the options available. Adjust the layout by drag and dropping below options.', // UNTRANSLATED
|
||||
add_sign_in_method: 'Add Sign-in Method', // UNTRANSLATED
|
||||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.', // UNTRANSLATED
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN IN', // UNTRANSLATED
|
||||
social_sign_in: 'Social sign in', // UNTRANSLATED
|
||||
title: 'SOCIAL SIGN-IN', // UNTRANSLATED
|
||||
social_sign_in: 'Social sign-in', // UNTRANSLATED
|
||||
description:
|
||||
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.', // UNTRANSLATED
|
||||
add_social_connector: 'Add Social Connector', // UNTRANSLATED
|
||||
'Depending on the mandatory identifier you set up, your user may be asked to provide an identifier when signing up via social connector.', // UNTRANSLATED
|
||||
add_social_connector: 'Link Social Connector', // UNTRANSLATED
|
||||
set_up_hint: {
|
||||
not_in_list: 'Not in the list?', // UNTRANSLATED
|
||||
set_up_more: 'Set up more', // UNTRANSLATED
|
||||
go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED
|
||||
set_up_more: 'Set up', // UNTRANSLATED
|
||||
go_to: 'other social connectors now.', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
tip: {
|
||||
set_a_password: 'A unique set of a password to your username is a must.', // UNTRANSLATED
|
||||
verify_at_sign_up:
|
||||
'Right now we only support email verified at sign up but soon to open this capability!', // UNTRANSLATED
|
||||
'We currently only support verified email. Your user base may contain a large number of poor-quality email addresses if no validation.', // UNTRANSLATED
|
||||
password_auth:
|
||||
'This is essential as you have enabled the option to set a password during the sign-up process.', // UNTRANSLATED
|
||||
verification_code_auth:
|
||||
|
@ -86,28 +90,6 @@ const sign_in_exp = {
|
|||
'This is essential as you have selected {{identifier}} as a required identifier.', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: '로그인 방법',
|
||||
primary: '메인 로그인 방법',
|
||||
enable_secondary: '백업 로그인 방법 활성화',
|
||||
enable_secondary_description:
|
||||
'백업 로그인 활성화를 통하여 메인 로그인 방법이외의 로그인 방법을 사용자에게 제공해보세요.',
|
||||
methods: '로그인 방법',
|
||||
methods_sms: 'SMS 로그인',
|
||||
methods_email: '이메일 로그인',
|
||||
methods_social: '소셜 로그인',
|
||||
methods_username: '사용자 이름&비밀번호 로그인',
|
||||
methods_primary_tag: '(메인)',
|
||||
define_social_methods: '소셜 로그인 방법 설정',
|
||||
transfer: {
|
||||
title: '소셜 연동',
|
||||
footer: {
|
||||
not_in_list: '리스트에 없나요?',
|
||||
set_up_more: '더 설정하기',
|
||||
go_to: '를 눌러 설정하러 가기',
|
||||
},
|
||||
},
|
||||
},
|
||||
others: {
|
||||
terms_of_use: {
|
||||
title: '이용 약관',
|
||||
|
@ -159,21 +141,21 @@ const sign_in_exp = {
|
|||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_sms:
|
||||
'SMS 연동이 아직 설정되지 않았어요. 설정이 완료될 때 까지, 사용자는 이 로그인 방법을 사용할 수 없어요.',
|
||||
'No SMS connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_connector_email:
|
||||
'이메일 연동이 아직 설정되지 않았어요. 설정이 완료될 때 까지, 사용자는 이 로그인 방법을 사용할 수 없어요.',
|
||||
'No email connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_connector_social:
|
||||
'소셜 연동이 아직 설정되지 않았어요. 설정이 완료될 때 까지, 사용자는 이 로그인 방법을 사용할 수 없어요.',
|
||||
'No social connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_added_social_connector:
|
||||
'보다 많은 소셜 연동들을 설정하여, 고객에게 보다 나은 경험을 제공해보세요.',
|
||||
},
|
||||
save_alert: {
|
||||
description:
|
||||
'로그인 방법이 수정되었어요. 일부 사용자에게 영향을 미칠 수 있어요. 정말로 진행할까요?',
|
||||
'You are implementing new sign-in and sign-up procedures. All of your users may be affected by the new set-up. Are you sure to commit to the change?', // UNTRANSLATED
|
||||
before: '이전',
|
||||
after: '이후',
|
||||
sign_up: 'Sign up', // UNTRANSLATED
|
||||
sign_in: 'Sign in', // UNTRANSLATED
|
||||
sign_up: 'Sign-up', // UNTRANSLATED
|
||||
sign_in: 'Sign-in', // UNTRANSLATED
|
||||
social: 'Social', // UNTRANSLATED
|
||||
},
|
||||
preview: {
|
||||
|
|
|
@ -39,48 +39,52 @@ const sign_in_exp = {
|
|||
slogan_placeholder: 'Liberte a sua criatividade',
|
||||
},
|
||||
sign_up_and_sign_in: {
|
||||
identifiers: 'Sign up identifiers', // UNTRANSLATED
|
||||
identifiers: 'Sign-up identifiers', // UNTRANSLATED
|
||||
identifiers_email: 'Email address', // UNTRANSLATED
|
||||
identifiers_sms: 'Phone number', // UNTRANSLATED
|
||||
identifiers_username: 'Username', // UNTRANSLATED
|
||||
identifiers_email_or_sms: 'Email address or phone number', // UNTRANSLATED
|
||||
identifiers_none: 'None', // UNTRANSLATED
|
||||
identifiers_none: 'Not applicable', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
sign_up: {
|
||||
title: 'SIGN UP', // UNTRANSLATED
|
||||
sign_up_identifier: 'Sign up identifier', // UNTRANSLATED
|
||||
sign_up_authentication: 'Sign up authentication', // UNTRANSLATED
|
||||
set_a_password_option: 'Set a password', // UNTRANSLATED
|
||||
verify_at_sign_up_option: 'Verify at sign up', // UNTRANSLATED
|
||||
sign_up_identifier: 'Sign-up identifier', // UNTRANSLATED
|
||||
identifier_description:
|
||||
'The sign-up identifier is required for account creation and must be included in your sign-in screen.', // UNTRANSLATED
|
||||
sign_up_authentication: 'Authentication setting for sign-up', // UNTRANSLATED
|
||||
authentication_description:
|
||||
'All selected actions will be obligatory for users to complete the flow.', // UNTRANSLATED
|
||||
set_a_password_option: 'Create your password', // UNTRANSLATED
|
||||
verify_at_sign_up_option: 'Verify at sign-up', // UNTRANSLATED
|
||||
social_only_creation_description: '(This apply to social only account creation)', // UNTRANSLATED
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Identifier and authentication settings for sign-in', // UNTRANSLATED
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.', // UNTRANSLATED
|
||||
'Users can sign in using any of the options available. Adjust the layout by drag and dropping below options.', // UNTRANSLATED
|
||||
add_sign_in_method: 'Add Sign-in Method', // UNTRANSLATED
|
||||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.', // UNTRANSLATED
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN IN', // UNTRANSLATED
|
||||
social_sign_in: 'Social sign in', // UNTRANSLATED
|
||||
title: 'SOCIAL SIGN-IN', // UNTRANSLATED
|
||||
social_sign_in: 'Social sign-in', // UNTRANSLATED
|
||||
description:
|
||||
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.', // UNTRANSLATED
|
||||
add_social_connector: 'Add Social Connector', // UNTRANSLATED
|
||||
'Depending on the mandatory identifier you set up, your user may be asked to provide an identifier when signing up via social connector.', // UNTRANSLATED
|
||||
add_social_connector: 'Link Social Connector', // UNTRANSLATED
|
||||
set_up_hint: {
|
||||
not_in_list: 'Not in the list?', // UNTRANSLATED
|
||||
set_up_more: 'Set up more', // UNTRANSLATED
|
||||
go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED
|
||||
set_up_more: 'Set up', // UNTRANSLATED
|
||||
go_to: 'other social connectors now.', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
tip: {
|
||||
set_a_password: 'A unique set of a password to your username is a must.', // UNTRANSLATED
|
||||
verify_at_sign_up:
|
||||
'Right now we only support email verified at sign up but soon to open this capability!', // UNTRANSLATED
|
||||
'We currently only support verified email. Your user base may contain a large number of poor-quality email addresses if no validation.', // UNTRANSLATED
|
||||
password_auth:
|
||||
'This is essential as you have enabled the option to set a password during the sign-up process.', // UNTRANSLATED
|
||||
verification_code_auth:
|
||||
|
@ -89,28 +93,6 @@ const sign_in_exp = {
|
|||
'This is essential as you have selected {{identifier}} as a required identifier.', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: 'MÉTODOS DE LOGIN',
|
||||
primary: 'Método de login principal',
|
||||
enable_secondary: 'Ativar login secundário',
|
||||
enable_secondary_description:
|
||||
'Depois de ativado, a sua app oferecerá suporte a mais métodos de login além do principal. ',
|
||||
methods: 'Método de login',
|
||||
methods_sms: 'SMS',
|
||||
methods_email: 'Email',
|
||||
methods_social: 'Rede social',
|
||||
methods_username: 'Utilizador e password',
|
||||
methods_primary_tag: '(Primário)',
|
||||
define_social_methods: 'Definir métodos de login social',
|
||||
transfer: {
|
||||
title: 'Conectores sociais',
|
||||
footer: {
|
||||
not_in_list: 'Não está na lista?',
|
||||
set_up_more: 'Configurar mais',
|
||||
go_to: 'conectores sociais ou vá para a seção "Conectores".',
|
||||
},
|
||||
},
|
||||
},
|
||||
others: {
|
||||
terms_of_use: {
|
||||
title: 'TERMOS DE USO',
|
||||
|
@ -162,21 +144,21 @@ const sign_in_exp = {
|
|||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_sms:
|
||||
'Ainda não configurou um conector de SMS. A experiência de login não será ativada até que conclua as configurações primeiro. ',
|
||||
'No SMS connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_connector_email:
|
||||
'Ainda não configurou um conector de email. A experiência de login não será ativada até que conclua as configurações primeiro. ',
|
||||
'No email connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_connector_social:
|
||||
'Ainda não configurou um conector social. A experiência de login não será ativada até que conclua as configurações primeiro. ',
|
||||
'No social connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_added_social_connector:
|
||||
'Configurou alguns conectores sociais agora. Certifique-se de adicionar alguns a experiência de login.',
|
||||
},
|
||||
save_alert: {
|
||||
description:
|
||||
'Está alterando os métodos de login. Isso afetará alguns dos seus utilizadoress. Tem a certeza que deseja fazer isso?',
|
||||
'You are implementing new sign-in and sign-up procedures. All of your users may be affected by the new set-up. Are you sure to commit to the change?', // UNTRANSLATED
|
||||
before: 'Antes',
|
||||
after: 'Depois',
|
||||
sign_up: 'Sign up', // UNTRANSLATED
|
||||
sign_in: 'Sign in', // UNTRANSLATED
|
||||
sign_up: 'Sign-up', // UNTRANSLATED
|
||||
sign_in: 'Sign-in', // UNTRANSLATED
|
||||
social: 'Social', // UNTRANSLATED
|
||||
},
|
||||
preview: {
|
||||
|
|
|
@ -40,48 +40,52 @@ const sign_in_exp = {
|
|||
slogan_placeholder: 'Yaratıcılığınızı açığa çıkarın',
|
||||
},
|
||||
sign_up_and_sign_in: {
|
||||
identifiers: 'Sign up identifiers', // UNTRANSLATED
|
||||
identifiers: 'Sign-up identifiers', // UNTRANSLATED
|
||||
identifiers_email: 'Email address', // UNTRANSLATED
|
||||
identifiers_sms: 'Phone number', // UNTRANSLATED
|
||||
identifiers_username: 'Username', // UNTRANSLATED
|
||||
identifiers_email_or_sms: 'Email address or phone number', // UNTRANSLATED
|
||||
identifiers_none: 'None', // UNTRANSLATED
|
||||
identifiers_none: 'Not applicable', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
sign_up: {
|
||||
title: 'SIGN UP', // UNTRANSLATED
|
||||
sign_up_identifier: 'Sign up identifier', // UNTRANSLATED
|
||||
sign_up_authentication: 'Sign up authentication', // UNTRANSLATED
|
||||
set_a_password_option: 'Set a password', // UNTRANSLATED
|
||||
verify_at_sign_up_option: 'Verify at sign up', // UNTRANSLATED
|
||||
sign_up_identifier: 'Sign-up identifier', // UNTRANSLATED
|
||||
identifier_description:
|
||||
'The sign-up identifier is required for account creation and must be included in your sign-in screen.', // UNTRANSLATED
|
||||
sign_up_authentication: 'Authentication setting for sign-up', // UNTRANSLATED
|
||||
authentication_description:
|
||||
'All selected actions will be obligatory for users to complete the flow.', // UNTRANSLATED
|
||||
set_a_password_option: 'Create your password', // UNTRANSLATED
|
||||
verify_at_sign_up_option: 'Verify at sign-up', // UNTRANSLATED
|
||||
social_only_creation_description: '(This apply to social only account creation)', // UNTRANSLATED
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Identifier and authentication settings for sign-in', // UNTRANSLATED
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.', // UNTRANSLATED
|
||||
'Users can sign in using any of the options available. Adjust the layout by drag and dropping below options.', // UNTRANSLATED
|
||||
add_sign_in_method: 'Add Sign-in Method', // UNTRANSLATED
|
||||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.', // UNTRANSLATED
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN IN', // UNTRANSLATED
|
||||
social_sign_in: 'Social sign in', // UNTRANSLATED
|
||||
title: 'SOCIAL SIGN-IN', // UNTRANSLATED
|
||||
social_sign_in: 'Social sign-in', // UNTRANSLATED
|
||||
description:
|
||||
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.', // UNTRANSLATED
|
||||
add_social_connector: 'Add Social Connector', // UNTRANSLATED
|
||||
'Depending on the mandatory identifier you set up, your user may be asked to provide an identifier when signing up via social connector.', // UNTRANSLATED
|
||||
add_social_connector: 'Link Social Connector', // UNTRANSLATED
|
||||
set_up_hint: {
|
||||
not_in_list: 'Not in the list?', // UNTRANSLATED
|
||||
set_up_more: 'Set up more', // UNTRANSLATED
|
||||
go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED
|
||||
set_up_more: 'Set up', // UNTRANSLATED
|
||||
go_to: 'other social connectors now.', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
tip: {
|
||||
set_a_password: 'A unique set of a password to your username is a must.', // UNTRANSLATED
|
||||
verify_at_sign_up:
|
||||
'Right now we only support email verified at sign up but soon to open this capability!', // UNTRANSLATED
|
||||
'We currently only support verified email. Your user base may contain a large number of poor-quality email addresses if no validation.', // UNTRANSLATED
|
||||
password_auth:
|
||||
'This is essential as you have enabled the option to set a password during the sign-up process.', // UNTRANSLATED
|
||||
verification_code_auth:
|
||||
|
@ -90,28 +94,6 @@ const sign_in_exp = {
|
|||
'This is essential as you have selected {{identifier}} as a required identifier.', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: 'OTURUM AÇMA YÖNTEMLERİ',
|
||||
primary: 'Birincil oturum açma yöntemi',
|
||||
enable_secondary: 'İkincil oturum açmayı etkinleştir',
|
||||
enable_secondary_description:
|
||||
'Açıldığında, uygulamanız birincil yöntemin yanı sıra daha fazla oturum açma yöntemini destekleyecektir. ',
|
||||
methods: 'Oturum açma yöntemi',
|
||||
methods_sms: 'Telefon numarası girişi',
|
||||
methods_email: 'E-posta adresi girişi',
|
||||
methods_social: 'Sosyal platform girişi',
|
||||
methods_username: 'Kullanıcı adı ve şifre ile oturum açma',
|
||||
methods_primary_tag: '(Primary)',
|
||||
define_social_methods: 'Sosyal platform oturum açma yöntemlerini tanımlama',
|
||||
transfer: {
|
||||
title: 'Social connectorlar',
|
||||
footer: {
|
||||
not_in_list: 'Listede yok?',
|
||||
set_up_more: 'Daha fazlasını ayarla',
|
||||
go_to: 'Social connectorlara veya “Connectors” bölümüne git.',
|
||||
},
|
||||
},
|
||||
},
|
||||
others: {
|
||||
terms_of_use: {
|
||||
title: 'KULLANIM KOŞULLARI',
|
||||
|
@ -163,21 +145,21 @@ const sign_in_exp = {
|
|||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_sms:
|
||||
'Henüz bir SMS bağlayıcısı kurmadınız. Öncelikle ayarları tamamlayana kadar oturum açma deneyiminiz yayınlanmayacaktır. ',
|
||||
'No SMS connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_connector_email:
|
||||
'Henüz bir e-posta adresi bağlayıcısı kurmadınız. Öncelikle ayarları tamamlayana kadar oturum açma deneyiminiz yayınlanmayacaktır. ',
|
||||
'No email connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_connector_social:
|
||||
'Henüz herhangi bir social connector kurmadınız. Öncelikle ayarları tamamlayana kadar oturum açma deneyiminiz yayınlanmayacaktır. ',
|
||||
'No social connector set-up yet. Until you finish configuring your social connector, you won’t be able to sign in.', // UNTRANSLATED
|
||||
no_added_social_connector:
|
||||
'Şimdi birkaç social connector kurdunuz. Oturum açma deneyiminize bazı şeyler eklediğinizden emin olun.',
|
||||
},
|
||||
save_alert: {
|
||||
description:
|
||||
'Oturum açma yöntemlerini değiştiriyorsunuz. Bu, bazı kullanıcılarınızı etkileyecektir. Bunu yapmak istediğine emin misin?',
|
||||
'You are implementing new sign-in and sign-up procedures. All of your users may be affected by the new set-up. Are you sure to commit to the change?', // UNTRANSLATED
|
||||
before: 'Önce',
|
||||
after: 'Sonra',
|
||||
sign_up: 'Sign up', // UNTRANSLATED
|
||||
sign_in: 'Sign in', // UNTRANSLATED
|
||||
sign_up: 'Sign-up', // UNTRANSLATED
|
||||
sign_in: 'Sign-in', // UNTRANSLATED
|
||||
social: 'Social', // UNTRANSLATED
|
||||
},
|
||||
preview: {
|
||||
|
|
|
@ -3,7 +3,7 @@ const sign_in_exp = {
|
|||
description: '自定义登录界面,并实时预览真实效果',
|
||||
tabs: {
|
||||
branding: '品牌',
|
||||
sign_up_and_sign_in: 'Sign up and Sign in', // UNTRANSLATED
|
||||
sign_up_and_sign_in: '注册与登录',
|
||||
others: '其它',
|
||||
},
|
||||
welcome: {
|
||||
|
@ -37,75 +37,53 @@ const sign_in_exp = {
|
|||
slogan_placeholder: '释放你的创意',
|
||||
},
|
||||
sign_up_and_sign_in: {
|
||||
identifiers: 'Sign up identifiers', // UNTRANSLATED
|
||||
identifiers_email: 'Email address', // UNTRANSLATED
|
||||
identifiers_sms: 'Phone number', // UNTRANSLATED
|
||||
identifiers_username: 'Username', // UNTRANSLATED
|
||||
identifiers_email_or_sms: 'Email address or phone number', // UNTRANSLATED
|
||||
identifiers_none: 'None', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
identifiers: '注册标识',
|
||||
identifiers_email: '邮件地址',
|
||||
identifiers_sms: '手机号码',
|
||||
identifiers_username: '用户名',
|
||||
identifiers_email_or_sms: '邮件地址或手机号码',
|
||||
identifiers_none: '无',
|
||||
and: '与',
|
||||
or: '或',
|
||||
sign_up: {
|
||||
title: 'SIGN UP', // UNTRANSLATED
|
||||
sign_up_identifier: 'Sign up identifier', // UNTRANSLATED
|
||||
sign_up_authentication: 'Sign up authentication', // UNTRANSLATED
|
||||
set_a_password_option: 'Set a password', // UNTRANSLATED
|
||||
verify_at_sign_up_option: 'Verify at sign up', // UNTRANSLATED
|
||||
social_only_creation_description: '(This apply to social only account creation)', // UNTRANSLATED
|
||||
title: '注册',
|
||||
sign_up_identifier: '注册标识',
|
||||
identifier_description: '创建账户时你需要设定注册标识。这些信息在用户登录时,属于必选项。',
|
||||
sign_up_authentication: '注册身份认证设置',
|
||||
authentication_description: '注册时,你的用户将要完成以下所有勾选的任务。',
|
||||
set_a_password_option: '创建密码',
|
||||
verify_at_sign_up_option: '注册时验证身份',
|
||||
social_only_creation_description: '(仅对社交注册用户适用)',
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication', // UNTRANSLATED
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.', // UNTRANSLATED
|
||||
add_sign_in_method: 'Add Sign-in Method', // UNTRANSLATED
|
||||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
|
||||
title: '登录',
|
||||
sign_in_identifier_and_auth: '登录标识和身份认证设置',
|
||||
description: '用户可以使用任何可用的选项进行登录。拖拽选项即可调整页面布局。',
|
||||
add_sign_in_method: '添加登录方式',
|
||||
password_auth: '密码',
|
||||
verification_code_auth: '验证码',
|
||||
auth_swap_tip: '交换以下选项的位置即可设定它们在用户登录流程中出现的先后。',
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN IN', // UNTRANSLATED
|
||||
social_sign_in: 'Social sign in', // UNTRANSLATED
|
||||
description:
|
||||
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.', // UNTRANSLATED
|
||||
add_social_connector: 'Add Social Connector', // UNTRANSLATED
|
||||
title: '社交登录',
|
||||
social_sign_in: '社交登录',
|
||||
description: '你已设定特定的标识。用户在通过社交连接器注册时可能会被要求提供一个对应的标识。',
|
||||
add_social_connector: '添加社交连接器',
|
||||
set_up_hint: {
|
||||
not_in_list: 'Not in the list?', // UNTRANSLATED
|
||||
set_up_more: 'Set up more', // UNTRANSLATED
|
||||
go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED
|
||||
not_in_list: '没有你想要的连接器?',
|
||||
set_up_more: '立即设置',
|
||||
go_to: '其他社交连接器。',
|
||||
},
|
||||
},
|
||||
tip: {
|
||||
set_a_password: 'A unique set of a password to your username is a must.', // UNTRANSLATED
|
||||
set_a_password: '启用户名注册,必须设置密码。',
|
||||
verify_at_sign_up:
|
||||
'Right now we only support email verified at sign up but soon to open this capability!', // UNTRANSLATED
|
||||
password_auth:
|
||||
'This is essential as you have enabled the option to set a password during the sign-up process.', // UNTRANSLATED
|
||||
'我们目前仅支持经过验证的邮件地址登录。如果没有验证,你的用户信息中可能出现大量无效电子邮件地址。',
|
||||
password_auth: '因注册设置里你启用了用户名密码标识。这个信息在用户登录时,属于必选项。',
|
||||
verification_code_auth:
|
||||
'This is essential as you have only enabled the option to provide verification code when signing up. You’re free to uncheck the box when password set-up is allowed at the sign-up process.', // UNTRANSLATED
|
||||
'因注册设置里你启用了验证码标识,验证码属于用户必选项。开启密码注册后,你可以选择关闭验证码登录。',
|
||||
delete_sign_in_method:
|
||||
'This is essential as you have selected {{identifier}} as a required identifier.', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: '登录方式',
|
||||
primary: '主要登录方式',
|
||||
enable_secondary: '启用其它登录方式',
|
||||
enable_secondary_description: '开启后,除了主要登录方式,你的 app 将会支持更多其它的登录方式 ',
|
||||
methods: '登录方式',
|
||||
methods_sms: '手机号登录',
|
||||
methods_email: '邮箱登录',
|
||||
methods_social: '社交帐号登录',
|
||||
methods_username: '用户名密码登录',
|
||||
methods_primary_tag: '(主要)',
|
||||
define_social_methods: '定义社交登录方式',
|
||||
transfer: {
|
||||
title: '社交连接器',
|
||||
footer: {
|
||||
not_in_list: '不在列表里?',
|
||||
set_up_more: '设置更多',
|
||||
go_to: '社交连接器,或前往连接器模块进行设置。',
|
||||
},
|
||||
'因注册设置里你启用了{{identifier}}标识。这些信息在用户登录时,属于必选项。',
|
||||
},
|
||||
},
|
||||
others: {
|
||||
|
@ -156,18 +134,19 @@ const sign_in_exp = {
|
|||
},
|
||||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_sms: '你还没有设置 SMS 连接器。你需完成设置后登录体验才会生效。',
|
||||
no_connector_email: '你还没有设置 email 连接器。你需完成设置后登录体验才会生效。',
|
||||
no_connector_social: '你还没有设置社交连接器。你需完成设置后登录体验才会生效。',
|
||||
no_connector_sms: '你尚未设置 SMS 短信连接器。在完成该配置前,你将无法登录。',
|
||||
no_connector_email: '你尚未设置电子邮件连接器。在完成该配置前,你将无法登录。',
|
||||
no_connector_social: '你尚未设置社交连接器。在完成该配置前,你将无法登录。',
|
||||
no_added_social_connector: '你已经成功设置了一些社交连接器。点按「+」添加一些到你的登录体验。',
|
||||
},
|
||||
save_alert: {
|
||||
description: '你正在修改登录方式,这可能会影响部分用户。是否继续保存修改?',
|
||||
before: '修改前',
|
||||
after: '修改后',
|
||||
sign_up: 'Sign up', // UNTRANSLATED
|
||||
sign_in: 'Sign in', // UNTRANSLATED
|
||||
social: 'Social', // UNTRANSLATED
|
||||
description:
|
||||
'你正在进行登录注册设置的变更。当前你的所有用户会受到新设置的影响。确认保存该设置吗?',
|
||||
before: '设置前',
|
||||
after: '设置后',
|
||||
sign_up: '注册',
|
||||
sign_in: '登录',
|
||||
social: '社交',
|
||||
},
|
||||
preview: {
|
||||
title: '登录预览',
|
||||
|
|
|
@ -9,6 +9,7 @@ import usePreview from './hooks/use-preview';
|
|||
import initI18n from './i18n/init';
|
||||
import Callback from './pages/Callback';
|
||||
import Consent from './pages/Consent';
|
||||
import Continue from './pages/Continue';
|
||||
import ErrorPage from './pages/ErrorPage';
|
||||
import ForgotPassword from './pages/ForgotPassword';
|
||||
import Passcode from './pages/Passcode';
|
||||
|
@ -67,7 +68,7 @@ const App = () => {
|
|||
/>
|
||||
|
||||
<Route element={<LoadingLayerProvider />}>
|
||||
{/* sign-in */}
|
||||
{/* Sign-in */}
|
||||
<Route
|
||||
path="/sign-in"
|
||||
element={isRegisterOnly ? <Navigate replace to="/register" /> : <SignIn />}
|
||||
|
@ -76,7 +77,7 @@ const App = () => {
|
|||
<Route path="/sign-in/:method" element={<SecondarySignIn />} />
|
||||
<Route path="/sign-in/:method/password" element={<SignInPassword />} />
|
||||
|
||||
{/* register */}
|
||||
{/* Register */}
|
||||
<Route
|
||||
path="/register"
|
||||
element={isSignInOnly ? <Navigate replace to="/sign-in" /> : <Register />}
|
||||
|
@ -87,17 +88,19 @@ const App = () => {
|
|||
/>
|
||||
<Route path="/register/:method" element={<SecondaryRegister />} />
|
||||
|
||||
{/* forgot password */}
|
||||
{/* Forgot password */}
|
||||
<Route path="/forgot-password/reset" element={<ResetPassword />} />
|
||||
<Route path="/forgot-password/:method" element={<ForgotPassword />} />
|
||||
|
||||
{/* social sign-in pages */}
|
||||
{/* Continue set up missing profile */}
|
||||
<Route path="/continue/:method" element={<Continue />} />
|
||||
|
||||
{/* Social sign-in pages */}
|
||||
<Route path="/callback/:connector" element={<Callback />} />
|
||||
<Route path="/social/register/:connector" element={<SocialRegister />} />
|
||||
<Route path="/social/landing/:connector" element={<SocialLanding />} />
|
||||
|
||||
{/* always keep route path with param as the last one */}
|
||||
{/* Always keep route path with param as the last one */}
|
||||
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
|
||||
</Route>
|
||||
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
import {
|
||||
verifyForgotPasswordEmailPasscode,
|
||||
verifyForgotPasswordSmsPasscode,
|
||||
} from './forgot-password';
|
||||
import { verifyRegisterEmailPasscode, verifyRegisterSmsPasscode } from './register';
|
||||
import { verifySignInEmailPasscode, verifySignInSmsPasscode } from './sign-in';
|
||||
import { getVerifyPasscodeApi } from './utils';
|
||||
|
||||
describe('api', () => {
|
||||
it('getVerifyPasscodeApi', () => {
|
||||
expect(getVerifyPasscodeApi(UserFlow.register, SignInIdentifier.Sms)).toBe(
|
||||
verifyRegisterSmsPasscode
|
||||
);
|
||||
expect(getVerifyPasscodeApi(UserFlow.register, SignInIdentifier.Email)).toBe(
|
||||
verifyRegisterEmailPasscode
|
||||
);
|
||||
expect(getVerifyPasscodeApi(UserFlow.signIn, SignInIdentifier.Sms)).toBe(
|
||||
verifySignInSmsPasscode
|
||||
);
|
||||
expect(getVerifyPasscodeApi(UserFlow.signIn, SignInIdentifier.Email)).toBe(
|
||||
verifySignInEmailPasscode
|
||||
);
|
||||
expect(getVerifyPasscodeApi(UserFlow.forgotPassword, SignInIdentifier.Email)).toBe(
|
||||
verifyForgotPasswordEmailPasscode
|
||||
);
|
||||
expect(getVerifyPasscodeApi(UserFlow.forgotPassword, SignInIdentifier.Sms)).toBe(
|
||||
verifyForgotPasswordSmsPasscode
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,10 +1,12 @@
|
|||
import { PasscodeType } from '@logto/schemas';
|
||||
import ky from 'ky';
|
||||
|
||||
import {
|
||||
continueWithPassword,
|
||||
continueWithUsername,
|
||||
continueWithEmail,
|
||||
continueWithPhone,
|
||||
continueApi,
|
||||
sendContinueSetEmailPasscode,
|
||||
sendContinueSetPhonePasscode,
|
||||
verifyContinueSetEmailPasscode,
|
||||
verifyContinueSetSmsPasscode,
|
||||
} from './continue';
|
||||
|
||||
jest.mock('ky', () => ({
|
||||
|
@ -30,8 +32,8 @@ describe('continue API', () => {
|
|||
});
|
||||
|
||||
it('continue with password', async () => {
|
||||
await continueWithPassword('password');
|
||||
expect(ky.post).toBeCalledWith('/api/session/continue/password', {
|
||||
await continueApi('password', 'password');
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/continue/password', {
|
||||
json: {
|
||||
password: 'password',
|
||||
},
|
||||
|
@ -39,8 +41,8 @@ describe('continue API', () => {
|
|||
});
|
||||
|
||||
it('continue with username', async () => {
|
||||
await continueWithUsername('username');
|
||||
expect(ky.post).toBeCalledWith('/api/session/continue/username', {
|
||||
await continueApi('username', 'username');
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/continue/username', {
|
||||
json: {
|
||||
username: 'username',
|
||||
},
|
||||
|
@ -48,9 +50,9 @@ describe('continue API', () => {
|
|||
});
|
||||
|
||||
it('continue with email', async () => {
|
||||
await continueWithEmail('email');
|
||||
await continueApi('email', 'email');
|
||||
|
||||
expect(ky.post).toBeCalledWith('/api/session/continue/email', {
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/continue/email', {
|
||||
json: {
|
||||
email: 'email',
|
||||
},
|
||||
|
@ -58,12 +60,58 @@ describe('continue API', () => {
|
|||
});
|
||||
|
||||
it('continue with phone', async () => {
|
||||
await continueWithPhone('phone');
|
||||
await continueApi('phone', 'phone');
|
||||
|
||||
expect(ky.post).toBeCalledWith('/api/session/continue/sms', {
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/continue/sms', {
|
||||
json: {
|
||||
phone: 'phone',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sendContinueSetEmailPasscode', async () => {
|
||||
await sendContinueSetEmailPasscode('email');
|
||||
|
||||
expect(ky.post).toBeCalledWith('/api/session/passwordless/email/send', {
|
||||
json: {
|
||||
email: 'email',
|
||||
flow: PasscodeType.Continue,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sendContinueSetSmsPasscode', async () => {
|
||||
await sendContinueSetPhonePasscode('111111');
|
||||
|
||||
expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/send', {
|
||||
json: {
|
||||
phone: '111111',
|
||||
flow: PasscodeType.Continue,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('verifyContinueSetEmailPasscode', async () => {
|
||||
await verifyContinueSetEmailPasscode('email', 'passcode');
|
||||
|
||||
expect(ky.post).toBeCalledWith('/api/session/passwordless/email/verify', {
|
||||
json: {
|
||||
email: 'email',
|
||||
code: 'passcode',
|
||||
flow: PasscodeType.Continue,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('verifyContinueSetSmsPasscode', async () => {
|
||||
await verifyContinueSetSmsPasscode('phone', 'passcode');
|
||||
|
||||
expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/verify', {
|
||||
json: {
|
||||
phone: 'phone',
|
||||
code: 'passcode',
|
||||
flow: PasscodeType.Continue,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { PasscodeType } from '@logto/schemas';
|
||||
|
||||
import api from './api';
|
||||
import { bindSocialAccount } from './social';
|
||||
|
||||
|
@ -5,13 +7,15 @@ type Response = {
|
|||
redirectTo: string;
|
||||
};
|
||||
|
||||
const continueApiPrefix = '/api/session/continue';
|
||||
const passwordlessApiPrefix = '/api/session/passwordless';
|
||||
const continueApiPrefix = '/api/session/sign-in/continue';
|
||||
|
||||
// Only bind with social after the sign-in bind password flow
|
||||
export const continueWithPassword = async (password: string, socialToBind?: string) => {
|
||||
type ContinueKey = 'password' | 'username' | 'email' | 'phone';
|
||||
|
||||
export const continueApi = async (key: ContinueKey, value: string, socialToBind?: string) => {
|
||||
const result = await api
|
||||
.post(`${continueApiPrefix}/password`, {
|
||||
json: { password },
|
||||
.post(`${continueApiPrefix}/${key === 'phone' ? 'sms' : key}`, {
|
||||
json: { [key]: value },
|
||||
})
|
||||
.json<Response>();
|
||||
|
||||
|
@ -22,11 +26,48 @@ export const continueWithPassword = async (password: string, socialToBind?: stri
|
|||
return result;
|
||||
};
|
||||
|
||||
export const continueWithUsername = async (username: string) =>
|
||||
api.post(`${continueApiPrefix}/username`, { json: { username } }).json<Response>();
|
||||
export const sendContinueSetEmailPasscode = async (email: string) => {
|
||||
await api
|
||||
.post(`${passwordlessApiPrefix}/email/send`, {
|
||||
json: {
|
||||
email,
|
||||
flow: PasscodeType.Continue,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
export const continueWithEmail = async (email: string) =>
|
||||
api.post(`${continueApiPrefix}/email`, { json: { email } }).json<Response>();
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
export const continueWithPhone = async (phone: string) =>
|
||||
api.post(`${continueApiPrefix}/sms`, { json: { phone } }).json<Response>();
|
||||
export const sendContinueSetPhonePasscode = async (phone: string) => {
|
||||
await api
|
||||
.post(`${passwordlessApiPrefix}/sms/send`, {
|
||||
json: {
|
||||
phone,
|
||||
flow: PasscodeType.Continue,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
export const verifyContinueSetEmailPasscode = async (email: string, code: string) => {
|
||||
await api
|
||||
.post(`${passwordlessApiPrefix}/email/verify`, {
|
||||
json: { email, code, flow: PasscodeType.Continue },
|
||||
})
|
||||
.json();
|
||||
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
export const verifyContinueSetSmsPasscode = async (phone: string, code: string) => {
|
||||
await api
|
||||
.post(`${passwordlessApiPrefix}/sms/verify`, {
|
||||
json: { phone, code, flow: PasscodeType.Continue },
|
||||
})
|
||||
.json();
|
||||
|
||||
return { success: true };
|
||||
};
|
||||
|
|
|
@ -2,27 +2,15 @@ import { SignInIdentifier } from '@logto/schemas';
|
|||
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
import {
|
||||
sendForgotPasswordEmailPasscode,
|
||||
sendForgotPasswordSmsPasscode,
|
||||
verifyForgotPasswordEmailPasscode,
|
||||
verifyForgotPasswordSmsPasscode,
|
||||
} from './forgot-password';
|
||||
import {
|
||||
verifyRegisterEmailPasscode,
|
||||
verifyRegisterSmsPasscode,
|
||||
sendRegisterEmailPasscode,
|
||||
sendRegisterSmsPasscode,
|
||||
} from './register';
|
||||
import {
|
||||
verifySignInEmailPasscode,
|
||||
verifySignInSmsPasscode,
|
||||
sendSignInEmailPasscode,
|
||||
sendSignInSmsPasscode,
|
||||
} from './sign-in';
|
||||
import { sendContinueSetEmailPasscode, sendContinueSetPhonePasscode } from './continue';
|
||||
import { sendForgotPasswordEmailPasscode, sendForgotPasswordSmsPasscode } from './forgot-password';
|
||||
import { sendRegisterEmailPasscode, sendRegisterSmsPasscode } from './register';
|
||||
import { sendSignInEmailPasscode, sendSignInSmsPasscode } from './sign-in';
|
||||
|
||||
export type PasscodeChannel = SignInIdentifier.Email | SignInIdentifier.Sms;
|
||||
|
||||
// TODO: @simeng-li merge in to one single api
|
||||
|
||||
export const getSendPasscodeApi = (
|
||||
type: UserFlow,
|
||||
method: PasscodeChannel
|
||||
|
@ -47,36 +35,13 @@ export const getSendPasscodeApi = (
|
|||
return sendRegisterEmailPasscode;
|
||||
}
|
||||
|
||||
return sendRegisterSmsPasscode;
|
||||
};
|
||||
|
||||
export const getVerifyPasscodeApi = (
|
||||
type: UserFlow,
|
||||
method: PasscodeChannel
|
||||
): ((
|
||||
_address: string,
|
||||
code: string,
|
||||
socialToBind?: string
|
||||
) => Promise<{ redirectTo?: string; success?: boolean }>) => {
|
||||
if (type === UserFlow.forgotPassword && method === SignInIdentifier.Email) {
|
||||
return verifyForgotPasswordEmailPasscode;
|
||||
}
|
||||
|
||||
if (type === UserFlow.forgotPassword && method === SignInIdentifier.Sms) {
|
||||
return verifyForgotPasswordSmsPasscode;
|
||||
}
|
||||
|
||||
if (type === UserFlow.signIn && method === SignInIdentifier.Email) {
|
||||
return verifySignInEmailPasscode;
|
||||
}
|
||||
|
||||
if (type === UserFlow.signIn && method === SignInIdentifier.Sms) {
|
||||
return verifySignInSmsPasscode;
|
||||
}
|
||||
|
||||
if (type === UserFlow.register && method === SignInIdentifier.Email) {
|
||||
return verifyRegisterEmailPasscode;
|
||||
}
|
||||
|
||||
return verifyRegisterSmsPasscode;
|
||||
if (type === UserFlow.register && method === SignInIdentifier.Sms) {
|
||||
return sendRegisterSmsPasscode;
|
||||
}
|
||||
|
||||
if (type === UserFlow.continue && method === SignInIdentifier.Email) {
|
||||
return sendContinueSetEmailPasscode;
|
||||
}
|
||||
|
||||
return sendContinueSetPhonePasscode;
|
||||
};
|
||||
|
|
|
@ -57,6 +57,10 @@
|
|||
|
||||
|
||||
:global(body.desktop) {
|
||||
.button {
|
||||
font: var(--font-label-2);
|
||||
}
|
||||
|
||||
.primary {
|
||||
&:focus-visible {
|
||||
outline: 3px solid var(--color-overlay-brand-focused);
|
||||
|
|
|
@ -11,6 +11,11 @@
|
|||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
outline: none;
|
||||
border-radius: 16px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
|
|
|
@ -11,6 +11,11 @@
|
|||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
outline: none;
|
||||
border-radius: var(--radius);
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
|
|
|
@ -47,6 +47,7 @@ body {
|
|||
|
||||
.placeHolder {
|
||||
flex: 1;
|
||||
min-height: _.unit(5);
|
||||
}
|
||||
|
||||
:global(body.mobile) {
|
||||
|
@ -84,5 +85,6 @@ body {
|
|||
border-radius: 16px;
|
||||
background: var(--color-bg-float);
|
||||
box-shadow: var(--color-shadow-2);
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ export type Props = {
|
|||
|
||||
const AppContent = ({ children }: Props) => {
|
||||
const theme = useTheme();
|
||||
const { toast, platform, setToast, experienceSettings } = useContext(PageContext);
|
||||
const { toast, platform, setToast } = useContext(PageContext);
|
||||
|
||||
// Prevent internal eventListener rebind
|
||||
const hideToast = useCallback(() => {
|
||||
|
|
49
packages/ui/src/containers/EmailForm/EmailContinue.test.tsx
Normal file
49
packages/ui/src/containers/EmailForm/EmailContinue.test.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { fireEvent, waitFor, act } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import { sendContinueSetEmailPasscode } from '@/apis/continue';
|
||||
|
||||
import EmailContinue from './EmailContinue';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
jest.mock('@/apis/continue', () => ({
|
||||
sendContinueSetEmailPasscode: jest.fn(() => ({ success: true })),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
describe('EmailContinue', () => {
|
||||
const email = 'foo@logto.io';
|
||||
|
||||
test('register form submit', async () => {
|
||||
const { container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<EmailContinue />
|
||||
</MemoryRouter>
|
||||
);
|
||||
const emailInput = container.querySelector('input[name="email"]');
|
||||
|
||||
if (emailInput) {
|
||||
fireEvent.change(emailInput, { target: { value: email } });
|
||||
}
|
||||
|
||||
const submitButton = getByText('action.continue');
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendContinueSetEmailPasscode).toBeCalledWith(email);
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
{ pathname: '/continue/email/passcode-validation', search: '' },
|
||||
{ state: { email } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
32
packages/ui/src/containers/EmailForm/EmailContinue.tsx
Normal file
32
packages/ui/src/containers/EmailForm/EmailContinue.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import usePasswordlessSendCode from '@/hooks/use-passwordless-send-code';
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
import EmailForm from './EmailForm';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
// eslint-disable-next-line react/boolean-prop-naming
|
||||
autoFocus?: boolean;
|
||||
hasSwitch?: boolean;
|
||||
};
|
||||
|
||||
const EmailContinue = (props: Props) => {
|
||||
const { onSubmit, errorMessage, clearErrorMessage } = usePasswordlessSendCode(
|
||||
UserFlow.continue,
|
||||
SignInIdentifier.Email
|
||||
);
|
||||
|
||||
return (
|
||||
<EmailForm
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
errorMessage={errorMessage}
|
||||
clearErrorMessage={clearErrorMessage}
|
||||
hasTerms={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailContinue;
|
|
@ -83,6 +83,7 @@ const EmailForm = ({
|
|||
{...rest}
|
||||
onClear={() => {
|
||||
setFieldValue((state) => ({ ...state, email: '' }));
|
||||
clearErrorMessage?.();
|
||||
}}
|
||||
/>
|
||||
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { default as EmailRegister } from './EmailRegister';
|
||||
export { default as EmailSignIn } from './EmailSignIn';
|
||||
export { default as EmailResetPassword } from './EmailResetPassword';
|
||||
export { default as EmailContinue } from './EmailContinue';
|
||||
|
|
|
@ -13,6 +13,10 @@ jest.mock('@/apis/sign-in', () => ({ signInWithEmailPassword: jest.fn(async () =
|
|||
jest.mock('react-device-detect', () => ({
|
||||
isMobile: true,
|
||||
}));
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('<EmailPassword>', () => {
|
||||
afterEach(() => {
|
||||
|
|
|
@ -2,6 +2,11 @@ import { SignInIdentifier } from '@logto/schemas';
|
|||
import { act, fireEvent, waitFor } from '@testing-library/react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import {
|
||||
verifyContinueSetEmailPasscode,
|
||||
continueApi,
|
||||
verifyContinueSetSmsPasscode,
|
||||
} from '@/apis/continue';
|
||||
import {
|
||||
verifyForgotPasswordEmailPasscode,
|
||||
verifyForgotPasswordSmsPasscode,
|
||||
|
@ -42,6 +47,12 @@ jest.mock('@/apis/forgot-password', () => ({
|
|||
verifyForgotPasswordSmsPasscode: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/apis/continue', () => ({
|
||||
verifyContinueSetEmailPasscode: jest.fn(),
|
||||
verifyContinueSetSmsPasscode: jest.fn(),
|
||||
continueApi: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('<PasscodeValidation />', () => {
|
||||
const email = 'foo@logto.io';
|
||||
const phone = '18573333333';
|
||||
|
@ -234,36 +245,98 @@ describe('<PasscodeValidation />', () => {
|
|||
expect(mockedNavigate).toBeCalledWith('/forgot-password/reset', { replace: true });
|
||||
});
|
||||
});
|
||||
|
||||
it('fire sms forgot-password validate passcode event', async () => {
|
||||
(verifyForgotPasswordSmsPasscode as jest.Mock).mockImplementationOnce(() => ({
|
||||
success: true,
|
||||
}));
|
||||
|
||||
const { container } = renderWithPageContext(
|
||||
<PasscodeValidation
|
||||
type={UserFlow.forgotPassword}
|
||||
method={SignInIdentifier.Sms}
|
||||
target={phone}
|
||||
/>
|
||||
);
|
||||
|
||||
const inputs = container.querySelectorAll('input');
|
||||
|
||||
for (const input of inputs) {
|
||||
act(() => {
|
||||
fireEvent.input(input, { target: { value: '1' } });
|
||||
});
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(verifyForgotPasswordSmsPasscode).toBeCalledWith(phone, '111111');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).not.toBeCalled();
|
||||
expect(mockedNavigate).toBeCalledWith('/forgot-password/reset', { replace: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('fire Sms forgot-password validate passcode event', async () => {
|
||||
(verifyForgotPasswordSmsPasscode as jest.Mock).mockImplementationOnce(() => ({
|
||||
success: true,
|
||||
}));
|
||||
describe('continue flow', () => {
|
||||
it('set email', async () => {
|
||||
(verifyContinueSetEmailPasscode as jest.Mock).mockImplementationOnce(() => ({
|
||||
success: true,
|
||||
}));
|
||||
(continueApi as jest.Mock).mockImplementationOnce(() => ({ redirectTo: '/redirect' }));
|
||||
|
||||
const { container } = renderWithPageContext(
|
||||
<PasscodeValidation
|
||||
type={UserFlow.forgotPassword}
|
||||
method={SignInIdentifier.Sms}
|
||||
target={phone}
|
||||
/>
|
||||
);
|
||||
const { container } = renderWithPageContext(
|
||||
<PasscodeValidation
|
||||
type={UserFlow.continue}
|
||||
method={SignInIdentifier.Email}
|
||||
target={email}
|
||||
/>
|
||||
);
|
||||
|
||||
const inputs = container.querySelectorAll('input');
|
||||
const inputs = container.querySelectorAll('input');
|
||||
|
||||
for (const input of inputs) {
|
||||
act(() => {
|
||||
fireEvent.input(input, { target: { value: '1' } });
|
||||
for (const input of inputs) {
|
||||
act(() => {
|
||||
fireEvent.input(input, { target: { value: '1' } });
|
||||
});
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(verifyContinueSetEmailPasscode).toBeCalledWith(email, '111111');
|
||||
});
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(verifyForgotPasswordSmsPasscode).toBeCalledWith(phone, '111111');
|
||||
await waitFor(() => {
|
||||
expect(continueApi).toBeCalledWith('email', email, undefined);
|
||||
expect(window.location.replace).toBeCalledWith('/redirect');
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).not.toBeCalled();
|
||||
expect(mockedNavigate).toBeCalledWith('/forgot-password/reset', { replace: true });
|
||||
it('set Phone', async () => {
|
||||
(verifyContinueSetSmsPasscode as jest.Mock).mockImplementationOnce(() => ({
|
||||
success: true,
|
||||
}));
|
||||
(continueApi as jest.Mock).mockImplementationOnce(() => ({ redirectTo: '/redirect' }));
|
||||
|
||||
const { container } = renderWithPageContext(
|
||||
<PasscodeValidation type={UserFlow.continue} method={SignInIdentifier.Sms} target={phone} />
|
||||
);
|
||||
|
||||
const inputs = container.querySelectorAll('input');
|
||||
|
||||
for (const input of inputs) {
|
||||
act(() => {
|
||||
fireEvent.input(input, { target: { value: '1' } });
|
||||
});
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(verifyContinueSetSmsPasscode).toBeCalledWith(phone, '111111');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(continueApi).toBeCalledWith('phone', phone, undefined);
|
||||
expect(window.location.replace).toBeCalledWith('/redirect');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
|
||||
import { verifyContinueSetEmailPasscode, continueApi } from '@/apis/continue';
|
||||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
|
||||
import { UserFlow, SearchParameters } from '@/types';
|
||||
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 { sharedErrorHandlers, errorMessage, clearErrorMessage } = useSharedErrorHandler();
|
||||
|
||||
const requiredProfileErrorHandler = useRequiredProfileErrorHandler(true);
|
||||
|
||||
const identifierNotExistErrorHandler = useIdentifierErrorAlert(
|
||||
UserFlow.continue,
|
||||
SignInIdentifier.Email,
|
||||
email
|
||||
);
|
||||
|
||||
const verifyPasscodeErrorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
...sharedErrorHandlers,
|
||||
callback: errorCallback,
|
||||
}),
|
||||
[errorCallback, sharedErrorHandlers]
|
||||
);
|
||||
|
||||
const { run: verifyPasscode } = useApi(
|
||||
verifyContinueSetEmailPasscode,
|
||||
verifyPasscodeErrorHandlers
|
||||
);
|
||||
|
||||
const setEmailErrorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.email_not_exists': identifierNotExistErrorHandler,
|
||||
...requiredProfileErrorHandler,
|
||||
callback: errorCallback,
|
||||
}),
|
||||
[errorCallback, identifierNotExistErrorHandler, requiredProfileErrorHandler]
|
||||
);
|
||||
|
||||
const { run: setEmail } = useApi(continueApi, setEmailErrorHandlers);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (code: string) => {
|
||||
const verified = await verifyPasscode(email, code);
|
||||
|
||||
if (!verified) {
|
||||
return;
|
||||
}
|
||||
|
||||
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
|
||||
|
||||
const result = await setEmail('email', email, socialToBind);
|
||||
|
||||
if (result?.redirectTo) {
|
||||
window.location.replace(result.redirectTo);
|
||||
}
|
||||
},
|
||||
[email, setEmail, verifyPasscode]
|
||||
);
|
||||
|
||||
return {
|
||||
errorMessage,
|
||||
clearErrorMessage,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export default useContinueSetEmailPasscodeValidation;
|
|
@ -0,0 +1,72 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
|
||||
import { verifyContinueSetSmsPasscode, continueApi } from '@/apis/continue';
|
||||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
|
||||
import { UserFlow, SearchParameters } from '@/types';
|
||||
import { getSearchParameters } from '@/utils';
|
||||
|
||||
import useIdentifierErrorAlert from './use-identifier-error-alert';
|
||||
import useSharedErrorHandler from './use-shared-error-handler';
|
||||
|
||||
const useContinueSetSmsPasscodeValidation = (phone: string, errorCallback?: () => void) => {
|
||||
const { sharedErrorHandlers, errorMessage, clearErrorMessage } = useSharedErrorHandler();
|
||||
|
||||
const requiredProfileErrorHandler = useRequiredProfileErrorHandler(true);
|
||||
|
||||
const identifierNotExistErrorHandler = useIdentifierErrorAlert(
|
||||
UserFlow.continue,
|
||||
SignInIdentifier.Sms,
|
||||
phone
|
||||
);
|
||||
|
||||
const verifyPasscodeErrorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
...sharedErrorHandlers,
|
||||
callback: errorCallback,
|
||||
}),
|
||||
[errorCallback, sharedErrorHandlers]
|
||||
);
|
||||
|
||||
const { run: verifyPasscode } = useApi(verifyContinueSetSmsPasscode, verifyPasscodeErrorHandlers);
|
||||
|
||||
const setPhoneErrorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.phone_not_exists': identifierNotExistErrorHandler,
|
||||
...requiredProfileErrorHandler,
|
||||
callback: errorCallback,
|
||||
}),
|
||||
[errorCallback, identifierNotExistErrorHandler, requiredProfileErrorHandler]
|
||||
);
|
||||
|
||||
const { run: setPhone } = useApi(continueApi, setPhoneErrorHandlers);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (code: string) => {
|
||||
const verified = await verifyPasscode(phone, code);
|
||||
|
||||
if (!verified) {
|
||||
return;
|
||||
}
|
||||
|
||||
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
|
||||
|
||||
const result = await setPhone('phone', phone, socialToBind);
|
||||
|
||||
if (result?.redirectTo) {
|
||||
window.location.replace(result.redirectTo);
|
||||
}
|
||||
},
|
||||
[phone, setPhone, verifyPasscode]
|
||||
);
|
||||
|
||||
return {
|
||||
errorMessage,
|
||||
clearErrorMessage,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export default useContinueSetSmsPasscodeValidation;
|
|
@ -8,6 +8,7 @@ import { signInWithEmail } from '@/apis/sign-in';
|
|||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
|
@ -30,6 +31,8 @@ const useRegisterWithEmailPasscodeValidation = (email: string, errorCallback?: (
|
|||
email
|
||||
);
|
||||
|
||||
const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true);
|
||||
|
||||
const emailExistSignInErrorHandler = useCallback(async () => {
|
||||
const [confirm] = await show({
|
||||
confirmText: 'action.sign_in',
|
||||
|
@ -59,12 +62,14 @@ const useRegisterWithEmailPasscodeValidation = (email: string, errorCallback?: (
|
|||
? identifierExistErrorHandler
|
||||
: emailExistSignInErrorHandler,
|
||||
...sharedErrorHandlers,
|
||||
...requiredProfileErrorHandlers,
|
||||
callback: errorCallback,
|
||||
}),
|
||||
[
|
||||
emailExistSignInErrorHandler,
|
||||
errorCallback,
|
||||
identifierExistErrorHandler,
|
||||
requiredProfileErrorHandlers,
|
||||
sharedErrorHandlers,
|
||||
signInMode,
|
||||
]
|
||||
|
|
|
@ -8,6 +8,7 @@ import { signInWithSms } from '@/apis/sign-in';
|
|||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
import { UserFlow } from '@/types';
|
||||
import { formatPhoneNumberWithCountryCallingCode } from '@/utils/country-code';
|
||||
|
@ -30,6 +31,8 @@ const useRegisterWithSmsPasscodeValidation = (phone: string, errorCallback?: ()
|
|||
formatPhoneNumberWithCountryCallingCode(phone)
|
||||
);
|
||||
|
||||
const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true);
|
||||
|
||||
const phoneExistSignInErrorHandler = useCallback(async () => {
|
||||
const [confirm] = await show({
|
||||
confirmText: 'action.sign_in',
|
||||
|
@ -59,14 +62,16 @@ const useRegisterWithSmsPasscodeValidation = (phone: string, errorCallback?: ()
|
|||
? identifierExistErrorHandler
|
||||
: phoneExistSignInErrorHandler,
|
||||
...sharedErrorHandlers,
|
||||
...requiredProfileErrorHandlers,
|
||||
callback: errorCallback,
|
||||
}),
|
||||
[
|
||||
phoneExistSignInErrorHandler,
|
||||
errorCallback,
|
||||
identifierExistErrorHandler,
|
||||
sharedErrorHandlers,
|
||||
signInMode,
|
||||
identifierExistErrorHandler,
|
||||
phoneExistSignInErrorHandler,
|
||||
sharedErrorHandlers,
|
||||
requiredProfileErrorHandlers,
|
||||
errorCallback,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { verifySignInEmailPasscode } from '@/apis/sign-in';
|
|||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
import { UserFlow, SearchParameters } from '@/types';
|
||||
import { getSearchParameters } from '@/utils';
|
||||
|
@ -33,6 +34,8 @@ const useSignInWithEmailPasscodeValidation = (email: string, errorCallback?: ()
|
|||
email
|
||||
);
|
||||
|
||||
const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true);
|
||||
|
||||
const emailNotExistRegisterErrorHandler = useCallback(async () => {
|
||||
const [confirm] = await show({
|
||||
confirmText: 'action.create',
|
||||
|
@ -63,12 +66,14 @@ const useSignInWithEmailPasscodeValidation = (email: string, errorCallback?: ()
|
|||
? identifierNotExistErrorHandler
|
||||
: emailNotExistRegisterErrorHandler,
|
||||
...sharedErrorHandlers,
|
||||
...requiredProfileErrorHandlers,
|
||||
callback: errorCallback,
|
||||
}),
|
||||
[
|
||||
emailNotExistRegisterErrorHandler,
|
||||
errorCallback,
|
||||
identifierNotExistErrorHandler,
|
||||
requiredProfileErrorHandlers,
|
||||
sharedErrorHandlers,
|
||||
signInMode,
|
||||
socialToBind,
|
||||
|
|
|
@ -8,6 +8,7 @@ import { verifySignInSmsPasscode } from '@/apis/sign-in';
|
|||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
import { UserFlow, SearchParameters } from '@/types';
|
||||
import { getSearchParameters } from '@/utils';
|
||||
|
@ -33,6 +34,8 @@ const useSignInWithSmsPasscodeValidation = (phone: string, errorCallback?: () =>
|
|||
phone
|
||||
);
|
||||
|
||||
const requiredProfileErrorHandlers = useRequiredProfileErrorHandler(true);
|
||||
|
||||
const phoneNotExistRegisterErrorHandler = useCallback(async () => {
|
||||
const [confirm] = await show({
|
||||
confirmText: 'action.create',
|
||||
|
@ -63,15 +66,17 @@ const useSignInWithSmsPasscodeValidation = (phone: string, errorCallback?: () =>
|
|||
? identifierNotExistErrorHandler
|
||||
: phoneNotExistRegisterErrorHandler,
|
||||
...sharedErrorHandlers,
|
||||
...requiredProfileErrorHandlers,
|
||||
callback: errorCallback,
|
||||
}),
|
||||
[
|
||||
phoneNotExistRegisterErrorHandler,
|
||||
errorCallback,
|
||||
identifierNotExistErrorHandler,
|
||||
sharedErrorHandlers,
|
||||
signInMode,
|
||||
socialToBind,
|
||||
identifierNotExistErrorHandler,
|
||||
phoneNotExistRegisterErrorHandler,
|
||||
sharedErrorHandlers,
|
||||
requiredProfileErrorHandlers,
|
||||
errorCallback,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import { SignInIdentifier } from '@logto/schemas';
|
|||
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
import useContinueSetEmailPasscodeValidation from './use-continue-set-email-passcode-validation';
|
||||
import useContinueSetSmsPasscodeValidation from './use-continue-set-sms-passcode-validation';
|
||||
import useForgotPasswordEmailPasscodeValidation from './use-forgot-password-email-passcode-validation';
|
||||
import useForgotPasswordSmsPasscodeValidation from './use-forgot-password-sms-passcode-validation';
|
||||
import useRegisterWithEmailPasscodeValidation from './use-register-with-email-passcode-validation';
|
||||
|
@ -27,9 +29,8 @@ export const getPasscodeValidationHook = (
|
|||
? useForgotPasswordEmailPasscodeValidation
|
||||
: useForgotPasswordSmsPasscodeValidation;
|
||||
default:
|
||||
// TODO: continue flow hook
|
||||
return method === SignInIdentifier.Email
|
||||
? useRegisterWithEmailPasscodeValidation
|
||||
: useRegisterWithSmsPasscodeValidation;
|
||||
? useContinueSetEmailPasscodeValidation
|
||||
: useContinueSetSmsPasscodeValidation;
|
||||
}
|
||||
};
|
||||
|
|
57
packages/ui/src/containers/PhoneForm/SmsContinue.test.tsx
Normal file
57
packages/ui/src/containers/PhoneForm/SmsContinue.test.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { fireEvent, waitFor, act } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import { sendContinueSetPhonePasscode } from '@/apis/continue';
|
||||
import { getDefaultCountryCallingCode } from '@/utils/country-code';
|
||||
|
||||
import SmsContinue from './SmsContinue';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
// PhoneNum CountryCode detection
|
||||
jest.mock('i18next', () => ({
|
||||
language: 'en',
|
||||
}));
|
||||
|
||||
jest.mock('@/apis/continue', () => ({
|
||||
sendContinueSetPhonePasscode: jest.fn(() => ({ success: true })),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
describe('SmsContinue', () => {
|
||||
const phone = '8573333333';
|
||||
const defaultCountryCallingCode = getDefaultCountryCallingCode();
|
||||
const fullPhoneNumber = `${defaultCountryCallingCode}${phone}`;
|
||||
|
||||
test('register form submit', async () => {
|
||||
const { container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<SmsContinue />
|
||||
</MemoryRouter>
|
||||
);
|
||||
const phoneInput = container.querySelector('input[name="phone"]');
|
||||
|
||||
if (phoneInput) {
|
||||
fireEvent.change(phoneInput, { target: { value: phone } });
|
||||
}
|
||||
|
||||
const submitButton = getByText('action.continue');
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendContinueSetPhonePasscode).toBeCalledWith(fullPhoneNumber);
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
{ pathname: '/continue/sms/passcode-validation', search: '' },
|
||||
{ state: { phone: fullPhoneNumber } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
32
packages/ui/src/containers/PhoneForm/SmsContinue.tsx
Normal file
32
packages/ui/src/containers/PhoneForm/SmsContinue.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import usePasswordlessSendCode from '@/hooks/use-passwordless-send-code';
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
import SmsForm from './PhoneForm';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
// eslint-disable-next-line react/boolean-prop-naming
|
||||
autoFocus?: boolean;
|
||||
hasSwitch?: boolean;
|
||||
};
|
||||
|
||||
const SmsContinue = (props: Props) => {
|
||||
const { onSubmit, errorMessage, clearErrorMessage } = usePasswordlessSendCode(
|
||||
UserFlow.continue,
|
||||
SignInIdentifier.Sms
|
||||
);
|
||||
|
||||
return (
|
||||
<SmsForm
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
errorMessage={errorMessage}
|
||||
clearErrorMessage={clearErrorMessage}
|
||||
hasTerms={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmsContinue;
|
|
@ -1,3 +1,4 @@
|
|||
export { default as SmsRegister } from './SmsRegister';
|
||||
export { default as SmsSignIn } from './SmsSignIn';
|
||||
export { default as SmsResetPassword } from './SmsResetPassword';
|
||||
export { default as SmsContinue } from './SmsContinue';
|
||||
|
|
|
@ -18,6 +18,10 @@ jest.mock('react-device-detect', () => ({
|
|||
jest.mock('i18next', () => ({
|
||||
language: 'en',
|
||||
}));
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('<PhonePassword>', () => {
|
||||
afterEach(() => {
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { fireEvent, act, waitFor } from '@testing-library/react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import { continueApi } from '@/apis/continue';
|
||||
|
||||
import SetUsername from '.';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('@/apis/continue', () => ({
|
||||
continueApi: jest.fn(async () => ({})),
|
||||
}));
|
||||
|
||||
describe('<UsernameRegister />', () => {
|
||||
test('default render', () => {
|
||||
const { queryByText, container } = renderWithPageContext(<SetUsername />);
|
||||
expect(container.querySelector('input[name="new-username"]')).not.toBeNull();
|
||||
expect(queryByText('action.continue')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('submit form properly', async () => {
|
||||
const { getByText, container } = renderWithPageContext(<SetUsername />);
|
||||
const submitButton = getByText('action.continue');
|
||||
const usernameInput = container.querySelector('input[name="new-username"]');
|
||||
|
||||
if (usernameInput) {
|
||||
fireEvent.change(usernameInput, { target: { value: 'username' } });
|
||||
}
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(continueApi).toBeCalledWith('username', 'username', undefined);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import UsernameForm from '../UsernameForm';
|
||||
import useSetUsername from './use-set-username';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const SetUsername = ({ className }: Props) => {
|
||||
const { errorMessage, clearErrorMessage, onSubmit } = useSetUsername();
|
||||
|
||||
return (
|
||||
<UsernameForm
|
||||
className={className}
|
||||
hasTerms={false}
|
||||
errorMessage={errorMessage}
|
||||
clearErrorMessage={clearErrorMessage}
|
||||
submitText="action.continue"
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetUsername;
|
|
@ -0,0 +1,50 @@
|
|||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { continueApi } from '@/apis/continue';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
|
||||
import { SearchParameters } from '@/types';
|
||||
import { getSearchParameters } from '@/utils';
|
||||
|
||||
const useSetUsername = () => {
|
||||
const navigate = useNavigate();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const clearErrorMessage = useCallback(() => {
|
||||
setErrorMessage('');
|
||||
}, []);
|
||||
|
||||
const requiredProfileErrorHandler = useRequiredProfileErrorHandler();
|
||||
|
||||
const errorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.username_exists_register': (error) => {
|
||||
setErrorMessage(error.message);
|
||||
},
|
||||
...requiredProfileErrorHandler,
|
||||
}),
|
||||
[requiredProfileErrorHandler]
|
||||
);
|
||||
|
||||
const { result, run: setUsername } = useApi(continueApi, errorHandlers);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (username: string) => {
|
||||
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
|
||||
await setUsername('username', username, socialToBind);
|
||||
},
|
||||
[setUsername]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (result?.redirectTo) {
|
||||
window.location.replace(result.redirectTo);
|
||||
}
|
||||
}, [navigate, result]);
|
||||
|
||||
return { errorMessage, clearErrorMessage, onSubmit };
|
||||
};
|
||||
|
||||
export default useSetUsername;
|
|
@ -2,49 +2,66 @@ import { fireEvent, act, waitFor } from '@testing-library/react';
|
|||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { checkUsername } from '@/apis/register';
|
||||
|
||||
import UsernameRegister from '.';
|
||||
import UsernameForm from './UsernameForm';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('@/apis/register', () => ({
|
||||
checkUsername: jest.fn(async () => ({})),
|
||||
}));
|
||||
const onSubmit = jest.fn();
|
||||
const onClearErrorMessage = jest.fn();
|
||||
|
||||
describe('<UsernameRegister />', () => {
|
||||
test('default render', () => {
|
||||
const { queryByText, container } = renderWithPageContext(<UsernameRegister />);
|
||||
test('default render without terms', () => {
|
||||
const { queryByText, container } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<UsernameForm hasTerms={false} onSubmit={onSubmit} />
|
||||
</SettingsProvider>
|
||||
);
|
||||
|
||||
expect(container.querySelector('input[name="new-username"]')).not.toBeNull();
|
||||
expect(queryByText('description.terms_of_use')).toBeNull();
|
||||
expect(queryByText('action.create_account')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('render with terms settings enabled', () => {
|
||||
const { queryByText } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<UsernameRegister />
|
||||
<UsernameForm onSubmit={onSubmit} />
|
||||
</SettingsProvider>
|
||||
);
|
||||
expect(queryByText('description.terms_of_use')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('render with error message', () => {
|
||||
const { queryByText, getByText } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<UsernameForm
|
||||
errorMessage="error_message"
|
||||
clearErrorMessage={onClearErrorMessage}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</SettingsProvider>
|
||||
);
|
||||
expect(queryByText('error_message')).not.toBeNull();
|
||||
|
||||
const submitButton = getByText('action.create_account');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(onClearErrorMessage).toBeCalled();
|
||||
});
|
||||
|
||||
test('username are required', () => {
|
||||
const { queryByText, getByText } = renderWithPageContext(<UsernameRegister />);
|
||||
const { queryByText, getByText } = renderWithPageContext(<UsernameForm onSubmit={onSubmit} />);
|
||||
const submitButton = getByText('action.create_account');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(queryByText('username_required')).not.toBeNull();
|
||||
|
||||
expect(checkUsername).not.toBeCalled();
|
||||
expect(onSubmit).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('username with initial numeric char should throw', () => {
|
||||
const { queryByText, getByText, container } = renderWithPageContext(<UsernameRegister />);
|
||||
const { queryByText, getByText, container } = renderWithPageContext(
|
||||
<UsernameForm onSubmit={onSubmit} />
|
||||
);
|
||||
const submitButton = getByText('action.create_account');
|
||||
|
||||
const usernameInput = container.querySelector('input[name="new-username"]');
|
||||
|
@ -57,7 +74,7 @@ describe('<UsernameRegister />', () => {
|
|||
|
||||
expect(queryByText('username_should_not_start_with_number')).not.toBeNull();
|
||||
|
||||
expect(checkUsername).not.toBeCalled();
|
||||
expect(onSubmit).not.toBeCalled();
|
||||
|
||||
// Clear error
|
||||
if (usernameInput) {
|
||||
|
@ -68,7 +85,9 @@ describe('<UsernameRegister />', () => {
|
|||
});
|
||||
|
||||
test('username with special character should throw', () => {
|
||||
const { queryByText, getByText, container } = renderWithPageContext(<UsernameRegister />);
|
||||
const { queryByText, getByText, container } = renderWithPageContext(
|
||||
<UsernameForm onSubmit={onSubmit} />
|
||||
);
|
||||
const submitButton = getByText('action.create_account');
|
||||
const usernameInput = container.querySelector('input[name="new-username"]');
|
||||
|
||||
|
@ -80,7 +99,7 @@ describe('<UsernameRegister />', () => {
|
|||
|
||||
expect(queryByText('username_valid_charset')).not.toBeNull();
|
||||
|
||||
expect(checkUsername).not.toBeCalled();
|
||||
expect(onSubmit).not.toBeCalled();
|
||||
|
||||
// Clear error
|
||||
if (usernameInput) {
|
||||
|
@ -93,7 +112,7 @@ describe('<UsernameRegister />', () => {
|
|||
test('submit form properly with terms settings enabled', async () => {
|
||||
const { getByText, container } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<UsernameRegister />
|
||||
<UsernameForm onSubmit={onSubmit} />
|
||||
</SettingsProvider>
|
||||
);
|
||||
const submitButton = getByText('action.create_account');
|
||||
|
@ -111,7 +130,7 @@ describe('<UsernameRegister />', () => {
|
|||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkUsername).toBeCalledWith('username');
|
||||
expect(onSubmit).toBeCalledWith('username');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,24 +1,25 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import type { I18nKey } from '@logto/phrases-ui';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { checkUsername } from '@/apis/register';
|
||||
import Button from '@/components/Button';
|
||||
import ErrorMessage from '@/components/ErrorMessage';
|
||||
import Input from '@/components/Input';
|
||||
import TermsOfUse from '@/containers/TermsOfUse';
|
||||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useForm from '@/hooks/use-form';
|
||||
import useTerms from '@/hooks/use-terms';
|
||||
import { UserFlow } from '@/types';
|
||||
import { usernameValidation } from '@/utils/field-validations';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
hasTerms?: boolean;
|
||||
onSubmit: (username: string) => Promise<void>;
|
||||
errorMessage?: string;
|
||||
clearErrorMessage?: () => void;
|
||||
submitText?: I18nKey;
|
||||
};
|
||||
|
||||
type FieldState = {
|
||||
|
@ -29,57 +30,41 @@ const defaultState: FieldState = {
|
|||
username: '',
|
||||
};
|
||||
|
||||
const UsernameRegister = ({ className }: Props) => {
|
||||
const UsernameForm = ({
|
||||
className,
|
||||
hasTerms = true,
|
||||
onSubmit,
|
||||
errorMessage,
|
||||
submitText = 'action.create_account',
|
||||
clearErrorMessage,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { termsValidation } = useTerms();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
setFieldErrors,
|
||||
register: fieldRegister,
|
||||
validateForm,
|
||||
} = useForm(defaultState);
|
||||
|
||||
const errorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.username_exists_register': () => {
|
||||
setFieldErrors((state) => ({
|
||||
...state,
|
||||
username: 'username_exists',
|
||||
}));
|
||||
},
|
||||
}),
|
||||
[setFieldErrors]
|
||||
);
|
||||
|
||||
const { run: asyncCheckUsername } = useApi(checkUsername, errorHandlers);
|
||||
|
||||
const onSubmitHandler = useCallback(
|
||||
async (event?: React.FormEvent<HTMLFormElement>) => {
|
||||
event?.preventDefault();
|
||||
|
||||
clearErrorMessage?.();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await termsValidation())) {
|
||||
if (hasTerms && !(await termsValidation())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { username } = fieldValue;
|
||||
|
||||
// Use sync call for this api to make sure the username value being passed to the password set page stays the same
|
||||
const result = await asyncCheckUsername(username);
|
||||
|
||||
if (result) {
|
||||
navigate(`/${UserFlow.register}/${SignInIdentifier.Username}/password`, {
|
||||
state: { username },
|
||||
});
|
||||
}
|
||||
void onSubmit(fieldValue.username);
|
||||
},
|
||||
[validateForm, termsValidation, fieldValue, asyncCheckUsername, navigate]
|
||||
[clearErrorMessage, validateForm, hasTerms, termsValidation, onSubmit, fieldValue.username]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -93,14 +78,15 @@ const UsernameRegister = ({ className }: Props) => {
|
|||
setFieldValue((state) => ({ ...state, username: '' }));
|
||||
}}
|
||||
/>
|
||||
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
|
||||
|
||||
<TermsOfUse className={styles.terms} />
|
||||
{hasTerms && <TermsOfUse className={styles.terms} />}
|
||||
|
||||
<Button title="action.create_account" onClick={async () => onSubmitHandler()} />
|
||||
<Button title={submitText} onClick={async () => onSubmitHandler()} />
|
||||
|
||||
<input hidden type="submit" />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsernameRegister;
|
||||
export default UsernameForm;
|
|
@ -0,0 +1,51 @@
|
|||
import { fireEvent, act, waitFor } from '@testing-library/react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { checkUsername } from '@/apis/register';
|
||||
|
||||
import UsernameRegister from '.';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('@/apis/register', () => ({
|
||||
checkUsername: jest.fn(async () => ({})),
|
||||
}));
|
||||
|
||||
describe('<UsernameRegister />', () => {
|
||||
test('default render', () => {
|
||||
const { queryByText, container } = renderWithPageContext(<UsernameRegister />);
|
||||
expect(container.querySelector('input[name="new-username"]')).not.toBeNull();
|
||||
expect(queryByText('action.create_account')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('submit form properly', async () => {
|
||||
const { getByText, container } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<UsernameRegister />
|
||||
</SettingsProvider>
|
||||
);
|
||||
const submitButton = getByText('action.create_account');
|
||||
const usernameInput = container.querySelector('input[name="new-username"]');
|
||||
|
||||
if (usernameInput) {
|
||||
fireEvent.change(usernameInput, { target: { value: 'username' } });
|
||||
}
|
||||
|
||||
const termsButton = getByText('description.agree_with_terms');
|
||||
fireEvent.click(termsButton);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkUsername).toBeCalledWith('username');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
import UsernameForm from '../UsernameForm';
|
||||
import useUsernameRegister from './use-username-register';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const UsernameRegister = ({ className }: Props) => {
|
||||
const { errorMessage, clearErrorMessage, onSubmit } = useUsernameRegister();
|
||||
|
||||
return (
|
||||
<UsernameForm
|
||||
className={className}
|
||||
errorMessage={errorMessage}
|
||||
clearErrorMessage={clearErrorMessage}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsernameRegister;
|
|
@ -0,0 +1,45 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { checkUsername } from '@/apis/register';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
const useUsernameRegister = () => {
|
||||
const navigate = useNavigate();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const clearErrorMessage = useCallback(() => {
|
||||
setErrorMessage('');
|
||||
}, []);
|
||||
|
||||
const errorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.username_exists_register': (error) => {
|
||||
setErrorMessage(error.message);
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const { run: asyncCheckUsername } = useApi(checkUsername, errorHandlers);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (username: string) => {
|
||||
const result = await asyncCheckUsername(username);
|
||||
|
||||
if (result) {
|
||||
navigate(`/${UserFlow.register}/${SignInIdentifier.Username}/password`, {
|
||||
state: { username },
|
||||
});
|
||||
}
|
||||
},
|
||||
[asyncCheckUsername, navigate]
|
||||
);
|
||||
|
||||
return { errorMessage, clearErrorMessage, onSubmit };
|
||||
};
|
||||
|
||||
export default useUsernameRegister;
|
|
@ -11,4 +11,9 @@
|
|||
.terms {
|
||||
margin-bottom: _.unit(4);
|
||||
}
|
||||
|
||||
.formErrors {
|
||||
margin-top: _.unit(-2);
|
||||
margin-bottom: _.unit(4);
|
||||
}
|
||||
}
|
2
packages/ui/src/containers/UsernameForm/index.ts
Normal file
2
packages/ui/src/containers/UsernameForm/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as UsernameRegister } from './UsernameRegister';
|
||||
export { default as SetUsername } from './SetUsername';
|
|
@ -14,6 +14,10 @@ jest.mock('@/apis/sign-in', () => ({ signInWithUsername: jest.fn(async () => 0)
|
|||
jest.mock('react-device-detect', () => ({
|
||||
isMobile: true,
|
||||
}));
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('<UsernameSignIn>', () => {
|
||||
afterEach(() => {
|
||||
|
|
|
@ -7,10 +7,20 @@ import { registerWithSocial, bindSocialRelatedUser } from '@/apis/social';
|
|||
import useApi from '@/hooks/use-api';
|
||||
import { bindSocialStateGuard } from '@/types/guard';
|
||||
|
||||
import useRequiredProfileErrorHandler from './use-required-profile-error-handler';
|
||||
|
||||
const useBindSocial = () => {
|
||||
const { state } = useLocation();
|
||||
const { result: registerResult, run: asyncRegisterWithSocial } = useApi(registerWithSocial);
|
||||
const { result: bindUserResult, run: asyncBindSocialRelatedUser } = useApi(bindSocialRelatedUser);
|
||||
const requiredProfileErrorHandlers = useRequiredProfileErrorHandler();
|
||||
|
||||
const { result: registerResult, run: asyncRegisterWithSocial } = useApi(
|
||||
registerWithSocial,
|
||||
requiredProfileErrorHandlers
|
||||
);
|
||||
const { result: bindUserResult, run: asyncBindSocialRelatedUser } = useApi(
|
||||
bindSocialRelatedUser,
|
||||
requiredProfileErrorHandlers
|
||||
);
|
||||
|
||||
const createAccountHandler = useCallback(
|
||||
(connectorId: string) => {
|
||||
|
|
|
@ -11,6 +11,8 @@ import useApi from '@/hooks/use-api';
|
|||
import { SearchParameters } from '@/types';
|
||||
import { getSearchParameters } from '@/utils';
|
||||
|
||||
import useRequiredProfileErrorHandler from './use-required-profile-error-handler';
|
||||
|
||||
const apiMap = {
|
||||
[SignInIdentifier.Username]: signInWithUsername,
|
||||
[SignInIdentifier.Email]: signInWithEmailPassword,
|
||||
|
@ -24,13 +26,16 @@ const usePasswordSignIn = (method: SignInIdentifier) => {
|
|||
setErrorMessage('');
|
||||
}, []);
|
||||
|
||||
const requiredProfileErrorHandler = useRequiredProfileErrorHandler();
|
||||
|
||||
const errorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'session.invalid_credentials': (error) => {
|
||||
setErrorMessage(error.message);
|
||||
},
|
||||
...requiredProfileErrorHandler,
|
||||
}),
|
||||
[setErrorMessage]
|
||||
[requiredProfileErrorHandler]
|
||||
);
|
||||
|
||||
const { result, run: asyncSignIn } = useApi(apiMap[method], errorHandlers);
|
||||
|
|
63
packages/ui/src/hooks/use-required-profile-error-handler.ts
Normal file
63
packages/ui/src/hooks/use-required-profile-error-handler.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
const useRequiredProfileErrorHandler = (replace?: boolean) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const requiredProfileErrorHandler = useMemo(
|
||||
() => ({
|
||||
'user.require_password': () => {
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/password`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
},
|
||||
'user.require_username': () => {
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/username`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
},
|
||||
'user.require_email': () => {
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/email`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
},
|
||||
'user.require_sms': () => {
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/sms`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
},
|
||||
'user.require_email_or_sms': () => {
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/email`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
},
|
||||
}),
|
||||
[navigate, replace]
|
||||
);
|
||||
|
||||
return requiredProfileErrorHandler;
|
||||
};
|
||||
|
||||
export default useRequiredProfileErrorHandler;
|
|
@ -9,7 +9,11 @@ export const useSieMethods = () => {
|
|||
return {
|
||||
signUpMethods: methods ?? [],
|
||||
signUpSettings: { password, verify },
|
||||
signInMethods: experienceSettings?.signIn.methods ?? [],
|
||||
signInMethods:
|
||||
experienceSettings?.signIn.methods.filter(
|
||||
// Filter out empty settings
|
||||
({ password, verificationCode }) => password || verificationCode
|
||||
) ?? [],
|
||||
socialConnectors: experienceSettings?.socialConnectors ?? [],
|
||||
signInMode: experienceSettings?.signInMode,
|
||||
forgotPassword: experienceSettings?.forgotPassword,
|
||||
|
|
|
@ -10,9 +10,11 @@ import { stateValidation } from '@/utils/social-connectors';
|
|||
import type { ErrorHandlers } from './use-api';
|
||||
import useApi from './use-api';
|
||||
import { PageContext } from './use-page-context';
|
||||
import useRequiredProfileErrorHandler from './use-required-profile-error-handler';
|
||||
|
||||
const useSocialSignInListener = () => {
|
||||
const { setToast, experienceSettings } = useContext(PageContext);
|
||||
const requiredProfileErrorHandlers = useRequiredProfileErrorHandler();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const parameters = useParams();
|
||||
|
@ -35,8 +37,15 @@ const useSocialSignInListener = () => {
|
|||
});
|
||||
}
|
||||
},
|
||||
...requiredProfileErrorHandlers,
|
||||
}),
|
||||
[experienceSettings?.signInMode, navigate, parameters.connector, setToast]
|
||||
[
|
||||
experienceSettings?.signInMode,
|
||||
navigate,
|
||||
parameters.connector,
|
||||
requiredProfileErrorHandlers,
|
||||
setToast,
|
||||
]
|
||||
);
|
||||
|
||||
const { result, run: asyncSignInWithSocial } = useApi(
|
||||
|
|
57
packages/ui/src/pages/Continue/SetEmail/index.test.tsx
Normal file
57
packages/ui/src/pages/Continue/SetEmail/index.test.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { mockSignInExperienceSettings } from '@/__mocks__/logto';
|
||||
|
||||
import SetEmail from '.';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
useLocation: () => ({ pathname: '' }),
|
||||
}));
|
||||
|
||||
describe('SetEmail', () => {
|
||||
it('render set email', () => {
|
||||
const { queryByText, container } = renderWithPageContext(
|
||||
<SettingsProvider
|
||||
settings={{
|
||||
...mockSignInExperienceSettings,
|
||||
signUp: { ...mockSignInExperienceSettings.signUp, methods: [SignInIdentifier.Email] },
|
||||
}}
|
||||
>
|
||||
<SetEmail />
|
||||
</SettingsProvider>
|
||||
);
|
||||
|
||||
expect(queryByText('description.link_email')).not.toBeNull();
|
||||
expect(queryByText('description.link_email_description')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="email"]')).not.toBeNull();
|
||||
expect(queryByText('action.continue')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('render set email with phone alterations', () => {
|
||||
const { queryByText, container } = renderWithPageContext(
|
||||
<SettingsProvider
|
||||
settings={{
|
||||
...mockSignInExperienceSettings,
|
||||
signUp: {
|
||||
...mockSignInExperienceSettings.signUp,
|
||||
methods: [SignInIdentifier.Email, SignInIdentifier.Sms],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SetEmail />
|
||||
</SettingsProvider>
|
||||
);
|
||||
|
||||
expect(queryByText('description.link_email_or_phone')).not.toBeNull();
|
||||
expect(queryByText('description.link_email_or_phone_description')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="email"]')).not.toBeNull();
|
||||
expect(queryByText('action.continue')).not.toBeNull();
|
||||
expect(queryByText('action.switch_to')).not.toBeNull();
|
||||
});
|
||||
});
|
31
packages/ui/src/pages/Continue/SetEmail/index.tsx
Normal file
31
packages/ui/src/pages/Continue/SetEmail/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import SecondaryPageWrapper from '@/components/SecondaryPageWrapper';
|
||||
import { EmailContinue } from '@/containers/EmailForm';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
import ErrorPage from '@/pages/ErrorPage';
|
||||
|
||||
const SetEmail = () => {
|
||||
const { signUpMethods } = useSieMethods();
|
||||
|
||||
if (!signUpMethods.includes(SignInIdentifier.Email)) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
const phoneSignUpAlteration = signUpMethods.includes(SignInIdentifier.Sms);
|
||||
|
||||
return (
|
||||
<SecondaryPageWrapper
|
||||
title={phoneSignUpAlteration ? 'description.link_email_or_phone' : 'description.link_email'}
|
||||
description={
|
||||
phoneSignUpAlteration
|
||||
? 'description.link_email_or_phone_description'
|
||||
: 'description.link_email_description'
|
||||
}
|
||||
>
|
||||
<EmailContinue autoFocus hasSwitch={phoneSignUpAlteration} />
|
||||
</SecondaryPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetEmail;
|
58
packages/ui/src/pages/Continue/SetPassword/index.test.tsx
Normal file
58
packages/ui/src/pages/Continue/SetPassword/index.test.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { act, waitFor, fireEvent } from '@testing-library/react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { continueApi } from '@/apis/continue';
|
||||
|
||||
import SetPassword from '.';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('@/apis/continue', () => ({
|
||||
continueApi: jest.fn(async () => ({ redirectTo: '/' })),
|
||||
}));
|
||||
|
||||
describe('SetPassword', () => {
|
||||
it('render set-password page properly', () => {
|
||||
const { queryByText, container } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<SetPassword />
|
||||
</SettingsProvider>
|
||||
);
|
||||
expect(container.querySelector('input[name="new-password"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="confirm-new-password"]')).not.toBeNull();
|
||||
expect(queryByText('action.save_password')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should submit properly', async () => {
|
||||
const { getByText, container } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<SetPassword />
|
||||
</SettingsProvider>
|
||||
);
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="new-password"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]');
|
||||
|
||||
act(() => {
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: '123456' } });
|
||||
}
|
||||
|
||||
if (confirmPasswordInput) {
|
||||
fireEvent.change(confirmPasswordInput, { target: { value: '123456' } });
|
||||
}
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(continueApi).toBeCalledWith('password', '123456', undefined);
|
||||
});
|
||||
});
|
||||
});
|
25
packages/ui/src/pages/Continue/SetPassword/index.tsx
Normal file
25
packages/ui/src/pages/Continue/SetPassword/index.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import SecondaryPageWrapper from '@/components/SecondaryPageWrapper';
|
||||
import SetPasswordForm from '@/containers/SetPassword';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
import ErrorPage from '@/pages/ErrorPage';
|
||||
|
||||
import useSetPassword from './use-set-password';
|
||||
|
||||
const SetPassword = () => {
|
||||
const { setPassword } = useSetPassword();
|
||||
|
||||
const { signUpSettings } = useSieMethods();
|
||||
|
||||
// Password not enabled for sign-up identifiers
|
||||
if (!signUpSettings.password) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SecondaryPageWrapper title="description.set_password">
|
||||
<SetPasswordForm autoFocus onSubmit={setPassword} />
|
||||
</SecondaryPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetPassword;
|
|
@ -0,0 +1,50 @@
|
|||
import { useMemo, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { continueApi } from '@/apis/continue';
|
||||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
|
||||
import { SearchParameters } from '@/types';
|
||||
import { getSearchParameters } from '@/utils';
|
||||
|
||||
const useSetPassword = () => {
|
||||
const navigate = useNavigate();
|
||||
const { show } = useConfirmModal();
|
||||
|
||||
const requiredProfileErrorHandler = useRequiredProfileErrorHandler(true);
|
||||
|
||||
const errorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.password_exists': async (error) => {
|
||||
await show({ type: 'alert', ModalContent: error.message, cancelText: 'action.got_it' });
|
||||
navigate(-1);
|
||||
},
|
||||
...requiredProfileErrorHandler,
|
||||
}),
|
||||
[navigate, requiredProfileErrorHandler, show]
|
||||
);
|
||||
|
||||
const { result, run: asyncSetPassword } = useApi(continueApi, errorHandlers);
|
||||
|
||||
const setPassword = useCallback(
|
||||
async (password: string) => {
|
||||
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
|
||||
await asyncSetPassword('password', password, socialToBind);
|
||||
},
|
||||
[asyncSetPassword]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (result?.redirectTo) {
|
||||
window.location.replace(result.redirectTo);
|
||||
}
|
||||
}, [navigate, result]);
|
||||
|
||||
return {
|
||||
setPassword,
|
||||
};
|
||||
};
|
||||
|
||||
export default useSetPassword;
|
61
packages/ui/src/pages/Continue/SetPhone/index.test.tsx
Normal file
61
packages/ui/src/pages/Continue/SetPhone/index.test.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { mockSignInExperienceSettings } from '@/__mocks__/logto';
|
||||
|
||||
import SetPhone from '.';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
// PhoneNum CountryCode detection
|
||||
jest.mock('i18next', () => ({
|
||||
language: 'en',
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
useLocation: () => ({ pathname: '' }),
|
||||
}));
|
||||
|
||||
describe('SetPhone', () => {
|
||||
it('render set phone', () => {
|
||||
const { queryByText, container } = renderWithPageContext(
|
||||
<SettingsProvider
|
||||
settings={{
|
||||
...mockSignInExperienceSettings,
|
||||
signUp: { ...mockSignInExperienceSettings.signUp, methods: [SignInIdentifier.Sms] },
|
||||
}}
|
||||
>
|
||||
<SetPhone />
|
||||
</SettingsProvider>
|
||||
);
|
||||
|
||||
expect(queryByText('description.link_phone')).not.toBeNull();
|
||||
expect(queryByText('description.link_phone_description')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="phone"]')).not.toBeNull();
|
||||
expect(queryByText('action.continue')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('render set phone with email alterations', () => {
|
||||
const { queryByText, container } = renderWithPageContext(
|
||||
<SettingsProvider
|
||||
settings={{
|
||||
...mockSignInExperienceSettings,
|
||||
signUp: {
|
||||
...mockSignInExperienceSettings.signUp,
|
||||
methods: [SignInIdentifier.Email, SignInIdentifier.Sms],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SetPhone />
|
||||
</SettingsProvider>
|
||||
);
|
||||
|
||||
expect(queryByText('description.link_email_or_phone')).not.toBeNull();
|
||||
expect(queryByText('description.link_email_or_phone_description')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="phone"]')).not.toBeNull();
|
||||
expect(queryByText('action.continue')).not.toBeNull();
|
||||
expect(queryByText('action.switch_to')).not.toBeNull();
|
||||
});
|
||||
});
|
31
packages/ui/src/pages/Continue/SetPhone/index.tsx
Normal file
31
packages/ui/src/pages/Continue/SetPhone/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import SecondaryPageWrapper from '@/components/SecondaryPageWrapper';
|
||||
import { SmsContinue } from '@/containers/PhoneForm';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
import ErrorPage from '@/pages/ErrorPage';
|
||||
|
||||
const SetPhone = () => {
|
||||
const { signUpMethods } = useSieMethods();
|
||||
|
||||
if (!signUpMethods.includes(SignInIdentifier.Sms)) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
const emailSignUpAlteration = signUpMethods.includes(SignInIdentifier.Email);
|
||||
|
||||
return (
|
||||
<SecondaryPageWrapper
|
||||
title={emailSignUpAlteration ? 'description.link_email_or_phone' : 'description.link_phone'}
|
||||
description={
|
||||
emailSignUpAlteration
|
||||
? 'description.link_email_or_phone_description'
|
||||
: 'description.link_phone_description'
|
||||
}
|
||||
>
|
||||
<SmsContinue autoFocus hasSwitch={emailSignUpAlteration} />
|
||||
</SecondaryPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetPhone;
|
52
packages/ui/src/pages/Continue/SetUsername/index.test.tsx
Normal file
52
packages/ui/src/pages/Continue/SetUsername/index.test.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { act, waitFor, fireEvent } from '@testing-library/react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { continueApi } from '@/apis/continue';
|
||||
|
||||
import SetUsername from '.';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('@/apis/continue', () => ({
|
||||
continueApi: jest.fn(async () => ({ redirectTo: '/' })),
|
||||
}));
|
||||
|
||||
describe('SetPassword', () => {
|
||||
it('render set-password page properly', () => {
|
||||
const { queryByText, container } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<SetUsername />
|
||||
</SettingsProvider>
|
||||
);
|
||||
expect(container.querySelector('input[name="new-username"]')).not.toBeNull();
|
||||
expect(queryByText('action.continue')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should submit properly', async () => {
|
||||
const { getByText, container } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<SetUsername />
|
||||
</SettingsProvider>
|
||||
);
|
||||
const submitButton = getByText('action.continue');
|
||||
const usernameInput = container.querySelector('input[name="new-username"]');
|
||||
|
||||
act(() => {
|
||||
if (usernameInput) {
|
||||
fireEvent.change(usernameInput, { target: { value: 'username' } });
|
||||
}
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(continueApi).toBeCalledWith('username', 'username', undefined);
|
||||
});
|
||||
});
|
||||
});
|
25
packages/ui/src/pages/Continue/SetUsername/index.tsx
Normal file
25
packages/ui/src/pages/Continue/SetUsername/index.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import SecondaryPageWrapper from '@/components/SecondaryPageWrapper';
|
||||
import { SetUsername as SetUsernameForm } from '@/containers/UsernameForm';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
import ErrorPage from '@/pages/ErrorPage';
|
||||
|
||||
const SetUsername = () => {
|
||||
const { signUpMethods } = useSieMethods();
|
||||
|
||||
if (!signUpMethods.includes(SignInIdentifier.Username)) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SecondaryPageWrapper
|
||||
title="description.enter_username"
|
||||
description="description.enter_username_description"
|
||||
>
|
||||
<SetUsernameForm />
|
||||
</SecondaryPageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetUsername;
|
37
packages/ui/src/pages/Continue/index.tsx
Normal file
37
packages/ui/src/pages/Continue/index.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import ErrorPage from '@/pages/ErrorPage';
|
||||
|
||||
import SetEmail from './SetEmail';
|
||||
import SetPassword from './SetPassword';
|
||||
import SetPhone from './SetPhone';
|
||||
import SetUsername from './SetUsername';
|
||||
|
||||
type Parameters = {
|
||||
method?: string;
|
||||
};
|
||||
|
||||
const Continue = () => {
|
||||
const { method = '' } = useParams<Parameters>();
|
||||
|
||||
if (method === 'password') {
|
||||
return <SetPassword />;
|
||||
}
|
||||
|
||||
if (method === SignInIdentifier.Username) {
|
||||
return <SetUsername />;
|
||||
}
|
||||
|
||||
if (method === SignInIdentifier.Email) {
|
||||
return <SetEmail />;
|
||||
}
|
||||
|
||||
if (method === SignInIdentifier.Sms) {
|
||||
return <SetPhone />;
|
||||
}
|
||||
|
||||
return <ErrorPage />;
|
||||
};
|
||||
|
||||
export default Continue;
|
|
@ -3,7 +3,7 @@ import type { SignInIdentifier, ConnectorMetadata } from '@logto/schemas';
|
|||
import { EmailRegister } from '@/containers/EmailForm';
|
||||
import { SmsRegister } from '@/containers/PhoneForm';
|
||||
import SocialSignIn from '@/containers/SocialSignIn';
|
||||
import UsernameRegister from '@/containers/UsernameRegister';
|
||||
import { UsernameRegister } from '@/containers/UsernameForm';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
|
|
@ -25,6 +25,14 @@ export const userFlowGuard = s.union([
|
|||
s.literal('sign-in'),
|
||||
s.literal('register'),
|
||||
s.literal('forgot-password'),
|
||||
s.literal('continue'),
|
||||
]);
|
||||
|
||||
export const continueMethodGuard = s.union([
|
||||
s.literal('password'),
|
||||
s.literal('username'),
|
||||
s.literal(SignInIdentifier.Email),
|
||||
s.literal(SignInIdentifier.Sms),
|
||||
]);
|
||||
|
||||
export const usernameGuard = s.object({
|
||||
|
|
Loading…
Add table
Reference in a new issue