From a8c709b41cd87c53416ba0d372900b9605c0bcf4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 13 Jul 2022 11:40:02 -0500 Subject: [PATCH] Refactor formatPhoneNumber to accept countryCode --- .../ui/phone-input/country-code-dropdown.tsx | 34 +++++++++++++++++ .../components/ui/phone-input/phone-input.tsx | 37 ++++++++++++++----- .../ui/components/modals/verify-sms-modal.tsx | 12 ++---- app/soapbox/utils/__tests__/phone.test.ts | 16 ++++---- app/soapbox/utils/phone.ts | 27 ++++++++++---- 5 files changed, 94 insertions(+), 32 deletions(-) create mode 100644 app/soapbox/components/ui/phone-input/country-code-dropdown.tsx diff --git a/app/soapbox/components/ui/phone-input/country-code-dropdown.tsx b/app/soapbox/components/ui/phone-input/country-code-dropdown.tsx new file mode 100644 index 000000000..e40709a0e --- /dev/null +++ b/app/soapbox/components/ui/phone-input/country-code-dropdown.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; +import { COUNTRY_CODES, CountryCode } from 'soapbox/utils/phone'; + +import type { Menu } from 'soapbox/components/dropdown_menu'; + +interface ICountryCodeDropdown { + countryCode: CountryCode, + onChange(countryCode: CountryCode): void, +} + +/** Dropdown menu to select a country code. */ +const CountryCodeDropdown: React.FC = ({ countryCode, onChange }) => { + + const handleMenuItem = (code: CountryCode) => { + return () => { + onChange(code); + }; + }; + + const menu: Menu = COUNTRY_CODES.map(code => ({ + text: <>{code}, + action: handleMenuItem(code), + })); + + return ( + + <>{countryCode} + + ); +}; + +export default CountryCodeDropdown; diff --git a/app/soapbox/components/ui/phone-input/phone-input.tsx b/app/soapbox/components/ui/phone-input/phone-input.tsx index c3ccae6cb..7b0f0a658 100644 --- a/app/soapbox/components/ui/phone-input/phone-input.tsx +++ b/app/soapbox/components/ui/phone-input/phone-input.tsx @@ -1,33 +1,52 @@ import React from 'react'; -import { formatPhoneNumber } from 'soapbox/utils/phone'; +import { CountryCode, formatPhoneNumber } from 'soapbox/utils/phone'; +import HStack from '../hstack/hstack'; import Input from '../input/input'; -interface IPhoneInput extends Pick, 'required'> { +import CountryCodeDropdown from './country-code-dropdown'; + +interface IPhoneInput extends Pick, 'required' | 'autoFocus'> { /** Input phone number. */ value?: string, + /** E164 country code. */ + countryCode?: CountryCode, /** Change event handler taking the formatted input. */ onChange?: (phone: string) => void, } /** Internationalized phone input with country code picker. */ const PhoneInput: React.FC = (props) => { - const { onChange, ...rest } = props; + const { countryCode = 1, value = '', onChange, ...rest } = props; + + const handleCountryChange = (code: CountryCode) => { + if (onChange) { + onChange(formatPhoneNumber(countryCode, value)); + } + }; /** Pass the formatted phone to the handler. */ const handleChange: React.ChangeEventHandler = ({ target }) => { if (onChange) { - onChange(formatPhoneNumber(target.value)); + onChange(formatPhoneNumber(countryCode, target.value)); } }; return ( - + + + + + ); }; diff --git a/app/soapbox/features/ui/components/modals/verify-sms-modal.tsx b/app/soapbox/features/ui/components/modals/verify-sms-modal.tsx index b89733486..10e97d3ca 100644 --- a/app/soapbox/features/ui/components/modals/verify-sms-modal.tsx +++ b/app/soapbox/features/ui/components/modals/verify-sms-modal.tsx @@ -6,11 +6,10 @@ import { verifyCredentials } from 'soapbox/actions/auth'; import { closeModal } from 'soapbox/actions/modals'; import snackbar from 'soapbox/actions/snackbar'; import { reConfirmPhoneVerification, reRequestPhoneVerification } from 'soapbox/actions/verification'; -import { FormGroup, Input, Modal, Stack, Text } from 'soapbox/components/ui'; +import { FormGroup, PhoneInput, Modal, Stack, Text } from 'soapbox/components/ui'; import { validPhoneNumberRegex } from 'soapbox/features/verification/steps/sms-verification'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { getAccessToken } from 'soapbox/utils/auth'; -import { formatPhoneNumber } from 'soapbox/utils/phone'; interface IVerifySmsModal { onClose: (type: string) => void, @@ -38,10 +37,8 @@ const VerifySmsModal: React.FC = ({ onClose }) => { const isValid = validPhoneNumberRegex.test(phone); - const onChange = useCallback((event: React.ChangeEvent) => { - const formattedPhone = formatPhoneNumber(event.target.value); - - setPhone(formattedPhone); + const onChange = useCallback((phone: string) => { + setPhone(phone); }, []); const handleSubmit = (event: React.MouseEvent) => { @@ -141,8 +138,7 @@ const VerifySmsModal: React.FC = ({ onClose }) => { case Statuses.READY: return ( - { it('Properly formats', () => { let number = ''; - expect(formatPhoneNumber(number)).toEqual(''); + expect(formatPhoneNumber(1, number)).toEqual(''); number = '5'; - expect(formatPhoneNumber(number)).toEqual('+1 (5'); + expect(formatPhoneNumber(1, number)).toEqual('+1 (5'); number = '55'; - expect(formatPhoneNumber(number)).toEqual('+1 (55'); + expect(formatPhoneNumber(1, number)).toEqual('+1 (55'); number = '555'; - expect(formatPhoneNumber(number)).toEqual('+1 (555'); + expect(formatPhoneNumber(1, number)).toEqual('+1 (555'); number = '55513'; - expect(formatPhoneNumber(number)).toEqual('+1 (555) 13'); + expect(formatPhoneNumber(1, number)).toEqual('+1 (555) 13'); number = '555135'; - expect(formatPhoneNumber(number)).toEqual('+1 (555) 135'); + expect(formatPhoneNumber(1, number)).toEqual('+1 (555) 135'); number = '5551350'; - expect(formatPhoneNumber(number)).toEqual('+1 (555) 135-0'); + expect(formatPhoneNumber(1, number)).toEqual('+1 (555) 135-0'); number = '5551350123'; - expect(formatPhoneNumber(number)).toEqual('+1 (555) 135-0123'); + expect(formatPhoneNumber(1, number)).toEqual('+1 (555) 135-0123'); }); }); diff --git a/app/soapbox/utils/phone.ts b/app/soapbox/utils/phone.ts index d112e66eb..913042a6d 100644 --- a/app/soapbox/utils/phone.ts +++ b/app/soapbox/utils/phone.ts @@ -1,3 +1,14 @@ +/** List of supported E164 country codes. */ +const COUNTRY_CODES = [ + 1, + 44, +] as const; + +/** Supported E164 country code. */ +type CountryCode = typeof COUNTRY_CODES[number]; + +/** Check whether a given value is a country code. */ +const isCountryCode = (value: any): value is CountryCode => COUNTRY_CODES.includes(value); function removeFormattingFromNumber(number = '') { if (number) { @@ -7,17 +18,14 @@ function removeFormattingFromNumber(number = '') { return number; } -function formatPhoneNumber(phoneNumber = '') { +function formatPhoneNumber(countryCode: CountryCode, phoneNumber = '') { let formattedPhoneNumber = ''; - let strippedPhone = removeFormattingFromNumber(phoneNumber); - if (strippedPhone.slice(0, 1) === '1') { - strippedPhone = strippedPhone.slice(1); - } + const strippedPhone = removeFormattingFromNumber(phoneNumber); for (let i = 0; i < strippedPhone.length && i < 10; i++) { const character = strippedPhone.charAt(i); if (i === 0) { - const prefix = '+1 ('; + const prefix = `+${countryCode} (`; formattedPhoneNumber += prefix + character; } else if (i === 3) { formattedPhoneNumber += `) ${character}`; @@ -30,4 +38,9 @@ function formatPhoneNumber(phoneNumber = '') { return formattedPhoneNumber; } -export { formatPhoneNumber }; +export { + COUNTRY_CODES, + CountryCode, + isCountryCode, + formatPhoneNumber, +};