diff --git a/web/src/pages/addDevice/steps/AddDeviceTokenStep/AddDeviceTokenStep.tsx b/web/src/pages/addDevice/steps/AddDeviceTokenStep/AddDeviceTokenStep.tsx index 61a6f48b74..8ab83f0294 100644 --- a/web/src/pages/addDevice/steps/AddDeviceTokenStep/AddDeviceTokenStep.tsx +++ b/web/src/pages/addDevice/steps/AddDeviceTokenStep/AddDeviceTokenStep.tsx @@ -3,6 +3,7 @@ import './style.scss'; import parse from 'html-react-parser'; import { isUndefined } from 'lodash-es'; import { ReactNode, useEffect, useMemo } from 'react'; +import QRCode from 'react-qr-code'; import { useNavigate } from 'react-router'; import { shallow } from 'zustand/shallow'; @@ -13,12 +14,29 @@ import { Card } from '../../../../shared/defguard-ui/components/Layout/Card/Card import { ExpandableCard } from '../../../../shared/defguard-ui/components/Layout/ExpandableCard/ExpandableCard'; import { MessageBox } from '../../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox'; import { MessageBoxType } from '../../../../shared/defguard-ui/components/Layout/MessageBox/types'; +import { isPresent } from '../../../../shared/defguard-ui/utils/isPresent'; import { useAppStore } from '../../../../shared/hooks/store/useAppStore'; import { useAuthStore } from '../../../../shared/hooks/store/useAuthStore'; import useApi from '../../../../shared/hooks/useApi'; import { useClipboard } from '../../../../shared/hooks/useClipboard'; import { useAddDevicePageStore } from '../../hooks/useAddDevicePageStore'; +const useLocalProxy = import.meta.env.DEV; + +const extractProxyPort = (input: string): string | undefined => { + try { + const url = new URL(input); + const port = url.port; + const parsed = port ? parseInt(port, 10) : undefined; + if (parsed && !isNaN(parsed)) { + return `:${parsed}`; + } + return undefined; + } catch { + return undefined; + } +}; + export const AddDeviceTokenStep = () => { const { writeToClipboard } = useClipboard(); const { LL } = useI18nContext(); @@ -40,6 +58,26 @@ export const AddDeviceTokenStep = () => { shallow, ); + const mobileQrData = useMemo(() => { + if (isPresent(url) && isPresent(token)) { + let targetUrl: string; + if (useLocalProxy) { + const proxyPort = extractProxyPort(url) ?? ''; + targetUrl = `http://10.0.2.2${proxyPort}`; + } else { + targetUrl = url; + } + const registration = { + token, + url: targetUrl, + }; + const registrationJson = JSON.stringify(registration); + const encoded = btoa(registrationJson); + return encoded; + } + return undefined; + }, [token, url]); + const tokenActions = useMemo( (): ReactNode[] => [ {

{token}

+ + {isPresent(mobileQrData) && ( + + + + )} ); diff --git a/web/src/pages/addDevice/steps/AddDeviceTokenStep/style.scss b/web/src/pages/addDevice/steps/AddDeviceTokenStep/style.scss index 72f1b14387..277e89b450 100644 --- a/web/src/pages/addDevice/steps/AddDeviceTokenStep/style.scss +++ b/web/src/pages/addDevice/steps/AddDeviceTokenStep/style.scss @@ -3,5 +3,23 @@ .expandable-card { margin-bottom: 25px; } + + #mobile-qr-code { + .expanded-content { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + overflow: hidden; + box-sizing: border-box; + row-gap: 20px; + + p { + max-width: 80%; + margin: 0; + padding: 0; + } + } + } } } diff --git a/web/src/pages/overview-index/OverviewIndexPage.tsx b/web/src/pages/overview-index/OverviewIndexPage.tsx index 4903d259b4..ff297442a5 100644 --- a/web/src/pages/overview-index/OverviewIndexPage.tsx +++ b/web/src/pages/overview-index/OverviewIndexPage.tsx @@ -1,7 +1,7 @@ import './style.scss'; import { useQuery } from '@tanstack/react-query'; -import { range } from 'lodash-es'; +import { orderBy, range } from 'lodash-es'; import { useEffect } from 'react'; import Skeleton from 'react-loading-skeleton'; import { useLocation, useNavigate } from 'react-router'; @@ -35,6 +35,8 @@ export const OverviewIndexPage = () => { queryKey: ['network'], queryFn: getNetworks, placeholderData: (perv) => perv, + select: (networks) => + orderBy(networks, (network) => network.name.toLowerCase(), ['asc']), }); const resetWizard = useWizardStore((state) => state.resetState); diff --git a/web/src/pages/overview-index/components/OverviewNetworkSelection/OverviewNetworkSelection.tsx b/web/src/pages/overview-index/components/OverviewNetworkSelection/OverviewNetworkSelection.tsx index 5f8f544159..117ced75c9 100644 --- a/web/src/pages/overview-index/components/OverviewNetworkSelection/OverviewNetworkSelection.tsx +++ b/web/src/pages/overview-index/components/OverviewNetworkSelection/OverviewNetworkSelection.tsx @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { orderBy } from 'lodash-es'; import { useMemo } from 'react'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; @@ -26,6 +27,8 @@ export const OverviewNetworkSelection = () => { queryKey: ['network'], queryFn: getNetworks, placeholderData: (perv) => perv, + select: (networks) => + orderBy(networks, (network) => network.name.toLowerCase(), ['asc']), }); const selectionValue = useMemo(() => { diff --git a/web/src/pages/overview/OverviewPage.tsx b/web/src/pages/overview/OverviewPage.tsx index 616a34f5ae..09bce1726d 100644 --- a/web/src/pages/overview/OverviewPage.tsx +++ b/web/src/pages/overview/OverviewPage.tsx @@ -47,6 +47,8 @@ export const OverviewPage = () => { queryKey: ['network'], queryFn: getNetworks, placeholderData: (perv) => perv, + select: (networks) => + orderBy(networks, (network) => network.name.toLowerCase(), ['asc']), }); const { data: networkStats } = useQuery({