diff --git a/nym-connect/desktop/src/components/AppWindowFrame.tsx b/nym-connect/desktop/src/components/AppWindowFrame.tsx index 929b0f269f..90e01fffa7 100644 --- a/nym-connect/desktop/src/components/AppWindowFrame.tsx +++ b/nym-connect/desktop/src/components/AppWindowFrame.tsx @@ -6,12 +6,13 @@ import { CustomTitleBar } from './CustomTitleBar'; export const AppWindowFrame: FCWithChildren = ({ children }) => { const location = useLocation(); - const { userDefinedGateway, setUserDefinedGateway } = useClientContext(); + const { userDefinedGateway, setUserDefinedGateway, userDefinedSPAddress, setUserDefinedSPAddress } = + useClientContext(); // defined functions to be used when moving away from pages const onBack = () => { switch (location.pathname) { - case '/menu/settings': + case '/menu/settings/gateway': return () => { // when the user moves away from the settings page and the gateway is not valid // set isActive to false @@ -19,6 +20,14 @@ export const AppWindowFrame: FCWithChildren = ({ children }) => { setUserDefinedGateway((current) => ({ ...current, isActive: false })); } }; + case '/menu/settings/service-provider': + return () => { + // when the user moves away from the settings page and the sp is not valid + // set isActive to false + if (!userDefinedSPAddress?.address) { + setUserDefinedSPAddress((current) => ({ ...current, isActive: false })); + } + }; default: return undefined; } diff --git a/nym-connect/desktop/src/context/main.tsx b/nym-connect/desktop/src/context/main.tsx index 2efdc49289..b999b957a2 100644 --- a/nym-connect/desktop/src/context/main.tsx +++ b/nym-connect/desktop/src/context/main.tsx @@ -4,13 +4,14 @@ import { invoke } from '@tauri-apps/api'; import { Error } from 'src/types/error'; import { getVersion } from '@tauri-apps/api/app'; import { useEvents } from 'src/hooks/events'; -import { UserDefinedGateway } from 'src/types/gateway'; -import { forage } from '@tauri-apps/tauri-forage'; +import { UserDefinedGateway, UserDefinedSPAddress } from 'src/types/service-provider'; +import { getItemFromStorage, setItemInStorage } from 'src/utils'; import { ConnectionStatusKind, GatewayPerformance } from '../types'; import { ConnectionStatsItem } from '../components/ConnectionStats'; import { ServiceProvider } from '../types/directory'; -const FORAGE_KEY = 'nym-connect-user-gateway'; +const FORAGE_GATEWAY_KEY = 'nym-connect-user-gateway'; +const FORAGE_SP_KEY = 'nym-connect-user-sp'; type ModeType = 'light' | 'dark'; @@ -25,6 +26,7 @@ export type TClientContext = { selectedProvider?: ServiceProvider; showInfoModal: boolean; userDefinedGateway?: UserDefinedGateway; + userDefinedSPAddress: UserDefinedSPAddress; serviceProviders?: ServiceProvider[]; setMode: (mode: ModeType) => void; clearError: () => void; @@ -32,10 +34,11 @@ export type TClientContext = { setConnectionStats: (connectionStats: ConnectionStatsItem[] | undefined) => void; setConnectedSince: (connectedSince: DateTime | undefined) => void; setShowInfoModal: (show: boolean) => void; - setRandomSerivceProvider: () => void; + setSerivceProvider: () => void; startConnecting: () => Promise; startDisconnecting: () => Promise; setUserDefinedGateway: React.Dispatch>; + setUserDefinedSPAddress: React.Dispatch>; }; export const ClientContext = createContext({} as TClientContext); @@ -51,43 +54,39 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => { const [appVersion, setAppVersion] = useState(); const [gatewayPerformance, setGatewayPerformance] = useState('Good'); const [showInfoModal, setShowInfoModal] = useState(false); - const [userDefinedGateway, setUserDefinedGateway] = useState({ isActive: false, gateway: '' }); + const [userDefinedGateway, setUserDefinedGateway] = useState({ + isActive: false, + gateway: undefined, + }); + const [userDefinedSPAddress, setUserDefinedSPAddress] = useState({ + isActive: false, + address: undefined, + }); const getAppVersion = async () => { const version = await getVersion(); return version; }; - const setUserGatewayInStorage = async (gateway: UserDefinedGateway) => { - try { - await forage.setItem({ - key: FORAGE_KEY, - value: gateway, - } as any)(); - } catch (e) { - console.warn(e); - } - return undefined; - }; + useEffect(() => { + setItemInStorage({ key: FORAGE_GATEWAY_KEY, value: userDefinedGateway }); + }, [userDefinedGateway]); - const getUserGatewayFromStorage = async (): Promise => { - try { - const gatewayFromStorage = await forage.getItem({ key: FORAGE_KEY })(); - return gatewayFromStorage; - } catch (e) { - console.warn(e); - } - return undefined; - }; + useEffect(() => { + setItemInStorage({ key: FORAGE_SP_KEY, value: userDefinedSPAddress }); + }, [userDefinedSPAddress]); const initialiseApp = async () => { const services = await invoke('get_services'); const AppVersion = await getAppVersion(); - const storedUserDefinedGateway = await getUserGatewayFromStorage(); + const storedUserDefinedGateway = await getItemFromStorage({ key: FORAGE_GATEWAY_KEY }); + const storedUserDefinedSP = await getItemFromStorage({ key: FORAGE_SP_KEY }); setAppVersion(AppVersion); setServiceProviders(services as ServiceProvider[]); + if (storedUserDefinedGateway) setUserDefinedGateway(storedUserDefinedGateway); + if (storedUserDefinedSP) setUserDefinedSPAddress(storedUserDefinedSP); }; useEvents({ @@ -126,27 +125,37 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => { }, []); const shouldUseUserGateway = !!userDefinedGateway.gateway && userDefinedGateway.isActive; + const shouldUseUserSP = !!userDefinedSPAddress.address && userDefinedSPAddress.isActive; const setServiceProvider = async (newServiceProvider: ServiceProvider) => { await invoke('set_gateway', { - gateway: newServiceProvider.gateway, + gateway: shouldUseUserGateway ? userDefinedGateway.gateway : newServiceProvider.gateway, + }); + await invoke('set_service_provider', { + serviceProvider: shouldUseUserSP ? userDefinedSPAddress.address : newServiceProvider.address, }); - await invoke('set_service_provider', { serviceProvider: newServiceProvider.address }); }; const getRandomSPFromList = (services: ServiceProvider[]) => { const randomSelection = services[Math.floor(Math.random() * services.length)]; - - if (shouldUseUserGateway) return { ...randomSelection, gateway: userDefinedGateway.gateway } as ServiceProvider; return randomSelection; }; - const setRandomSerivceProvider = async () => { + const buildServiceProvider = async (serviceProvider: ServiceProvider) => { + const sp = { ...serviceProvider }; + + if (shouldUseUserGateway) sp.gateway = userDefinedGateway.gateway as string; + if (shouldUseUserSP) sp.address = userDefinedSPAddress.address as string; + + return sp; + }; + + const setSerivceProvider = async () => { if (serviceProviders) { const randomServiceProvider = getRandomSPFromList(serviceProviders); - await setServiceProvider(randomServiceProvider); - await setUserGatewayInStorage(userDefinedGateway); - setSelectedProvider(randomServiceProvider); + const withUserDefinitions = await buildServiceProvider(randomServiceProvider); + await setServiceProvider(withUserDefinitions); + setSelectedProvider(withUserDefinitions); } return undefined; }; @@ -169,13 +178,15 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => { serviceProviders, connectedSince, setConnectedSince, - setRandomSerivceProvider, + setSerivceProvider, startConnecting, startDisconnecting, gatewayPerformance, setShowInfoModal, + userDefinedSPAddress, userDefinedGateway, setUserDefinedGateway, + setUserDefinedSPAddress, }), [ mode, @@ -190,6 +201,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => { gatewayPerformance, selectedProvider, userDefinedGateway, + userDefinedSPAddress, ], ); diff --git a/nym-connect/desktop/src/context/mocks/main.tsx b/nym-connect/desktop/src/context/mocks/main.tsx index 6d0f7d9a82..78028b03f8 100644 --- a/nym-connect/desktop/src/context/mocks/main.tsx +++ b/nym-connect/desktop/src/context/mocks/main.tsx @@ -10,6 +10,7 @@ const mockValues: TClientContext = { gatewayPerformance: 'Good', showInfoModal: false, userDefinedGateway: { isActive: false, gateway: '' }, + userDefinedSPAddress: { isActive: false, address: '' }, setShowInfoModal: () => {}, setMode: () => {}, clearError: () => {}, @@ -18,8 +19,9 @@ const mockValues: TClientContext = { setConnectionStatus: () => {}, startConnecting: async () => {}, startDisconnecting: async () => {}, - setRandomSerivceProvider: () => {}, + setSerivceProvider: () => {}, setUserDefinedGateway: () => {}, + setUserDefinedSPAddress: () => {}, }; export const MockProvider: FCWithChildren<{ diff --git a/nym-connect/desktop/src/pages/connection/index.tsx b/nym-connect/desktop/src/pages/connection/index.tsx index 028ddcf21d..2ba081331f 100644 --- a/nym-connect/desktop/src/pages/connection/index.tsx +++ b/nym-connect/desktop/src/pages/connection/index.tsx @@ -28,7 +28,7 @@ export const ConnectionPage = () => { // eslint-disable-next-line default-case switch (currentStatus) { case 'disconnected': - await context.setRandomSerivceProvider(); + await context.setSerivceProvider(); await context.startConnecting(); context.setConnectedSince(DateTime.now()); context.setShowInfoModal(true); diff --git a/nym-connect/desktop/src/pages/menu/settings/ServiceProviderSettings.tsx b/nym-connect/desktop/src/pages/menu/settings/ServiceProviderSettings.tsx index 8906c67129..8f18caaacd 100644 --- a/nym-connect/desktop/src/pages/menu/settings/ServiceProviderSettings.tsx +++ b/nym-connect/desktop/src/pages/menu/settings/ServiceProviderSettings.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent, useState } from 'react'; +import React, { ChangeEvent } from 'react'; import { Autocomplete, Box, @@ -15,18 +15,26 @@ import { ConnectionStatusKind } from 'src/types'; import { useClientContext } from 'src/context/main'; export const ServiceProviderSettings = () => { - const [spAddress, setSPAddress] = useState(); - const [isOn, setIsOn] = useState(false); - - const { connectionStatus, serviceProviders } = useClientContext(); + const { connectionStatus, serviceProviders, userDefinedSPAddress, setUserDefinedSPAddress } = useClientContext(); const toggleOnOff = (e: ChangeEvent) => { - if (!isOn) setSPAddress(undefined); - setIsOn(e.target.checked); + setUserDefinedSPAddress((current) => ({ ...current, isActive: e.target.checked })); }; const handleSelectFromList = (value: string | null) => { - setSPAddress(value ?? undefined); + setUserDefinedSPAddress((current) => ({ ...current, address: value ?? undefined })); + }; + + const getSPDescription = (spAddress: string) => { + const match = serviceProviders?.find((sp) => sp.address === spAddress); + + if (match) return match.description; + + return 'The service provider specified is not in our known list.'; + }; + + const validateInput = (value: string) => { + setUserDefinedSPAddress((current) => ({ ...current, address: !value.length ? undefined : value })); }; return ( @@ -43,40 +51,43 @@ export const ServiceProviderSettings = () => { } - label={isOn ? 'On' : 'Off'} + label={userDefinedSPAddress.isActive ? 'On' : 'Off'} /> {connectionStatus === ConnectionStatusKind.connected && ( This setting is disabled during an active connection )} - {isOn && serviceProviders && ( + {userDefinedSPAddress.isActive && serviceProviders && ( `${sp.address.substring(0, 20)}...`)} + options={serviceProviders.map((sp) => sp.address)} freeSolo + value={userDefinedSPAddress.address || ''} onChange={(e, value) => handleSelectFromList(value)} - value={spAddress} size="small" renderInput={(params) => ( console.log(e.target.value)} placeholder="Service provider" + onChange={(e) => validateInput(e.target.value)} /> )} ListboxProps={{ style: { background: 'unset', fontSize: '14px' } }} /> )} + {userDefinedSPAddress.address && userDefinedSPAddress.isActive && ( + {getSPDescription(userDefinedSPAddress.address)} + )} diff --git a/nym-connect/desktop/src/types/gateway.ts b/nym-connect/desktop/src/types/gateway.ts deleted file mode 100644 index 735a3cdb03..0000000000 --- a/nym-connect/desktop/src/types/gateway.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface UserDefinedGateway { - isActive: boolean; - gateway?: string; -} diff --git a/nym-connect/desktop/src/types/service-provider.ts b/nym-connect/desktop/src/types/service-provider.ts new file mode 100644 index 0000000000..ed370e2755 --- /dev/null +++ b/nym-connect/desktop/src/types/service-provider.ts @@ -0,0 +1,9 @@ +export interface UserDefinedGateway { + isActive: boolean; + gateway?: string; +} + +export interface UserDefinedSPAddress { + isActive: boolean; + address?: string; +} diff --git a/nym-connect/desktop/src/types/storage.ts b/nym-connect/desktop/src/types/storage.ts new file mode 100644 index 0000000000..ca792b65d9 --- /dev/null +++ b/nym-connect/desktop/src/types/storage.ts @@ -0,0 +1,4 @@ +export interface StorageKeyValue { + key: string; + value: T; +} diff --git a/nym-connect/desktop/src/utils/index.ts b/nym-connect/desktop/src/utils/index.ts index fefef166d7..04b415c026 100644 --- a/nym-connect/desktop/src/utils/index.ts +++ b/nym-connect/desktop/src/utils/index.ts @@ -1,6 +1,8 @@ import { useEffect, useRef } from 'react'; import { EventName, listen, UnlistenFn, EventCallback } from '@tauri-apps/api/event'; import { invoke } from '@tauri-apps/api'; +import { forage } from '@tauri-apps/tauri-forage'; +import { StorageKeyValue } from 'src/types/storage'; export const useTauriEvents = (event: EventName, handler: EventCallback) => { const unlisten = useRef(); @@ -22,3 +24,25 @@ export const useTauriEvents = (event: EventName, handler: EventCallback) = export const toggleLogViewer = async () => { await invoke('help_log_toggle_window'); }; + +export async function setItemInStorage({ key, value }: StorageKeyValue) { + try { + await forage.setItem({ + key, + value, + } as any)(); + } catch (e) { + console.warn(e); + } + return undefined; +} + +export const getItemFromStorage = async ({ key }: Pick, 'key'>) => { + try { + const gatewayFromStorage = await forage.getItem({ key })(); + return gatewayFromStorage; + } catch (e) { + console.warn(e); + } + return undefined; +};