0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-24 22:05:56 -05:00

refactor(experience): remove redundant defaultType prop for SmartInputField (#6517)

This commit is contained in:
Xiao Yijun 2024-08-26 19:35:57 +08:00 committed by GitHub
parent f545716353
commit ef78823948
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 54 additions and 87 deletions

View file

@ -85,7 +85,7 @@ describe('<IdentifierRegisterForm />', () => {
])('%p %p register form', (...signUpMethods) => {
test('default render', () => {
const { queryByText, container } = renderForm(signUpMethods);
expect(container.querySelector('input[name="id"]')).not.toBeNull();
expect(container.querySelector('input[name=identifier]')).not.toBeNull();
expect(queryByText('action.create_account')).not.toBeNull();
expect(queryByText('description.terms_of_use')).not.toBeNull();
});
@ -110,7 +110,7 @@ describe('<IdentifierRegisterForm />', () => {
test('username with initial numeric char should throw', async () => {
const { queryByText, getByText, container } = renderForm();
const submitButton = getByText('action.create_account');
const usernameInput = container.querySelector('input[name="id"]');
const usernameInput = container.querySelector('input[name=identifier]');
assert(usernameInput, new Error('username input not found'));
@ -137,7 +137,7 @@ describe('<IdentifierRegisterForm />', () => {
test('username with special character should throw', async () => {
const { queryByText, getByText, container } = renderForm();
const submitButton = getByText('action.create_account');
const usernameInput = container.querySelector('input[name="id"]');
const usernameInput = container.querySelector('input[name=identifier]');
assert(usernameInput, new Error('username input not found'));
@ -165,7 +165,7 @@ describe('<IdentifierRegisterForm />', () => {
const { getByText, queryByText, container } = renderForm();
const submitButton = getByText('action.create_account');
const termsButton = getByText('description.agree_with_terms');
const usernameInput = container.querySelector('input[name="id"]');
const usernameInput = container.querySelector('input[name=identifier]');
assert(usernameInput, new Error('username input not found'));
@ -200,7 +200,7 @@ describe('<IdentifierRegisterForm />', () => {
const { queryByText, getByText, container } = renderForm(signUpMethods);
const submitButton = getByText('action.create_account');
const emailInput = container.querySelector('input[name="id"]');
const emailInput = container.querySelector('input[name=identifier]');
assert(emailInput, new Error('email input not found'));
@ -230,7 +230,7 @@ describe('<IdentifierRegisterForm />', () => {
const submitButton = getByText('action.create_account');
const termsButton = getByText('description.agree_with_terms');
const emailInput = container.querySelector('input[name="id"]');
const emailInput = container.querySelector('input[name=identifier]');
assert(emailInput, new Error('email input not found'));
@ -260,7 +260,7 @@ describe('<IdentifierRegisterForm />', () => {
const { queryByText, getByText, container } = renderForm(signUpMethods);
const submitButton = getByText('action.create_account');
const phoneInput = container.querySelector('input[name="id"]');
const phoneInput = container.querySelector('input[name=identifier]');
assert(phoneInput, new Error('phone input not found'));
@ -289,7 +289,7 @@ describe('<IdentifierRegisterForm />', () => {
const submitButton = getByText('action.create_account');
const termsButton = getByText('description.agree_with_terms');
const phoneInput = container.querySelector('input[name="id"]');
const phoneInput = container.querySelector('input[name=identifier]');
assert(phoneInput, new Error('phone input not found'));
@ -326,7 +326,7 @@ describe('<IdentifierRegisterForm />', () => {
it('should not call check single sign-on connector when no single sign-on connector is enabled', async () => {
const { getByText, container, queryByText } = renderForm([SignInIdentifier.Email]);
const submitButton = getByText('action.create_account');
const emailInput = container.querySelector('input[name="id"]');
const emailInput = container.querySelector('input[name=identifier]');
const termsButton = getByText('description.agree_with_terms');
assert(emailInput, new Error('username input not found'));
@ -358,7 +358,7 @@ describe('<IdentifierRegisterForm />', () => {
mockSsoConnectors
);
const submitButton = getByText('action.create_account');
const emailInput = container.querySelector('input[name="id"]');
const emailInput = container.querySelector('input[name=identifier]');
const termsButton = getByText('description.agree_with_terms');
assert(emailInput, new Error('username input not found'));
@ -393,7 +393,7 @@ describe('<IdentifierRegisterForm />', () => {
[SignInIdentifier.Email],
mockSsoConnectors
);
const emailInput = container.querySelector('input[name="id"]');
const emailInput = container.querySelector('input[name=identifier]');
const termsButton = getByText('description.agree_with_terms');
assert(emailInput, new Error('username input not found'));

View file

@ -27,7 +27,7 @@ type Props = {
};
type FormState = {
id: IdentifierInputValue;
identifier: IdentifierInputValue;
};
const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props) => {
@ -46,11 +46,13 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
control,
} = useForm<FormState>({
reValidateMode: 'onBlur',
defaultValues: { id: prefilledIdentifier },
defaultValues: { identifier: prefilledIdentifier },
});
// Watch identifier field and check single sign on method availability
const { showSingleSignOnForm, navigateToSingleSignOn } = useSingleSignOnWatch(watch('id'));
const { showSingleSignOnForm, navigateToSingleSignOn } = useSingleSignOnWatch(
watch('identifier')
);
useEffect(() => {
if (!isValid) {
@ -62,7 +64,7 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
async (event?: React.FormEvent<HTMLFormElement>) => {
clearErrorMessage();
void handleSubmit(async ({ id: { type, value } }) => {
void handleSubmit(async ({ identifier: { type, value } }) => {
if (!type) {
return;
}
@ -101,7 +103,7 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
<Controller
control={control}
name="id"
name="identifier"
rules={{
validate: ({ type, value }) => {
if (!type || !value) {
@ -126,9 +128,8 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
className={styles.inputField}
{...field}
defaultValue={field.value.value}
defaultType={field.value.type}
isDanger={!!errors.id || !!errorMessage}
errorMessage={errors.id?.message}
isDanger={!!errors.identifier || !!errorMessage}
errorMessage={errors.identifier?.message}
enabledTypes={signUpMethods}
/>
)}

View file

@ -130,7 +130,6 @@ const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) =>
isDanger={!!errors.identifier || !!errorMessage}
errorMessage={errors.identifier?.message}
enabledTypes={enabledSignInMethods}
defaultType={field.value.type}
defaultValue={field.value.value}
/>
)}

View file

@ -23,7 +23,6 @@ type Props = Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'prefix' | 'value'>
readonly isDanger?: boolean;
readonly enabledTypes?: IdentifierInputType[];
readonly defaultType?: IdentifierInputType;
readonly defaultValue?: string;
readonly onChange?: (data: IdentifierInputValue) => void;
@ -32,7 +31,7 @@ type Props = Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'prefix' | 'value'>
const AnimatedInputField = animated(InputField);
const SmartInputField = (
{ defaultValue, defaultType, enabledTypes = [], onChange, ...rest }: Props,
{ defaultValue, enabledTypes = [], onChange, ...rest }: Props,
ref: Ref<Nullable<HTMLInputElement>>
) => {
const innerRef = useRef<HTMLInputElement>(null);
@ -46,7 +45,6 @@ const SmartInputField = (
onInputValueClear,
identifierType,
} = useSmartInputField({
_defaultType: defaultType,
defaultValue,
enabledTypes,
});

View file

@ -1,5 +1,4 @@
import { SignInIdentifier } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import { useState, useCallback, useMemo } from 'react';
import type { ChangeEventHandler } from 'react';
@ -27,26 +26,16 @@ export type IdentifierInputValue = {
type Props = {
defaultValue?: string;
_defaultType?: IdentifierInputType;
enabledTypes: IdentifierInputType[];
};
const useSmartInputField = ({ _defaultType, defaultValue, enabledTypes }: Props) => {
const useSmartInputField = ({ defaultValue, enabledTypes }: Props) => {
const enabledTypeSet = useMemo(() => new Set(enabledTypes), [enabledTypes]);
assert(
!_defaultType || enabledTypeSet.has(_defaultType),
new Error(
`Invalid input type. Current inputType ${
_defaultType ?? ''
} is detected but missing in enabledTypes`
)
);
// Parse default type from enabled types if default type is not provided and only one type is enabled
// Parse default type from enabled types and default value
const defaultType = useMemo(
() => _defaultType ?? (enabledTypes.length === 1 ? enabledTypes[0] : undefined),
[_defaultType, enabledTypes]
() => detectIdentifierType({ value: defaultValue ?? '', enabledTypeSet }),
[defaultValue, enabledTypeSet]
);
// Parse default value if provided
@ -55,14 +44,7 @@ const useSmartInputField = ({ _defaultType, defaultValue, enabledTypes }: Props)
[defaultType, defaultValue]
);
const [currentType, setCurrentType] = useState(
detectIdentifierType({
value: defaultValue ?? '',
enabledTypeSet,
defaultType,
currentType: defaultType,
})
);
const [currentType, setCurrentType] = useState(defaultType);
const [countryCode, setCountryCode] = useState<string>(
defaultCountryCode ?? getDefaultCountryCallingCode()
@ -71,8 +53,8 @@ const useSmartInputField = ({ _defaultType, defaultValue, enabledTypes }: Props)
const [inputValue, setInputValue] = useState<string>(defaultInputValue ?? '');
const detectInputType = useCallback(
(value: string) => detectIdentifierType({ value, enabledTypeSet, defaultType, currentType }),
[defaultType, currentType, enabledTypeSet]
(value: string) => detectIdentifierType({ value, enabledTypeSet, currentType }),
[currentType, enabledTypeSet]
);
const onCountryCodeChange = useCallback(

View file

@ -46,23 +46,27 @@ const digitsRegex = /^\d*$/;
type DetectIdentifierTypeParams = {
value: string;
enabledTypeSet: Set<IdentifierInputType>;
defaultType?: IdentifierInputType;
currentType?: IdentifierInputType;
};
export const detectIdentifierType = ({
value,
enabledTypeSet,
defaultType,
currentType,
}: DetectIdentifierTypeParams) => {
// Reset InputType
if (!value && enabledTypeSet.size > 1) {
/**
* Multiple types are enabled, so we cannot detect the type without the value.
* Return `undefined` since the type is not determined.
*/
return;
}
if (enabledTypeSet.size === 1) {
return defaultType;
/**
* Only one type enabled, so we limit the type to the default type.
*/
return Array.from(enabledTypeSet)[0];
}
const hasAtSymbol = value.includes('@');

View file

@ -130,7 +130,6 @@ const PasswordSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
errorMessage={errors.identifier?.message}
enabledTypes={signInMethods}
defaultValue={field.value.value}
defaultType={field.value.type}
/>
)}
/>

View file

@ -105,7 +105,6 @@ const IdentifierProfileForm = ({
autoFocus={autoFocus}
className={styles.inputField}
{...field}
defaultType={defaultType}
isDanger={!!errors.identifier}
errorMessage={errors.identifier?.message}
enabledTypes={enabledTypes}

View file

@ -32,12 +32,11 @@ describe('ForgotPasswordForm', () => {
const phone = '13911111111';
const originalLocation = window.location;
const renderForm = (defaultType: VerificationCodeIdentifier, defaultValue?: string) =>
const renderForm = (defaultValue?: string) =>
renderWithPageContext(
<UserInteractionContextProvider>
<ForgotPasswordForm
enabledTypes={[SignInIdentifier.Email, SignInIdentifier.Phone]}
defaultType={defaultType}
defaultValue={defaultValue}
/>
</UserInteractionContextProvider>
@ -57,7 +56,7 @@ describe('ForgotPasswordForm', () => {
'identifier: %s, value: %s',
({ identifier, value }) => {
test(`forgot password form render properly with default ${identifier} value ${value}`, async () => {
const { container, queryByText } = renderForm(identifier, value);
const { container, queryByText } = renderForm(value);
const identifierInput = container.querySelector(`input[name="identifier"]`);
assert(identifierInput, new Error('identifier input should not be null'));
@ -73,7 +72,7 @@ describe('ForgotPasswordForm', () => {
});
test(`send ${identifier} verification code properly`, async () => {
const { container, getByText } = renderForm(identifier, value);
const { container, getByText } = renderForm(value);
const identifierInput = container.querySelector(`input[name="identifier"]`);
assert(identifierInput, new Error('identifier input should not be null'));

View file

@ -19,7 +19,6 @@ type Props = {
// eslint-disable-next-line react/boolean-prop-naming
readonly autoFocus?: boolean;
readonly defaultValue?: string;
readonly defaultType?: VerificationCodeIdentifier;
readonly enabledTypes: VerificationCodeIdentifier[];
};
@ -30,13 +29,7 @@ type FormState = {
};
};
const ForgotPasswordForm = ({
className,
autoFocus,
defaultType,
defaultValue = '',
enabledTypes,
}: Props) => {
const ForgotPasswordForm = ({ className, autoFocus, defaultValue = '', enabledTypes }: Props) => {
const { t } = useTranslation();
const { errorMessage, clearErrorMessage, onSubmit } = useSendVerificationCode(
UserFlow.ForgotPassword
@ -52,7 +45,6 @@ const ForgotPasswordForm = ({
reValidateMode: 'onBlur',
defaultValues: {
identifier: {
type: defaultType,
value: defaultValue,
},
},
@ -111,7 +103,6 @@ const ForgotPasswordForm = ({
autoFocus={autoFocus}
className={styles.inputField}
{...field}
defaultType={defaultType}
defaultValue={defaultValue}
isDanger={!!errors.identifier}
errorMessage={errors.identifier?.message}

View file

@ -54,12 +54,7 @@ const ForgotPassword = () => {
types: enabledMethods.map((method) => t(identifierInputDescriptionMap[method])),
}}
>
<ForgotPasswordForm
autoFocus
defaultType={defaultType}
defaultValue={defaultValue}
enabledTypes={enabledMethods}
/>
<ForgotPasswordForm autoFocus defaultValue={defaultValue} enabledTypes={enabledMethods} />
</SecondaryPageLayout>
);
};

View file

@ -35,7 +35,7 @@ describe('<Register />', () => {
test.each(signUpTestCases)('renders with %o sign up settings', async (...signUp) => {
const { queryByText, queryAllByText, container } = renderRegisterPage();
expect(container.querySelector('input[name="id"]')).not.toBeNull();
expect(container.querySelector('input[name=identifier]')).not.toBeNull();
expect(queryByText('action.create_account')).not.toBeNull();
// Social
@ -53,7 +53,7 @@ describe('<Register />', () => {
mockSignInExperienceSettings.socialConnectors.length
);
expect(container.querySelector('input[name="id"]')).toBeNull();
expect(container.querySelector('input[name=identifier]')).toBeNull();
expect(queryByText('action.create_account')).toBeNull();
});

View file

@ -67,7 +67,7 @@ describe('smoke testing for console admin account creation and sign-in', () => {
expect(page.url()).toBe(new URL('register', logtoConsoleUrl).href);
await expect(page).toFill('input[name=id]', consoleUsername);
await expect(page).toFill('input[name=identifier]', consoleUsername);
await expectNavigation(expect(page).toClick('button[name=submit]'));
expect(page.url()).toBe(appendPathname('/register/password', logtoConsoleUrl).href);

View file

@ -24,7 +24,7 @@ describe('smoke testing on the demo app', () => {
// Open the demo app and navigate to the register page
await experience.startWith(demoAppUrl, 'register');
await experience.toFillInput('id', credentials.username, { submit: true });
await experience.toFillInput('identifier', credentials.username, { submit: true });
// Simple password tests
experience.toBeAt('register/password');

View file

@ -56,6 +56,6 @@ describe('identifier input cache', () => {
await experience.toClick('a', 'Create account');
experience.toMatchUrl(appendPath(new URL(logtoUrl), 'register').href);
// The input should be empty
await experience.toMatchElement('input[name="id"][value=""]');
await experience.toMatchElement('input[name=identifier][value=""]');
});
});

View file

@ -46,7 +46,7 @@ describe('MFA - Backup Code', () => {
const experience = new ExpectWebAuthnExperience(await browser.newPage());
await experience.setupVirtualAuthenticator();
await experience.startWith(demoAppUrl, 'register');
await experience.toFillInput('id', username, { submit: true });
await experience.toFillInput('identifier', username, { submit: true });
experience.toBeAt('register/password');
await experience.toFillNewPasswords(password);
experience.toBeAt('mfa-binding/WebAuthn');

View file

@ -66,7 +66,7 @@ describe('MFA - TOTP', () => {
const experience = new ExpectTotpExperience(await browser.newPage());
await experience.startWith(demoAppUrl, 'register');
await experience.toFillInput('id', context.userEmail, { submit: true });
await experience.toFillInput('identifier', context.userEmail, { submit: true });
await experience.toCompleteVerification('register', 'Email');
context.setUpTotpSecret(await experience.toBindTotp());
await experience.verifyThenEnd();
@ -146,7 +146,7 @@ describe('MFA - TOTP', () => {
const experience = new ExpectTotpExperience(await browser.newPage());
await experience.startWith(demoAppUrl, 'register');
await experience.toFillInput('id', context.userPhone, { submit: true });
await experience.toFillInput('identifier', context.userPhone, { submit: true });
await experience.toCompleteVerification('register', 'Sms');
context.setUpTotpSecret(await experience.toBindTotp());

View file

@ -58,7 +58,7 @@ describe('MFA - TOTP', () => {
const experience = new ExpectTotpExperience(await browser.newPage());
await experience.startWith(demoAppUrl, 'register');
await experience.toFillInput('id', context.username, { submit: true });
await experience.toFillInput('identifier', context.username, { submit: true });
experience.toBeAt('register/password');
await experience.toFillNewPasswords(context.userPassword);

View file

@ -50,7 +50,7 @@ describe('MFA - User controlled', () => {
// Register
await experience.startWith(demoAppUrl, 'register');
await experience.toFillInput('id', username, { submit: true });
await experience.toFillInput('identifier', username, { submit: true });
experience.toBeAt('register/password');
await experience.toFillNewPasswords(password);

View file

@ -47,7 +47,7 @@ describe('MFA - WebAuthn', () => {
const experience = new ExpectWebAuthnExperience(await browser.newPage());
await experience.setupVirtualAuthenticator();
await experience.startWith(demoAppUrl, 'register');
await experience.toFillInput('id', username, { submit: true });
await experience.toFillInput('identifier', username, { submit: true });
experience.toBeAt('register/password');
await experience.toFillNewPasswords(password);
await experience.toCreatePasskey();

View file

@ -41,7 +41,7 @@ describe('password policy', () => {
// Open the demo app and navigate to the register page
await experience.startWith(demoAppUrl, 'register');
await experience.toFillInput('id', username, { submit: true });
await experience.toFillInput('identifier', username, { submit: true });
// Password tests
await experience.waitForPathname('register/password');
@ -69,7 +69,7 @@ describe('password policy', () => {
await experience.startWith(demoAppUrl, 'register');
// Complete verification code flow
await experience.toFillInput('id', email, { submit: true });
await experience.toFillInput('identifier', email, { submit: true });
await experience.toCompleteVerification('register', 'Email');
await experience.waitForPathname('continue/password');