Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / components / containers / payments / TaxCountrySelector.tsx
blob3610d53316c4cd13a724f218c6e5837b05923feb
1 import { useEffect, useMemo, useState } from 'react';
3 import { c } from 'ttag';
5 import { InlineLinkButton } from '@proton/atoms';
6 import Option from '@proton/components/components/option/Option';
7 import SearchableSelect from '@proton/components/components/selectTwo/SearchableSelect';
8 import type { SelectChangeEvent } from '@proton/components/components/selectTwo/select';
9 import Tooltip from '@proton/components/components/tooltip/Tooltip';
10 import { type BillingAddress, DEFAULT_TAX_BILLING_ADDRESS, type PaymentMethodStatusExtended } from '@proton/payments';
11 import clsx from '@proton/utils/clsx';
13 import type { SearcheableSelectProps } from '../../components/selectTwo/SearchableSelect';
14 import CountriesDropdown, { useCountries } from './CountriesDropdown';
15 import { countriesWithStates, getBillingAddressStatus } from './subscription/helpers';
17 function getStateList(countryCode: string) {
18     if (countryCode === 'US') {
19         return [
20             { stateName: 'Alabama', stateCode: 'AL' },
21             { stateName: 'Alaska', stateCode: 'AK' },
22             { stateName: 'Arizona', stateCode: 'AZ' },
23             { stateName: 'Arkansas', stateCode: 'AR' },
24             { stateName: 'California', stateCode: 'CA' },
25             { stateName: 'Colorado', stateCode: 'CO' },
26             { stateName: 'Connecticut', stateCode: 'CT' },
27             { stateName: 'Delaware', stateCode: 'DE' },
28             { stateName: 'Florida', stateCode: 'FL' },
29             { stateName: 'Georgia', stateCode: 'GA' },
30             { stateName: 'Hawaii', stateCode: 'HI' },
31             { stateName: 'Idaho', stateCode: 'ID' },
32             { stateName: 'Illinois', stateCode: 'IL' },
33             { stateName: 'Indiana', stateCode: 'IN' },
34             { stateName: 'Iowa', stateCode: 'IA' },
35             { stateName: 'Kansas', stateCode: 'KS' },
36             { stateName: 'Kentucky', stateCode: 'KY' },
37             { stateName: 'Louisiana', stateCode: 'LA' },
38             { stateName: 'Maine', stateCode: 'ME' },
39             { stateName: 'Maryland', stateCode: 'MD' },
40             { stateName: 'Massachusetts', stateCode: 'MA' },
41             { stateName: 'Michigan', stateCode: 'MI' },
42             { stateName: 'Minnesota', stateCode: 'MN' },
43             { stateName: 'Mississippi', stateCode: 'MS' },
44             { stateName: 'Missouri', stateCode: 'MO' },
45             { stateName: 'Montana', stateCode: 'MT' },
46             { stateName: 'Nebraska', stateCode: 'NE' },
47             { stateName: 'Nevada', stateCode: 'NV' },
48             { stateName: 'New Hampshire', stateCode: 'NH' },
49             { stateName: 'New Jersey', stateCode: 'NJ' },
50             { stateName: 'New Mexico', stateCode: 'NM' },
51             { stateName: 'New York', stateCode: 'NY' },
52             { stateName: 'North Carolina', stateCode: 'NC' },
53             { stateName: 'North Dakota', stateCode: 'ND' },
54             { stateName: 'Ohio', stateCode: 'OH' },
55             { stateName: 'Oklahoma', stateCode: 'OK' },
56             { stateName: 'Oregon', stateCode: 'OR' },
57             { stateName: 'Pennsylvania', stateCode: 'PA' },
58             { stateName: 'Rhode Island', stateCode: 'RI' },
59             { stateName: 'South Carolina', stateCode: 'SC' },
60             { stateName: 'South Dakota', stateCode: 'SD' },
61             { stateName: 'Tennessee', stateCode: 'TN' },
62             { stateName: 'Texas', stateCode: 'TX' },
63             { stateName: 'Utah', stateCode: 'UT' },
64             { stateName: 'Vermont', stateCode: 'VT' },
65             { stateName: 'Virginia', stateCode: 'VA' },
66             { stateName: 'Washington', stateCode: 'WA' },
67             { stateName: 'West Virginia', stateCode: 'WV' },
68             { stateName: 'Wisconsin', stateCode: 'WI' },
69             { stateName: 'Wyoming', stateCode: 'WY' },
70             { stateName: 'District of Columbia', stateCode: 'DC' },
71             { stateName: 'American Samoa', stateCode: 'AS' },
72             { stateName: 'Micronesia', stateCode: 'FM' },
73             { stateName: 'Guam', stateCode: 'GU' },
74             { stateName: 'Puerto Rico', stateCode: 'PR' },
75             { stateName: 'Virgin Islands, U.S.', stateCode: 'VI' },
76             { stateName: 'Marshall Islands', stateCode: 'MH' },
77             { stateName: 'Northern Mariana Islands', stateCode: 'MP' },
78             { stateName: 'Palau', stateCode: 'PW' },
79         ];
80     }
82     if (countryCode === 'CA') {
83         return [
84             { stateName: 'Alberta', stateCode: 'AB' },
85             { stateName: 'British Columbia', stateCode: 'BC' },
86             { stateName: 'Manitoba', stateCode: 'MB' },
87             { stateName: 'New Brunswick', stateCode: 'NB' },
88             { stateName: 'Newfoundland and Labrador', stateCode: 'NL' },
89             { stateName: 'Northwest Territories', stateCode: 'NT' },
90             { stateName: 'Nova Scotia', stateCode: 'NS' },
91             { stateName: 'Nunavut', stateCode: 'NU' },
92             { stateName: 'Ontario', stateCode: 'ON' },
93             { stateName: 'Prince Edward Island', stateCode: 'PE' },
94             { stateName: 'Quebec', stateCode: 'QC' },
95             { stateName: 'Saskatchewan', stateCode: 'SK' },
96             { stateName: 'Yukon', stateCode: 'YT' },
97         ];
98     }
100     return [];
103 function getStateName(countryCode: string, stateCode: string) {
104     const state = getStateList(countryCode).find(({ stateCode: code }) => code === stateCode);
105     return state?.stateName ?? '';
108 export type OnBillingAddressChange = (billingAddress: BillingAddress) => void;
110 interface HookProps {
111     onBillingAddressChange?: OnBillingAddressChange;
112     statusExtended?: Pick<PaymentMethodStatusExtended, 'CountryCode' | 'State'>;
115 interface HookResult {
116     selectedCountryCode: string;
117     setSelectedCountry: (countryCode: string) => void;
118     federalStateCode: string | null;
119     setFederalStateCode: (federalStateCode: string) => void;
122 export const useTaxCountry = (props: HookProps): HookResult => {
123     const billingAddress: BillingAddress = props.statusExtended
124         ? ({
125               CountryCode: props.statusExtended.CountryCode,
126               State: props.statusExtended.State,
127           } as BillingAddress)
128         : DEFAULT_TAX_BILLING_ADDRESS;
130     const [taxBillingAddress, setTaxBillingAddress] = useState<BillingAddress>(billingAddress);
132     useEffect(() => {
133         props.onBillingAddressChange?.(taxBillingAddress);
134     }, [taxBillingAddress]);
136     const selectedCountryCode = taxBillingAddress.CountryCode;
137     const federalStateCode = taxBillingAddress.State ?? null;
139     const setSelectedCountry = (CountryCode: string) => {
140         const State = countriesWithStates.includes(CountryCode) ? getStateList(CountryCode)[0].stateCode : null;
142         setTaxBillingAddress({
143             CountryCode,
144             State,
145         });
146     };
148     const setFederalStateCode = (federalStateCode: string) => {
149         setTaxBillingAddress((prev) => ({
150             ...prev,
151             State: federalStateCode,
152         }));
153     };
155     return {
156         selectedCountryCode,
157         setSelectedCountry,
158         federalStateCode,
159         setFederalStateCode,
160     };
163 export type TaxCountrySelectorProps = HookResult & {
164     className?: string;
167 type StateSelectorProps = {
168     onStateChange: (stateCode: string) => void;
169     federalStateCode: string | null;
170     selectedCountryCode: string;
173 const StateSelector = ({ onStateChange, federalStateCode, selectedCountryCode }: StateSelectorProps) => {
174     const states = useMemo(() => getStateList(selectedCountryCode), [selectedCountryCode]);
176     const props: SearcheableSelectProps<string> = {
177         onChange: ({ value: stateCode }: SelectChangeEvent<string>) => onStateChange?.(stateCode),
178         value: federalStateCode ?? '',
179         id: 'tax-state',
180         className: 'mt-1',
181         placeholder: c('Placeholder').t`Select state`,
182         children: states.map(({ stateName, stateCode }) => {
183             return (
184                 <Option key={stateCode} value={stateCode} title={stateName} data-testid={`state-${stateCode}`}>
185                     {stateName}
186                 </Option>
187             );
188         }),
189     };
191     return <SearchableSelect {...props} data-testid="tax-state-dropdown" />;
194 const TaxCountrySelector = ({
195     selectedCountryCode,
196     setSelectedCountry,
197     setFederalStateCode,
198     federalStateCode,
199     className,
200 }: TaxCountrySelectorProps) => {
201     const showStateCode = countriesWithStates.includes(selectedCountryCode);
203     // If there is no state selection, then we collapse the component by default
204     // if there is state selection and state **is** specified, then we **collapse** the component by default
205     // If there is state selection and state **is not** specified, then we **expand** the component by default
206     const initialCollapsedState: boolean = !showStateCode || (showStateCode && !!federalStateCode);
208     const [collapsed, setCollapsed] = useState(initialCollapsedState);
209     const { getCountryByCode } = useCountries();
210     const selectedCountry = getCountryByCode(selectedCountryCode);
211     const [isDropdownOpen, setIsDropdownOpen] = useState(false);
213     const { valid: billingAddressValid, reason: billingAddressInvalidReason } = getBillingAddressStatus({
214         CountryCode: selectedCountryCode,
215         State: federalStateCode,
216     });
218     const [showTooltip, setShowTooltip] = useState(false);
219     useEffect(() => {
220         let timeout: any;
221         if (!billingAddressValid) {
222             timeout = setTimeout(() => {
223                 setShowTooltip(true);
224             }, 2000);
225         } else {
226             setShowTooltip(false);
227         }
229         return () => {
230             clearTimeout(timeout);
231         };
232     }, [billingAddressValid]);
234     const collapsedText = (() => {
235         if (selectedCountry?.label) {
236             let text = selectedCountry.label;
237             if (federalStateCode && showStateCode) {
238                 text += `, ${getStateName(selectedCountryCode, federalStateCode)}`;
239             }
241             return text;
242         }
244         return c('Action').t`Select country`;
245     })();
247     const tooltipText = (() => {
248         if (billingAddressInvalidReason === 'missingCountry') {
249             return c('Payments').t`Please select billing country`;
250         }
252         if (billingAddressInvalidReason === 'missingState') {
253             // translator: "state" as in "United States of America"
254             return c('Payments').t`Please select billing state`;
255         }
257         return null;
258     })();
260     return (
261         <div className={clsx('field-two-container', className)}>
262             <div className="pt-1 mb-1" data-testid="billing-country">
263                 <Tooltip title={tooltipText} isOpen={showTooltip && !!tooltipText}>
264                     <span className="text-bold">{c('Payments').t`Billing Country`}</span>
265                 </Tooltip>
266                 {collapsed && (
267                     <>
268                         <span className="text-bold mr-2">:</span>
269                         <InlineLinkButton
270                             onClick={() => {
271                                 setCollapsed(false);
272                                 setIsDropdownOpen(true);
273                             }}
274                             data-testid="billing-country-collapsed"
275                         >
276                             {collapsedText}
277                         </InlineLinkButton>
278                     </>
279                 )}
280             </div>
281             {!collapsed && (
282                 <>
283                     <CountriesDropdown
284                         selectedCountryCode={selectedCountryCode}
285                         onChange={setSelectedCountry}
286                         id="tax-country"
287                         isOpen={isDropdownOpen}
288                         onOpen={() => setIsDropdownOpen(true)}
289                         onClose={() => setIsDropdownOpen(false)}
290                         data-testid="tax-country-dropdown"
291                     />
292                     {showStateCode && (
293                         <StateSelector
294                             onStateChange={setFederalStateCode}
295                             federalStateCode={federalStateCode}
296                             selectedCountryCode={selectedCountryCode}
297                         />
298                     )}
299                 </>
300             )}
301         </div>
302     );
305 export const WrappedTaxCountrySelector = ({
306     className,
307     onBillingAddressChange,
308     statusExtended,
309 }: {
310     className?: string;
311     onBillingAddressChange?: OnBillingAddressChange;
312     statusExtended?: Pick<PaymentMethodStatusExtended, 'CountryCode' | 'State'>;
313 }) => {
314     const taxCountryHook = useTaxCountry({
315         onBillingAddressChange,
316         statusExtended,
317     });
319     return <TaxCountrySelector {...taxCountryHook} className={className} />;
322 export default TaxCountrySelector;