1 import { memo } from 'react';
3 import { c } from 'ttag';
5 import { useUser } from '@proton/account/user/hooks';
6 import { Button, ButtonLike } from '@proton/atoms';
7 import Icon from '@proton/components/components/icon/Icon';
8 import SettingsLink from '@proton/components/components/link/SettingsLink';
9 import Table from '@proton/components/components/table/Table';
10 import TableBody from '@proton/components/components/table/TableBody';
11 import TableCell from '@proton/components/components/table/TableCell';
12 import TableRow from '@proton/components/components/table/TableRow';
13 import Tooltip from '@proton/components/components/tooltip/Tooltip';
14 import useApi from '@proton/components/hooks/useApi';
15 import { PLANS } from '@proton/payments';
16 import { getVPNServerConfig } from '@proton/shared/lib/api/vpn';
17 import downloadFile from '@proton/shared/lib/helpers/downloadFile';
18 import type { Logical } from '@proton/shared/lib/vpn/Logical';
19 import clsx from '@proton/utils/clsx';
20 import isTruthy from '@proton/utils/isTruthy';
22 import type { CountryOptions } from '../../../helpers/countries';
23 import Country from './Country';
24 import LoadIndicator from './LoadIndicator';
25 import type { EnhancedLogical } from './interface';
26 import { normalizeName } from './normalizeName';
27 import { isP2PEnabled, isTorEnabled } from './utils';
29 export enum CATEGORY {
30 SECURE_CORE = 'SecureCore',
36 const PlusBadge = () => (
37 <span className="ml-2">
38 <Tooltip title="Plus">
39 <div className="text-center rounded">P</div>
44 const ServerDown = () => (
45 <span className="ml-2">
46 <Tooltip title={c('Info').t`Server is currently down`}>
47 <div className="flex inline-flex *:self-center">
48 <Icon className="color-danger" size={5} name="exclamation-circle" />
54 export const P2PIcon = () => (
55 <span className="mx-2">
56 <Tooltip title={c('Info').t`P2P`}>
57 <Icon name="arrow-right-arrow-left" size={4.5} className="rounded bg-strong p-1" />
62 export const TorIcon = () => (
63 <span className="mx-2">
64 <Tooltip title={c('Info').t`Tor`}>
65 <Icon name="brand-tor" size={4.5} className="rounded bg-strong p-1" />
71 servers: EnhancedLogical[];
77 onSelect?: (server: Logical) => void;
78 countryOptions: CountryOptions;
81 // TODO: Add icons instead of text for p2p and tor when they are ready
82 const ConfigsTable = ({
93 const [{ hasPaidVpn }] = useUser();
95 const handleClickDownload =
96 ({ ID, ExitCountry, Tier, Name }: Logical) =>
98 const buffer = await api(
100 LogicalID: category === CATEGORY.COUNTRY ? undefined : ID,
103 Country: ExitCountry,
106 const blob = new Blob([buffer], { type: 'application/x-openvpn-profile' });
107 const name = category === CATEGORY.COUNTRY ? ExitCountry.toLowerCase() : normalizeName({ Tier, Name });
108 downloadFile(blob, `${name}.protonvpn.${protocol}.ovpn`);
116 className={clsx(['w-auto', category === CATEGORY.SERVER ? 'md:w-1/4' : 'md:w-1/3'])}
119 {[CATEGORY.SERVER, CATEGORY.FREE].includes(category)
120 ? c('TableHeader').t`Name`
121 : c('TableHeader').t`Country`}
123 {category === CATEGORY.SERVER ? (
124 <TableCell className="w-auto md:w-1/4" type="header">{c('TableHeader').t`City`}</TableCell>
127 className={clsx(['w-auto', category === CATEGORY.SERVER ? 'md:w-1/4' : 'md:w-1/3'])}
129 >{c('TableHeader').t`Status`}</TableCell>
131 className={clsx(['w-auto', category === CATEGORY.SERVER ? 'md:w-1/4' : 'md:w-1/3'])}
133 >{c('TableHeader').t`Action`}</TableCell>
136 <TableBody loading={loading} colSpan={4}>
137 {servers.map((server) => (
141 [CATEGORY.SERVER, CATEGORY.FREE].includes(category) ? (
144 <Country key="country" server={server} countryOptions={countryOptions} />
146 category === CATEGORY.SERVER ? (
147 <div className="inline-flex *:self-center" key="city">
151 <div className="inline-flex *:self-center" key="status">
152 <LoadIndicator server={server} />
153 {server.Tier === 2 && <PlusBadge />}
154 {server.Servers?.every(({ Status }) => !Status) && <ServerDown />}
155 {isP2PEnabled(server.Features) && <P2PIcon />}
156 {isTorEnabled(server.Features) && <TorIcon />}
158 server.isUpgradeRequired ? (
163 ? c('Info').t`Plus or Visionary subscription required`
164 : c('Info').t`Basic, Plus or Visionary subscription required`
171 path={hasPaidVpn ? `/dashboard?plan=${PLANS.VPN2024}` : '/upgrade'}
172 >{c('Action').t`Upgrade`}</ButtonLike>
175 <Button size="small" onClick={() => onSelect(server)} loading={selecting}>
176 {c('Action').t`Create`}
179 <Button size="small" onClick={handleClickDownload(server)}>
180 {c('Action').t`Download`}
191 export default memo(ConfigsTable);