diff --git a/webnext/messages/en.json b/webnext/messages/en.json
index 54f0163a..28f2511d 100644
--- a/webnext/messages/en.json
+++ b/webnext/messages/en.json
@@ -11,6 +11,7 @@
"form_error_min_len": "Minimum length of {length}",
"form_error_email": "Enter valid email",
"form_error_required": "Field is required",
+ "form_error_token": "Token is not valid",
"form_label_token": "Token",
"form_label_email": "email",
"form_label_url": "URL",
diff --git a/webnext/src/pages/ClientDownload/ClientDownloadPage.tsx b/webnext/src/pages/ClientDownload/ClientDownloadPage.tsx
index d0647645..5bc93153 100644
--- a/webnext/src/pages/ClientDownload/ClientDownloadPage.tsx
+++ b/webnext/src/pages/ClientDownload/ClientDownloadPage.tsx
@@ -1,26 +1,95 @@
import './style.scss';
import { useNavigate } from '@tanstack/react-router';
-import { useState } from 'react';
+import { useMemo, useState } from 'react';
import { m } from '../../paraglide/messages';
import { Page } from '../../shared/components/Page/Page';
import { PageNavigation } from '../../shared/components/PageNavigation/PageNavigation';
import { EnrollmentStep } from '../../shared/components/Step/Step';
+import { externalLink } from '../../shared/consts';
import { Button } from '../../shared/defguard-ui/components/Button/Button';
+import { ButtonMenu } from '../../shared/defguard-ui/components/ButtonMenu/MenuButton';
import { Icon } from '../../shared/defguard-ui/components/Icon';
import type { IconKindValue } from '../../shared/defguard-ui/components/Icon/icon-types';
+import type { MenuItemsGroup } from '../../shared/defguard-ui/components/Menu/types';
import { Modal } from '../../shared/defguard-ui/components/Modal/Modal';
import { ModalControls } from '../../shared/defguard-ui/components/ModalControls/ModalControls';
import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox';
import { ThemeSpacing } from '../../shared/defguard-ui/types';
+import { isPresent } from '../../shared/defguard-ui/utils/isPresent';
import androidIcon from './assets/android.png';
import iosIcon from './assets/ios.png';
import laptopIcon from './assets/laptop.png';
import desktopIcon from './assets/pc-tower.png';
+// open link in onClick handler
+const openLink = (value: string): void => {
+ const anchorElement = document.createElement('a');
+ anchorElement.style.display = 'none';
+ anchorElement.href = value;
+ anchorElement.target = '_blank';
+ anchorElement.rel = 'noopener noreferrer';
+ document.body.appendChild(anchorElement);
+ anchorElement.click();
+ anchorElement.remove();
+};
+
+const linuxMenu: MenuItemsGroup[] = [
+ {
+ items: [
+ {
+ text: 'Deb X86',
+ onClick: () => openLink(externalLink.client.desktop.linux.deb.amd),
+ },
+ {
+ text: 'Deb ARM',
+ onClick: () => openLink(externalLink.client.desktop.linux.deb.arm),
+ },
+ {
+ text: 'RPM X86',
+ onClick: () => openLink(externalLink.client.desktop.linux.rpm.amd),
+ },
+ {
+ text: 'RPM ARM',
+ onClick: () => openLink(externalLink.client.desktop.linux.rpm.arm),
+ },
+ {
+ text: 'ArchLinux',
+ onClick: () => openLink(externalLink.client.desktop.linux.arch),
+ },
+ ],
+ },
+];
+
export const ClientDownloadPage = () => {
- const [modalOpen, setModalOpen] = useState(false);
const navigate = useNavigate();
+ const [confirmModalOpen, setConfirmModalOpen] = useState(false);
+ const [_appleHelpModalOpen, setAppleHelpModalOpen] = useState(false);
+
+ const appleMenu = useMemo(
+ (): MenuItemsGroup[] => [
+ {
+ header: {
+ text: 'Apple',
+ onHelp: () => {
+ setAppleHelpModalOpen(true);
+ },
+ },
+ items: [
+ {
+ text: 'Intel',
+ onClick: () => openLink(externalLink.client.desktop.macos.intel),
+ },
+ {
+ text: 'ARM',
+ onClick: () => openLink(externalLink.client.desktop.macos.arm),
+ },
+ ],
+ },
+ ],
+ [],
+ );
+
return (
@@ -42,6 +111,7 @@ export const ClientDownloadPage = () => {
})}
buttonText={m.client_download_for({ platform: 'Windows' })}
buttonIconKind="windows"
+ directLink={externalLink.client.desktop.windows}
icon={desktopIcon}
/>
{
})}
buttonText={m.client_download_for({ platform: 'Linux' })}
buttonIconKind="linux"
+ menuItems={linuxMenu}
icon={desktopIcon}
/>
{
})}
buttonText={m.client_download_for({ platform: 'Mac' })}
buttonIconKind="app-store"
+ menuItems={appleMenu}
icon={laptopIcon}
/>
@@ -78,6 +150,7 @@ export const ClientDownloadPage = () => {
})}
buttonText={m.client_download_for({ platform: 'Android' })}
buttonIconKind="android"
+ directLink={externalLink.client.mobile.google}
icon={androidIcon}
/>
{
})}
buttonText={m.client_download_for({ platform: 'iOS' })}
buttonIconKind="apple"
+ directLink={externalLink.client.mobile.apple}
icon={iosIcon}
/>
{
- setModalOpen(false);
+ setConfirmModalOpen(false);
}}
>
{m.client_download_modal_content()}
setModalOpen(false),
+ onClick: () => setConfirmModalOpen(false),
}}
submitProps={{
text: m.controls_continue(),
@@ -126,7 +200,7 @@ export const ClientDownloadPage = () => {
}}
nextText={m.controls_continue()}
onNext={() => {
- setModalOpen(true);
+ setConfirmModalOpen(true);
}}
/>
@@ -140,6 +214,8 @@ const Platform = ({
subtitle,
title,
testId,
+ menuItems,
+ directLink,
}: {
icon: string;
title: string;
@@ -147,6 +223,8 @@ const Platform = ({
buttonText: string;
buttonIconKind: IconKindValue;
testId: string;
+ directLink?: string;
+ menuItems?: MenuItemsGroup[];
}) => {
return (
@@ -155,15 +233,19 @@ const Platform = ({
{title}
{subtitle}
-