0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-10 22:22:45 -05:00

refactor(ui): refactor country code (#1483)

* refactor(ui): refactor country code

refactor country code

* test(ui): fix ut

fix ut

* fix(ui): cr fix

cr fix
This commit is contained in:
simeng-li 2022-07-08 18:21:15 +08:00 committed by GitHub
parent 650cbd4746
commit 64bb5fd159
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 286 additions and 339 deletions

View file

@ -59,7 +59,6 @@
"react-i18next": "^11.15.4",
"react-modal": "^3.14.4",
"react-modal-promise": "^1.0.2",
"react-phone-number-input": "^3.1.46",
"react-router-dom": "^6.2.2",
"react-string-replace": "^1.0.0",
"react-timer-hook": "^3.0.5",

View file

@ -1,10 +1,14 @@
import { render, fireEvent } from '@testing-library/react';
import React from 'react';
import { defaultCountryCallingCode, countryList } from '@/hooks/use-phone-number';
import { getCountryList, getDefaultCountryCallingCode } from '@/utils/country-code';
import PhoneInput from './PhoneInput';
jest.mock('i18next', () => ({
language: 'en',
}));
describe('Phone Input Field UI Component', () => {
const onChange = jest.fn();
@ -12,6 +16,8 @@ describe('Phone Input Field UI Component', () => {
onChange.mockClear();
});
const defaultCountryCallingCode = getDefaultCountryCallingCode();
it('render empty PhoneInput', () => {
const { queryByText, container } = render(
<PhoneInput name="PhoneInput" nationalNumber="" onChange={onChange} />
@ -21,18 +27,18 @@ describe('Phone Input Field UI Component', () => {
});
it('render with country list', () => {
const { queryByText, container } = render(
const { queryAllByText, container } = render(
<PhoneInput
name="PhoneInput"
nationalNumber=""
countryList={countryList}
countryList={getCountryList()}
countryCallingCode={defaultCountryCallingCode}
onChange={onChange}
/>
);
const countryCode = queryByText(`+${defaultCountryCallingCode}`);
expect(countryCode).not.toBeNull();
const countryCode = queryAllByText(`+${defaultCountryCallingCode}`);
expect(countryCode).toHaveLength(2);
const selector = container.querySelector('select');
expect(selector).not.toBeNull();

View file

@ -39,7 +39,7 @@ const PhoneInput = ({
const inputReference = useRef<HTMLInputElement>(null);
const countrySelector = useMemo(() => {
if (!countryCallingCode || !countryList) {
if (countryCallingCode === undefined || !countryList?.length) {
return null;
}
@ -60,9 +60,9 @@ const PhoneInput = ({
}
}}
>
{countryList.map(({ countryCode, countryCallingCode, countryName }) => (
{countryList.map(({ countryCallingCode, countryCode }) => (
<option key={countryCode} value={countryCallingCode}>
{`${countryName ?? countryCode}: +${countryCallingCode}`}
{`+${countryCallingCode}`}
</option>
))}
</select>

View file

@ -6,7 +6,7 @@ import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { sendRegisterSmsPasscode } from '@/apis/register';
import { sendSignInSmsPasscode } from '@/apis/sign-in';
import { defaultCountryCallingCode } from '@/hooks/use-phone-number';
import { getDefaultCountryCallingCode } from '@/utils/country-code';
import PhonePasswordless from './PhonePasswordless';
@ -16,9 +16,13 @@ jest.mock('@/apis/sign-in', () => ({
jest.mock('@/apis/register', () => ({
sendRegisterSmsPasscode: jest.fn(async () => Promise.resolve()),
}));
jest.mock('i18next', () => ({
language: 'en',
}));
describe('<PhonePasswordless/>', () => {
const phoneNumber = '18888888888';
const phoneNumber = '8573333333';
const defaultCountryCallingCode = getDefaultCountryCallingCode();
test('render', () => {
const { queryByText, container } = renderWithPageContext(

View file

@ -10,7 +10,7 @@ import TermsOfUse from '@/containers/TermsOfUse';
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 usePhoneNumber from '@/hooks/use-phone-number';
import useTerms from '@/hooks/use-terms';
import { UserFlow, SearchParameters } from '@/types';
import { getSearchParameters } from '@/utils';
@ -35,7 +35,7 @@ const PhonePasswordless = ({ type, autoFocus, className }: Props) => {
const { setToast } = useContext(PageContext);
const [showPasswordlessConfirmModal, setShowPasswordlessConfirmModal] = useState(false);
const { t } = useTranslation();
const { phoneNumber, setPhoneNumber, isValidPhoneNumber } = usePhoneNumber();
const { countryList, phoneNumber, setPhoneNumber, isValidPhoneNumber } = usePhoneNumber();
const navigate = useNavigate();
const { termsValidation } = useTerms();
const { fieldValue, setFieldValue, setFieldErrors, validateForm, register } =

View file

@ -5,40 +5,22 @@
import {
parsePhoneNumberWithError,
getCountries,
getCountryCallingCode,
CountryCallingCode,
CountryCode,
E164Number,
ParseError,
} from 'libphonenumber-js';
CountryCode,
} from 'libphonenumber-js/mobile';
import { useState } from 'react';
// Should not need the react-phone-number-input package, but we use its locale country name for now
import en from 'react-phone-number-input/locale/en.json';
import { getDefaultCountryCallingCode, getCountryList } from '@/utils/country-code';
export type { CountryCallingCode } from 'libphonenumber-js';
/**
* Provide Country Code Options
* TODO: Country Name i18n
*/
export type CountryMetaData = {
countryCode: CountryCode;
countryCallingCode: CountryCallingCode;
countryName?: string;
};
export const countryList: CountryMetaData[] = getCountries().map((code) => {
const callingCode = getCountryCallingCode(code);
const countryName = en[code];
return {
countryCode: code,
countryCallingCode: callingCode,
countryName,
};
});
type PhoneNumberData = {
countryCallingCode: string;
nationalNumber: string;
@ -65,16 +47,14 @@ const isValidPhoneNumber = (value: string): boolean => {
}
};
export const defaultCountryCode: CountryCode = 'CN';
export const defaultCountryCallingCode = getCountryCallingCode(defaultCountryCode);
const usePhoneNumber = () => {
const [phoneNumber, setPhoneNumber] = useState<PhoneNumberData>({
countryCallingCode: defaultCountryCallingCode,
countryCallingCode: getDefaultCountryCallingCode(),
nationalNumber: '',
});
return {
countryList: getCountryList(),
phoneNumber,
setPhoneNumber,
isValidPhoneNumber,

View file

@ -5,6 +5,9 @@ import { Routes, Route, MemoryRouter } from 'react-router-dom';
import Register from '@/pages/Register';
jest.mock('@/apis/register', () => ({ register: jest.fn(async () => Promise.resolve()) }));
jest.mock('i18next', () => ({
language: 'en',
}));
describe('<Register />', () => {
test('renders without exploding', async () => {

View file

@ -5,6 +5,9 @@ import { Routes, Route, MemoryRouter } from 'react-router-dom';
import SecondarySignIn from '@/pages/SecondarySignIn';
jest.mock('@/apis/register', () => ({ register: jest.fn(async () => Promise.resolve()) }));
jest.mock('i18next', () => ({
language: 'en',
}));
describe('<SecondarySignIn />', () => {
test('renders without exploding', async () => {

View file

@ -7,6 +7,10 @@ import { mockSignInExperienceSettings } from '@/__mocks__/logto';
import { defaultSize } from '@/containers/SocialSignIn/SocialSignInList';
import SignIn from '@/pages/SignIn';
jest.mock('i18next', () => ({
language: 'en',
}));
describe('<SignIn />', () => {
test('renders with username as primary', async () => {
const { queryByText, container } = renderWithPageContext(

View file

@ -0,0 +1,79 @@
import i18nNext from 'i18next';
import {
isValidCountryCode,
getDefaultCountryCode,
getDefaultCountryCallingCode,
getCountryList,
} from './country-code';
describe('country-code', () => {
void i18nNext.init();
it('isValidCountryCode', () => {
expect(isValidCountryCode('CN')).toBeTruthy();
expect(isValidCountryCode('xy')).toBeFalsy();
});
it('getDefaultCountryCode', async () => {
await i18nNext.changeLanguage('zh');
expect(getDefaultCountryCode()).toEqual('CN');
await i18nNext.changeLanguage('en');
expect(getDefaultCountryCode()).toEqual('US');
await i18nNext.changeLanguage('zh-CN');
expect(getDefaultCountryCode()).toEqual('CN');
await i18nNext.changeLanguage('zh-TW');
expect(getDefaultCountryCode()).toEqual('TW');
await i18nNext.changeLanguage('en-US');
expect(getDefaultCountryCode()).toEqual('US');
await i18nNext.changeLanguage('en-CA');
expect(getDefaultCountryCode()).toEqual('CA');
});
it('getDefaultCountryCallingCode', async () => {
await i18nNext.changeLanguage('zh');
expect(getDefaultCountryCallingCode()).toEqual('86');
await i18nNext.changeLanguage('en');
expect(getDefaultCountryCallingCode()).toEqual('1');
await i18nNext.changeLanguage('zh-CN');
expect(getDefaultCountryCallingCode()).toEqual('86');
await i18nNext.changeLanguage('zh-TW');
expect(getDefaultCountryCallingCode()).toEqual('886');
await i18nNext.changeLanguage('en-US');
expect(getDefaultCountryCallingCode()).toEqual('1');
await i18nNext.changeLanguage('en-CA');
expect(getDefaultCountryCallingCode()).toEqual('1');
});
it('getCountryList should sort properly', async () => {
await i18nNext.changeLanguage('zh');
const countryList = getCountryList();
expect(countryList[0]).toEqual({
countryCode: 'CN',
countryCallingCode: '86',
});
expect(countryList[1]?.countryCallingCode).toEqual('1');
});
it('getCountryList should remove duplicate', async () => {
await i18nNext.changeLanguage('zh');
const countryList = getCountryList();
expect(countryList.filter(({ countryCallingCode }) => countryCallingCode === '1')).toHaveLength(
1
);
expect(countryList[0]?.countryCallingCode).toEqual('86');
});
});

View file

@ -0,0 +1,80 @@
import i18next from 'i18next';
import {
getCountries,
CountryCode,
CountryCallingCode,
getCountryCallingCode,
} from 'libphonenumber-js';
export const fallbackCountryCode = 'US';
export const countryCallingCodeMap: Record<string, CountryCode> = {
zh: 'CN',
en: 'US',
};
export const isValidCountryCode = (countryCode: string): countryCode is CountryCode => {
try {
// Use getCountryCallingCode method to guard the input's value is in CountryCode union type, if type not match exceptions are expected
getCountryCallingCode(countryCode as CountryCode);
return true;
} catch {
return false;
}
};
export const getDefaultCountryCode = (): CountryCode => {
const { language } = i18next;
// Extract the country code from language tag suffix
const [, countryCode] = language.split('-');
if (countryCode && isValidCountryCode(countryCode)) {
return countryCode;
}
return countryCallingCodeMap[language] ?? fallbackCountryCode;
};
export const getDefaultCountryCallingCode = () => getCountryCallingCode(getDefaultCountryCode());
/**
* Provide Country Code Options
*/
export type CountryMetaData = {
countryCode: CountryCode;
countryCallingCode: CountryCallingCode;
};
export const getCountryList = (): CountryMetaData[] => {
const defaultCountryCode = getDefaultCountryCode();
const defaultCountryCallingCode = getCountryCallingCode(defaultCountryCode);
const countryList = getCountries()
.map((code) => ({
countryCode: code,
countryCallingCode: getCountryCallingCode(code),
}))
// Filter the detected default countryCode & duplicates
.filter(({ countryCallingCode }, index, self) => {
if (countryCallingCode === defaultCountryCallingCode) {
return false;
}
return (
self.findIndex((element) => element.countryCallingCode === countryCallingCode) === index
);
})
.slice()
// Sort by countryCallingCode
.sort((previous, next) => (next.countryCallingCode > previous.countryCallingCode ? -1 : 1));
return [
{
countryCode: defaultCountryCode,
countryCallingCode: defaultCountryCallingCode,
},
...countryList,
];
};

389
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff