diff --git a/webnext/src/pages/ClientDownload/ClientDownloadPage.tsx b/webnext/src/pages/ClientDownload/ClientDownloadPage.tsx index 6a10d301..23f30a75 100644 --- a/webnext/src/pages/ClientDownload/ClientDownloadPage.tsx +++ b/webnext/src/pages/ClientDownload/ClientDownloadPage.tsx @@ -1,7 +1,9 @@ import './style.scss'; -import { useLoaderData, useNavigate } from '@tanstack/react-router'; +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from '@tanstack/react-router'; import { useMemo, useState } from 'react'; import { m } from '../../paraglide/messages'; +import { AppleHelpModal } from '../../shared/components/AppleHelpModal/AppleHelpModal'; import { Page } from '../../shared/components/Page/Page'; import { PageNavigation } from '../../shared/components/PageNavigation/PageNavigation'; import { EnrollmentStep } from '../../shared/components/Step/Step'; @@ -16,19 +18,16 @@ import { ModalControls } from '../../shared/defguard-ui/components/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 { getClientArtifactsQueryOptions } from '../../shared/query/queryOptions'; import { openVirtualLink } from '../../shared/utils/openVirtualLink'; import androidIcon from './assets/android.png'; -import apple_video_src from './assets/apple_hardware_help.mp4'; import iosIcon from './assets/ios.png'; import laptopIcon from './assets/laptop.png'; import desktopIcon from './assets/pc-tower.png'; -// open link in onClick handler - export const ClientDownloadPage = () => { - const pageData = useLoaderData({ - from: '/download', - }); + const { data: pageData } = useQuery(getClientArtifactsQueryOptions); + const navigate = useNavigate(); const [confirmModalOpen, setConfirmModalOpen] = useState(false); @@ -163,6 +162,12 @@ export const ClientDownloadPage = () => { icon={iosIcon} /> + { + setAppleHelpModalOpen(false); + }} + /> { }} /> - { - setAppleHelpModalOpen(false); - }} - > -

{m.client_download_apple_help_content_1()}

