mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
refactor(ui): implement errorHander in useApi hook (#684)
* refactor(ui): implement api error handler implement api error handler * refactor(ui): extend register account exist flow extend register account exist flow * feat(ui): username exsit error username exsit error * refactor(ui): redirect pack if no value found in passcode validation page redirect pack if no value found in passcode validation page
This commit is contained in:
parent
d108f4b883
commit
b601fb45ce
13 changed files with 286 additions and 150 deletions
|
@ -45,6 +45,7 @@ const translation = {
|
|||
forgot_password: 'Forgot Password?',
|
||||
or: 'Or',
|
||||
enter_passcode: 'The passcode has been sent to {{address}}',
|
||||
passcode_sent: 'The passcode has been sent',
|
||||
resend_after_seconds: 'Resend after {{seconds}} seconds',
|
||||
resend_passcode: 'Resend Passcode',
|
||||
continue_with: 'Continue with',
|
||||
|
|
|
@ -47,6 +47,7 @@ const translation = {
|
|||
forgot_password: '忘记密码?',
|
||||
or: '或',
|
||||
enter_passcode: '验证码已经发送至 {{ address }}',
|
||||
passcode_sent: '验证码已经发送',
|
||||
resend_after_seconds: '在 {{ seconds }} 秒后重发',
|
||||
resend_passcode: '重发验证码',
|
||||
continue_with: '通过以下方式继续',
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, {
|
|||
ClipboardEventHandler,
|
||||
} from 'react';
|
||||
|
||||
import ErrorMessage, { ErrorType } from '../ErrorMessage';
|
||||
import ErrorMessage from '../ErrorMessage';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
export const defaultLength = 6;
|
||||
|
@ -19,7 +19,7 @@ export type Props = {
|
|||
className?: string;
|
||||
length?: number;
|
||||
value: string[];
|
||||
error?: ErrorType;
|
||||
error?: string;
|
||||
onChange: (value: string[]) => void;
|
||||
};
|
||||
|
||||
|
@ -180,12 +180,10 @@ const Passcode = ({ name, className, value, length = defaultLength, error, onCha
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
// Clear field and focus
|
||||
onChange([]);
|
||||
if (value.length === 0) {
|
||||
inputReferences.current[0]?.focus();
|
||||
}
|
||||
}, [error, onChange]);
|
||||
}, [value, onChange]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
|
@ -198,7 +196,6 @@ const Passcode = ({ name, className, value, length = defaultLength, error, onCha
|
|||
}}
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${name}_${index}`}
|
||||
autoFocus={index === 0}
|
||||
name={`${name}_${index}`}
|
||||
data-id={index}
|
||||
value={codes[index]}
|
||||
|
@ -213,7 +210,7 @@ const Passcode = ({ name, className, value, length = defaultLength, error, onCha
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
{error && <ErrorMessage error={error} className={styles.errorMessage} />}
|
||||
{error && <ErrorMessage className={styles.errorMessage}>{error}</ErrorMessage>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
/**
|
||||
* TODO:
|
||||
* 1. API redesign handle api error and loading status globally in PageContext
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { useEffect, useCallback, useContext } from 'react';
|
||||
import React, { useEffect, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { register } from '@/apis/register';
|
||||
|
@ -12,9 +7,8 @@ import Button from '@/components/Button';
|
|||
import Input from '@/components/Input';
|
||||
import PasswordInput from '@/components/Input/PasswordInput';
|
||||
import TermsOfUse from '@/containers/TermsOfUse';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useApi, { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useForm from '@/hooks/use-form';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import useTerms from '@/hooks/use-terms';
|
||||
import {
|
||||
usernameValidation,
|
||||
|
@ -41,17 +35,30 @@ const defaultState: FieldState = {
|
|||
};
|
||||
|
||||
const CreateAccount = ({ className }: Props) => {
|
||||
const { t, i18n } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
const { termsValidation } = useTerms();
|
||||
const { setToast } = useContext(PageContext);
|
||||
const { error, result, run: asyncRegister } = useApi(register);
|
||||
const {
|
||||
fieldValue,
|
||||
setFieldValue,
|
||||
setFieldErrors,
|
||||
register: fieldRegister,
|
||||
validateForm,
|
||||
} = useForm(defaultState);
|
||||
|
||||
const registerErrorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.username_exists_register': () => {
|
||||
setFieldErrors((state) => ({
|
||||
...state,
|
||||
username: 'username_exists',
|
||||
}));
|
||||
},
|
||||
}),
|
||||
[setFieldErrors]
|
||||
);
|
||||
|
||||
const { result, run: asyncRegister } = useApi(register, registerErrorHandlers);
|
||||
|
||||
const onSubmitHandler = useCallback(() => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
|
@ -70,13 +77,6 @@ const CreateAccount = ({ className }: Props) => {
|
|||
}
|
||||
}, [result]);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: username exist error message
|
||||
if (error) {
|
||||
setToast(error.message);
|
||||
}
|
||||
}, [error, i18n, setToast, t]);
|
||||
|
||||
return (
|
||||
<form className={classNames(styles.form, className)}>
|
||||
<Input
|
||||
|
|
|
@ -5,10 +5,9 @@ import reactStringReplace from 'react-string-replace';
|
|||
import { useTimer } from 'react-timer-hook';
|
||||
|
||||
import { getSendPasscodeApi, getVerifyPasscodeApi } from '@/apis/utils';
|
||||
import { ErrorType } from '@/components/ErrorMessage';
|
||||
import Passcode, { defaultLength } from '@/components/Passcode';
|
||||
import TextLink from '@/components/TextLink';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useApi, { ErrorHandlers } from '@/hooks/use-api';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import { UserFlow, SearchParameters } from '@/types';
|
||||
import { getSearchParameters } from '@/utils';
|
||||
|
@ -33,7 +32,7 @@ const getTimeout = () => {
|
|||
|
||||
const PasscodeValidation = ({ type, method, className, target }: Props) => {
|
||||
const [code, setCode] = useState<string[]>([]);
|
||||
const [error, setError] = useState<ErrorType>();
|
||||
const [error, setError] = useState<string>();
|
||||
const { setToast } = useContext(PageContext);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
|
||||
|
@ -42,17 +41,29 @@ const PasscodeValidation = ({ type, method, className, target }: Props) => {
|
|||
expiryTimestamp: getTimeout(),
|
||||
});
|
||||
|
||||
const {
|
||||
error: verifyPasscodeError,
|
||||
result: verifyPasscodeResult,
|
||||
run: verifyPassCode,
|
||||
} = useApi(getVerifyPasscodeApi(type, method));
|
||||
const verifyPasscodeErrorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'passcode.expired': (error) => {
|
||||
setError(error.message);
|
||||
},
|
||||
'passcode.code_mismatch': (error) => {
|
||||
setError(error.message);
|
||||
},
|
||||
callback: () => {
|
||||
setCode([]);
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const {
|
||||
error: sendPasscodeError,
|
||||
result: sendPasscodeResult,
|
||||
run: sendPassCode,
|
||||
} = useApi(getSendPasscodeApi(type, method));
|
||||
const { result: verifyPasscodeResult, run: verifyPassCode } = useApi(
|
||||
getVerifyPasscodeApi(type, method),
|
||||
verifyPasscodeErrorHandlers
|
||||
);
|
||||
|
||||
const { result: sendPasscodeResult, run: sendPassCode } = useApi(
|
||||
getSendPasscodeApi(type, method)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (code.length === defaultLength && code.every(Boolean)) {
|
||||
|
@ -64,9 +75,10 @@ const PasscodeValidation = ({ type, method, className, target }: Props) => {
|
|||
useEffect(() => {
|
||||
// Restart count down
|
||||
if (sendPasscodeResult) {
|
||||
setToast(t('description.passcode_sent'));
|
||||
restart(getTimeout(), true);
|
||||
}
|
||||
}, [sendPasscodeResult, restart]);
|
||||
}, [sendPasscodeResult, restart, setToast, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (verifyPasscodeResult?.redirectTo) {
|
||||
|
@ -74,20 +86,6 @@ const PasscodeValidation = ({ type, method, className, target }: Props) => {
|
|||
}
|
||||
}, [verifyPasscodeResult]);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: move to global handling
|
||||
if (sendPasscodeError) {
|
||||
setToast(t('error.request', { ...sendPasscodeError }));
|
||||
}
|
||||
}, [sendPasscodeError, setToast, t]);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: load error from api response
|
||||
if (verifyPasscodeError) {
|
||||
setError('invalid_passcode');
|
||||
}
|
||||
}, [verifyPasscodeError]);
|
||||
|
||||
const renderCountDownMessage = useMemo(() => {
|
||||
const contents = t('description.resend_after_seconds', { seconds });
|
||||
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
/**
|
||||
* TODO:
|
||||
* 1. API redesign handle api error and loading status globally in PageContext
|
||||
*/
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useEffect, useContext } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { getSendPasscodeApi } from '@/apis/utils';
|
||||
import Button from '@/components/Button';
|
||||
import Input from '@/components/Input';
|
||||
import PasswordlessConfirmModal from '@/containers/PasswordlessConfirmModal';
|
||||
import TermsOfUse from '@/containers/TermsOfUse';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useApi, { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useForm from '@/hooks/use-form';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import useTerms from '@/hooks/use-terms';
|
||||
import { UserFlow } from '@/types';
|
||||
import { emailValidation } from '@/utils/field-validations';
|
||||
|
@ -32,14 +28,30 @@ type FieldState = {
|
|||
const defaultState: FieldState = { email: '' };
|
||||
|
||||
const EmailPasswordless = ({ type, className }: Props) => {
|
||||
const [showPasswordlessConfirmModal, setShowPasswordlessConfirmModal] = useState(false);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
const { setToast } = useContext(PageContext);
|
||||
const navigate = useNavigate();
|
||||
const { termsValidation } = useTerms();
|
||||
const { fieldValue, setFieldValue, register, validateForm } = useForm(defaultState);
|
||||
const { fieldValue, setFieldValue, setFieldErrors, register, validateForm } =
|
||||
useForm(defaultState);
|
||||
|
||||
const errorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.email_not_exists': () => {
|
||||
setShowPasswordlessConfirmModal(true);
|
||||
},
|
||||
'user.email_exists_register': () => {
|
||||
setShowPasswordlessConfirmModal(true);
|
||||
},
|
||||
'guard.invalid_input': () => {
|
||||
setFieldErrors({ email: 'invalid_email' });
|
||||
},
|
||||
}),
|
||||
[setFieldErrors]
|
||||
);
|
||||
|
||||
const sendPasscode = getSendPasscodeApi(type, 'email');
|
||||
const { error, result, run: asyncSendPasscode } = useApi(sendPasscode);
|
||||
const { result, run: asyncSendPasscode } = useApi(sendPasscode, errorHandlers);
|
||||
|
||||
const onSubmitHandler = useCallback(() => {
|
||||
if (!validateForm()) {
|
||||
|
@ -53,6 +65,10 @@ const EmailPasswordless = ({ type, className }: Props) => {
|
|||
void asyncSendPasscode(fieldValue.email);
|
||||
}, [validateForm, termsValidation, asyncSendPasscode, fieldValue.email]);
|
||||
|
||||
const onModalCloseHandler = useCallback(() => {
|
||||
setShowPasswordlessConfirmModal(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (result) {
|
||||
navigate(
|
||||
|
@ -65,30 +81,32 @@ const EmailPasswordless = ({ type, className }: Props) => {
|
|||
}
|
||||
}, [fieldValue.email, navigate, result, type]);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: request error
|
||||
if (error) {
|
||||
setToast(t('error.request', { ...error }));
|
||||
}
|
||||
}, [error, t, setToast]);
|
||||
|
||||
return (
|
||||
<form className={classNames(styles.form, className)}>
|
||||
<Input
|
||||
className={styles.inputField}
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
placeholder={t('input.email')}
|
||||
{...register('email', emailValidation)}
|
||||
onClear={() => {
|
||||
setFieldValue((state) => ({ ...state, email: '' }));
|
||||
}}
|
||||
<>
|
||||
<form className={classNames(styles.form, className)}>
|
||||
<Input
|
||||
className={styles.inputField}
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
placeholder={t('input.email')}
|
||||
{...register('email', emailValidation)}
|
||||
onClear={() => {
|
||||
setFieldValue((state) => ({ ...state, email: '' }));
|
||||
}}
|
||||
/>
|
||||
|
||||
<TermsOfUse className={styles.terms} />
|
||||
|
||||
<Button onClick={onSubmitHandler}>{t('action.continue')}</Button>
|
||||
</form>
|
||||
<PasswordlessConfirmModal
|
||||
isOpen={showPasswordlessConfirmModal}
|
||||
type={type === 'sign-in' ? 'register' : 'sign-in'}
|
||||
method="email"
|
||||
value={fieldValue.email}
|
||||
onClose={onModalCloseHandler}
|
||||
/>
|
||||
|
||||
<TermsOfUse className={styles.terms} />
|
||||
|
||||
<Button onClick={onSubmitHandler}>{t('action.continue')}</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
/**
|
||||
* TODO:
|
||||
* 1. API redesign handle api error and loading status globally in PageContext
|
||||
*/
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useEffect, useContext } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { getSendPasscodeApi } from '@/apis/utils';
|
||||
import Button from '@/components/Button';
|
||||
import PhoneInput from '@/components/Input/PhoneInput';
|
||||
import PasswordlessConfirmModal from '@/containers/PasswordlessConfirmModal';
|
||||
import TermsOfUse from '@/containers/TermsOfUse';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useApi, { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useForm from '@/hooks/use-form';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import usePhoneNumber, { countryList } from '@/hooks/use-phone-number';
|
||||
import useTerms from '@/hooks/use-terms';
|
||||
import { UserFlow } from '@/types';
|
||||
|
@ -32,15 +28,31 @@ type FieldState = {
|
|||
const defaultState: FieldState = { phone: '' };
|
||||
|
||||
const PhonePasswordless = ({ type, className }: Props) => {
|
||||
const [showPasswordlessConfirmModal, setShowPasswordlessConfirmModal] = useState(false);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
const { phoneNumber, setPhoneNumber, isValidPhoneNumber } = usePhoneNumber();
|
||||
const { fieldValue, setFieldValue, validateForm, register } = useForm(defaultState);
|
||||
const { setToast } = useContext(PageContext);
|
||||
const navigate = useNavigate();
|
||||
const { termsValidation } = useTerms();
|
||||
const { fieldValue, setFieldValue, setFieldErrors, validateForm, register } =
|
||||
useForm(defaultState);
|
||||
|
||||
const errorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'user.phone_not_exists': () => {
|
||||
setShowPasswordlessConfirmModal(true);
|
||||
},
|
||||
'user.phone_exists_register': () => {
|
||||
setShowPasswordlessConfirmModal(true);
|
||||
},
|
||||
'guard.invalid_input': () => {
|
||||
setFieldErrors({ phone: 'invalid_phone' });
|
||||
},
|
||||
}),
|
||||
[setFieldErrors]
|
||||
);
|
||||
|
||||
const sendPasscode = getSendPasscodeApi(type, 'sms');
|
||||
const { error, result, run: asyncSendPasscode } = useApi(sendPasscode);
|
||||
const { result, run: asyncSendPasscode } = useApi(sendPasscode, errorHandlers);
|
||||
|
||||
const phoneNumberValidation = useCallback(
|
||||
(phoneNumber: string) => {
|
||||
|
@ -63,6 +75,10 @@ const PhonePasswordless = ({ type, className }: Props) => {
|
|||
void asyncSendPasscode(fieldValue.phone);
|
||||
}, [validateForm, termsValidation, asyncSendPasscode, fieldValue.phone]);
|
||||
|
||||
const onModalCloseHandler = useCallback(() => {
|
||||
setShowPasswordlessConfirmModal(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Sync phoneNumber
|
||||
setFieldValue((previous) => ({
|
||||
|
@ -75,36 +91,39 @@ const PhonePasswordless = ({ type, className }: Props) => {
|
|||
if (result) {
|
||||
navigate(
|
||||
{ pathname: `/${type}/sms/passcode-validation`, search: location.search },
|
||||
{ state: { phone: fieldValue.phone } }
|
||||
{ state: { sms: fieldValue.phone } }
|
||||
);
|
||||
}
|
||||
}, [fieldValue.phone, navigate, result, type]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setToast(t('error.request', { ...error }));
|
||||
}
|
||||
}, [error, t, setToast]);
|
||||
|
||||
return (
|
||||
<form className={classNames(styles.form, className)}>
|
||||
<PhoneInput
|
||||
name="phone"
|
||||
className={styles.inputField}
|
||||
autoComplete="mobile"
|
||||
placeholder={t('input.phone_number')}
|
||||
countryCallingCode={phoneNumber.countryCallingCode}
|
||||
nationalNumber={phoneNumber.nationalNumber}
|
||||
countryList={countryList}
|
||||
{...register('phone', phoneNumberValidation)}
|
||||
onChange={(data) => {
|
||||
setPhoneNumber((previous) => ({ ...previous, ...data }));
|
||||
}}
|
||||
/>
|
||||
<TermsOfUse className={styles.terms} />
|
||||
<>
|
||||
<form className={classNames(styles.form, className)}>
|
||||
<PhoneInput
|
||||
name="phone"
|
||||
className={styles.inputField}
|
||||
autoComplete="mobile"
|
||||
placeholder={t('input.phone_number')}
|
||||
countryCallingCode={phoneNumber.countryCallingCode}
|
||||
nationalNumber={phoneNumber.nationalNumber}
|
||||
countryList={countryList}
|
||||
{...register('phone', phoneNumberValidation)}
|
||||
onChange={(data) => {
|
||||
setPhoneNumber((previous) => ({ ...previous, ...data }));
|
||||
}}
|
||||
/>
|
||||
<TermsOfUse className={styles.terms} />
|
||||
|
||||
<Button onClick={onSubmitHandler}>{t('action.continue')}</Button>
|
||||
</form>
|
||||
<Button onClick={onSubmitHandler}>{t('action.continue')}</Button>
|
||||
</form>
|
||||
<PasswordlessConfirmModal
|
||||
isOpen={showPasswordlessConfirmModal}
|
||||
type={type === 'sign-in' ? 'register' : 'sign-in'}
|
||||
method="sms"
|
||||
value={fieldValue.phone}
|
||||
onClose={onModalCloseHandler}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { getSendPasscodeApi, PasscodeChannel } from '@/apis/utils';
|
||||
import ConfirmModal from '@/components/ConfirmModal';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
isOpen?: boolean;
|
||||
type: UserFlow;
|
||||
method: PasscodeChannel;
|
||||
value: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const PasswordlessConfirmModal = ({ className, isOpen, type, method, value, onClose }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
const sendPasscode = getSendPasscodeApi(type, method);
|
||||
const navigate = useNavigate();
|
||||
const methodLocalName = t(`input.${method === 'email' ? 'email' : 'phone_number'}`);
|
||||
|
||||
const { result, run: asyncSendPasscode } = useApi(sendPasscode);
|
||||
|
||||
const onConfirmHandler = useCallback(() => {
|
||||
onClose();
|
||||
void asyncSendPasscode(value);
|
||||
}, [asyncSendPasscode, onClose, value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (result) {
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${type}/${method}/passcode-validation`,
|
||||
},
|
||||
{ state: { [method]: value } }
|
||||
);
|
||||
}
|
||||
}, [method, result, type, value, navigate, onClose]);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
className={className}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirmHandler}
|
||||
>
|
||||
{t(
|
||||
type === 'sign-in'
|
||||
? 'description.create_account_id_exists'
|
||||
: 'description.sign_in_id_does_not_exists',
|
||||
{ type: methodLocalName, value }
|
||||
)}
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordlessConfirmModal;
|
|
@ -1,20 +1,15 @@
|
|||
/**
|
||||
* TODO:
|
||||
* 1. API redesign handle api error and loading status globally in PageContext
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useEffect, useContext } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { signInBasic } from '@/apis/sign-in';
|
||||
import Button from '@/components/Button';
|
||||
import ErrorMessage from '@/components/ErrorMessage';
|
||||
import Input from '@/components/Input';
|
||||
import PasswordInput from '@/components/Input/PasswordInput';
|
||||
import TermsOfUse from '@/containers/TermsOfUse';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useApi, { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useForm from '@/hooks/use-form';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import useTerms from '@/hooks/use-terms';
|
||||
import { SearchParameters } from '@/types';
|
||||
import { getSearchParameters } from '@/utils';
|
||||
|
@ -38,10 +33,26 @@ const defaultState: FieldState = {
|
|||
|
||||
const UsernameSignin = ({ className }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
const { setToast } = useContext(PageContext);
|
||||
const { error, result, run: asyncSignInBasic } = useApi(signInBasic);
|
||||
const { termsValidation } = useTerms();
|
||||
const { fieldValue, setFieldValue, register, validateForm } = useForm(defaultState);
|
||||
const {
|
||||
fieldValue,
|
||||
responseErrorMessage,
|
||||
setFieldValue,
|
||||
register,
|
||||
validateForm,
|
||||
setResponseErrorMessage,
|
||||
} = useForm(defaultState);
|
||||
|
||||
const errorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'session.invalid_credentials': (error) => {
|
||||
setResponseErrorMessage(error.message);
|
||||
},
|
||||
}),
|
||||
[setResponseErrorMessage]
|
||||
);
|
||||
|
||||
const { result, run: asyncSignInBasic } = useApi(signInBasic, errorHandlers);
|
||||
|
||||
const onSubmitHandler = useCallback(async () => {
|
||||
if (!validateForm()) {
|
||||
|
@ -63,13 +74,6 @@ const UsernameSignin = ({ className }: Props) => {
|
|||
}
|
||||
}, [result]);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: API error message
|
||||
if (error) {
|
||||
setToast(t('error.request', { ...error }));
|
||||
}
|
||||
}, [error, t, setToast]);
|
||||
|
||||
return (
|
||||
<form className={classNames(styles.form, className)}>
|
||||
<Input
|
||||
|
@ -89,7 +93,7 @@ const UsernameSignin = ({ className }: Props) => {
|
|||
placeholder={t('input.password')}
|
||||
{...register('password', passwordValidation)}
|
||||
/>
|
||||
|
||||
{responseErrorMessage && <ErrorMessage>{responseErrorMessage}</ErrorMessage>}
|
||||
<TermsOfUse className={styles.terms} />
|
||||
|
||||
<Button onClick={onSubmitHandler}>{t('action.sign_in')}</Button>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { LogtoErrorCode } from '@logto/phrases';
|
||||
import { RequestErrorBody } from '@logto/schemas';
|
||||
import { HTTPError } from 'ky';
|
||||
import { useState, useCallback, useContext } from 'react';
|
||||
import { useState, useCallback, useContext, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
|
||||
|
@ -10,13 +12,22 @@ type UseApi<T extends any[], U> = {
|
|||
run: (...args: T) => Promise<void>;
|
||||
};
|
||||
|
||||
export type ErrorHandlers = {
|
||||
[key in LogtoErrorCode]?: (error: RequestErrorBody) => void;
|
||||
} & {
|
||||
global?: (error: RequestErrorBody) => void;
|
||||
callback?: (error: RequestErrorBody) => void;
|
||||
};
|
||||
|
||||
function useApi<Args extends any[], Response>(
|
||||
api: (...args: Args) => Promise<Response>
|
||||
api: (...args: Args) => Promise<Response>,
|
||||
errorHandlers?: ErrorHandlers
|
||||
): UseApi<Args, Response> {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
const [error, setError] = useState<RequestErrorBody>();
|
||||
const [result, setResult] = useState<Response>();
|
||||
|
||||
const { setLoading } = useContext(PageContext);
|
||||
const { setLoading, setToast } = useContext(PageContext);
|
||||
|
||||
const run = useCallback(
|
||||
async (...args: Args) => {
|
||||
|
@ -44,6 +55,25 @@ function useApi<Args extends any[], Response>(
|
|||
[api, setLoading]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { code } = error;
|
||||
const handler = errorHandlers?.[code] ?? errorHandlers?.global;
|
||||
|
||||
errorHandlers?.callback?.(error);
|
||||
|
||||
if (handler) {
|
||||
handler(error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setToast(t('error.request', { ...error }));
|
||||
}, [error, errorHandlers, setToast, t]);
|
||||
|
||||
return {
|
||||
error,
|
||||
result,
|
||||
|
|
|
@ -14,6 +14,7 @@ const useForm = <T>(initialState: T) => {
|
|||
|
||||
const [fieldValue, setFieldValue] = useState<T>(initialState);
|
||||
const [fieldErrors, setFieldErrors] = useState<ErrorState>({});
|
||||
const [responseErrorMessage, setResponseErrorMessage] = useState<string>();
|
||||
|
||||
const fieldValidationsRef = useRef<FieldValidations>({});
|
||||
|
||||
|
@ -61,9 +62,11 @@ const useForm = <T>(initialState: T) => {
|
|||
return {
|
||||
fieldValue,
|
||||
fieldErrors,
|
||||
responseErrorMessage,
|
||||
validateForm,
|
||||
setFieldValue,
|
||||
setFieldErrors,
|
||||
setResponseErrorMessage,
|
||||
register,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -25,22 +25,28 @@ const Passcode = () => {
|
|||
const { method, type } = useParams<Parameters>();
|
||||
const state = useLocation().state as StateType;
|
||||
const invalidSignInMethod = type !== 'sign-in' && type !== 'register';
|
||||
const invalidChannel = method !== 'email' && method !== 'sms';
|
||||
const invalidMethod = method !== 'email' && method !== 'sms';
|
||||
|
||||
useEffect(() => {
|
||||
if (invalidSignInMethod || invalidChannel) {
|
||||
if (invalidSignInMethod || invalidMethod) {
|
||||
navigate('/404', { replace: true });
|
||||
}
|
||||
}, [invalidChannel, invalidSignInMethod, navigate]);
|
||||
|
||||
if (invalidSignInMethod || invalidChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to the back if no method value found
|
||||
if (!state?.[method]) {
|
||||
navigate(-1);
|
||||
}
|
||||
}, [invalidMethod, invalidSignInMethod, method, navigate, state]);
|
||||
|
||||
if (invalidSignInMethod || invalidMethod) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const target = state ? state[method] : undefined;
|
||||
const target = state?.[method];
|
||||
|
||||
if (!target) {
|
||||
// TODO: no email or phone found
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ const SignIn = () => {
|
|||
|
||||
return (
|
||||
<div className={classNames(styles.wrapper)}>
|
||||
{/* TODO: load content from sign-in experience */}
|
||||
<BrandingHeader
|
||||
className={styles.header}
|
||||
headline={style === BrandingStyle.Logo_Slogan ? slogan : undefined}
|
||||
|
|
Loading…
Add table
Reference in a new issue