NC Android - gateway settings (#3041)

* feat(nc-android): gateway settings (client)

* feat(nc-android): gateway settings (client)

* feat(nc-android): handle init_config failing

* feat(nc-android): some UI changes

* feat(nc-android): some UI changes
This commit is contained in:
Pierre Dommerc
2023-02-16 12:11:45 +01:00
committed by GitHub
parent b5adf2bb42
commit bc75dd7e15
12 changed files with 184 additions and 28 deletions
+8 -2
View File
@@ -175,10 +175,16 @@ impl State {
self.set_state(ConnectionStatusKind::Connecting, window);
let res = self.init_config().await;
match &res {
match res {
Ok(_) => {}
Err(e) => {
dbg!(e);
log::error!("Failed to initialize: {e}");
// Wait a little to give the user some rudimentary feedback that the click actually
// registered.
tokio::time::sleep(Duration::from_secs(1)).await;
self.set_state(ConnectionStatusKind::Disconnected, window);
return Err(e);
}
};
let (config, keys) = res.unwrap();
@@ -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,8 +32,8 @@ export const AppWindowFrame: FCWithChildren = ({ children }) => {
height: '100vh',
}}
>
<CustomTitleBar path={location.pathname} />
<Box style={{ padding: '16px' }}>{children}</Box>
<CustomTitleBar path={location.pathname} onBack={onBack()} />
<Box style={{ padding: '16px', paddingTop: '24px' }}>{children}</Box>
</Box>
);
};
@@ -28,16 +28,20 @@ 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) => {
if (path !== '/') {
const title = path.split('/').slice(-1);
return (
<Typography textTransform="capitalize" fontWeight={700}>
<Typography textTransform="capitalize" fontSize="16px" fontWeight={700}>
{title}
</Typography>
);
@@ -45,10 +49,11 @@ const getTitleIcon = (path: string) => {
return <NymWordmark width={36} />;
};
export const CustomTitleBar = ({ path = '/' }: { path?: string }) => (
<Box data-tauri-drag-region style={customTitleBarStyles.titlebar}>
export const CustomTitleBar = ({ path = '/', onBack }: { path?: string; onBack?: () => void }) => (
<Box 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)}
{path !== '/' && <Box sx={{ width: '40px' }} />}
</Box>
);
+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,
],
);
@@ -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<{
+5 -8
View File
@@ -9,28 +9,25 @@ const appsSchema = {
export const CompatibleApps = () => (
<Box>
<Typography fontSize="small" color="grey.600" sx={{ mb: 2 }}>
<Typography fontWeight={600} sx={{ mb: 3 }}>
Supported apps
</Typography>
<Typography color="nym.highlight" sx={{ mb: 2 }}>
<Typography color="nym.highlight" fontWeight={400} sx={{ mb: 2 }}>
Messaging apps
</Typography>
<Divider sx={{ mb: 2 }} />
<Box sx={{ mb: 4 }}>
<Box sx={{ mb: 3 }}>
{appsSchema.messagingApps.map((app) => (
<Typography variant="body2" color="grey.400" sx={{ mb: 2 }} key={app}>
{app}
</Typography>
))}
</Box>
<Typography color="nym.highlight" sx={{ mb: 2 }}>
<Divider sx={{ mb: 3 }} />
<Typography color="nym.highlight" fontWeight={400} sx={{ mb: 2 }}>
Wallets
</Typography>
<Divider sx={{ mb: 2 }} />
<Box sx={{ mb: 4 }}>
{appsSchema.wallets.map((wallet) => (
<Typography variant="body2" color="grey.400" sx={{ mb: 2 }} key={wallet}>
@@ -0,0 +1,80 @@
import React, { ChangeEvent, useState } from 'react';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { Box, FormControl, FormControlLabel, FormHelperText, 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>) => {
console.warn('HANERE***');
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} fontSize="14px">
Select your Gateway
</Typography>
<Typography color="grey.300" variant="body2" mb={3}>
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: 3 }}
disabled={connectionStatus === 'connected' || !userDefinedGateway?.isActive}
/>
)}
</FormControl>
</Box>
<Box>
<Typography variant="body2" mb={4}>
To find a gateway go to{' '}
<Typography variant="body2" color="nym.cta">
explorer.nymtech.net/network-components/gateways
</Typography>
</Typography>
<AppVersion />
</Box>
</Stack>
</Box>
);
};
+5 -4
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, Typography } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import { AppVersion } from 'src/components/AppVersion';
@@ -7,15 +7,16 @@ 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>
<List 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>
<ListItemButton disableGutters>
<ListItemIcon sx={{ minWidth: 25 }}>
<item.icon sx={{ fontSize: '18px' }} />
</ListItemIcon>{' '}
+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;
}