Merge pull request #1427 from nymtech/feature/nym-connect-directory

nym-connect: add ability to select service provider and gateway
This commit is contained in:
Jon Häggblad
2022-07-04 15:50:17 +02:00
committed by GitHub
21 changed files with 365 additions and 67 deletions
+2
View File
@@ -9,6 +9,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- socks5 client/websocket client: add `--force-register-gateway` flag, useful when rerunning init ([#1353])
- nym-connect: initial proof-of-concept of a UI around the socks5 client was added
- nym-connect: add ability to select network requester and gateway ([#1427]).
- all: added network compilation target to `--help` (or `--version`) commands ([#1256]).
- explorer-api: learned how to sum the delegations by owner in a new endpoint.
- explorer-api: add apy values to `mix_nodes` endpoint
@@ -63,6 +64,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
[#1388]: https://github.com/nymtech/nym/pull/1388
[#1393]: https://github.com/nymtech/nym/pull/1393
[#1404]: https://github.com/nymtech/nym/pull/1404
[#1427]: https://github.com/nymtech/nym/pull/1427
## [nym-contracts-v1.0.1](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.1) (2022-06-22)
+1
View File
@@ -3385,6 +3385,7 @@ dependencies = [
"nym-socks5-client",
"pretty_env_logger",
"rand 0.7.3",
"reqwest",
"serde",
"serde_json",
"tauri",
+1
View File
@@ -35,6 +35,7 @@ url = "2.2"
log = "0.4"
pretty_env_logger = "0.4.0"
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", branch = "release"}
reqwest = { version = "0.11", features = ["json"] }
client-core = { path = "../../clients/client-core" }
config = { path = "../../common/config" }
+47 -21
View File
@@ -2,31 +2,52 @@ use std::path::PathBuf;
use client_core::config::GatewayEndpoint;
use log::info;
use std::sync::Arc;
use tokio::sync::RwLock;
use client_core::config::Config as BaseConfig;
use config::NymConfig;
use nym_socks5::client::config::Config as Socks5Config;
pub static SOCKS5_CONFIG_ID: &str = "nym-connect";
use crate::{error::BackendError, state::State};
// This is an open-proxy network-requester for testing
// TODO: make this configurable from the UI
// TODO: once we can set this is the UI, consider just removing it, and put in guards to halt if
// user hasn't chosen the provider
pub static PROVIDER_ADDRESS: &str = "8CrdmK4mYgZ5caMxGU4AvNeT1dXL8VSbgMYAjSFvnfut.2GLdZ1Jn9vkTBMf858evGNGDsPoeivUPw7zFNceLiLX3@BNjYZPxzcJwczXHHgBxCAyVJKxN6LPteDRrKapxWmexv";
pub static SOCKS5_CONFIG_ID: &str = "nym-connect";
const DEFAULT_ETH_ENDPOINT: &str = "https://rinkeby.infura.io/v3/00000000000000000000000000000000";
const DEFAULT_ETH_PRIVATE_KEY: &str =
"0000000000000000000000000000000000000000000000000000000000000001";
#[tauri::command]
pub fn get_config_file_location() -> String {
let id: &str = SOCKS5_CONFIG_ID;
Config::config_file_location(id)
.to_string_lossy()
.to_string()
pub fn append_config_id(gateway_id: &str) -> String {
use std::fmt::Write as _;
let mut id = SOCKS5_CONFIG_ID.to_string();
write!(id, "-{}", gateway_id).expect("Failed to set config id");
id
}
#[tauri::command]
pub async fn get_config_id(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<String, BackendError> {
let guard = state.read().await;
// TODO: return error instead
let gateway_id = guard
.get_gateway()
.as_ref()
.expect("The config id can not be determined before setting the gateway");
Ok(append_config_id(gateway_id))
}
#[tauri::command]
pub async fn get_config_file_location(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<String, BackendError> {
let id = get_config_id(state).await?;
Ok(Config::config_file_location(&id)
.to_string_lossy()
.to_string())
}
#[derive(Debug)]
pub struct Config {
socks5: Socks5Config,
}
@@ -55,9 +76,7 @@ impl Config {
self.socks5.get_base_mut()
}
pub async fn init(service_provider: Option<&String>, chosen_gateway_id: Option<&String>) {
let service_provider = service_provider.map_or(PROVIDER_ADDRESS, String::as_str);
let chosen_gateway_id = chosen_gateway_id.map(String::as_str);
pub async fn init(service_provider: &str, chosen_gateway_id: &str) {
info!("Initialising...");
init_socks5(service_provider, chosen_gateway_id).await;
info!("Configuration saved 🚀");
@@ -68,16 +87,17 @@ impl Config {
}
}
pub async fn init_socks5(provider_address: &str, chosen_gateway_id: Option<&str>) {
pub async fn init_socks5(provider_address: &str, chosen_gateway_id: &str) {
log::info!("Initialising client...");
let id: &str = SOCKS5_CONFIG_ID;
// Append the gateway id to the name id that we store the config under
let id = append_config_id(chosen_gateway_id);
log::debug!(
"Attempting to use config file location: {}",
Config::config_file_location(id).to_string_lossy(),
Config::config_file_location(&id).to_string_lossy(),
);
let already_init = Config::config_file_location(id).exists();
let already_init = Config::config_file_location(&id).exists();
if already_init {
log::info!(
"SOCKS5 client \"{}\" was already initialised before! \
@@ -92,7 +112,7 @@ pub async fn init_socks5(provider_address: &str, chosen_gateway_id: Option<&str>
let register_gateway = !already_init || user_wants_force_register;
log::trace!("Creating config for id: {}", id);
let mut config = Config::new(id, provider_address);
let mut config = Config::new(id.as_str(), provider_address);
// As far as I'm aware, these two are not used, they are only set because the socks5 init code
// requires them for initialising the bandwidth controller.
@@ -103,7 +123,13 @@ pub async fn init_socks5(provider_address: &str, chosen_gateway_id: Option<&str>
.get_base_mut()
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY);
let gateway = setup_gateway(id, register_gateway, chosen_gateway_id, config.get_socks5()).await;
let gateway = setup_gateway(
&id,
register_gateway,
Some(chosen_gateway_id),
config.get_socks5(),
)
.await;
config.get_base_mut().with_gateway_endpoint(gateway);
let config_save_location = config.get_socks5().get_config_file_save_location();
+5
View File
@@ -14,6 +14,11 @@ pub enum BackendError {
NoServiceProviderSet,
#[error("No gateway provider set")]
NoGatewaySet,
#[error("{source}")]
ReqwestError {
#[from]
source: reqwest::Error,
},
}
impl Serialize for BackendError {
+6
View File
@@ -35,6 +35,7 @@ fn main() {
.manage(Arc::new(RwLock::new(State::new())))
.invoke_handler(tauri::generate_handler![
crate::config::get_config_file_location,
crate::config::get_config_id,
crate::operations::connection::connect::get_gateway,
crate::operations::connection::connect::get_service_provider,
crate::operations::connection::connect::set_gateway,
@@ -42,6 +43,7 @@ fn main() {
crate::operations::connection::connect::start_connecting,
crate::operations::connection::disconnect::start_disconnecting,
crate::operations::window::hide_window,
crate::operations::directory::get_services,
])
.menu(Menu::new().add_default_app_submenu_if_macos())
.system_tray(create_tray_menu())
@@ -61,5 +63,9 @@ fn setup_logging() {
log_builder
.filter_module("handlebars", log::LevelFilter::Warn)
.filter_module("mio", log::LevelFilter::Warn)
.filter_module("sled", log::LevelFilter::Warn)
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
.filter_module("tungstenite", log::LevelFilter::Warn)
.init();
}
+20
View File
@@ -30,3 +30,23 @@ pub const APP_EVENT_CONNECTION_STATUS_CHANGED: &str = "app:connection-status-cha
pub struct AppEventConnectionStatusChangedPayload {
pub status: ConnectionStatusKind,
}
#[cfg_attr(test, derive(ts_rs::TS))]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct DirectoryService {
pub id: String,
pub description: String,
pub items: Vec<DirectoryServiceProvider>,
}
#[cfg_attr(test, derive(ts_rs::TS))]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct DirectoryServiceProvider {
pub id: String,
pub description: String,
/// Address of the network requester in the form "<gateway_id>.<service_provider_id>"
/// e.g. DpB3cHAchJiNBQi5FrZx2csXb1mrHkpYh9Wzf8Rjsuko.ANNWrvHqMYuertHGHUrZdBntQhpzfbWekB39qez9U2Vx@2BuMSfMW3zpeAjKXyKLhmY4QW1DXurrtSPEJ6CjX3SEh
pub address: String,
/// Address of the gateway, e.g. 2BuMSfMW3zpeAjKXyKLhmY4QW1DXurrtSPEJ6CjX3SEh
pub gateway: String,
}
@@ -11,6 +11,9 @@ pub async fn start_connecting(
) -> Result<ConnectResult, BackendError> {
let mut guard = state.write().await;
log::trace!("Start connecting with:");
log::trace!(" service_provider: {:?}", guard.get_service_provider());
log::trace!(" gateway: {:?}", guard.get_gateway());
guard.start_connecting(&window).await;
Ok(ConnectResult {
@@ -35,6 +38,7 @@ pub async fn set_service_provider(
service_provider: String,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<(), BackendError> {
log::trace!("Setting service_provider: {service_provider}");
let mut guard = state.write().await;
guard.set_service_provider(service_provider);
Ok(())
@@ -56,6 +60,7 @@ pub async fn set_gateway(
gateway: String,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<(), BackendError> {
log::trace!("Setting gateway: {gateway}");
let mut guard = state.write().await;
guard.set_gateway(gateway);
Ok(())
@@ -0,0 +1,14 @@
use crate::error::BackendError;
use crate::models::DirectoryService;
static SERVICE_PROVIDER_WELLKNOWN_URL: &str =
"https://nymtech.net/.wellknown/connect/service-providers.json";
#[tauri::command]
pub async fn get_services() -> Result<Vec<DirectoryService>, BackendError> {
let res = reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL)
.await?
.json::<Vec<DirectoryService>>()
.await?;
Ok(res)
}
@@ -1,2 +1,3 @@
pub mod connection;
pub mod directory;
pub mod window;
+24 -10
View File
@@ -8,10 +8,12 @@ use config::NymConfig;
use nym_socks5::client::NymClient as Socks5NymClient;
use nym_socks5::client::{Socks5ControlMessage, Socks5ControlMessageSender};
use crate::config::SOCKS5_CONFIG_ID;
use crate::models::{
AppEventConnectionStatusChangedPayload, ConnectionStatusKind,
APP_EVENT_CONNECTION_STATUS_CHANGED,
use crate::{
config::append_config_id,
models::{
AppEventConnectionStatusChangedPayload, ConnectionStatusKind,
APP_EVENT_CONNECTION_STATUS_CHANGED,
},
};
use tauri::Manager;
@@ -64,7 +66,15 @@ impl State {
}
pub async fn init_config(&self) {
crate::config::Config::init(self.service_provider.as_ref(), self.gateway.as_ref()).await;
let service_provider = self
.service_provider
.as_ref()
.expect("Attempting to init without service provider");
let gateway = self
.gateway
.as_ref()
.expect("Attempting to init without gateway");
crate::config::Config::init(service_provider, gateway).await;
}
pub async fn start_connecting(&mut self, window: &tauri::Window<tauri::Wry>) {
@@ -76,7 +86,12 @@ impl State {
self.init_config().await;
// Kick of the main task and get the channel for controlling it
let (sender, used_gateway) = start_nym_socks5_client();
let id = append_config_id(
self.gateway
.as_ref()
.expect("Attempting to start without gateway"),
);
let (sender, used_gateway) = start_nym_socks5_client(&id);
self.gateway = Some(used_gateway.gateway_id);
self.socks5_client_sender = Some(sender);
@@ -99,10 +114,9 @@ impl State {
}
}
fn start_nym_socks5_client() -> (Socks5ControlMessageSender, GatewayEndpoint) {
let id: &str = SOCKS5_CONFIG_ID;
info!("Loading config from file");
fn start_nym_socks5_client(id: &str) -> (Socks5ControlMessageSender, GatewayEndpoint) {
info!("Loading config from file: {id}");
// TODO: handle this gracefully!
let config = nym_socks5::client::config::Config::load_from_file(Some(id)).unwrap();
let used_gateway = config.get_base().get_gateway_endpoint().clone();
+11 -1
View File
@@ -7,6 +7,7 @@ import { ConnectedLayout } from './layouts/ConnectedLayout';
export const App: React.FC = () => {
const context = useClientContext();
const [busy, setBusy] = React.useState<boolean>();
const handleConnectClick = React.useCallback(async () => {
const oldStatus = context.connectionStatus;
if (oldStatus === ConnectionStatusKind.connected || oldStatus === ConnectionStatusKind.disconnected) {
@@ -29,7 +30,15 @@ export const App: React.FC = () => {
context.connectionStatus === ConnectionStatusKind.disconnected ||
context.connectionStatus === ConnectionStatusKind.connecting
) {
return <DefaultLayout status={context.connectionStatus} busy={busy} onConnectClick={handleConnectClick} />;
return (
<DefaultLayout
status={context.connectionStatus}
busy={busy}
onConnectClick={handleConnectClick}
services={context.services}
onServiceProviderChange={context.setServiceProvider}
/>
);
}
return (
@@ -40,6 +49,7 @@ export const App: React.FC = () => {
ipAddress="127.0.0.1"
port={1080}
connectedSince={context.connectedSince}
serviceProvider={context.serviceProvider}
stats={[
{
label: 'in:',
@@ -1,6 +1,5 @@
import React from 'react';
import { Box } from '@mui/material';
import { invoke } from '@tauri-apps/api/tauri';
export const AppWindowFrame: React.FC = ({ children }) => (
<Box
@@ -14,11 +13,10 @@ export const AppWindowFrame: React.FC = ({ children }) => (
}}
>
<Box display="flex" justifyContent="space-between" alignItems="center">
<svg width="22" height="6" viewBox="0 0 22 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.86777 6H5.35495L1.22609 1.32517V6H0V0H1.54986L5.67872 4.67354V0H6.86777V6ZM20.4496 0L18.5658 2.13277L16.6821 0H15.1322V6H16.3578V1.32517L18.2959 3.52046C18.4457 3.68998 18.6865 3.68998 18.8363 3.52046L20.7745 1.32517V6H22V0H20.4496ZM10.4063 3.13181V6H11.6318V3.13181L14.4527 0H12.9028L11.018 2.13277L9.13421 0H7.58435L10.4063 3.13181Z"
fill="#F2F2F2"
/>
<svg width="22" height="6" viewBox="0 0 210 56" fill="#F2F2F2" xmlns="http://www.w3.org/2000/svg">
<path d="M45.8829 0.142822H45.7169V0.28114V48.637L25.3289 0.225818L25.3012 0.142822H25.1905H13.6272H0.652966H0.514648V0.28114V55.7189V55.8572H0.652966H13.6272H13.7655V55.7189V7.28002L34.2365 55.7742L34.2642 55.8572H34.3748H45.8829H58.8294H58.9677V55.7189V0.28114V0.142822H58.8294H45.8829Z" />
<path d="M209.347 0.142822H184.616H184.477L184.45 0.253483L171.78 48.8583L159.082 0.253483L159.054 0.142822H158.944H134.157H133.991V0.28114V55.7189V55.8572H134.157H147.104H147.242V55.7189V7.66731L159.774 55.7466L159.801 55.8572H159.94H183.564H183.675L183.703 55.7466L196.234 7.66731V55.7189V55.8572H196.373H209.347H209.485V55.7189V0.28114V0.142822H209.347Z" />
<path d="M112.663 0.142822H112.58L112.552 0.198153L96.8116 27.5574L80.988 0.198153L80.9604 0.142822H80.8774H65.9114H65.6348L65.7731 0.364136L90.1447 42.5787V55.7189V55.8572H90.283H103.257H103.396V55.7189V42.5787L127.767 0.364136L127.905 0.142822H127.629H112.663Z" />
</svg>
</Box>
{children}
@@ -3,17 +3,21 @@ import { ConnectionStatusKind } from '../types';
export const ConnectionButton: React.FC<{
status: ConnectionStatusKind;
disabled?: boolean;
busy?: boolean;
isError?: boolean;
onClick?: (status: ConnectionStatusKind) => void;
}> = ({ status, isError, onClick, busy }) => {
}> = ({ status, disabled, isError, onClick, busy }) => {
const [hover, setHover] = React.useState<boolean>(false);
const handleClick = React.useCallback(() => {
if (disabled === true) {
return;
}
if (onClick) {
onClick(status);
}
}, [status]);
}, [status, disabled]);
const statusText = getStatusText(status, hover);
const statusTextColor = isError ? '#40475C' : '#FFF';
@@ -21,16 +25,17 @@ export const ConnectionButton: React.FC<{
return (
<svg
opacity={disabled ? 0.75 : 1}
width="208"
height="208"
viewBox="0 0 208 208"
fill="none"
xmlns="http://www.w3.org/2000/svg"
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onMouseEnter={() => !disabled && setHover(true)}
onMouseLeave={() => !disabled && setHover(false)}
>
<g transform="translate(-46 -46)">
<g onClick={handleClick} style={{ cursor: 'pointer' }}>
<g onClick={handleClick} style={{ cursor: disabled ? 'not-allowed' : 'pointer' }}>
<g filter="url(#filter0_f_2_303)">
<circle cx="150" cy="150" r="70" fill="#3B445F" />
</g>
@@ -4,6 +4,7 @@ import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined';
import { DateTime } from 'luxon';
import { ConnectionStatusKind } from '../types';
import { ServiceProvider } from '../types/directory';
const FONT_SIZE = '16px';
const FONT_WEIGHT = '600';
@@ -57,7 +58,8 @@ const ConnectionStatusContent: React.FC<{
export const ConnectionStatus: React.FC<{
status: ConnectionStatusKind;
connectedSince?: DateTime;
}> = ({ status, connectedSince }) => {
serviceProvider?: ServiceProvider;
}> = ({ status, connectedSince, serviceProvider }) => {
const color =
status === ConnectionStatusKind.connected || status === ConnectionStatusKind.disconnecting ? '#21D072' : '#888';
const [duration, setDuration] = React.useState<string>();
@@ -72,13 +74,16 @@ export const ConnectionStatus: React.FC<{
};
}, [status, connectedSince]);
return (
<Box display="flex" justifyContent="space-between">
<Box color={color} fontSize={FONT_SIZE} display="flex" alignItems="center">
<ConnectionStatusContent status={status} />
<>
<Box display="flex" justifyContent="space-between">
<Box color={color} fontSize={FONT_SIZE} display="flex" alignItems="center">
<ConnectionStatusContent status={status} />
</Box>
<Typography color={color} fontWeight={FONT_WEIGHT} fontStyle={FONT_STYLE}>
{status === ConnectionStatusKind.connected && duration}
</Typography>
</Box>
<Typography color={color} fontWeight={FONT_WEIGHT} fontStyle={FONT_STYLE}>
{status === ConnectionStatusKind.connected && duration}
</Typography>
</Box>
<Box>{serviceProvider && <Typography fontSize={12}>{serviceProvider.description}</Typography>}</Box>
</>
);
};
@@ -0,0 +1,110 @@
import React from 'react';
import IconButton from '@mui/material/IconButton';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ArrowDropDownCircleIcon from '@mui/icons-material/ArrowDropDownCircle';
import { Box, CircularProgress, Stack, Tooltip, Typography } from '@mui/material';
import { ServiceProvider, Services } from '../types/directory';
export const ServiceProviderSelector: React.FC<{
onChange?: (serviceProvider: ServiceProvider) => void;
services?: Services;
}> = ({ services, onChange }) => {
const [serviceProvider, setServiceProvider] = React.useState<ServiceProvider | undefined>();
const textEl = React.useRef<null | HTMLElement>(null);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = () => {
setAnchorEl(textEl.current);
};
const handleClose = (newServiceProvider?: ServiceProvider) => {
if (newServiceProvider) {
setServiceProvider(newServiceProvider);
onChange?.(newServiceProvider);
}
setAnchorEl(null);
};
if (!services) {
return (
<Box display="flex" alignItems="center" justifyContent="space-between" sx={{ mt: 3 }}>
<Typography fontSize={14} fontWeight={700} color={(theme) => theme.palette.common.white}>
<CircularProgress size={14} sx={{ mr: 1 }} color="inherit" />
Loading services...
</Typography>
<IconButton id="service-provider-button" disabled>
<ArrowDropDownCircleIcon />
</IconButton>
</Box>
);
}
return (
<>
<Box display="flex" alignItems="center" justifyContent="space-between" sx={{ mt: 3 }}>
<Typography
ref={textEl}
fontSize={14}
fontWeight={700}
color={(theme) => (serviceProvider ? undefined : theme.palette.primary.main)}
>
{serviceProvider ? serviceProvider.description : 'Select a service'}
</Typography>
<IconButton
id="service-provider-button"
aria-controls={open ? 'basic-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
>
<ArrowDropDownCircleIcon />
</IconButton>
</Box>
<Menu
id="service-provider-menu"
anchorEl={anchorEl}
open={open}
onClose={() => handleClose()}
MenuListProps={{
'aria-labelledby': 'service-provider-button',
}}
>
{services.map((service) => (
<>
<MenuItem disabled dense sx={{ fontSize: 'small', fontWeight: 'bold', mb: -1 }}>
{service.description}
</MenuItem>
{service.items.map((sp) => (
<MenuItem dense sx={{ fontSize: 'small', ml: 2, height: 'auto' }} onClick={() => handleClose(sp)}>
<Tooltip
title={
<Stack direction="column">
<Typography fontSize="inherit">
<code>{sp.id}</code>
</Typography>
<Typography fontSize="inherit" fontWeight={700}>
{sp.description}
</Typography>
<Typography fontSize="inherit">
Gateway <code>{sp.gateway.slice(0, 10)}...</code>
</Typography>
<Typography fontSize="inherit">
Provider <code>{sp.address.slice(0, 10)}...</code>
</Typography>
</Stack>
}
arrow
placement="top"
>
<Typography fontSize="inherit" noWrap>
{sp.description}
</Typography>
</Tooltip>
</MenuItem>
))}
</>
))}
</Menu>
</>
);
};
+21
View File
@@ -4,6 +4,7 @@ import { invoke } from '@tauri-apps/api/tauri';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import { ConnectionStatusKind } from '../types';
import { ConnectionStatsItem } from '../components/ConnectionStats';
import { ServiceProvider, Services } from '../types/directory';
const TAURI_EVENT_STATUS_CHANGED = 'app:connection-status-changed';
@@ -14,11 +15,14 @@ type TClientContext = {
connectionStatus: ConnectionStatusKind;
connectionStats?: ConnectionStatsItem[];
connectedSince?: DateTime;
services?: Services;
serviceProvider?: ServiceProvider;
setMode: (mode: ModeType) => void;
setConnectionStatus: (connectionStatus: ConnectionStatusKind) => void;
setConnectionStats: (connectionStats: ConnectionStatsItem[] | undefined) => void;
setConnectedSince: (connectedSince: DateTime | undefined) => void;
setServiceProvider: (serviceProvider: ServiceProvider) => void;
startConnecting: () => Promise<void>;
startDisconnecting: () => Promise<void>;
@@ -31,6 +35,14 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatusKind>(ConnectionStatusKind.disconnected);
const [connectionStats, setConnectionStats] = useState<ConnectionStatsItem[]>();
const [connectedSince, setConnectedSince] = useState<DateTime>();
const [services, setServices] = React.useState<Services>();
const [serviceProvider, setRawServiceProvider] = React.useState<ServiceProvider>();
useEffect(() => {
invoke('get_services').then((result) => {
setServices(result as Services);
});
}, []);
useEffect(() => {
let unlisten: UnlistenFn | undefined;
@@ -59,6 +71,12 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
await invoke('start_disconnecting');
}, []);
const setServiceProvider = useCallback(async (newServiceProvider: ServiceProvider) => {
await invoke('set_gateway', { gateway: newServiceProvider.gateway });
await invoke('set_service_provider', { serviceProvider: newServiceProvider.address });
setRawServiceProvider(newServiceProvider);
}, []);
return (
<ClientContext.Provider
value={{
@@ -72,6 +90,9 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
setConnectedSince,
startConnecting,
startDisconnecting,
services,
serviceProvider,
setServiceProvider,
}}
>
{children}
+5 -3
View File
@@ -8,6 +8,7 @@ import { ConnectionStats, ConnectionStatsItem } from '../components/ConnectionSt
import { NeedHelp } from '../components/NeedHelp';
import { ConnectionButton } from '../components/ConnectionButton';
import { IpAddressAndPort } from '../components/IpAddressAndPort';
import { ServiceProvider } from '../types/directory';
export const ConnectedLayout: React.FC<{
status: ConnectionStatusKind;
@@ -18,15 +19,16 @@ export const ConnectedLayout: React.FC<{
busy?: boolean;
isError?: boolean;
onConnectClick?: (status: ConnectionStatusKind) => void;
}> = ({ status, stats, ipAddress, port, connectedSince, busy, isError, onConnectClick }) => (
serviceProvider?: ServiceProvider;
}> = ({ status, stats, ipAddress, port, connectedSince, busy, isError, serviceProvider, onConnectClick }) => (
<AppWindowFrame>
<Box pb={4}>
<ConnectionStatus status={status} connectedSince={connectedSince} />
<ConnectionStatus status={status} connectedSince={connectedSince} serviceProvider={serviceProvider} />
</Box>
<Box pb={4}>
<IpAddressAndPort label="SOCKS5 Proxy" ipAddress={ipAddress} port={port} />
</Box>
<ConnectionStats stats={stats} />
{/* <ConnectionStats stats={stats} /> */}
<ConnectionButton status={status} busy={busy} onClick={onConnectClick} isError={isError} />
<NeedHelp />
</AppWindowFrame>
+30 -12
View File
@@ -4,21 +4,39 @@ import { AppWindowFrame } from '../components/AppWindowFrame';
import { ConnectionButton } from '../components/ConnectionButton';
import { ConnectionStatusKind } from '../types';
import { NeedHelp } from '../components/NeedHelp';
import { ServiceProviderSelector } from '../components/ServiceProviderSelector';
import { ServiceProvider, Services } from '../types/directory';
export const DefaultLayout: React.FC<{
status: ConnectionStatusKind;
services?: Services;
busy?: boolean;
isError?: boolean;
onConnectClick?: (status: ConnectionStatusKind) => void;
}> = ({ status, busy, isError, onConnectClick }) => (
<AppWindowFrame>
<Typography fontWeight="700" fontSize="14px" textAlign="center">
Connect, your privacy will be 100% protected thanks to the Nym Mixnet
</Typography>
<Typography fontWeight="700" fontSize="14px" textAlign="center" color="#60D6EF" pt={2}>
You are not protected now
</Typography>
<ConnectionButton status={status} busy={busy} isError={isError} onClick={onConnectClick} />
<NeedHelp />
</AppWindowFrame>
);
onServiceProviderChange?: (serviceProvider: ServiceProvider) => void;
}> = ({ status, services, busy, isError, onConnectClick, onServiceProviderChange }) => {
const [serviceProvider, setServiceProvider] = React.useState<ServiceProvider | undefined>();
const handleServiceProviderChange = (newServiceProvider: ServiceProvider) => {
setServiceProvider(newServiceProvider);
onServiceProviderChange?.(newServiceProvider);
};
return (
<AppWindowFrame>
<Typography fontWeight="700" fontSize="14px" textAlign="center">
Connect, your privacy will be 100% protected thanks to the Nym Mixnet
</Typography>
<Typography fontWeight="700" fontSize="14px" textAlign="center" color="#60D6EF" pt={2}>
You are not protected now
</Typography>
<ServiceProviderSelector services={services} onChange={handleServiceProviderChange} />
<ConnectionButton
status={status}
disabled={serviceProvider === undefined}
busy={busy}
isError={isError}
onClick={onConnectClick}
/>
<NeedHelp />
</AppWindowFrame>
);
};
+21 -1
View File
@@ -7,6 +7,7 @@ import { useClientContext } from '../context/main';
import { ConnectionStatusKind } from '../types';
import { DefaultLayout } from '../layouts/DefaultLayout';
import { ConnectedLayout } from '../layouts/ConnectedLayout';
import { Services } from '../types/directory';
export default {
title: 'App/Flow',
@@ -16,6 +17,19 @@ export default {
export const Mock: ComponentStory<typeof AppWindowFrame> = () => {
const context = useClientContext();
const [busy, setBusy] = React.useState<boolean>();
const services: Services = [
{
id: 'keybase',
description: 'Keybase',
items: [
{
id: 'nym-keybase',
description: 'Nym Keybase Service Provider',
address: '1234.5678',
},
],
},
];
const handleConnectClick = React.useCallback(() => {
const oldStatus = context.connectionStatus;
if (oldStatus === ConnectionStatusKind.connected || oldStatus === ConnectionStatusKind.disconnected) {
@@ -53,7 +67,12 @@ export const Mock: ComponentStory<typeof AppWindowFrame> = () => {
) {
return (
<Box p={4} sx={{ background: 'white' }}>
<DefaultLayout status={context.connectionStatus} busy={busy} onConnectClick={handleConnectClick} />
<DefaultLayout
status={context.connectionStatus}
busy={busy}
onConnectClick={handleConnectClick}
services={services}
/>
</Box>
);
}
@@ -67,6 +86,7 @@ export const Mock: ComponentStory<typeof AppWindowFrame> = () => {
ipAddress="127.0.0.1"
port={1080}
connectedSince={context.connectedSince}
serviceProvider={services[0].items[0]}
stats={[
{
label: 'in:',
+14
View File
@@ -0,0 +1,14 @@
export interface ServiceProvider {
id: string;
description: string;
address: string;
gateway: string;
}
export interface Service {
id: string;
description: string;
items: ServiceProvider[];
}
export type Services = Service[];