Remove option component
[ProtonMail-WebClient.git] / packages / components / containers / vpn / WireGuardConfigurationSection / WireGuardConfigurationSection.tsx
blob0cd4b40a3d8b3789809563ba228c312f5ecb1956
1 import type { ChangeEvent, FormEvent } from 'react';
2 import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3 import { flushSync } from 'react-dom';
5 import { Point, utils } from '@noble/ed25519';
6 import { c } from 'ttag';
8 import { Button, CircleLoader, Href } from '@proton/atoms';
9 import Alert from '@proton/components/components/alert/Alert';
10 import Row from '@proton/components/components/container/Row';
11 import Icon from '@proton/components/components/icon/Icon';
12 import Radio from '@proton/components/components/input/Radio';
13 import TextArea from '@proton/components/components/input/TextArea';
14 import Info from '@proton/components/components/link/Info';
15 import ConfirmModal from '@proton/components/components/modal/Confirm';
16 import Option from '@proton/components/components/option/Option';
17 import Toggle from '@proton/components/components/toggle/Toggle';
18 import downloadFile from '@proton/shared/lib/helpers/downloadFile';
19 import { base64StringToUint8Array, uint8ArrayToBase64String } from '@proton/shared/lib/helpers/encoding';
20 import { readableTime } from '@proton/shared/lib/helpers/time';
21 import type { Logical } from '@proton/shared/lib/vpn/Logical';
23 import { ErrorButton, InputFieldTwo, SelectTwo, useModalTwoStatic } from '../../../components';
24 import Details from '../../../components/container/Details';
25 import Summary from '../../../components/container/Summary';
26 import { getObjectKeys } from '../../../helpers';
27 import { getCountryOptions, getLocalizedCountryByAbbr } from '../../../helpers/countries';
28 import {
29     useApi,
30     useApiResult,
31     useModals,
32     useNotifications,
33     useUser,
34     useUserSettings,
35     useUserVPN,
36     useVPNLogicals,
37 } from '../../../hooks';
38 import { SettingsParagraph, SettingsSectionWide } from '../../account';
39 import type { Certificate } from '../Certificate';
40 import { CATEGORY } from '../OpenVPNConfigurationSection/ConfigsTable';
41 import OpenVPNConfigurationSection from '../OpenVPNConfigurationSection/OpenVPNConfigurationSection';
42 import { getFlagSvg } from '../flag';
43 import type { CertificateDTO, CertificateDeletionParams, CertificateGenerationParams } from './Certificate';
44 import type { KeyPair } from './KeyPair';
45 import WireGuardCreationModal from './WireGuardCreationModal';
46 import { deleteCertificates, generateCertificate, getKey, queryVPNClientConfig } from './api';
47 import { CURVE } from './curve';
48 import type { FeatureFlagsConfig, FeatureOption, FeatureSelection, FeaturesConfig, FeaturesValues } from './feature';
49 import {
50     clientConfigKeys,
51     formatFeatureShortName,
52     formatFeatureValue,
53     getKeyOfCheck,
54     initialFeaturesConfig,
55     isFeatureSelection,
56 } from './feature';
57 import { normalize } from './normalize';
58 import useCertificates from './useCertificates';
60 enum PLATFORM {
61     MACOS = 'macOS',
62     LINUX = 'Linux',
63     WINDOWS = 'Windows',
64     ANDROID = 'Android',
65     IOS = 'iOS',
66     ROUTER = 'Router',
69 interface Peer {
70     name: string;
71     publicKey: string;
72     ip: string;
73     label: string;
76 interface ExtraCertificateFeatures {
77     peerName: Peer['name'];
78     peerPublicKey: Peer['publicKey'];
79     peerIp: Peer['ip'];
80     label: Peer['label'];
81     platform: PLATFORM;
84 const isExtraFeatureKey = getKeyOfCheck<ExtraCertificateFeatures>(['peerIp', 'peerName', 'peerPublicKey', 'platform']);
86 // From https://github.com/paulmillr/noble-ed25519/blob/d87d6e953304c9d4dbfb275e8e67a0c975d3262b/index.js
87 const bytesToNumberLE = (uint8a: Uint8Array) => {
88     let value = BigInt(0);
90     for (let i = 0; i < uint8a.length; i++) {
91         value += BigInt(uint8a[i]) << (BigInt(8) * BigInt(i));
92     }
94     return value;
97 const unarmor = (key: string): string => `\n${key}\n`.replace(/\n---.+\n/g, '').replace(/\s/g, '');
99 const randomPrivateKey = () => {
100     if (!CURVE) {
101         throw new Error('BigInt not supported');
102     }
104     let i = 1024;
106     while (i--) {
107         const b32 = crypto.getRandomValues(new Uint8Array(32));
108         const num = bytesToNumberLE(b32);
110         if (num > BigInt(1) && num < CURVE.n) {
111             return b32;
112         }
113     }
115     throw new Error('Valid private key was not found in 1024 iterations. PRNG is broken');
118 const getPublicKey = async (privateKey: Uint8Array): Promise<Uint8Array> => {
119     const key = await Point.fromPrivateKey(privateKey);
121     return key.toRawBytes();
124 const getFeatureLink = (feature: FeatureOption<any>) =>
125     feature.url ? (
126         <>
127             {' '}
128             &nbsp; <Href className="text-no-bold" href={feature.url}>{c('Info').t`Learn more`}</Href>
129         </>
130     ) : (
131         ''
132     );
134 const getConfigTemplate = (
135     interfacePrivateKey: string,
136     name: string | undefined,
137     features: Partial<FeaturesValues & ExtraCertificateFeatures> | undefined,
138     peer: Peer
139 ) => `[Interface]${name ? `\n# Key for ${name}` : ''}${getObjectKeys(features)
140     .map((key) =>
141         isExtraFeatureKey(key) ? '' : `\n# ${formatFeatureShortName(key)} = ${formatFeatureValue(features, key)}`
142     )
143     .join('')}
144 PrivateKey = ${interfacePrivateKey}
145 Address = 10.2.0.2/32
146 DNS = 10.2.0.1
148 [Peer]
149 # ${features?.peerName || peer.name}
150 PublicKey = ${features?.peerPublicKey || peer.publicKey}
151 AllowedIPs = ${features?.platform === PLATFORM.WINDOWS ? '0.0.0.0/1, 128.0.0.0/1' : '0.0.0.0/0'}
152 Endpoint = ${features?.peerIp || peer.ip}:51820`;
154 const privateKeyPlaceholder = '*****';
156 const getCertificateModel = (
157     certificateDto: CertificateDTO & { id?: string },
158     peer: Peer,
159     privateKey?: string,
160     id?: string
161 ): Certificate => {
162     if (!id && !certificateDto.id) {
163         certificateDto.id = `c${Date.now()}-${Math.random()}`;
164     }
166     const name = certificateDto?.DeviceName;
167     const features = certificateDto?.Features;
169     return {
170         id: `${id || certificateDto.id}`,
171         name,
172         features,
173         serialNumber: certificateDto.SerialNumber,
174         privateKey: privateKey || privateKeyPlaceholder,
175         publicKey: certificateDto.ClientKey,
176         publicKeyFingerprint: certificateDto.ClientKeyFingerprint,
177         expirationTime: certificateDto.ExpirationTime,
178         config: getConfigTemplate(privateKey || privateKeyPlaceholder, name, features, peer),
179     };
182 const paginationSize = 50;
184 const formatServerName = (bestServerName: string, alt: (code: string) => string | undefined) => {
185     const countryCode = bestServerName.split(/#/g)[0];
186     const flag = getFlagSvg(countryCode);
188     return (
189         <>
190             {flag && <img width={20} className="mx-2 border" src={flag} alt={alt(countryCode)} />}
191             <strong className="align-middle">{bestServerName}</strong>
192         </>
193     );
196 const getX25519PrivateKey = async (privateKey: string): Promise<string> => {
197     const sha512 = (await utils.sha512(base64StringToUint8Array(privateKey))).slice(0, 32);
198     sha512[0] &= 0xf8;
199     sha512[31] &= 0x7f;
200     sha512[31] |= 0x40;
201     return uint8ArrayToBase64String(sha512);
204 const WireGuardConfigurationSection = () => {
205     const [platform, setPlatform] = useState(PLATFORM.ANDROID);
206     const [featuresConfig, setFeaturesConfig] = useState<FeaturesConfig>(initialFeaturesConfig);
207     const api = useApi();
208     const [peer, setPeer] = useState<Peer>({
209         name: '',
210         publicKey: '',
211         ip: '',
212         label: '',
213     });
214     const certificateCacheRef = useRef<Record<string, Certificate>>({});
215     const certificateCache = certificateCacheRef.current;
216     const [logical, setLogical] = useState<Logical | undefined>();
217     const [creationModal, handleShowCreationModal] = useModalTwoStatic(WireGuardCreationModal);
218     const [creating, setCreating] = useState<boolean>(false);
219     const [removing, setRemoving] = useState<Record<string, boolean>>({});
220     const [removedCertificates, setRemovedCertificates] = useState<string[]>([]);
221     const [currentCertificate, setCurrentCertificate] = useState<string | undefined>();
222     const [certificates, setCertificates] = useState<Certificate[]>([]);
223     const [{ hasPaidVpn }] = useUser();
224     const [userSettings] = useUserSettings();
225     const { result, loading: vpnLoading, fetch: fetchUserVPN } = useUserVPN();
226     const userVPN = result?.VPN;
227     const nameInputRef = useRef<HTMLInputElement>(null);
228     const { createModal } = useModals();
229     const { createNotification } = useNotifications();
230     const { result: clientConfig = { FeatureFlags: {} as FeatureFlagsConfig } } = useApiResult<
231         { FeatureFlags: FeatureFlagsConfig },
232         typeof queryVPNClientConfig
233     >(queryVPNClientConfig, []);
234     const {
235         loading: logicalsLoading,
236         result: logicalsResult = { LogicalServers: [] },
237         fetch: fetchLogicals,
238     } = useVPNLogicals();
239     const [limit, setLimit] = useState(paginationSize);
240     const { loading: certificatesLoading, result: certificatesResult, moreToLoad } = useCertificates(limit);
242     const countryOptions = getCountryOptions(userSettings);
244     const logicalInfoLoading = logicalsLoading || vpnLoading;
245     const maxTier = userVPN?.MaxTier || 0;
246     const logicals = useMemo(
247         () =>
248             ((!logicalInfoLoading && logicalsResult.LogicalServers) || [])
249                 .map((logical) => ({
250                     ...logical,
251                     Servers: (logical.Servers || []).filter((server) => server.X25519PublicKey),
252                 }))
253                 .filter((logical) => logical.Servers.length),
254         [logicalInfoLoading, logicalsResult.LogicalServers]
255     );
256     const bestLogicals = logicals
257         .filter((server) => server.Tier <= maxTier && (server.Features & 3) === 0)
258         .sort((a, b) => a.Score - b.Score);
259     const bestLogical = bestLogicals[0];
260     const bestServerName = bestLogical?.Name;
261     const formattedBestServerName = bestServerName
262         ? formatServerName(bestServerName, (code) => getLocalizedCountryByAbbr(code, countryOptions))
263         : '';
265     const getCertificates = (): Certificate[] => {
266         certificatesResult.forEach((certificateDto) => {
267             if (
268                 removedCertificates.indexOf(certificateDto.ClientKeyFingerprint) !== -1 ||
269                 certificateDto.ExpirationTime <
270                     (certificateCache[certificateDto.ClientKeyFingerprint]?.expirationTime || 0)
271             ) {
272                 return;
273             }
275             certificateCache[certificateDto.ClientKeyFingerprint] = getCertificateModel(certificateDto, peer);
276         });
278         certificates.forEach((certificate) => {
279             if (
280                 removedCertificates.indexOf(certificate.publicKeyFingerprint) !== -1 ||
281                 certificate.expirationTime < (certificateCache[certificate.publicKeyFingerprint]?.expirationTime || 0)
282             ) {
283                 return;
284             }
286             certificateCache[certificate.publicKeyFingerprint] = certificate;
287         });
289         return Object.values(certificateCache);
290     };
292     const setFeature = <K extends keyof FeaturesConfig>(key: K, value: FeaturesConfig[K]['value']) => {
293         setFeaturesConfig({
294             ...featuresConfig,
295             [key]: {
296                 ...featuresConfig[key],
297                 value,
298             },
299         });
300     };
302     const getKeyPair = async (): Promise<{ privateKey: string; publicKey: string }> => {
303         try {
304             const privateKey = randomPrivateKey();
306             return {
307                 privateKey: uint8ArrayToBase64String(privateKey),
308                 publicKey: uint8ArrayToBase64String(await getPublicKey(privateKey)),
309             };
310         } catch (e) {
311             console.warn(e);
312             console.info(
313                 'Fallback to server-side generated key. Upgrade to a modern browser, to generate right from your device.'
314             );
316             const { PrivateKey, PublicKey } = await api<KeyPair>(getKey());
318             return { privateKey: unarmor(PrivateKey), publicKey: unarmor(PublicKey) };
319         }
320     };
322     const getToggleCallback = (certificate: Certificate) => (event: ChangeEvent<HTMLDetailsElement>) => {
323         if (!event?.target?.hasAttribute('open')) {
324             return;
325         }
327         if (certificate.id === currentCertificate) {
328             return;
329         }
331         setCurrentCertificate(certificate.id);
332     };
334     const queryCertificate = (
335         publicKey: string,
336         deviceName?: string | null | undefined,
337         features?: Record<string, string | number | boolean | null> | undefined,
338         options: Partial<CertificateGenerationParams> = {}
339     ): Promise<CertificateDTO> =>
340         api<CertificateDTO>(
341             generateCertificate({
342                 ClientPublicKey: publicKey,
343                 Mode: 'persistent',
344                 DeviceName: deviceName,
345                 Features: features,
346                 ...options,
347             })
348         );
350     const getFeatureKeys = () =>
351         getObjectKeys(featuresConfig).filter(
352             (key) => maxTier >= (featuresConfig[key].tier || 0) && clientConfig?.FeatureFlags?.[clientConfigKeys[key]]
353         );
354     const getFeatureValues = useCallback(
355         (addedPeer?: Peer) => {
356             const peerFeatures = addedPeer || peer;
357             const label = peerFeatures?.label;
359             return Object.assign(
360                 label ? { Bouncing: label } : {},
361                 Object.fromEntries(
362                     getFeatureKeys().map((key) => [
363                         key,
364                         ((featuresConfig[key].transform || ((v: any) => v)) as <T>(value: T) => T)(
365                             featuresConfig[key].value
366                         ),
367                     ])
368                 ),
369                 peerFeatures
370                     ? {
371                           peerName: peerFeatures.name,
372                           peerIp: peerFeatures.ip,
373                           peerPublicKey: peerFeatures.publicKey,
374                           platform,
375                       }
376                     : {}
377             );
378         },
379         [featuresConfig, peer, platform]
380     );
382     const getDownloadCallback = (certificate: Certificate) => () => {
383         if (creating) {
384             return;
385         }
387         const serverName = `${certificate?.features?.peerName || peer.name}`.substring(0, 20);
389         downloadFile(
390             new Blob([certificate.config || '']),
391             normalize((certificate.name || 'wg') + '-' + serverName) + '.conf'
392         );
393     };
395     const add = async (addedPeer?: Peer, silent = false) => {
396         if (creating) {
397             return;
398         }
400         setCreating(true);
402         try {
403             const serverName = addedPeer?.name;
405             if (!silent && serverName) {
406                 handleShowCreationModal({
407                     open: true,
408                     serverName: serverName,
409                     text: undefined,
410                     config: undefined,
411                     onClose() {
412                         silent = true;
413                         handleShowCreationModal({ open: false });
414                     },
415                 });
416             }
418             try {
419                 const { privateKey, publicKey } = await getKeyPair();
420                 const x25519PrivateKey = await getX25519PrivateKey(privateKey);
421                 const deviceName = nameInputRef?.current?.value || '';
422                 const certificate = await queryCertificate(publicKey, deviceName, getFeatureValues(addedPeer));
424                 if (!certificate.DeviceName) {
425                     certificate.DeviceName = deviceName;
426                 }
428                 const newCertificate = getCertificateModel(certificate, peer, x25519PrivateKey);
429                 const id = newCertificate.id;
430                 let name = newCertificate.name || newCertificate.publicKeyFingerprint || newCertificate.publicKey;
432                 if (name.length > 46) {
433                     name = name.substring(0, 21) + '…' + name.substring(name.length - 21);
434                 }
436                 if (!silent) {
437                     const downloadCallback = getDownloadCallback(newCertificate);
439                     handleShowCreationModal({
440                         open: true,
441                         serverName: serverName,
442                         // translator: name a name given by the user to a config file
443                         text: c('Success notification')
444                             .t`Config "${name}" created, note that the private key is not stored and won't be shown again, you should copy or download this config.`,
445                         config: newCertificate?.config || '',
446                         onDownload() {
447                             downloadCallback();
448                             handleShowCreationModal({ open: false });
449                         },
450                         onClose() {
451                             handleShowCreationModal({ open: false });
452                         },
453                     });
454                 }
456                 flushSync(() => {
457                     setCurrentCertificate(id);
458                     setCertificates([...(certificates || []), newCertificate]);
459                 });
461                 document.querySelector(`[data-certificate-id="${id}"]`)?.scrollIntoView();
463                 if (nameInputRef?.current) {
464                     nameInputRef.current.value = '';
465                 }
466             } catch (e) {
467                 if (!silent && serverName) {
468                     handleShowCreationModal({ open: false });
469                 }
471                 throw e;
472             }
473         } finally {
474             setCreating(false);
475         }
476     };
478     const selectLogical = useCallback(
479         async (logical: Logical, silent = false, doAdd = false) => {
480             const servers = logical?.Servers || [];
481             const numberOfServers = servers.length;
482             const server = servers[Math.floor(Math.random() * numberOfServers)];
483             const serverName = logical?.Name;
484             let addPromise: Promise<void> | undefined = undefined;
486             if (server) {
487                 const newPeer = {
488                     name: serverName,
489                     publicKey: `${server.X25519PublicKey}`,
490                     ip: server.EntryIP,
491                     label: server.Label || '',
492                 };
494                 if (doAdd) {
495                     addPromise = add(newPeer, silent);
496                 }
498                 if (peer.ip !== server.EntryIP) {
499                     setPeer(newPeer);
500                 }
501             }
503             setLogical({ ...logical });
505             if (addPromise) {
506                 await addPromise;
507             }
508         },
509         [peer, getFeatureValues, platform, creating]
510     );
512     if (!logicalInfoLoading && logicals.length && typeof logical === 'undefined') {
513         void selectLogical(bestLogical, true);
514     }
516     const createWithLogical = useCallback(
517         (logical: Logical, silent = false) => selectLogical(logical, silent, true),
518         [selectLogical]
519     );
521     const revokeCertificate = (name: string) => {
522         createNotification({
523             // translator: name is arbitrary name given by the user or a random key if not set
524             text: c('Success notification').t`Certificate ${name} revoked`,
525         });
526     };
528     const confirmRevocation = async (name: string) => {
529         return new Promise<void>((resolve, reject) => {
530             createModal(
531                 <ConfirmModal
532                     small={false}
533                     title={c('Title').t`Revoke certificate`}
534                     onConfirm={resolve}
535                     confirm={<ErrorButton type="submit">{c('Action').t`Delete`}</ErrorButton>}
536                     onClose={reject}
537                 >
538                     <Alert className="mb-4" type="info">
539                         {
540                             // translator: name is arbitrary name given by the user or a random key if not set
541                             c('Info').t`Revoke certificate ${name}`
542                         }
543                     </Alert>
544                     <Alert className="mb-4" type="error">
545                         {c('Alter').t`This will disconnect all the routers and clients using this certificate.`}
546                     </Alert>
547                 </ConfirmModal>
548             );
549         });
550     };
552     const getDeleteFilter = (certificate: Certificate): CertificateDeletionParams => {
553         if (certificate.serialNumber) {
554             return { SerialNumber: certificate.serialNumber };
555         }
557         if (certificate.publicKeyFingerprint) {
558             return { ClientPublicKeyFingerprint: certificate.publicKeyFingerprint };
559         }
561         return { ClientPublicKey: certificate.publicKey };
562     };
564     const askForRevocation = (certificate: Certificate) => async () => {
565         const key = certificate.publicKeyFingerprint || certificate.publicKey || certificate.id;
567         setRemoving({
568             ...removing,
569             [key]: true,
570         });
572         const end = () => {
573             const newValues = { ...removing };
574             delete newValues[key];
576             setRemoving(newValues);
577         };
579         try {
580             const name = certificate.name || certificate.publicKeyFingerprint || certificate.publicKey || '';
581             await confirmRevocation(name);
582             const { Count } = await api(deleteCertificates(getDeleteFilter(certificate)));
584             if (!Count) {
585                 createNotification({
586                     type: 'warning',
587                     // translator: name is arbitrary name given by the user or a random key if not set
588                     text: c('Error notification').t`Certificate ${name} not found or already revoked`,
589                 });
590                 end();
592                 return;
593             }
595             delete certificateCache[certificate.publicKeyFingerprint];
596             setRemovedCertificates([...removedCertificates, certificate.publicKeyFingerprint]);
597             setCertificates(certificates.filter((c) => c.id !== certificate.id));
598             revokeCertificate(name);
599         } catch (e) {
600             // Abort revocation
601         } finally {
602             end();
603         }
604     };
606     const handleFormSubmit = (e: FormEvent) => {
607         e.preventDefault();
609         return createWithLogical(bestLogical);
610     };
612     const getExtendCallback = (certificate: Certificate) => async () => {
613         if (creating) {
614             return;
615         }
617         setCreating(true);
619         try {
620             const renewedCertificate = await queryCertificate(
621                 certificate.publicKey,
622                 certificate.name,
623                 certificate.features || getFeatureValues(),
624                 { Renew: true }
625             );
627             if (!renewedCertificate.DeviceName) {
628                 renewedCertificate.DeviceName = nameInputRef?.current?.value || '';
629             }
631             const newCertificate = getCertificateModel(renewedCertificate, peer, certificate.privateKey);
632             setCertificates([...(certificates || []), newCertificate]);
633             setCurrentCertificate(newCertificate.id);
634             const formattedExpirationDate = readableTime(newCertificate.expirationTime, {
635                 format: 'PPp',
636             });
637             createNotification({
638                 // translator: formattedExpirationDate is a date+time such as "Jan 31, 2023, 7:57 PM" with format appropriately localized to match current locale
639                 text: c('Success notification').t`Certificate extended until ${formattedExpirationDate}`,
640             });
641         } finally {
642             setCreating(false);
643         }
644     };
646     useEffect(() => {
647         fetchUserVPN(30_000);
648         fetchLogicals(30_000);
649     }, [hasPaidVpn]);
651     return (
652         <SettingsSectionWide>
653             <SettingsParagraph>
654                 {c('Info').t`These configurations are provided to work with WireGuard routers and official clients.`}
655             </SettingsParagraph>
656             {creationModal}
657             {logicalInfoLoading || certificatesLoading ? (
658                 <div aria-busy="true" className="text-center mb-4">
659                     <CircleLoader />
660                 </div>
661             ) : (
662                 <>
663                     {getCertificates().map((certificate) => {
664                         const name = certificate.name || certificate.publicKeyFingerprint || certificate.publicKey;
665                         const expirationDate = readableTime(certificate.expirationTime);
666                         // translator: expirationDate is a date such as "Dec 11, 2022" (formatted according to current language)
667                         const expirationString = c('Info').t`expires ${expirationDate}`;
668                         const serverName = certificate?.features?.peerName || peer.name;
670                         return (
671                             <Details
672                                 data-certificate-id={certificate.id}
673                                 key={certificate.id}
674                                 open={certificate.id === currentCertificate}
675                                 onToggle={getToggleCallback(certificate)}
676                             >
677                                 <Summary>
678                                     <Row className="justify-space-between" collapseOnMobile={false}>
679                                         <div className="text-ellipsis">{name}</div>
680                                         <div className="shrink-0">
681                                             &nbsp;&nbsp;
682                                             {expirationString}
683                                             &nbsp;&nbsp;
684                                             {certificate.serialNumber ||
685                                             certificate.publicKeyFingerprint ||
686                                             certificate.publicKey ? (
687                                                 removing[
688                                                     certificate.publicKeyFingerprint ||
689                                                         certificate.publicKey ||
690                                                         certificate.id
691                                                 ] ? (
692                                                     <CircleLoader />
693                                                 ) : (
694                                                     <button
695                                                         type="button"
696                                                         className="label-stack-item-delete shrink-0"
697                                                         onClick={askForRevocation(certificate)}
698                                                         title={
699                                                             // translator: name is arbitrary name given by the user or a random key if not set
700                                                             c('Action').t`Revoke ${name}`
701                                                         }
702                                                     >
703                                                         <Icon
704                                                             name="cross"
705                                                             size={3}
706                                                             className="label-stack-item-delete-icon"
707                                                             alt={c('Action').t`Revoke`}
708                                                         />
709                                                     </button>
710                                                 )
711                                             ) : (
712                                                 ''
713                                             )}
714                                         </div>
715                                     </Row>
716                                 </Summary>
717                                 <div>
718                                     <Button loading={creating} onClick={getExtendCallback(certificate)}>{c('Action')
719                                         .t`Extend`}</Button>
720                                     {certificate?.config && (
721                                         <Button
722                                             className="ml-4"
723                                             loading={creating}
724                                             onClick={getDownloadCallback(certificate)}
725                                         >{c('Action').t`Download`}</Button>
726                                     )}
727                                     <label className="block my-2">
728                                         {c('Label').t`Config to connect to ${serverName}`}
729                                         <TextArea
730                                             className="block mt-2"
731                                             value={certificate?.config}
732                                             readOnly
733                                             rows={14}
734                                         />
735                                     </label>
736                                 </div>
737                             </Details>
738                         );
739                     })}
740                     {moreToLoad && (
741                         <div aria-busy="true" className="text-center mt-4">
742                             <Button
743                                 type="button"
744                                 onClick={() => setLimit(limit + paginationSize)}
745                                 title={c('Action').t`Load more`}
746                             >{c('Action').t`Load more`}</Button>
747                         </div>
748                     )}
749                     <form onSubmit={handleFormSubmit} className="mt-8">
750                         <h3 className="mt-8 mb-2">{c('Title').t`1. Give a name to the config to be generated`}</h3>
751                         <InputFieldTwo
752                             id="certificate-device-name"
753                             ref={nameInputRef}
754                             label={
755                                 <>
756                                     {c('Label').t`Device/certificate name`}
757                                     <Info
758                                         className="ml-1"
759                                         title={c('Info')
760                                             .t`A name to help you identify where you use it so you can easily revoke it or extend it later.`}
761                                     />
762                                 </>
763                             }
764                             placeholder={c('Label').t`Choose a name for the generated certificate file`}
765                             maxLength={100}
766                         />
767                         <h3 className="mt-8 mb-2">{c('Title').t`2. Select platform`}</h3>
768                         <div className="flex flex-column md:flex-row">
769                             {[
770                                 {
771                                     value: PLATFORM.ANDROID,
772                                     label: c('Option').t`Android`,
773                                 },
774                                 {
775                                     value: PLATFORM.IOS,
776                                     label: c('Option').t`iOS`,
777                                 },
778                                 {
779                                     value: PLATFORM.WINDOWS,
780                                     label: c('Option').t`Windows`,
781                                 },
782                                 {
783                                     value: PLATFORM.MACOS,
784                                     label: c('Option').t`macOS`,
785                                 },
786                                 {
787                                     value: PLATFORM.LINUX,
788                                     label: c('Option').t`GNU/Linux`,
789                                 },
790                                 {
791                                     value: PLATFORM.ROUTER,
792                                     label: c('Option').t`Router`,
793                                 },
794                             ].map(({ value, label }) => {
795                                 return (
796                                     <div key={'wg-' + value} className="mr-8 mb-4">
797                                         <Radio
798                                             id={'wg-platform-' + value}
799                                             onChange={() => setPlatform(value)}
800                                             checked={platform === value}
801                                             name="platform"
802                                             className="flex inline-flex *:self-center mb-2"
803                                         >
804                                             {label}
805                                         </Radio>
806                                     </div>
807                                 );
808                             })}
809                         </div>
810                         <h3 className="mt-8 mb-2">{c('Title').t`3. Select VPN options`}</h3>
811                         {getFeatureKeys().map((key) => (
812                             <div className="mb-4" key={'wg-feature-' + key}>
813                                 <label className="field-two-container w-full" htmlFor={'wg-feature-' + key}>
814                                     {isFeatureSelection(featuresConfig[key]) ? (
815                                         <>
816                                             <div className="flex field-two-label-container justify-space-between flex-nowrap items-end">
817                                                 <span className="field-two-label">
818                                                     {featuresConfig[key].name}
819                                                     {getFeatureLink(featuresConfig[key])}
820                                                 </span>
821                                             </div>
822                                             <SelectTwo
823                                                 id={'wg-feature-' + key}
824                                                 key={'wg-feature-' + key}
825                                                 value={featuresConfig[key].value}
826                                                 onValue={(value) => setFeature(key, value)}
827                                             >
828                                                 {(featuresConfig[key] as FeatureSelection).values.map((option) => (
829                                                     <Option
830                                                         key={'wg-feature-' + key + '-' + option.value}
831                                                         title={option.name}
832                                                         value={option.value}
833                                                     />
834                                                 ))}
835                                             </SelectTwo>
836                                         </>
837                                     ) : (
838                                         <>
839                                             <Toggle
840                                                 key={'feature-' + key}
841                                                 id={'feature-' + key}
842                                                 checked={!!featuresConfig[key].value}
843                                                 onChange={() => setFeature(key, !featuresConfig[key].value)}
844                                             />
845                                             &nbsp; &nbsp;
846                                             {featuresConfig[key].name}
847                                             {getFeatureLink(featuresConfig[key])}
848                                         </>
849                                     )}
850                                 </label>
851                             </div>
852                         ))}
853                         {logical && (
854                             <>
855                                 <h3 className="mt-8 mb-2">{c('Title').t`4. Select a server to connect to`}</h3>
857                                 <div className="mb-8">
858                                     {bestServerName && (
859                                         <>
860                                             <p className="mt-0">
861                                                 {c('Info')
862                                                     .jt`Use the best server according to current load and position: ${formattedBestServerName}`}
863                                             </p>
864                                             <div className="mt-4">
865                                                 <Button type="submit" color="norm" loading={creating}>{c('Action')
866                                                     .t`Create`}</Button>
867                                             </div>
868                                         </>
869                                     )}
870                                 </div>
872                                 <p className="my-8">{c('Info').t`Or select a particular server:`}</p>
874                                 <OpenVPNConfigurationSection
875                                     onSelect={createWithLogical}
876                                     selecting={creating}
877                                     listOnly={true}
878                                     excludedCategories={[CATEGORY.COUNTRY]}
879                                 />
880                             </>
881                         )}
882                     </form>
883                 </>
884             )}
885         </SettingsSectionWide>
886     );
889 export default WireGuardConfigurationSection;