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:
parent
16c295c677
commit
bc83b00ce8
12 changed files with 102 additions and 244 deletions
|
@ -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', {
|
||||
|
|
39
packages/ui/src/apis/interaction.ts
Normal file
39
packages/ui/src/apis/interaction.ts
Normal 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>();
|
||||
};
|
|
@ -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>();
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue