0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-20 21:32:31 -05:00

refactor(ui): replace the password sign-in api (#2730)

This commit is contained in:
simeng-li 2022-12-28 10:11:49 +08:00 committed by GitHub
parent 16c295c677
commit bc83b00ce8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 102 additions and 244 deletions

View file

@ -20,15 +20,12 @@ import {
verifyRegisterSmsPasscode,
} from './register';
import {
signInWithUsername,
signInWithSms,
signInWithEmail,
sendSignInSmsPasscode,
sendSignInEmailPasscode,
verifySignInEmailPasscode,
verifySignInSmsPasscode,
signInWithEmailPassword,
signInWithPhonePassword,
} from './sign-in';
import {
invokeSocialSignIn,
@ -58,91 +55,6 @@ describe('api', () => {
mockKyPost.mockClear();
});
it('signInWithUsername', async () => {
mockKyPost.mockReturnValueOnce({
json: () => ({
redirectTo: '/',
}),
});
await signInWithUsername(username, password);
expect(ky.post).toBeCalledWith('/api/session/sign-in/password/username', {
json: {
username,
password,
},
});
});
it('signInWithEmailPassword', async () => {
mockKyPost.mockReturnValueOnce({
json: () => ({
redirectTo: '/',
}),
});
await signInWithEmailPassword(email, password);
expect(ky.post).toBeCalledWith('/api/session/sign-in/password/email', {
json: {
email,
password,
},
});
});
it('signInWithEmailPassword with bind social account', async () => {
mockKyPost.mockReturnValueOnce({
json: () => ({
redirectTo: '/',
}),
});
await signInWithEmailPassword(email, password, 'github');
expect(ky.post).toHaveBeenNthCalledWith(1, '/api/session/sign-in/password/email', {
json: {
email,
password,
},
});
expect(ky.post).toHaveBeenNthCalledWith(2, '/api/session/bind-social', {
json: {
connectorId: 'github',
},
});
});
it('signInWithPhonePassword', async () => {
mockKyPost.mockReturnValueOnce({
json: () => ({
redirectTo: '/',
}),
});
await signInWithPhonePassword(phone, password);
expect(ky.post).toBeCalledWith('/api/session/sign-in/password/sms', {
json: {
phone,
password,
},
});
});
it('signInWithPhonePassword with bind social account', async () => {
mockKyPost.mockReturnValueOnce({
json: () => ({
redirectTo: '/',
}),
});
await signInWithPhonePassword(phone, password, 'github');
expect(ky.post).toHaveBeenNthCalledWith(1, '/api/session/sign-in/password/sms', {
json: {
phone,
password,
},
});
expect(ky.post).toHaveBeenNthCalledWith(2, '/api/session/bind-social', {
json: {
connectorId: 'github',
},
});
});
it('signInWithSms', async () => {
mockKyPost.mockReturnValueOnce({
json: () => ({
@ -163,26 +75,6 @@ describe('api', () => {
expect(ky.post).toBeCalledWith('/api/session/sign-in/passwordless/email');
});
it('signInWithUsername with bind social account', async () => {
mockKyPost.mockReturnValueOnce({
json: () => ({
redirectTo: '/',
}),
});
await signInWithUsername(username, password, 'github');
expect(ky.post).toHaveBeenNthCalledWith(1, '/api/session/sign-in/password/username', {
json: {
username,
password,
},
});
expect(ky.post).toHaveBeenNthCalledWith(2, '/api/session/bind-social', {
json: {
connectorId: 'github',
},
});
});
it('sendSignInSmsPasscode', async () => {
await sendSignInSmsPasscode(phone);
expect(ky.post).toBeCalledWith('/api/session/passwordless/sms/send', {

View file

@ -0,0 +1,39 @@
/* istanbul ignore file */
import { InteractionEvent } from '@logto/schemas';
import type {
UsernamePasswordPayload,
EmailPasswordPayload,
PhonePasswordPayload,
} from '@logto/schemas';
import api from './api';
const interactionPrefix = '/api/interaction';
type Response = {
redirectTo: string;
};
export type PasswordSignInPayload =
| UsernamePasswordPayload
| EmailPasswordPayload
| PhonePasswordPayload;
export const signInWithPasswordIdentifier = async (
payload: PasswordSignInPayload,
socialToBind?: string
) => {
await api.put(`${interactionPrefix}`, {
json: {
event: InteractionEvent.SignIn,
identifier: payload,
},
});
if (socialToBind) {
// TODO: bind social account
}
return api.post(`${interactionPrefix}/submit`).json<Response>();
};

View file

@ -9,69 +9,6 @@ type Response = {
redirectTo: string;
};
export const signInWithUsername = async (
username: string,
password: string,
socialToBind?: string
) => {
const result = await api
.post(`${apiPrefix}/sign-in/password/username`, {
json: {
username,
password,
},
})
.json<Response>();
if (result.redirectTo && socialToBind) {
await bindSocialAccount(socialToBind);
}
return result;
};
export const signInWithEmailPassword = async (
email: string,
password: string,
socialToBind?: string
) => {
const result = await api
.post(`${apiPrefix}/sign-in/password/email`, {
json: {
email,
password,
},
})
.json<Response>();
if (result.redirectTo && socialToBind) {
await bindSocialAccount(socialToBind);
}
return result;
};
export const signInWithPhonePassword = async (
phone: string,
password: string,
socialToBind?: string
) => {
const result = await api
.post(`${apiPrefix}/sign-in/password/sms`, {
json: {
phone,
password,
},
})
.json<Response>();
if (result.redirectTo && socialToBind) {
await bindSocialAccount(socialToBind);
}
return result;
};
export const signInWithSms = async (socialToBind?: string) => {
const result = await api.post(`${apiPrefix}/sign-in/passwordless/sms`).json<Response>();

View file

@ -4,12 +4,14 @@ import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { signInWithEmailPassword } from '@/apis/sign-in';
import { signInWithPasswordIdentifier } from '@/apis/interaction';
import ConfirmModalProvider from '@/containers/ConfirmModalProvider';
import EmailPassword from '.';
jest.mock('@/apis/sign-in', () => ({ signInWithEmailPassword: jest.fn(async () => 0) }));
jest.mock('@/apis/interaction', () => ({
signInWithPasswordIdentifier: jest.fn(async () => ({ redirectTo: '/' })),
}));
jest.mock('react-device-detect', () => ({
isMobile: true,
}));
@ -178,7 +180,13 @@ describe('<EmailPassword>', () => {
act(() => {
void waitFor(() => {
expect(signInWithEmailPassword).toBeCalledWith('email', 'password', undefined);
expect(signInWithPasswordIdentifier).toBeCalledWith(
{
email: 'email',
password: 'password',
},
undefined
);
});
});
});

View file

@ -35,7 +35,7 @@ const defaultState: FieldState = {
const EmailPassword = ({ className, autoFocus }: Props) => {
const { t } = useTranslation();
const { termsValidation } = useTerms();
const { errorMessage, clearErrorMessage, onSubmit } = usePasswordSignIn(SignInIdentifier.Email);
const { errorMessage, clearErrorMessage, onSubmit } = usePasswordSignIn();
const { isForgotPasswordEnabled, email } = useForgotPasswordSettings();
const { fieldValue, setFieldValue, register, validateForm } = useForm(defaultState);
@ -54,16 +54,9 @@ const EmailPassword = ({ className, autoFocus }: Props) => {
return;
}
void onSubmit(fieldValue.email, fieldValue.password);
void onSubmit(fieldValue);
},
[
clearErrorMessage,
validateForm,
termsValidation,
onSubmit,
fieldValue.email,
fieldValue.password,
]
[clearErrorMessage, validateForm, termsValidation, onSubmit, fieldValue]
);
return (

View file

@ -2,23 +2,21 @@ import { SignInIdentifier } from '@logto/schemas';
import { fireEvent, waitFor, act } from '@testing-library/react';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import {
signInWithEmailPassword,
signInWithPhonePassword,
sendSignInEmailPasscode,
sendSignInSmsPasscode,
} from '@/apis/sign-in';
import { signInWithPasswordIdentifier } from '@/apis/interaction';
import { sendSignInEmailPasscode, sendSignInSmsPasscode } from '@/apis/sign-in';
import { UserFlow } from '@/types';
import PasswordSignInForm from '.';
jest.mock('@/apis/sign-in', () => ({
signInWithEmailPassword: jest.fn(() => ({ redirectTo: '/' })),
signInWithPhonePassword: jest.fn(() => ({ redirectTo: '/' })),
sendSignInEmailPasscode: jest.fn(() => ({ success: true })),
sendSignInSmsPasscode: jest.fn(() => ({ success: true })),
}));
jest.mock('@/apis/interaction', () => ({
signInWithPasswordIdentifier: jest.fn(() => ({ redirectTo: '/' })),
}));
const mockedNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
@ -47,7 +45,7 @@ describe('PasswordSignInForm', () => {
});
await waitFor(() => {
expect(signInWithEmailPassword).not.toBeCalled();
expect(signInWithPasswordIdentifier).not.toBeCalled();
expect(queryByText('password_required')).not.toBeNull();
});
});
@ -70,7 +68,7 @@ describe('PasswordSignInForm', () => {
});
await waitFor(() => {
expect(signInWithEmailPassword).toBeCalledWith(email, password, undefined);
expect(signInWithPasswordIdentifier).toBeCalledWith({ email, password }, undefined);
});
const sendPasscodeLink = getByText('action.sign_in_via_passcode');
@ -115,7 +113,7 @@ describe('PasswordSignInForm', () => {
});
await waitFor(() => {
expect(signInWithPhonePassword).toBeCalledWith(phone, password, undefined);
expect(signInWithPasswordIdentifier).toBeCalledWith({ phone, password }, undefined);
});
const sendPasscodeLink = getByText('action.sign_in_via_passcode');

View file

@ -40,10 +40,8 @@ const PasswordSignInForm = ({
value,
}: Props) => {
const { t } = useTranslation();
const { errorMessage, clearErrorMessage, onSubmit } = usePasswordSignIn(method);
const { errorMessage, clearErrorMessage, onSubmit } = usePasswordSignIn();
const { fieldValue, register, validateForm } = useForm(defaultState);
const { isForgotPasswordEnabled, sms, email } = useForgotPasswordSettings();
const onSubmitHandler = useCallback(
@ -56,9 +54,14 @@ const PasswordSignInForm = ({
return;
}
void onSubmit(value, fieldValue.password);
const payload =
method === SignInIdentifier.Email
? { email: value, password: fieldValue.password }
: { phone: value, password: fieldValue.password };
void onSubmit(payload);
},
[clearErrorMessage, validateForm, onSubmit, value, fieldValue.password]
[clearErrorMessage, validateForm, onSubmit, method, value, fieldValue.password]
);
return (

View file

@ -4,12 +4,14 @@ import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { signInWithPhonePassword } from '@/apis/sign-in';
import { signInWithPasswordIdentifier } from '@/apis/interaction';
import ConfirmModalProvider from '@/containers/ConfirmModalProvider';
import PhonePassword from '.';
jest.mock('@/apis/sign-in', () => ({ signInWithPhonePassword: jest.fn(async () => 0) }));
jest.mock('@/apis/interaction', () => ({
signInWithPasswordIdentifier: jest.fn(async () => ({ redirectTo: '/' })),
}));
// Terms Iframe Modal only shown on mobile device
jest.mock('react-device-detect', () => ({
isMobile: true,
@ -185,7 +187,13 @@ describe('<PhonePassword>', () => {
act(() => {
void waitFor(() => {
expect(signInWithPhonePassword).toBeCalledWith('phone', 'password', undefined);
expect(signInWithPasswordIdentifier).toBeCalledWith(
{
phone: 'phone',
password: 'password',
},
undefined
);
});
});
});

View file

@ -36,7 +36,7 @@ const defaultState: FieldState = {
const PhonePassword = ({ className, autoFocus }: Props) => {
const { t } = useTranslation();
const { termsValidation } = useTerms();
const { errorMessage, clearErrorMessage, onSubmit } = usePasswordSignIn(SignInIdentifier.Sms);
const { errorMessage, clearErrorMessage, onSubmit } = usePasswordSignIn();
const { isForgotPasswordEnabled, sms } = useForgotPasswordSettings();
const { countryList, phoneNumber, setPhoneNumber, isValidPhoneNumber } = usePhoneNumber();
@ -74,16 +74,9 @@ const PhonePassword = ({ className, autoFocus }: Props) => {
return;
}
void onSubmit(fieldValue.phone, fieldValue.password);
void onSubmit(fieldValue);
},
[
clearErrorMessage,
validateForm,
termsValidation,
onSubmit,
fieldValue.phone,
fieldValue.password,
]
[clearErrorMessage, validateForm, termsValidation, onSubmit, fieldValue]
);
return (

View file

@ -5,12 +5,12 @@ import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { mockSignInExperienceSettings } from '@/__mocks__/logto';
import { signInWithUsername } from '@/apis/sign-in';
import { signInWithPasswordIdentifier } from '@/apis/interaction';
import ConfirmModalProvider from '@/containers/ConfirmModalProvider';
import UsernameSignIn from '.';
jest.mock('@/apis/sign-in', () => ({ signInWithUsername: jest.fn(async () => 0) }));
jest.mock('@/apis/interaction', () => ({ signInWithPasswordIdentifier: jest.fn(async () => 0) }));
jest.mock('react-device-detect', () => ({
isMobile: true,
}));
@ -190,7 +190,13 @@ describe('<UsernameSignIn>', () => {
act(() => {
void waitFor(() => {
expect(signInWithUsername).toBeCalledWith('username', 'password', undefined);
expect(signInWithPasswordIdentifier).toBeCalledWith(
{
username: 'username',
password: 'password',
},
undefined
);
});
});
});

View file

@ -36,9 +36,7 @@ const UsernameSignIn = ({ className, autoFocus }: Props) => {
const { t } = useTranslation();
const { termsValidation } = useTerms();
const { isForgotPasswordEnabled, email } = useForgotPasswordSettings();
const { errorMessage, clearErrorMessage, onSubmit } = usePasswordSignIn(
SignInIdentifier.Username
);
const { errorMessage, clearErrorMessage, onSubmit } = usePasswordSignIn();
const { fieldValue, setFieldValue, register, validateForm } = useForm(defaultState);
@ -56,16 +54,9 @@ const UsernameSignIn = ({ className, autoFocus }: Props) => {
return;
}
void onSubmit(fieldValue.username, fieldValue.password);
void onSubmit(fieldValue);
},
[
clearErrorMessage,
validateForm,
termsValidation,
onSubmit,
fieldValue.username,
fieldValue.password,
]
[clearErrorMessage, validateForm, termsValidation, onSubmit, fieldValue]
);
return (

View file

@ -1,11 +1,7 @@
import { SignInIdentifier } from '@logto/schemas';
import { useState, useMemo, useCallback, useEffect } from 'react';
import {
signInWithUsername,
signInWithEmailPassword,
signInWithPhonePassword,
} from '@/apis/sign-in';
import type { PasswordSignInPayload } from '@/apis/interaction';
import { signInWithPasswordIdentifier } from '@/apis/interaction';
import type { ErrorHandlers } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import { SearchParameters } from '@/types';
@ -13,13 +9,7 @@ import { getSearchParameters } from '@/utils';
import useRequiredProfileErrorHandler from './use-required-profile-error-handler';
const apiMap = {
[SignInIdentifier.Username]: signInWithUsername,
[SignInIdentifier.Email]: signInWithEmailPassword,
[SignInIdentifier.Sms]: signInWithPhonePassword,
};
const usePasswordSignIn = (method: SignInIdentifier) => {
const usePasswordSignIn = () => {
const [errorMessage, setErrorMessage] = useState<string>();
const clearErrorMessage = useCallback(() => {
@ -38,7 +28,7 @@ const usePasswordSignIn = (method: SignInIdentifier) => {
[requiredProfileErrorHandler]
);
const { result, run: asyncSignIn } = useApi(apiMap[method], errorHandlers);
const { result, run: asyncSignIn } = useApi(signInWithPasswordIdentifier, errorHandlers);
useEffect(() => {
if (result?.redirectTo) {
@ -47,9 +37,9 @@ const usePasswordSignIn = (method: SignInIdentifier) => {
}, [result]);
const onSubmit = useCallback(
async (identifier: string, password: string) => {
async (payload: PasswordSignInPayload) => {
const socialToBind = getSearchParameters(location.search, SearchParameters.bindWithSocial);
await asyncSignIn(identifier, password, socialToBind);
await asyncSignIn(payload, socialToBind);
},
[asyncSignIn]
);