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') {
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' },
82 if (countryCode === 'CA') {
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' },
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
125 CountryCode: props.statusExtended.CountryCode,
126 State: props.statusExtended.State,
128 : DEFAULT_TAX_BILLING_ADDRESS;
130 const [taxBillingAddress, setTaxBillingAddress] = useState<BillingAddress>(billingAddress);
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({
148 const setFederalStateCode = (federalStateCode: string) => {
149 setTaxBillingAddress((prev) => ({
151 State: federalStateCode,
163 export type TaxCountrySelectorProps = HookResult & {
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 ?? '',
181 placeholder: c('Placeholder').t`Select state`,
182 children: states.map(({ stateName, stateCode }) => {
184 <Option key={stateCode} value={stateCode} title={stateName} data-testid={`state-${stateCode}`}>
191 return <SearchableSelect {...props} data-testid="tax-state-dropdown" />;
194 const TaxCountrySelector = ({
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,
218 const [showTooltip, setShowTooltip] = useState(false);
221 if (!billingAddressValid) {
222 timeout = setTimeout(() => {
223 setShowTooltip(true);
226 setShowTooltip(false);
230 clearTimeout(timeout);
232 }, [billingAddressValid]);
234 const collapsedText = (() => {
235 if (selectedCountry?.label) {
236 let text = selectedCountry.label;
237 if (federalStateCode && showStateCode) {
238 text += `, ${getStateName(selectedCountryCode, federalStateCode)}`;
244 return c('Action').t`Select country`;
247 const tooltipText = (() => {
248 if (billingAddressInvalidReason === 'missingCountry') {
249 return c('Payments').t`Please select billing country`;
252 if (billingAddressInvalidReason === 'missingState') {
253 // translator: "state" as in "United States of America"
254 return c('Payments').t`Please select billing state`;
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>
268 <span className="text-bold mr-2">:</span>
272 setIsDropdownOpen(true);
274 data-testid="billing-country-collapsed"
284 selectedCountryCode={selectedCountryCode}
285 onChange={setSelectedCountry}
287 isOpen={isDropdownOpen}
288 onOpen={() => setIsDropdownOpen(true)}
289 onClose={() => setIsDropdownOpen(false)}
290 data-testid="tax-country-dropdown"
294 onStateChange={setFederalStateCode}
295 federalStateCode={federalStateCode}
296 selectedCountryCode={selectedCountryCode}
305 export const WrappedTaxCountrySelector = ({
307 onBillingAddressChange,
311 onBillingAddressChange?: OnBillingAddressChange;
312 statusExtended?: Pick<PaymentMethodStatusExtended, 'CountryCode' | 'State'>;
314 const taxCountryHook = useTaxCountry({
315 onBillingAddressChange,
319 return <TaxCountrySelector {...taxCountryHook} className={className} />;
322 export default TaxCountrySelector;