- -
{ diff --git a/webnext/src/pages/ClientDownload/style.scss b/webnext/src/pages/ClientDownload/style.scss index 5474a43e..d42321cf 100644 --- a/webnext/src/pages/ClientDownload/style.scss +++ b/webnext/src/pages/ClientDownload/style.scss @@ -34,6 +34,7 @@ & > img { width: 60px; height: 60px; + user-select: none; } .btn { diff --git a/webnext/src/pages/enrollment/ConfigureClient/ConfigureClientPage.tsx b/webnext/src/pages/enrollment/ConfigureClient/ConfigureClientPage.tsx index 1928d73a..639042d4 100644 --- a/webnext/src/pages/enrollment/ConfigureClient/ConfigureClientPage.tsx +++ b/webnext/src/pages/enrollment/ConfigureClient/ConfigureClientPage.tsx @@ -1,10 +1,12 @@ import './style.scss'; +import { useQuery } from '@tanstack/react-query'; import { useLoaderData } from '@tanstack/react-router'; import { capitalCase } from 'change-case'; import { QRCodeCanvas } from 'qrcode.react'; import { useMemo, useState } from 'react'; import { m } from '../../../paraglide/messages'; +import { AppleHelpModal } from '../../../shared/components/AppleHelpModal/AppleHelpModal'; import { ContactFooter } from '../../../shared/components/ContactFooter/ContactFooter'; import { ContainerWithIcon } from '../../../shared/components/ContainerWithIcon/ContainerWithIcon'; import { Page } from '../../../shared/components/Page/Page'; @@ -19,13 +21,17 @@ import { Icon } from '../../../shared/defguard-ui/components/Icon'; import type { MenuItemsGroup } from '../../../shared/defguard-ui/components/Menu/types'; import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../shared/defguard-ui/types'; +import { getClientArtifactsQueryOptions } from '../../../shared/query/queryOptions'; import { openVirtualLink } from '../../../shared/utils/openVirtualLink'; export const ConfigureClientPage = () => { const pageData = useLoaderData({ from: '/client-setup', }); - const clientLinks = pageData.clientDownload; + + const { data: clientLinks } = useQuery(getClientArtifactsQueryOptions); + + const [appleHelpModalOpen, setAppleHelpModalOpen] = useState(false); const clientDownloadMenu = useMemo( (): MenuItemsGroup[] => [ @@ -41,6 +47,7 @@ export const ConfigureClientPage = () => { { header: { text: m.client_download_apple_help_header(), + onHelp: () => setAppleHelpModalOpen(true), }, items: [ { @@ -214,6 +221,12 @@ export const ConfigureClientPage = () => {

{m.client_setup_footer_extra()}

+ { + setAppleHelpModalOpen(false); + }} + /> ); }; diff --git a/webnext/src/routes/client-setup.tsx b/webnext/src/routes/client-setup.tsx index 186ad7bc..297b894b 100644 --- a/webnext/src/routes/client-setup.tsx +++ b/webnext/src/routes/client-setup.tsx @@ -1,11 +1,12 @@ import { createFileRoute, redirect } from '@tanstack/react-router'; import z from 'zod'; +import { queryClient } from '../app/query'; import { ConfigureClientPage } from '../pages/enrollment/ConfigureClient/ConfigureClientPage'; import { api } from '../shared/api/api'; import type { EnrollmentStartResponse } from '../shared/api/types'; -import { updateServiceApi } from '../shared/api/update-service'; import { isPresent } from '../shared/defguard-ui/utils/isPresent'; import { useEnrollmentStore } from '../shared/hooks/useEnrollmentStore'; +import { getClientArtifactsQueryOptions } from '../shared/query/queryOptions'; const schema = z.object({ code: z.string().trim().optional(), @@ -39,7 +40,8 @@ export const Route = createFileRoute('/client-setup')({ }; }, loader: async ({ context: { openid } }) => { - const clientDownload = await updateServiceApi.getClientArtifacts().catch(() => null); + void queryClient.ensureQueryData(getClientArtifactsQueryOptions); + if (openid) { try { const openIdResponse = await api.openId.enrollmentCallback.callbackFn({ @@ -70,7 +72,6 @@ export const Route = createFileRoute('/client-setup')({ } const state = useEnrollmentStore.getState(); return { - clientDownload, token: state.token as string, enrollmentData: state.enrollmentData as EnrollmentStartResponse, }; diff --git a/webnext/src/routes/download.tsx b/webnext/src/routes/download.tsx index 21493425..81378f90 100644 --- a/webnext/src/routes/download.tsx +++ b/webnext/src/routes/download.tsx @@ -1,13 +1,9 @@ import { createFileRoute } from '@tanstack/react-router'; +import { queryClient } from '../app/query'; import { ClientDownloadPage } from '../pages/ClientDownload/ClientDownloadPage'; -import { updateServiceApi } from '../shared/api/update-service'; +import { getClientArtifactsQueryOptions } from '../shared/query/queryOptions'; export const Route = createFileRoute('/download')({ component: ClientDownloadPage, - loader: async () => { - const clientVersionData = await updateServiceApi - .getClientArtifacts() - .catch(() => null); - return clientVersionData; - }, + loader: () => queryClient.ensureQueryData(getClientArtifactsQueryOptions), }); diff --git a/webnext/src/shared/api/update-service.ts b/webnext/src/shared/api/update-service.ts index 6d70a67b..8371fea1 100644 --- a/webnext/src/shared/api/update-service.ts +++ b/webnext/src/shared/api/update-service.ts @@ -3,6 +3,8 @@ import qs from 'qs'; const baseUrl = import.meta.env.VITE_UPDATE_BASE_URL as string | undefined; +const clientDownloadFallback = 'https://defguard.net/download'; + const client = axios.create({ baseURL: baseUrl ?? 'https://update-service-dev.defguard.net/api', headers: { 'Content-Type': 'application/json' }, @@ -33,7 +35,32 @@ const updateServiceApi = { source: 'enrollment', }, }) - .then((response) => response.data), + .then((response) => { + const { data } = response; + const res: ClientVersionCheck = { + deb_amd64: data.deb_amd64 ?? clientDownloadFallback, + deb_arm64: data.deb_arm64 ?? clientDownloadFallback, + macos_amd64: data.macos_amd64 ?? clientDownloadFallback, + macos_arm64: data.macos_arm64 ?? clientDownloadFallback, + rpm_amd64: data.rpm_amd64 ?? clientDownloadFallback, + rpm_arm64: data.rpm_arm64 ?? clientDownloadFallback, + windows_amd64: data.windows_amd64 ?? clientDownloadFallback, + }; + return res; + }) + .catch((e) => { + console.error(e); + const fallback: ClientVersionCheck = { + deb_amd64: clientDownloadFallback, + deb_arm64: clientDownloadFallback, + macos_amd64: clientDownloadFallback, + macos_arm64: clientDownloadFallback, + rpm_amd64: clientDownloadFallback, + rpm_arm64: clientDownloadFallback, + windows_amd64: clientDownloadFallback, + }; + return fallback; + }), } as const; export { updateServiceApi }; diff --git a/webnext/src/shared/components/AppleHelpModal/AppleHelpModal.tsx b/webnext/src/shared/components/AppleHelpModal/AppleHelpModal.tsx new file mode 100644 index 00000000..9e285e01 --- /dev/null +++ b/webnext/src/shared/components/AppleHelpModal/AppleHelpModal.tsx @@ -0,0 +1,43 @@ +import { m } from '../../../paraglide/messages'; +import { Modal } from '../../defguard-ui/components/Modal/Modal'; +import { ModalControls } from '../../defguard-ui/components/ModalControls/ModalControls'; +import { SizedBox } from '../../defguard-ui/components/SizedBox/SizedBox'; +import { ThemeSpacing } from '../../defguard-ui/types'; +import apple_video_src from './assets/apple_hardware_help.mp4'; + +type Props = { + isOpen: boolean; + onClose: () => void; +}; + +export const AppleHelpModal = ({ isOpen, onClose }: Props) => { + return ( + +

{m.client_download_apple_help_content_1()}

+ +
+ ); +}; diff --git a/webnext/src/pages/ClientDownload/assets/apple_hardware_help.mp4 b/webnext/src/shared/components/AppleHelpModal/assets/apple_hardware_help.mp4 similarity index 100% rename from webnext/src/pages/ClientDownload/assets/apple_hardware_help.mp4 rename to webnext/src/shared/components/AppleHelpModal/assets/apple_hardware_help.mp4 diff --git a/webnext/src/shared/defguard-ui/components/Menu/Menu.tsx b/webnext/src/shared/defguard-ui/components/Menu/Menu.tsx index 0e0f754c..6be5111f 100644 --- a/webnext/src/shared/defguard-ui/components/Menu/Menu.tsx +++ b/webnext/src/shared/defguard-ui/components/Menu/Menu.tsx @@ -7,9 +7,16 @@ import { MenuHeader } from './components/MenuHeader'; import { MenuSpacer } from './components/MenuSpacer'; import type { MenuProps } from './types'; -export const Menu = ({ itemGroups, ref, className, onClose, ...props }: MenuProps) => { +export const Menu = ({ + itemGroups, + ref, + className, + onClose, + testId, + ...props +}: MenuProps) => { return ( -
+
{itemGroups.map((group, groupIndex) => ( {isPresent(group.header) && } diff --git a/webnext/src/shared/defguard-ui/components/Menu/components/MenuHeader.tsx b/webnext/src/shared/defguard-ui/components/Menu/components/MenuHeader.tsx index 341b11ae..70e8097f 100644 --- a/webnext/src/shared/defguard-ui/components/Menu/components/MenuHeader.tsx +++ b/webnext/src/shared/defguard-ui/components/Menu/components/MenuHeader.tsx @@ -3,12 +3,13 @@ import { isPresent } from '../../../utils/isPresent'; import { InteractionBox } from '../../InteractionBox/InteractionBox'; import type { MenuHeaderProps } from '../types'; -export const MenuHeader = ({ text, onHelp, onClose }: MenuHeaderProps) => { +export const MenuHeader = ({ text, testId, onHelp, onClose }: MenuHeaderProps) => { return (

{text}

{isPresent(onHelp) && ( diff --git a/webnext/src/shared/defguard-ui/components/Menu/components/MenuItem.tsx b/webnext/src/shared/defguard-ui/components/Menu/components/MenuItem.tsx index fd626e9a..b6bafc86 100644 --- a/webnext/src/shared/defguard-ui/components/Menu/components/MenuItem.tsx +++ b/webnext/src/shared/defguard-ui/components/Menu/components/MenuItem.tsx @@ -8,9 +8,10 @@ export const MenuItem = ({ text, icon, items, + testId, + variant, onClick, onClose, - variant, }: MenuItemProps) => { const hasItems = isPresent(items) && items.length > 0; const hasIcon = isPresent(icon); @@ -25,6 +26,7 @@ export const MenuItem = ({ 'grid-full': hasIcon && hasItems, nested: hasItems, })} + data-testid={testId} onClick={() => { if (!disabled) { onClick?.(); diff --git a/webnext/src/shared/defguard-ui/components/Menu/types.ts b/webnext/src/shared/defguard-ui/components/Menu/types.ts index 93c2c129..8537a932 100644 --- a/webnext/src/shared/defguard-ui/components/Menu/types.ts +++ b/webnext/src/shared/defguard-ui/components/Menu/types.ts @@ -3,8 +3,9 @@ import type { IconKindValue } from '../Icon/icon-types'; export interface MenuProps extends HTMLAttributes { itemGroups: MenuItemsGroup[]; - onClose?: () => void; ref?: Ref; + testId?: string; + onClose?: () => void; } export interface MenuItemsGroup { @@ -15,9 +16,10 @@ export interface MenuItemsGroup { export interface MenuItemProps { text: string; variant?: 'default' | 'danger'; + disabled?: boolean; icon?: IconKindValue; items?: MenuItemProps[]; - disabled?: boolean; + testId?: string; onClick?: () => void; onClose?: () => void; } @@ -25,6 +27,7 @@ export interface MenuItemProps { export interface MenuHeaderProps { text: string; tooltip?: string; + testId?: string; onClose?: () => void; onHelp?: () => void; } diff --git a/webnext/src/shared/query/queryOptions.ts b/webnext/src/shared/query/queryOptions.ts new file mode 100644 index 00000000..cae2776b --- /dev/null +++ b/webnext/src/shared/query/queryOptions.ts @@ -0,0 +1,11 @@ +import { queryOptions } from '@tanstack/react-query'; +import { updateServiceApi } from '../api/update-service'; + +export const getClientArtifactsQueryOptions = queryOptions({ + queryFn: updateServiceApi.getClientArtifacts, + queryKey: ['update-service', 'artifacts'], + staleTime: 180 * 1000, + refetchOnWindowFocus: false, + refetchOnMount: true, + refetchOnReconnect: true, +});