0
Fork 0
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:
simeng-li 2022-12-28 15:33:31 +08:00 committed by GitHub
parent 06ec86d831
commit e02f7e5ac2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 110 additions and 175 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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