mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
refactor(ui): replace continue api with patchProfile (#2744)
This commit is contained in:
parent
06ec86d831
commit
e02f7e5ac2
22 changed files with 110 additions and 175 deletions
|
@ -153,7 +153,7 @@ describe('verifyUserAccount', () => {
|
|||
};
|
||||
|
||||
await expect(verifyUserAccount(interaction)).rejects.toMatchError(
|
||||
new RequestError({ code: 'user.user_not_exist', status: 404 }, { identity: 'email' })
|
||||
new RequestError({ code: 'user.user_not_exist', status: 404 }, { identifier: 'email' })
|
||||
);
|
||||
|
||||
expect(findUserByIdentifierMock).toBeCalledWith({ email: 'email' });
|
||||
|
|
|
@ -26,7 +26,7 @@ const identifyUserByVerifiedEmailOrPhone = async (
|
|||
|
||||
assertThat(
|
||||
user,
|
||||
new RequestError({ code: 'user.user_not_exist', status: 404 }, { identity: identifier.value })
|
||||
new RequestError({ code: 'user.user_not_exist', status: 404 }, { identifier: identifier.value })
|
||||
);
|
||||
|
||||
const { id, isSuspended } = user;
|
||||
|
|
|
@ -62,7 +62,7 @@ const errors = {
|
|||
email_or_phone_required_in_profile:
|
||||
'You need to add an email address or phone number before signing-in.',
|
||||
suspended: 'This account is suspended.',
|
||||
user_not_exist: 'User with {{ identity }} does not exist.',
|
||||
user_not_exist: 'User with {{ identifier }} does not exist.',
|
||||
missing_profile: 'You need to provide additional info before signing-in.',
|
||||
},
|
||||
password: {
|
||||
|
|
|
@ -62,7 +62,7 @@ const errors = {
|
|||
email_or_phone_required_in_profile:
|
||||
'Você precisa adicionar um endereço de e-mail ou número de telefone antes de fazer login.',
|
||||
suspended: 'Esta conta está suspensa.',
|
||||
user_not_exist: 'O usuário com {{ identity }} não existe',
|
||||
user_not_exist: 'O usuário com {{ identifier }} não existe',
|
||||
missing_profile: 'Você precisa fornecer informações adicionais antes de fazer login.',
|
||||
},
|
||||
password: {
|
||||
|
|
|
@ -59,7 +59,7 @@ const errors = {
|
|||
phone_exists_in_profile: '当前用户已绑定手机号,无需重复操作。',
|
||||
email_or_phone_required_in_profile: '请绑定邮箱地址或手机号码。',
|
||||
suspended: '账号已被禁用。',
|
||||
user_not_exist: '未找到与 {{ identity }} 相关联的用户。',
|
||||
user_not_exist: '未找到与 {{ identifier }} 相关联的用户。',
|
||||
missing_profile: '请于登录时提供必要的用户补充信息。',
|
||||
},
|
||||
password: {
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
import ky from 'ky';
|
||||
|
||||
import { continueApi } from './continue';
|
||||
|
||||
jest.mock('ky', () => ({
|
||||
extend: () => ky,
|
||||
post: jest.fn(() => ({
|
||||
json: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('continue API', () => {
|
||||
const mockKyPost = ky.post as jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockKyPost.mockReturnValueOnce({
|
||||
json: () => ({
|
||||
redirectTo: '/',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockKyPost.mockClear();
|
||||
});
|
||||
|
||||
it('continue with password', async () => {
|
||||
await continueApi('password', 'password');
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/continue/password', {
|
||||
json: {
|
||||
password: 'password',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('continue with username', async () => {
|
||||
await continueApi('username', 'username');
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/continue/username', {
|
||||
json: {
|
||||
username: 'username',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('continue with email', async () => {
|
||||
await continueApi('email', 'email');
|
||||
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/continue/email', {
|
||||
json: {
|
||||
email: 'email',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('continue with phone', async () => {
|
||||
await continueApi('phone', 'phone');
|
||||
|
||||
expect(ky.post).toBeCalledWith('/api/session/sign-in/continue/sms', {
|
||||
json: {
|
||||
phone: 'phone',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
import api from './api';
|
||||
import { bindSocialAccount } from './social';
|
||||
|
||||
type Response = {
|
||||
redirectTo: string;
|
||||
};
|
||||
|
||||
const continueApiPrefix = '/api/session/sign-in/continue';
|
||||
|
||||
type ContinueKey = 'password' | 'username' | 'email' | 'phone';
|
||||
|
||||
export const continueApi = async (key: ContinueKey, value: string, socialToBind?: string) => {
|
||||
const result = await api
|
||||
.post(`${continueApiPrefix}/${key === 'phone' ? 'sms' : key}`, {
|
||||
json: { [key]: value },
|
||||
})
|
||||
.json<Response>();
|
||||
|
||||
if (result.redirectTo && socialToBind) {
|
||||
await bindSocialAccount(socialToBind);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
|
@ -152,3 +152,16 @@ export const registerWithVerifiedIdentifier = async (payload: SendPasscodePayloa
|
|||
|
||||
return api.post(`${interactionPrefix}/submit`).json<Response>();
|
||||
};
|
||||
|
||||
export const addProfile = async (
|
||||
payload: { username: string } | { password: string },
|
||||
socialToBind?: string
|
||||
) => {
|
||||
await api.patch(`${interactionPrefix}/profile`, { json: payload });
|
||||
|
||||
if (socialToBind) {
|
||||
// TODO: bind social account
|
||||
}
|
||||
|
||||
return api.post(`${interactionPrefix}/submit`).json<Response>();
|
||||
};
|
||||
|
|
|
@ -24,7 +24,7 @@ const useContinueSetEmailPasscodeValidation = (email: string, errorCallback?: ()
|
|||
|
||||
const verifyPasscodeErrorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.email_not_exist': identifierNotExistErrorHandler,
|
||||
'user.email_already_in_use': identifierNotExistErrorHandler,
|
||||
...requiredProfileErrorHandler,
|
||||
...sharedErrorHandlers,
|
||||
callback: errorCallback,
|
||||
|
|
|
@ -16,7 +16,7 @@ const useContinueSetSmsPasscodeValidation = (phone: string, errorCallback?: () =
|
|||
|
||||
const requiredProfileErrorHandler = useRequiredProfileErrorHandler(true);
|
||||
|
||||
const identifierNotExistErrorHandler = useIdentifierErrorAlert(
|
||||
const identifierExistErrorHandler = useIdentifierErrorAlert(
|
||||
UserFlow.continue,
|
||||
SignInIdentifier.Sms,
|
||||
phone
|
||||
|
@ -24,17 +24,12 @@ const useContinueSetSmsPasscodeValidation = (phone: string, errorCallback?: () =
|
|||
|
||||
const verifyPasscodeErrorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.phone_not_exist': identifierNotExistErrorHandler,
|
||||
'user.phone_already_in_use': identifierExistErrorHandler,
|
||||
...requiredProfileErrorHandler,
|
||||
...sharedErrorHandlers,
|
||||
callback: errorCallback,
|
||||
}),
|
||||
[
|
||||
errorCallback,
|
||||
identifierNotExistErrorHandler,
|
||||
requiredProfileErrorHandler,
|
||||
sharedErrorHandlers,
|
||||
]
|
||||
[errorCallback, identifierExistErrorHandler, requiredProfileErrorHandler, sharedErrorHandlers]
|
||||
);
|
||||
|
||||
const { run: verifyPasscode } = useApi(
|
||||
|
|
|
@ -22,7 +22,7 @@ const useForgotPasswordEmailPasscodeValidation = (email: string, errorCallback?:
|
|||
|
||||
const errorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.email_not_exist': identifierNotExistErrorHandler,
|
||||
'user.user_not_exist': identifierNotExistErrorHandler,
|
||||
'user.new_password_required_in_profile': () => {
|
||||
navigate(`/${UserFlow.forgotPassword}/reset`, { replace: true });
|
||||
},
|
||||
|
|
|
@ -22,7 +22,7 @@ const useForgotPasswordSmsPasscodeValidation = (phone: string, errorCallback?: (
|
|||
|
||||
const errorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.phone_not_exist': identifierNotExistErrorHandler,
|
||||
'user.user_not_exist': identifierNotExistErrorHandler,
|
||||
'user.new_password_required_in_profile': () => {
|
||||
navigate(`/${UserFlow.forgotPassword}/reset`, { replace: true });
|
||||
},
|
||||
|
|
|
@ -20,7 +20,7 @@ const useIdentifierErrorAlert = (
|
|||
await show({
|
||||
type: 'alert',
|
||||
ModalContent: t(
|
||||
flow === UserFlow.register
|
||||
flow === UserFlow.register || flow === UserFlow.continue
|
||||
? 'description.create_account_id_exists_alert'
|
||||
: 'description.sign_in_id_does_not_exist_alert',
|
||||
{
|
||||
|
|
|
@ -62,7 +62,7 @@ const useSignInWithEmailPasscodeValidation = (email: string, errorCallback?: ()
|
|||
|
||||
const errorHandlers = useMemo<ErrorHandlers>(
|
||||
() => ({
|
||||
'user.email_not_exist':
|
||||
'user.user_not_exist':
|
||||
// Block user auto register if is bind social or sign-in only flow
|
||||
signInMode === SignInMode.SignIn || socialToBind
|
||||
? identifierNotExistErrorHandler
|
||||
|
|
|
@ -62,7 +62,7 @@ const useSignInWithSmsPasscodeValidation = (phone: string, errorCallback?: () =>
|
|||
|
||||
const errorHandlers = useMemo<ErrorHandlers>(
|
||||
() => ({
|
||||
'user.phone_not_exist':
|
||||
'user.user_not_exist':
|
||||
// Block user auto register if is bind social or sign-in only flow
|
||||
signInMode === SignInMode.SignIn || socialToBind
|
||||
? identifierNotExistErrorHandler
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { fireEvent, act, waitFor } from '@testing-library/react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import { continueApi } from '@/apis/continue';
|
||||
import { addProfile } from '@/apis/interaction';
|
||||
|
||||
import SetUsername from '.';
|
||||
|
||||
|
@ -12,8 +12,8 @@ jest.mock('react-router-dom', () => ({
|
|||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('@/apis/continue', () => ({
|
||||
continueApi: jest.fn(async () => ({})),
|
||||
jest.mock('@/apis/interaction', () => ({
|
||||
addProfile: jest.fn(async () => ({})),
|
||||
}));
|
||||
|
||||
describe('<UsernameRegister />', () => {
|
||||
|
@ -37,7 +37,7 @@ describe('<UsernameRegister />', () => {
|
|||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(continueApi).toBeCalledWith('username', 'username', undefined);
|
||||
expect(addProfile).toBeCalledWith({ username: 'username' }, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { continueApi } from '@/apis/continue';
|
||||
import { addProfile } from '@/apis/interaction';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
|
||||
|
@ -28,14 +28,14 @@ const useSetUsername = () => {
|
|||
[requiredProfileErrorHandler]
|
||||
);
|
||||
|
||||
const { result, run: setUsername } = useApi(continueApi, errorHandlers);
|
||||
const { result, run: asyncAddProfile } = useApi(addProfile, errorHandlers);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (username: string) => {
|
||||
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
|
||||
await setUsername('username', username, socialToBind);
|
||||
await asyncAddProfile({ username }, socialToBind);
|
||||
},
|
||||
[setUsername]
|
||||
[asyncAddProfile]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,60 +1,63 @@
|
|||
import { useMemo } from 'react';
|
||||
import { MissingProfile } from '@logto/schemas';
|
||||
import { useMemo, useContext } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { validate } from 'superstruct';
|
||||
|
||||
import { UserFlow } from '@/types';
|
||||
import { missingProfileErrorDataGuard } from '@/types/guard';
|
||||
|
||||
import type { ErrorHandlers } from './use-api';
|
||||
import { PageContext } from './use-page-context';
|
||||
|
||||
const useRequiredProfileErrorHandler = (replace?: boolean) => {
|
||||
const navigate = useNavigate();
|
||||
const { setToast } = useContext(PageContext);
|
||||
|
||||
const requiredProfileErrorHandler = useMemo(
|
||||
const requiredProfileErrorHandler = useMemo<ErrorHandlers>(
|
||||
() => ({
|
||||
'user.password_required_in_profile': () => {
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/password`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
},
|
||||
'user.username_required_in_profile': () => {
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/username`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
},
|
||||
'user.email_required_in_profile': () => {
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/email`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
},
|
||||
'user.phone_required_in_profile': () => {
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/sms`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
},
|
||||
'user.email_or_phone_required_in_profile': () => {
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/email-or-sms/email`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
'user.missing_profile': (error) => {
|
||||
const [, data] = validate(error.data, missingProfileErrorDataGuard);
|
||||
const missingProfile = data?.missingProfile[0];
|
||||
|
||||
switch (missingProfile) {
|
||||
case MissingProfile.password:
|
||||
case MissingProfile.username:
|
||||
case MissingProfile.email:
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/${missingProfile}`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
break;
|
||||
case MissingProfile.phone:
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/sms`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
break;
|
||||
case MissingProfile.emailOrPhone:
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.continue}/email-or-sms/email`,
|
||||
search: location.search,
|
||||
},
|
||||
{ replace }
|
||||
);
|
||||
break;
|
||||
|
||||
default: {
|
||||
setToast(error.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
[navigate, replace]
|
||||
[navigate, replace, setToast]
|
||||
);
|
||||
|
||||
return requiredProfileErrorHandler;
|
||||
|
|
|
@ -2,7 +2,7 @@ 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 { addProfile } from '@/apis/interaction';
|
||||
|
||||
import SetPassword from '.';
|
||||
|
||||
|
@ -13,8 +13,8 @@ jest.mock('react-router-dom', () => ({
|
|||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('@/apis/continue', () => ({
|
||||
continueApi: jest.fn(async () => ({ redirectTo: '/' })),
|
||||
jest.mock('@/apis/interaction', () => ({
|
||||
addProfile: jest.fn(async () => ({ redirectTo: '/' })),
|
||||
}));
|
||||
|
||||
describe('SetPassword', () => {
|
||||
|
@ -52,7 +52,7 @@ describe('SetPassword', () => {
|
|||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(continueApi).toBeCalledWith('password', '123456', undefined);
|
||||
expect(addProfile).toBeCalledWith({ password: '123456' }, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { continueApi } from '@/apis/continue';
|
||||
import { addProfile } from '@/apis/interaction';
|
||||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
|
@ -26,14 +26,14 @@ const useSetPassword = () => {
|
|||
[navigate, requiredProfileErrorHandler, show]
|
||||
);
|
||||
|
||||
const { result, run: asyncSetPassword } = useApi(continueApi, errorHandlers);
|
||||
const { result, run: asyncAddProfile } = useApi(addProfile, errorHandlers);
|
||||
|
||||
const setPassword = useCallback(
|
||||
async (password: string) => {
|
||||
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
|
||||
await asyncSetPassword('password', password, socialToBind);
|
||||
await asyncAddProfile({ password }, socialToBind);
|
||||
},
|
||||
[asyncSetPassword]
|
||||
[asyncAddProfile]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -2,7 +2,7 @@ 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 { addProfile } from '@/apis/interaction';
|
||||
|
||||
import SetUsername from '.';
|
||||
|
||||
|
@ -13,8 +13,8 @@ jest.mock('react-router-dom', () => ({
|
|||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('@/apis/continue', () => ({
|
||||
continueApi: jest.fn(async () => ({ redirectTo: '/' })),
|
||||
jest.mock('@/apis/interaction', () => ({
|
||||
addProfile: jest.fn(async () => ({ redirectTo: '/' })),
|
||||
}));
|
||||
|
||||
describe('SetPassword', () => {
|
||||
|
@ -46,7 +46,7 @@ describe('SetPassword', () => {
|
|||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(continueApi).toBeCalledWith('username', 'username', undefined);
|
||||
expect(addProfile).toBeCalledWith({ username: 'username' }, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { SignInIdentifier, MissingProfile } from '@logto/schemas';
|
||||
import * as s from 'superstruct';
|
||||
|
||||
export const bindSocialStateGuard = s.object({
|
||||
|
@ -38,3 +38,15 @@ export const continueMethodGuard = s.union([
|
|||
export const usernameGuard = s.object({
|
||||
username: s.string(),
|
||||
});
|
||||
|
||||
export const missingProfileErrorDataGuard = s.object({
|
||||
missingProfile: s.array(
|
||||
s.union([
|
||||
s.literal(MissingProfile.password),
|
||||
s.literal(MissingProfile.email),
|
||||
s.literal(MissingProfile.phone),
|
||||
s.literal(MissingProfile.username),
|
||||
s.literal(MissingProfile.emailOrPhone),
|
||||
])
|
||||
),
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue