Feature/nym connect pick a gateway (#3008)

* Add new route and initial UI

* allow IdentityKeyFormField to have a small size option

* add disabled prop to the shared IdentityKeyFormField component

* defined custom gateway type

* use custom gateway in state

* set and validate custom gateway in settings page

* validate user gateway when moving away from page

* use storage

* hide gateway input when inactive

* add explorer link to settings page
This commit is contained in:
Fouad
2023-02-13 22:47:48 +00:00
committed by GitHub
parent 726a406797
commit 9348722b84
11 changed files with 176 additions and 13 deletions
+19 -1
View File
@@ -1,10 +1,28 @@
import React from 'react';
import { Box } from '@mui/material';
import { useLocation } from 'react-router-dom';
import { useClientContext } from 'src/context/main';
import { CustomTitleBar } from './CustomTitleBar';
export const AppWindowFrame: FCWithChildren = ({ children }) => {
const location = useLocation();
const { userDefinedGateway, setUserDefinedGateway } = useClientContext();
// defined functions to be used when moving away from pages
const onBack = () => {
switch (location.pathname) {
case '/menu/settings':
return () => {
// when the user moves away from the settings page and the gateway is not valid
// set isActive to false
if (!userDefinedGateway?.gateway) {
setUserDefinedGateway((current) => ({ ...current, isActive: false }));
}
};
default:
return undefined;
}
};
return (
<Box
@@ -14,7 +32,7 @@ export const AppWindowFrame: FCWithChildren = ({ children }) => {
height: '100vh',
}}
>
<CustomTitleBar path={location.pathname} />
<CustomTitleBar path={location.pathname} onBack={onBack()} />
<Box style={{ padding: '16px' }}>{children}</Box>
</Box>
);
@@ -29,9 +29,13 @@ const MenuIcon = () => {
return <CustomButton Icon={Menu} onClick={() => navigate('/menu')} />;
};
const ArrowBackIcon = () => {
const ArrowBackIcon = ({ onBack }: { onBack?: () => void }) => {
const navigate = useNavigate();
return <CustomButton Icon={ArrowBack} onClick={() => navigate(-1)} />;
const handleBack = () => {
onBack?.();
navigate(-1);
};
return <CustomButton Icon={ArrowBack} onClick={handleBack} />;
};
const getTitleIcon = (path: string) => {
@@ -46,10 +50,10 @@ const getTitleIcon = (path: string) => {
return <NymWordmark width={36} />;
};
export const CustomTitleBar = ({ path = '/' }: { path?: string }) => (
export const CustomTitleBar = ({ path = '/', onBack }: { path?: string; onBack?: () => void }) => (
<Box data-tauri-drag-region style={customTitleBarStyles.titlebar}>
{/* set width to keep logo centered */}
<Box sx={{ width: '40px' }}>{path === '/' ? <MenuIcon /> : <ArrowBackIcon />}</Box>
<Box sx={{ width: '40px' }}>{path === '/' ? <MenuIcon /> : <ArrowBackIcon onBack={onBack} />}</Box>
{getTitleIcon(path)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CustomButton Icon={Minimize} onClick={() => appWindow.minimize()} />
+45 -6
View File
@@ -4,10 +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 { ConnectionStatusKind, GatewayPerformance } from '../types';
import { ConnectionStatsItem } from '../components/ConnectionStats';
import { ServiceProvider } from '../types/directory';
const FORAGE_KEY = 'nym-connect-user-gateway';
type ModeType = 'light' | 'dark';
export type TClientContext = {
@@ -20,6 +24,7 @@ export type TClientContext = {
gatewayPerformance: GatewayPerformance;
selectedProvider?: ServiceProvider;
showInfoModal: boolean;
userDefinedGateway?: UserDefinedGateway;
setMode: (mode: ModeType) => void;
clearError: () => void;
setConnectionStatus: (connectionStatus: ConnectionStatusKind) => void;
@@ -29,6 +34,7 @@ export type TClientContext = {
setRandomSerivceProvider: () => void;
startConnecting: () => Promise<void>;
startDisconnecting: () => Promise<void>;
setUserDefinedGateway: React.Dispatch<React.SetStateAction<UserDefinedGateway>>;
};
export const ClientContext = createContext({} as TClientContext);
@@ -44,19 +50,43 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
const [appVersion, setAppVersion] = useState<string>();
const [gatewayPerformance, setGatewayPerformance] = useState<GatewayPerformance>('Good');
const [showInfoModal, setShowInfoModal] = useState(false);
const [userDefinedGateway, setUserDefinedGateway] = useState<UserDefinedGateway>({ isActive: false, gateway: '' });
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;
};
const getUserGatewayFromStorage = async (): Promise<UserDefinedGateway | undefined> => {
try {
const gatewayFromStorage = await forage.getItem({ key: FORAGE_KEY })();
return gatewayFromStorage;
} catch (e) {
console.warn(e);
}
return undefined;
};
const initialiseApp = async () => {
const services = await invoke('get_services');
const AppVersion = await getAppVersion();
console.log(services);
const storedUserDefinedGateway = await getUserGatewayFromStorage();
setAppVersion(AppVersion);
setServiceProviders(services as ServiceProvider[]);
if (storedUserDefinedGateway) setUserDefinedGateway(storedUserDefinedGateway);
};
useEvents({
@@ -94,15 +124,19 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
}
}, []);
const setServiceProvider = async (newServiceProvider?: ServiceProvider) => {
if (newServiceProvider) {
await invoke('set_gateway', { gateway: newServiceProvider.gateway });
await invoke('set_service_provider', { serviceProvider: newServiceProvider.address });
}
const shouldUseUserGateway = !!userDefinedGateway.gateway && userDefinedGateway.isActive;
const setServiceProvider = async (newServiceProvider: ServiceProvider) => {
await invoke('set_gateway', {
gateway: newServiceProvider.gateway,
});
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;
};
@@ -110,8 +144,10 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
if (serviceProviders) {
const randomServiceProvider = getRandomSPFromList(serviceProviders);
await setServiceProvider(randomServiceProvider);
await setUserGatewayInStorage(userDefinedGateway);
setSelectedProvider(randomServiceProvider);
}
return undefined;
};
const clearError = () => setError(undefined);
@@ -136,6 +172,8 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
startDisconnecting,
gatewayPerformance,
setShowInfoModal,
userDefinedGateway,
setUserDefinedGateway,
}),
[
mode,
@@ -148,6 +186,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
connectedSince,
gatewayPerformance,
selectedProvider,
userDefinedGateway,
],
);
+2
View File
@@ -9,6 +9,7 @@ const mockValues: TClientContext = {
selectedProvider: { id: '1', description: 'Keybase service provider', gateway: 'abc123', address: '123abc' },
gatewayPerformance: 'Good',
showInfoModal: false,
userDefinedGateway: { isActive: false, gateway: '' },
setShowInfoModal: () => {},
setMode: () => {},
clearError: () => {},
@@ -18,6 +19,7 @@ const mockValues: TClientContext = {
startConnecting: async () => {},
startDisconnecting: async () => {},
setRandomSerivceProvider: () => {},
setUserDefinedGateway: () => {},
};
export const MockProvider: FCWithChildren<{
+85
View File
@@ -0,0 +1,85 @@
import React, { ChangeEvent, useState } from 'react';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { Box, FormControl, FormControlLabel, FormHelperText, Link, Stack, Switch, Typography } from '@mui/material';
import { useClientContext } from 'src/context/main';
import { ConnectionStatusKind } from 'src/types';
import { AppVersion } from 'src/components/AppVersion';
export const Settings = () => {
const { userDefinedGateway, setUserDefinedGateway } = useClientContext();
const [gatewayKey, setGatewayKey] = useState<string | undefined>(userDefinedGateway?.gateway);
const handleIsValidGatewayKey = (isValid: boolean) => {
let gateway: string | undefined;
if (isValid) {
gateway = gatewayKey;
}
setUserDefinedGateway((current) => ({ ...current, gateway }));
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setUserDefinedGateway((current) => ({ ...current, isActive: e.target.checked }));
};
const { connectionStatus } = useClientContext();
return (
<Box height="100%">
<Stack justifyContent="space-between" height="100%">
<Box>
<Typography fontWeight="bold" variant="body2" mb={1}>
Select your Gateway
</Typography>
<Typography color="grey.300" variant="body2" mb={2}>
Use a gateway of your choice
</Typography>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={userDefinedGateway?.isActive}
onChange={handleChange}
disabled={connectionStatus === ConnectionStatusKind.connected}
size="small"
sx={{ ml: 1 }}
/>
}
label={userDefinedGateway?.isActive ? 'On' : 'Off'}
/>
{connectionStatus === ConnectionStatusKind.connected && (
<FormHelperText sx={{ m: 0, my: 1 }}>This setting is disabled during an active connection</FormHelperText>
)}
{userDefinedGateway?.isActive && (
<IdentityKeyFormField
size="small"
placeholder="Gateway identity key"
onChanged={setGatewayKey}
initialValue={gatewayKey}
onValidate={handleIsValidGatewayKey}
sx={{ mt: 1 }}
disabled={connectionStatus === 'connected' || !userDefinedGateway?.isActive}
/>
)}
</FormControl>
</Box>
<Box>
<Typography variant="body2" mb={3}>
To find a gateway go to the{' '}
<Link
underline="none"
target="_blank"
href="https://explorer.nymtech.net/network-components/gateways"
sx={{ cursor: 'pointer' }}
color="nym.cta"
>
Network Explorer
</Link>
</Typography>
<AppVersion />
</Box>
</Stack>
</Box>
);
};
+3 -2
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { Apps, HelpOutline } from '@mui/icons-material';
import { Apps, HelpOutline, Settings } from '@mui/icons-material';
import { Stack, Link, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import { AppVersion } from 'src/components/AppVersion';
@@ -7,13 +7,14 @@ import { AppVersion } from 'src/components/AppVersion';
const menuSchema = [
{ title: 'Supported apps', icon: Apps, path: 'apps' },
{ title: 'How to connect guide', icon: HelpOutline, path: 'guide' },
{ title: 'Settings', icon: Settings, path: 'settings' },
];
export const Menu = () => (
<Stack justifyContent="space-between" height="100%">
<List dense disablePadding>
{menuSchema.map((item) => (
<Link component={RouterLink} to={item.path} underline="none" color="white">
<Link component={RouterLink} to={item.path} underline="none" color="white" key={item.title}>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon sx={{ minWidth: 25 }}>
+2
View File
@@ -4,6 +4,7 @@ import { ConnectionPage } from 'src/pages/connection';
import { Menu } from 'src/pages/menu';
import { CompatibleApps } from 'src/pages/menu/Apps';
import { HelpGuide } from 'src/pages/menu/Guide';
import { Settings } from 'src/pages/menu/Settings';
export const AppRoutes = () => (
<Routes>
@@ -12,6 +13,7 @@ export const AppRoutes = () => (
<Route index element={<Menu />} />
<Route path="apps" element={<CompatibleApps />} />
<Route path="guide" element={<HelpGuide />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
);
+1
View File
@@ -29,6 +29,7 @@ declare module '@mui/material/styles' {
*/
interface NymPalette {
highlight: string;
cta: string;
success: string;
warning: string;
info: string;
+1
View File
@@ -21,6 +21,7 @@ import {
const nymPalette: NymPalette = {
/** emphasises important elements */
highlight: '#21D072',
cta: '#FB6E4E',
success: '#21D073',
info: '#60D7EF',
warning: '#FFE600',
+4
View File
@@ -0,0 +1,4 @@
export interface UserDefinedGateway {
isActive: boolean;
gateway?: string;
}
@@ -19,7 +19,9 @@ export const IdentityKeyFormField: FCWithChildren<{
onValidate?: (isValid: boolean, error?: string) => void;
textFieldProps?: TextFieldProps;
errorText?: string;
size?: 'small' | 'medium';
sx?: SxProps;
disabled?: boolean;
}> = ({
required,
fullWidth,
@@ -33,6 +35,8 @@ export const IdentityKeyFormField: FCWithChildren<{
onValidate,
textFieldProps,
showTickOnValid = true,
size,
disabled,
}) => {
const [value, setValue] = React.useState<string | undefined>(initialValue);
const [validationError, setValidationError] = React.useState<string | undefined>();
@@ -100,6 +104,8 @@ export const IdentityKeyFormField: FCWithChildren<{
defaultValue={initialValue}
onChange={handleChange}
InputLabelProps={{ shrink: true }}
size={size}
disabled={disabled}
/>
);
};