Remove payments API routing initialization
[ProtonMail-WebClient.git] / packages / components / containers / vpn / gateways / GatewayCountrySelection.tsx
blobb97440ef92f6eed68ffcb13f719bf96f19de79e5
1 import type { ChangeEvent } from 'react';
2 import { useRef } from 'react';
4 import { c, msgid } from 'ttag';
6 import { Checkbox, Info } from '@proton/components';
7 import Field from '@proton/components/components/container/Field';
8 import Row from '@proton/components/components/container/Row';
9 import Icon from '@proton/components/components/icon/Icon';
10 import Label from '@proton/components/components/label/Label';
11 import Option from '@proton/components/components/option/Option';
12 import SelectTwo from '@proton/components/components/selectTwo/SelectTwo';
13 import type { SelectChangeEvent } from '@proton/components/components/selectTwo/select';
14 import { useNow } from '@proton/components/hooks/useNow';
15 import { SECOND } from '@proton/shared/lib/constants';
17 import { type CountryOptions, getLocalizedCountryByAbbr } from '../../../helpers/countries';
18 import { ButtonNumberInput } from './ButtonNumberInput';
19 import { CountryFlagAndName } from './CountryFlagAndName';
20 import type { DeletedDedicatedIp } from './DeletedDedicatedIp';
21 import type { GatewayDto } from './GatewayDto';
22 import type { GatewayLocation } from './GatewayLocation';
23 import { getLocationDisplayName, getLocationFromId, getLocationId } from './helpers';
25 interface Props {
26     singleServer: boolean;
27     locations: readonly GatewayLocation[];
28     ownedCount: number;
29     usedCount: number;
30     addedCount: number;
31     deletedDedicatedIPs?: DeletedDedicatedIp[];
32     countryOptions: CountryOptions;
33     loading?: boolean;
34     model: GatewayDto;
35     onUpdateCheckedLocations: (checkedLocations: GatewayLocation[]) => void;
36     changeModel: (diff: Partial<GatewayDto>) => void;
39 export const GatewayCountrySelection = ({
40     singleServer,
41     locations,
42     ownedCount,
43     usedCount,
44     addedCount,
45     deletedDedicatedIPs,
46     countryOptions,
47     loading = false,
48     model,
49     onUpdateCheckedLocations,
50     changeModel,
51 }: Props) => {
52     const recentlyUsedServersRef = useRef<GatewayLocation[]>([]);
53     const remainingCount = ownedCount - usedCount;
54     const availableCount = Math.max(0, remainingCount - addedCount - (deletedDedicatedIPs?.length || 0));
55     const unassignedAvailableCount = deletedDedicatedIPs?.length || 0;
56     const totalCountExceeded = addedCount >= remainingCount - (deletedDedicatedIPs?.length || 0);
57     const now = useNow(10 * SECOND);
59     const handleUnassigningLocationChange = ({ value }: SelectChangeEvent<string>) =>
60         changeModel({ location: getLocationFromId(value) });
62     const handleUnassigningLocationChecked = (event: ChangeEvent<HTMLInputElement>, location: GatewayLocation) => {
63         const unassignedIpQuantities = {
64             ...model.unassignedIpQuantities,
65         };
67         const locationId = getLocationId(location);
69         if (!unassignedIpQuantities[locationId]) {
70             unassignedIpQuantities[locationId] = 0;
71         }
73         unassignedIpQuantities[locationId] += event.target.checked ? 1 : -1;
75         const mainLocation = getLocationFromId(
76             Object.keys(unassignedIpQuantities).reduce((previous, locationId) =>
77                 (unassignedIpQuantities[locationId] || 0) > (unassignedIpQuantities[previous] || 0)
78                     ? locationId
79                     : previous
80             )
81         );
83         if (mainLocation !== model.location) {
84             changeModel({ location: mainLocation });
85         }
87         const locationString = JSON.stringify(location);
88         if (event.target.checked) {
89             // Add/store location if it's not already present
90             if (!recentlyUsedServersRef.current.some((ip: GatewayLocation) => JSON.stringify(ip) === locationString)) {
91                 recentlyUsedServersRef.current = [...recentlyUsedServersRef.current, location];
92             }
93         } else {
94             // Remove location if unchecked
95             recentlyUsedServersRef.current = recentlyUsedServersRef.current.filter(
96                 (ip: GatewayLocation) => JSON.stringify(ip) !== locationString
97             );
98         }
100         onUpdateCheckedLocations(recentlyUsedServersRef.current);
102         return changeModel({ unassignedIpQuantities });
103     };
105     const handleQuantityChange = (newQuantity: number, locationId: string) => {
106         const quantities = {
107             ...model.quantities,
108             [locationId]: newQuantity,
109         };
111         const mainLocation = getLocationFromId(
112             Object.keys(quantities).reduce((previous, locationId) =>
113                 (quantities[locationId] || 0) > (quantities[previous] || 0) ? locationId : previous
114             )
115         );
117         if (mainLocation !== model.location) {
118             changeModel({ location: mainLocation });
119         }
121         if (newQuantity === 0) {
122             delete quantities[locationId];
123         }
125         changeModel({ quantities });
126     };
128     // Helper function to check if a location from recently used servers is already checked
129     const isLocationChecked = (location: GatewayLocation) => {
130         return model?.checkedLocations?.some((ip) => JSON.stringify(ip) === JSON.stringify(location));
131     };
133     return singleServer ? (
134         <Row>
135             <Label htmlFor="domain">{c('Label').t`Country`}</Label>
136             <Field>
137                 <SelectTwo value={getLocationId(model.location)} onChange={handleUnassigningLocationChange}>
138                     {locations.map((location) => {
139                         const country = getLocalizedCountryByAbbr(location.Country, countryOptions) || location.Country;
140                         const title = getLocationDisplayName(location, countryOptions);
141                         return (
142                             <Option key={getLocationId(location)} value={title} title={title}>
143                                 <CountryFlagAndName countryCode={location.Country} countryName={country} />
144                             </Option>
145                         );
146                     })}
147                 </SelectTwo>
148             </Field>
149         </Row>
150     ) : (
151         <>
152             {unassignedAvailableCount !== 0 && (
153                 <div>
154                     <h4 className="text-bold mb-1" style={{ marginTop: 0 }}>
155                         {c('Info').t`Select recently used servers`}{' '}
156                         <Info
157                             className="ml-1"
158                             title={
159                                 <>
160                                     <b>{c('Info').t`Recently used servers:`}</b>{' '}
161                                     {c('Info')
162                                         .t`When you remove a server from a Gateway, it enters a 10-day deactivation period. This server can be added to a new Gateway, but its country cannot be changed.`}
163                                 </>
164                             }
165                         />
166                     </h4>
167                     <p className="mb-5 color-weak" style={{ marginTop: 0 }}>
168                         {c('Info').ngettext(
169                             msgid`You have ${unassignedAvailableCount} recently used servers available. To allocate these servers to another country, you'll have to wait until deactivation is complete.`,
170                             `You have ${unassignedAvailableCount} recently used servers available. To allocate these servers to another country, you'll have to wait until deactivation is complete.`,
171                             unassignedAvailableCount
172                         )}
173                     </p>
174                     {deletedDedicatedIPs?.map((deletedDedicatedIp) => {
175                         const availableAgainAfterSeconds =
176                             deletedDedicatedIp.AvailableAgainAfter - now.getTime() / 1000;
177                         const availableAgainAfterHours = Math.ceil(availableAgainAfterSeconds / 3600);
178                         const availableAgainAfterDays = Math.ceil(availableAgainAfterSeconds / 3600 / 24);
179                         return (
180                             <div>
181                                 <Label>
182                                     <Checkbox
183                                         checked={isLocationChecked(deletedDedicatedIp.Location)}
184                                         onChange={(e) =>
185                                             handleUnassigningLocationChecked(e, deletedDedicatedIp.Location)
186                                         }
187                                     />{' '}
188                                     <CountryFlagAndName
189                                         countryCode={deletedDedicatedIp.Location.Country}
190                                         countryName={getLocationDisplayName(
191                                             deletedDedicatedIp.Location,
192                                             countryOptions
193                                         )}
194                                     />
195                                     <p
196                                         className="color-weak"
197                                         style={{ marginLeft: '60px', marginTop: 0, marginBottom: 0, fontSize: '0.9em' }}
198                                     >
199                                         {availableAgainAfterDays > 1
200                                             ? c('Info').ngettext(
201                                                   msgid`or assign to any country in ${availableAgainAfterDays} day`,
202                                                   `or assign to any country in ${availableAgainAfterDays} days`,
203                                                   availableAgainAfterDays
204                                               )
205                                             : c('Info').ngettext(
206                                                   msgid`or assign to any country in ${availableAgainAfterHours} hour`,
207                                                   `or assign to any country in ${availableAgainAfterHours} hours`,
208                                                   availableAgainAfterHours
209                                               )}
210                                     </p>
211                                 </Label>
212                             </div>
213                         );
214                     })}
215                 </div>
216             )}
217             <div>
218                 <h4 className="text-bold mb-1">Add new servers</h4>
219                 <p
220                     className="mb-5 color-weak"
221                     style={{ marginTop: 0, color: totalCountExceeded ? 'var(--signal-danger)' : '' }}
222                 >
223                     {c('Info').ngettext(
224                         msgid`You have ${availableCount} new server available.`,
225                         `You have ${availableCount} new servers available.`,
226                         availableCount
227                     )}
228                 </p>
229                 <p></p>
230                 {locations.map((location) => {
231                     const locationId = getLocationId(location);
232                     return (
233                         <div key={locationId} className="flex *:min-size-auto md:flex-nowrap items-center mb-1">
234                             <ButtonNumberInput
235                                 id={locationId}
236                                 value={model.quantities?.[locationId]}
237                                 min={0}
238                                 max={99}
239                                 disabled={
240                                     loading ||
241                                     (totalCountExceeded &&
242                                         model.quantities !== undefined &&
243                                         !(locationId in model.quantities))
244                                 }
245                                 onChange={(newQuantity: number) => handleQuantityChange(newQuantity, locationId)}
246                                 step={1}
247                                 location={location}
248                                 countryOptions={countryOptions}
249                                 ownedCount={ownedCount}
250                                 usedCount={usedCount}
251                             />
252                         </div>
253                     );
254                 })}
255             </div>
256             <div className="flex flex-nowrap mb-4 rounded p-2 bg-weak">
257                 <Icon name="info-circle" className="shrink-0" />
258                 <div className="ml-2">
259                     {c('Info').t`We recommend having multiple servers in different locations to provide redundancy.`}
260                 </div>
261             </div>
262         </>
263     );
266 export default GatewayCountrySelection;