1 import { useCallback, useEffect, useMemo, useState } from 'react';
3 import groupBy from 'lodash/groupBy';
4 import { c, msgid } from 'ttag';
6 import { usePlans } from '@proton/account/plans/hooks';
7 import { useUser } from '@proton/account/user/hooks';
8 import { useUserSettings } from '@proton/account/userSettings/hooks';
9 import { ButtonLike, Href } from '@proton/atoms';
10 import Icon from '@proton/components/components/icon/Icon';
11 import Radio from '@proton/components/components/input/Radio';
12 import RadioGroup from '@proton/components/components/input/RadioGroup';
13 import Info from '@proton/components/components/link/Info';
14 import SettingsLink from '@proton/components/components/link/SettingsLink';
15 import SettingsParagraph from '@proton/components/containers/account/SettingsParagraph';
16 import SettingsSectionWide from '@proton/components/containers/account/SettingsSectionWide';
17 import useUserVPN from '@proton/components/hooks/useUserVPN';
18 import useVPNLogicals from '@proton/components/hooks/useVPNLogicals';
19 import { PLANS } from '@proton/payments';
20 import { SORT_DIRECTION, VPN_APP_NAME, VPN_CONNECTIONS, VPN_HOSTNAME } from '@proton/shared/lib/constants';
21 import type { Logical } from '@proton/shared/lib/vpn/Logical';
27 getLocalizedCountryByAbbr,
28 } from '../../../helpers/countries';
29 import useSortedList from '../../../hooks/useSortedList';
30 import type { EnhancedLogical } from '../OpenVPNConfigurationSection/interface';
31 import ConfigsTable, { CATEGORY } from './ConfigsTable';
32 import ServerConfigs from './ServerConfigs';
33 import { isSecureCoreEnabled, isTorEnabled } from './utils';
50 onSelect?: (logical: Logical) => void;
53 excludedCategories?: CATEGORY[];
54 countryOptions?: CountryOptions;
57 const OpenVPNConfigurationSection = ({
58 countryOptions: maybeCountryOptions,
62 excludedCategories = [],
64 const [platform, setPlatform] = useState(PLATFORM.ANDROID);
65 const [protocol, setProtocol] = useState(PROTOCOL.UDP);
66 const [plansResult, loadingPlans] = usePlans();
67 const plans = plansResult?.plans || [];
68 const { loading, result, fetch: fetchLogicals } = useVPNLogicals();
69 const { result: vpnResult, loading: vpnLoading, fetch: fetchUserVPN } = useUserVPN();
70 const [{ hasPaidVpn }] = useUser();
71 const [userSettings] = useUserSettings();
72 const userVPN = vpnResult?.VPN;
73 const maxTier = userVPN?.MaxTier || 0;
74 const [category, setCategory] = useState(CATEGORY.FREE);
75 const excludeCategoryMap = excludedCategories.reduce<{ [key in CATEGORY]?: boolean }>((map, excludedCategory) => {
76 map[excludedCategory] = true;
81 excludeCategoryMap[CATEGORY.FREE] = true;
84 const selectedCategory = maxTier && category === CATEGORY.FREE ? CATEGORY.SERVER : category;
86 const countryOptions = maybeCountryOptions || getCountryOptions(userSettings);
88 const getIsUpgradeRequired = useCallback(
89 (server: Logical) => {
90 return !userVPN || (!hasPaidVpn && server.Tier > 0);
95 const servers = useMemo((): EnhancedLogical[] => {
96 return (result?.LogicalServers || []).map((server) => ({
98 country: getLocalizedCountryByAbbr(correctAbbr(server.ExitCountry), countryOptions),
99 isUpgradeRequired: getIsUpgradeRequired(server),
101 }, [result?.LogicalServers, getIsUpgradeRequired]);
103 const { sortedList: allServers } = useSortedList(servers, { key: 'country', direction: SORT_DIRECTION.ASC });
105 const isUpgradeRequiredForSecureCore = !Object.keys(userVPN || {}).length || !hasPaidVpn;
106 const isUpgradeRequiredForCountries = !Object.keys(userVPN || {}).length || !hasPaidVpn;
109 fetchUserVPN(30_000);
112 const secureCoreServers = useMemo(() => {
114 .filter(({ Features }) => isSecureCoreEnabled(Features))
118 isUpgradeRequired: isUpgradeRequiredForSecureCore,
121 }, [allServers, isUpgradeRequiredForSecureCore]);
123 const countryServers = useMemo(() => {
124 return Object.values(
126 allServers.filter(({ Tier, Features }) => {
127 return Tier === 2 && !isSecureCoreEnabled(Features) && !isTorEnabled(Features);
132 const [first] = groups;
133 const activeServers = groups.filter(({ Status }) => Status === 1);
134 const load = activeServers.reduce((acc, { Load }) => acc + (Load || 0), 0) / activeServers.length;
137 isUpgradeRequired: isUpgradeRequiredForCountries,
138 Load: Number.isNaN(load) ? 0 : Math.round(load),
139 Domain: `${first.EntryCountry.toLowerCase()}.protonvpn.net`, // Forging domain
140 Servers: groups.flatMap((logical) => logical.Servers || []),
143 }, [allServers, isUpgradeRequiredForCountries]);
145 const freeServers = useMemo(() => {
146 return allServers.filter(({ Tier }) => Tier === 0);
153 if (!hasPaidVpn || userVPN?.PlanName === 'trial') {
154 setCategory(CATEGORY.FREE);
159 void fetchUserVPN(30_000);
163 void fetchLogicals(30_000);
166 const vpnPlan = plans?.find(({ Name }) => Name === PLANS.VPN);
167 const plusVpnConnections = vpnPlan?.MaxVPN || VPN_CONNECTIONS;
169 const vpnPlus = vpnPlan?.Title;
172 <SettingsSectionWide>
179 .t`These configuration files let you choose which ${VPN_APP_NAME} server you connect to when using a third-party VPN app or setting up a VPN connection on a router.
182 <h3 className="mt-8 mb-2">{c('Title').t`1. Select platform`}</h3>
183 <div className="flex flex-column md:flex-row">
186 value: PLATFORM.ANDROID,
187 link: 'https://protonvpn.com/support/android-vpn-setup/',
188 label: c('Option').t`Android`,
192 link: 'https://protonvpn.com/support/ios-vpn-setup/',
193 label: c('Option').t`iOS`,
196 value: PLATFORM.WINDOWS,
197 link: 'https://protonvpn.com/support/openvpn-windows-setup/',
198 label: c('Option').t`Windows`,
201 value: PLATFORM.MACOS,
202 link: 'https://protonvpn.com/support/mac-vpn-setup/',
203 label: c('Option').t`macOS`,
206 value: PLATFORM.LINUX,
207 link: 'https://protonvpn.com/support/linux-vpn-setup/',
208 label: c('Option').t`GNU/Linux`,
211 value: PLATFORM.ROUTER,
212 link: 'https://protonvpn.com/support/installing-protonvpn-on-a-router/',
213 label: c('Option').t`Router`,
215 ].map(({ value, label, link }) => {
217 <div key={value} className="mr-8 mb-4">
219 id={'platform-' + value}
220 onChange={() => setPlatform(value)}
221 checked={platform === value}
223 className="flex inline-flex *:self-center mb-2"
229 className="text-sm m-0 block ml-custom"
230 style={{ '--ml-custom': '1.75rem' }}
231 >{c('Link').t`View guide`}</Href>
237 <h3 className="mt-8 mb-2">{c('Title').t`2. Select protocol`}</h3>
238 <div className="flex flex-column md:flex-row mb-2">
242 onChange={setProtocol}
244 { value: PROTOCOL.UDP, label: c('Option').t`UDP` },
245 { value: PROTOCOL.TCP, label: c('Option').t`TCP` },
249 <div className="mb-4">
250 <Href href="https://protonvpn.com/support/udp-tcp/" className="text-sm m-0">{c('Link')
251 .t`What is the difference between UDP and TCP protocols?`}</Href>
254 <h3 className="mt-8 mb-2">{c('Title').t`3. Select config file and download`}</h3>
257 <div className="flex flex-column md:flex-row mb-6">
259 name={'category' + (listOnly ? '-list' : '')}
260 value={selectedCategory}
261 onChange={setCategory}
263 { value: CATEGORY.COUNTRY, label: c('Option').t`Country configs` },
264 { value: CATEGORY.SERVER, label: c('Option').t`Standard server configs` },
265 { value: CATEGORY.FREE, label: c('Option').t`Free server configs` },
266 { value: CATEGORY.SECURE_CORE, label: c('Option').t`Secure Core configs` },
267 ].filter((option) => !excludeCategoryMap[option.value])}
272 <div className="mb-4">
273 {selectedCategory === CATEGORY.SECURE_CORE && (
275 <SettingsParagraph learnMoreUrl="https://protonvpn.com/support/secure-core-vpn">
277 .t`Install a Secure Core configuration file to benefit from an additional protection against VPN endpoint compromise.`}
279 {isUpgradeRequiredForSecureCore && (
281 <span className="block">{
282 // translator: ${vpnPlus} is "VPN Plus" (taken from plan title)
283 c('Info').t`${vpnPlus} required for Secure Core feature.`
285 <SettingsLink path="/upgrade">{c('Link').t`Learn more`}</SettingsLink>
289 category={CATEGORY.SECURE_CORE}
293 servers={secureCoreServers}
295 selecting={selecting}
296 countryOptions={countryOptions}
300 {selectedCategory === CATEGORY.COUNTRY && (
305 .t`Install a Country configuration file to connect to a random server in the country of your choice.`}
308 {isUpgradeRequiredForCountries && (
309 <SettingsParagraph learnMoreUrl={`https://${VPN_HOSTNAME}/dashboard`}>{
310 // translator: ${vpnPlus} is "VPN Plus" (taken from plan title)
311 // translator: This notice appears when a free user go to "OpenVPN configuration files" section and select "Country configs'
312 c('Info').t`${vpnPlus} required for Country level connection.`
313 }</SettingsParagraph>
316 category={CATEGORY.COUNTRY}
320 servers={countryServers}
322 selecting={selecting}
323 countryOptions={countryOptions}
327 {selectedCategory === CATEGORY.SERVER && (
330 <SettingsParagraph>{c('Info')
331 .t`Install a Server configuration file to connect to a specific server in the country of your choice.`}</SettingsParagraph>
334 category={selectedCategory}
341 selecting={selecting}
342 countryOptions={countryOptions}
346 {selectedCategory === CATEGORY.FREE && (
351 .t`Install a Free server configuration file to connect to a specific server in one of the three free locations.`}
355 countryOptions={countryOptions}
356 category={selectedCategory}
360 servers={freeServers}
363 selecting={selecting}
369 {!loadingPlans && (userVPN?.PlanName === 'trial' || !hasPaidVpn) && vpnPlus && (
370 <div className="border p-7 text-center">
371 <h3 className="color-primary mt-0 mb-4">{
372 // translator: ${vpnPlus} is "VPN Plus" (taken from plan title)
373 c('Title').t`Get ${vpnPlus} to access all servers`
375 <ul className="unstyled inline-flex mt-0 mb-8 flex-column md:flex-row">
376 <li className="flex flex-nowrap items-center mr-4">
377 <Icon name="checkmark" className="color-success mr-2" />
378 <span className="text-bold">{c('Feature').t`Access to all countries`}</span>
380 <li className="flex flex-nowrap items-center mr-4">
381 <Icon name="checkmark" className="color-success mr-2" />
382 <span className="text-bold">{c('Feature').t`Secure Core servers`}</span>
384 <li className="flex flex-nowrap items-center mr-4">
385 <Icon name="checkmark" className="color-success mr-2" />
386 <span className="text-bold">{c('Feature').t`Fastest VPN servers`}</span>
388 <li className="flex flex-nowrap items-center mr-4">
389 <Icon name="checkmark" className="color-success mr-2" />
390 <span className="text-bold">{c('Feature').t`Torrenting support (P2P)`}</span>
392 <li className="flex flex-nowrap items-center mr-4">
393 <Icon name="checkmark" className="color-success mr-2" />
394 <span className="text-bold">
395 {c('Feature').ngettext(
396 msgid`Connection for up to ${plusVpnConnections} device`,
397 `Connection for up to ${plusVpnConnections} devices`,
402 <li className="flex flex-nowrap items-center ">
403 <Icon name="checkmark" className="color-success mr-2" />
404 <span className="text-bold mr-2">{c('Feature')
405 .t`Secure streaming support`}</span>
407 url="https://protonvpn.com/support/streaming-guide/"
409 .t`Netflix, Amazon Prime Video, BBC iPlayer, ESPN+, Disney+, HBO Now, and more.`}
417 path={`/dashboard?plan=${PLANS.VPN2024}`}
420 // translator: ${vpnPlus} is "VPN Plus" (taken from plan title)
421 c('Action').t`Get ${vpnPlus}`
430 </SettingsSectionWide>
434 export default OpenVPNConfigurationSection;