diff --git a/.github/workflows/nym-connect.yml b/.github/workflows/nym-connect.yml
index 3a08744fc9..5a652e92d2 100644
--- a/.github/workflows/nym-connect.yml
+++ b/.github/workflows/nym-connect.yml
@@ -41,19 +41,19 @@ jobs:
- name: Keybase - Node Install
run: npm install
working-directory: .github/workflows/support-files
-# - name: Keybase - Send Notification
-# env:
-# NYM_NOTIFICATION_KIND: nym-connect
-# NYM_PROJECT_NAME: "nym-connect"
-# NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
-# NYM_CI_WWW_LOCATION: "nym-connect-${{ env.GITHUB_REF_SLUG }}"
-# GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
-# GIT_BRANCH: "${GITHUB_REF##*/}"
-# KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
-# KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
-# KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
-# KEYBASE_NYM_CHANNEL: "ci-nym-connect"
-# IS_SUCCESS: "${{ job.status == 'success' }}"
-# uses: docker://keybaseio/client:stable-node
-# with:
-# args: .github/workflows/support-files/notifications/entry_point.sh
+ - name: Keybase - Send Notification
+ env:
+ NYM_NOTIFICATION_KIND: nym-connect
+ NYM_PROJECT_NAME: "nym-connect"
+ NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
+ NYM_CI_WWW_LOCATION: "nym-connect-${{ env.GITHUB_REF_SLUG }}"
+ GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
+ GIT_BRANCH: "${GITHUB_REF##*/}"
+ KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
+ KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}"
+ KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}"
+ KEYBASE_NYM_CHANNEL: "ci-nym-connect"
+ IS_SUCCESS: "${{ job.status == 'success' }}"
+ uses: docker://keybaseio/client:stable-node
+ with:
+ args: .github/workflows/support-files/notifications/entry_point.sh
diff --git a/nym-connect/.storybook/preview.js b/nym-connect/.storybook/preview.js
index 2dbe041198..9c14c3ec99 100644
--- a/nym-connect/.storybook/preview.js
+++ b/nym-connect/.storybook/preview.js
@@ -1,26 +1,26 @@
import { NymMixnetTheme } from '../src/theme';
-import { ClientContextProvider } from '../src/context/main';
import { Fonts } from './preview-fonts';
-
-const withThemeProvider= (Story, context) =>{
+import { MockProvider } from '../src/context/mocks/main';
+const withThemeProvider = (Story, context) => {
return (
-
-
+
+
-
+
- )
-}
+ );
+};
+
export const decorators = [withThemeProvider];
export const parameters = {
- actions: { argTypesRegex: "^on[A-Z].*" },
+ actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
-}
\ No newline at end of file
+};
diff --git a/nym-connect/README.md b/nym-connect/README.md
index 5a247235ab..bbd9b55bbb 100644
--- a/nym-connect/README.md
+++ b/nym-connect/README.md
@@ -12,8 +12,8 @@ Nym Connects sets up a SOCKS5 proxy for local applications to use.
## Installation prerequisites - Linux / Mac
- `Yarn`
-- `NodeJS >= v16.8.0`
-- `Rust & cargo >= v1.56`
+- `NodeJS >= v16`
+- `Rust & cargo`
## Installation prerequisites - Windows
diff --git a/nym-connect/src-tauri/Cargo.toml b/nym-connect/src-tauri/Cargo.toml
index 11f79849b7..dfaf452dd1 100644
--- a/nym-connect/src-tauri/Cargo.toml
+++ b/nym-connect/src-tauri/Cargo.toml
@@ -32,7 +32,7 @@ reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tap = "1.0.1"
-tauri = { version = "^1.1.1", features = ["clipboard-write-text", "shell-open", "system-tray", "updater"] }
+tauri = { version = "^1.1.1", features = ["clipboard-write-text", "macos-private-api", "shell-open", "system-tray", "updater", "window-close", "window-start-dragging"] }
tendermint-rpc = "0.23.0"
thiserror = "1.0"
tokio = { version = "1.21.2", features = ["sync", "time"] }
diff --git a/nym-connect/src-tauri/src/models/mod.rs b/nym-connect/src-tauri/src/models/mod.rs
index ded6a479fc..e5076ac5c0 100644
--- a/nym-connect/src-tauri/src/models/mod.rs
+++ b/nym-connect/src-tauri/src/models/mod.rs
@@ -45,7 +45,7 @@ pub struct AppEventConnectionStatusChangedPayload {
}
#[cfg_attr(test, derive(ts_rs::TS))]
-#[derive(Clone, serde::Serialize, serde::Deserialize)]
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct DirectoryService {
pub id: String,
pub description: String,
@@ -53,7 +53,7 @@ pub struct DirectoryService {
}
#[cfg_attr(test, derive(ts_rs::TS))]
-#[derive(Clone, serde::Serialize, serde::Deserialize)]
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct DirectoryServiceProvider {
pub id: String,
pub description: String,
diff --git a/nym-connect/src-tauri/src/operations/directory/mod.rs b/nym-connect/src-tauri/src/operations/directory/mod.rs
index aaac2276d9..b6c45fa8c0 100644
--- a/nym-connect/src-tauri/src/operations/directory/mod.rs
+++ b/nym-connect/src-tauri/src/operations/directory/mod.rs
@@ -6,9 +6,11 @@ static SERVICE_PROVIDER_WELLKNOWN_URL: &str =
#[tauri::command]
pub async fn get_services() -> Result> {
+ log::trace!("Fetching services");
let res = reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL)
.await?
.json::>()
.await?;
+ log::trace!("Received: {:#?}", res);
Ok(res)
}
diff --git a/nym-connect/src-tauri/src/tasks.rs b/nym-connect/src-tauri/src/tasks.rs
index 85da77dfee..086b0487b1 100644
--- a/nym-connect/src-tauri/src/tasks.rs
+++ b/nym-connect/src-tauri/src/tasks.rs
@@ -66,6 +66,12 @@ pub fn start_nym_socks5_client(
Ok((socks5_ctrl_tx, socks5_status_rx, used_gateway))
}
+#[derive(Clone, serde::Serialize)]
+struct Payload {
+ title: String,
+ message: String,
+}
+
/// The disconnect listener listens to the channel setup between the socks5 proxy task and the main
/// tauri task. Primarily it listens for shutdown messages, and updates the state accordingly.
pub fn start_disconnect_listener(
@@ -78,14 +84,39 @@ pub fn start_disconnect_listener(
match status_receiver.await {
Ok(Socks5StatusMessage::Stopped) => {
log::info!("SOCKS5 task reported it has finished");
+ window
+ .emit(
+ "socks5-event",
+ Payload {
+ title: "SOCKS5 finished".into(),
+ message: "SOCKS5 task reported it has finished".into(),
+ },
+ )
+ .unwrap();
}
Ok(Socks5StatusMessage::FailedToStart) => {
log::info!("SOCKS5 task reported it failed to start");
+ window
+ .emit(
+ "socks5-event",
+ Payload {
+ title: "SOCKS5 error".into(),
+ message: "SOCKS5 failed to start".into(),
+ },
+ )
+ .unwrap();
}
Err(_) => {
log::info!("SOCKS5 task appears to have stopped abruptly");
- // TODO: we should probably generate some events here, or otherwise signal to the
- // frontend.
+ window
+ .emit(
+ "socks5-event",
+ Payload {
+ title: "SOCKS5 error".into(),
+ message: "SOCKS5 stopped abruptly".into(),
+ },
+ )
+ .unwrap();
}
}
diff --git a/nym-connect/src-tauri/tauri.conf.json b/nym-connect/src-tauri/tauri.conf.json
index c4fce76f06..b9cbfd8233 100644
--- a/nym-connect/src-tauri/tauri.conf.json
+++ b/nym-connect/src-tauri/tauri.conf.json
@@ -10,6 +10,7 @@
"beforeBuildCommand": ""
},
"tauri": {
+ "macOSPrivateApi": true,
"systemTray": {
"iconPath": "icons/tray_icon.png",
"iconAsTemplate": true
@@ -18,13 +19,7 @@
"active": true,
"targets": "all",
"identifier": "net.nymtech.connect",
- "icon": [
- "icons/32x32.png",
- "icons/128x128.png",
- "icons/128x128@2x.png",
- "icons/icon.icns",
- "icons/icon.ico"
- ],
+ "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"resources": [],
"externalBin": [],
"copyright": "Copyright © 2021-2022 Nym Technologies SA",
@@ -49,9 +44,7 @@
},
"updater": {
"active": true,
- "endpoints": [
- "https://nymtech.net/.wellknown/connect/updater.json"
- ],
+ "endpoints": ["https://nymtech.net/.wellknown/connect/updater.json"],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNzQ2M0E5N0VFODE2NApSV1JrZ2U2WE9rYTNETTg1OTBKdE5uWUEra0hML2syOVUvQ2lxZmFZRzZ1T3NWbGM0eVRzUTVhVwo="
},
@@ -61,14 +54,20 @@
},
"clipboard": {
"writeText": true
+ },
+ "window": {
+ "startDragging": true,
+ "close": true
}
},
"windows": [
{
"title": "NymConnect",
"width": 240,
- "height": 500,
- "resizable": false
+ "height": 540,
+ "resizable": false,
+ "decorations": false,
+ "transparent": true
}
],
"security": {
diff --git a/nym-connect/src/App.tsx b/nym-connect/src/App.tsx
index 10a2995f34..90dd1a4230 100644
--- a/nym-connect/src/App.tsx
+++ b/nym-connect/src/App.tsx
@@ -1,37 +1,50 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import { DateTime } from 'luxon';
import { ConnectionStatusKind } from './types';
import { useClientContext } from './context/main';
import { DefaultLayout } from './layouts/DefaultLayout';
import { ConnectedLayout } from './layouts/ConnectedLayout';
+import { HelpGuideLayout } from './layouts/HelpGuideLayout';
export const App: React.FC = () => {
const context = useClientContext();
const [busy, setBusy] = React.useState();
+ const [showInfoModal, setShowInfoModal] = React.useState(false);
const handleConnectClick = React.useCallback(async () => {
- const oldStatus = context.connectionStatus;
- if (oldStatus === ConnectionStatusKind.connected || oldStatus === ConnectionStatusKind.disconnected) {
+ const currentStatus = context.connectionStatus;
+ if (currentStatus === ConnectionStatusKind.connected || currentStatus === ConnectionStatusKind.disconnected) {
setBusy(true);
// eslint-disable-next-line default-case
- switch (oldStatus) {
+ switch (currentStatus) {
case ConnectionStatusKind.disconnected:
await context.startConnecting();
+ context.setConnectedSince(DateTime.now());
break;
case ConnectionStatusKind.connected:
await context.startDisconnecting();
+ context.setConnectedSince(undefined);
break;
}
setBusy(false);
}
}, [context.connectionStatus]);
+ useEffect(() => {
+ if (context.connectionStatus === ConnectionStatusKind.connected) setShowInfoModal(true);
+ }, [context.connectionStatus]);
+
+ if (context.showHelp) return ;
+
if (
context.connectionStatus === ConnectionStatusKind.disconnected ||
context.connectionStatus === ConnectionStatusKind.connecting
) {
return (
{
return (
setShowInfoModal(false)}
status={context.connectionStatus}
busy={busy}
onConnectClick={handleConnectClick}
diff --git a/nym-connect/src/assets/help-step-four.png b/nym-connect/src/assets/help-step-four.png
new file mode 100644
index 0000000000..5507962892
Binary files /dev/null and b/nym-connect/src/assets/help-step-four.png differ
diff --git a/nym-connect/src/assets/help-step-one.png b/nym-connect/src/assets/help-step-one.png
new file mode 100644
index 0000000000..5e91a29cff
Binary files /dev/null and b/nym-connect/src/assets/help-step-one.png differ
diff --git a/nym-connect/src/assets/help-step-three.png b/nym-connect/src/assets/help-step-three.png
new file mode 100644
index 0000000000..932b6c1171
Binary files /dev/null and b/nym-connect/src/assets/help-step-three.png differ
diff --git a/nym-connect/src/assets/help-step-two.png b/nym-connect/src/assets/help-step-two.png
new file mode 100644
index 0000000000..f2910c98cf
Binary files /dev/null and b/nym-connect/src/assets/help-step-two.png differ
diff --git a/nym-connect/src/components/AppWindowFrame.tsx b/nym-connect/src/components/AppWindowFrame.tsx
index fc30793a2d..5fcca9b3b3 100644
--- a/nym-connect/src/components/AppWindowFrame.tsx
+++ b/nym-connect/src/components/AppWindowFrame.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { Box } from '@mui/material';
-import { NymWordmark } from '@nymproject/react/logo/NymWordmark';
+import { CustomTitleBar } from './CustomTitleBar';
export const AppWindowFrame: React.FC = ({ children }) => (
(
borderRadius: '12px',
padding: '12px 16px',
display: 'grid',
- gridTemplateRows: '30px auto',
- width: '240px',
+ gridTemplateRows: '40px 1fr',
+ bgcolor: 'nym.background.dark',
+ height: '100vh',
}}
>
-
-
-
- {children}
+
+ {children}
);
diff --git a/nym-connect/src/components/ConnectionButton.tsx b/nym-connect/src/components/ConnectionButton.tsx
index 0ab2ce1fa9..42903a80ea 100644
--- a/nym-connect/src/components/ConnectionButton.tsx
+++ b/nym-connect/src/components/ConnectionButton.tsx
@@ -19,16 +19,16 @@ const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isErro
switch (status) {
case ConnectionStatusKind.disconnected:
if (hover) {
- return '#21D072';
+ return '#FFFF33';
}
- return '#F4B02D';
+ return '#FFE600';
case ConnectionStatusKind.connecting:
case ConnectionStatusKind.disconnecting:
- return '#F4B02D';
+ return '#FFE600';
default:
// connected
if (hover) {
- return '#DA465B';
+ return '#E43E3E';
}
return '#21D072';
}
@@ -81,69 +81,62 @@ export const ConnectionButton: React.FC<{
viewBox="0 0 208 208"
fill="none"
xmlns="http://www.w3.org/2000/svg"
- onMouseEnter={() => !disabled && setHover(true)}
- onMouseLeave={() => !disabled && setHover(false)}
>
-
+ !disabled && setHover(true)}
+ onMouseLeave={() => !disabled && setHover(false)}
+ >
-
-
+
+
-
-
-
- {busy && (
-
- )}
-
+
+
+
-
+
{status === ConnectionStatusKind.connected && hover ? (
) : (
)}
{statusText}
-
+
-
-
+
+
-
-
+
+
diff --git a/nym-connect/src/components/ConnectionStatus.tsx b/nym-connect/src/components/ConnectionStatus.tsx
index a866f8521d..c2bfcc82f0 100644
--- a/nym-connect/src/components/ConnectionStatus.tsx
+++ b/nym-connect/src/components/ConnectionStatus.tsx
@@ -1,12 +1,10 @@
import React from 'react';
import { Box, CircularProgress, Typography } from '@mui/material';
-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_SIZE = '10px';
const FONT_WEIGHT = '600';
const FONT_STYLE = 'normal';
@@ -16,39 +14,41 @@ const ConnectionStatusContent: React.FC<{
switch (status) {
case ConnectionStatusKind.connected:
return (
- <>
-
-
- Connected
-
- >
+
+ Connected
+
);
case ConnectionStatusKind.disconnecting:
return (
- <>
+
Disconnecting...
- >
+
);
case ConnectionStatusKind.connecting:
return (
- <>
+
Connecting...
- >
+
);
case ConnectionStatusKind.disconnected:
return (
- <>
-
-
- Disconnected
-
- >
+
+ You are not protected
+
);
default:
return null;
@@ -61,29 +61,22 @@ export const ConnectionStatus: React.FC<{
serviceProvider?: ServiceProvider;
}> = ({ status, connectedSince, serviceProvider }) => {
const color =
- status === ConnectionStatusKind.connected || status === ConnectionStatusKind.disconnecting ? '#21D072' : '#888';
- const [duration, setDuration] = React.useState();
- React.useEffect(() => {
- const intervalId = setInterval(() => {
- if (connectedSince) {
- setDuration(DateTime.now().diff(connectedSince).toFormat('hh:mm:ss'));
- }
- }, 500);
- return () => {
- clearInterval(intervalId);
- };
- }, [status, connectedSince]);
+ status === ConnectionStatusKind.connected || status === ConnectionStatusKind.disconnecting
+ ? '#21D072'
+ : 'warning.main';
+
return (
<>
-
-
-
-
-
- {status === ConnectionStatusKind.connected && duration}
-
+
+
+
+
+ {serviceProvider && (
+
+ To {serviceProvider.description}
+
+ )}
- {serviceProvider && {serviceProvider.description}}
>
);
};
diff --git a/nym-connect/src/components/ConntectionTimer.tsx b/nym-connect/src/components/ConntectionTimer.tsx
new file mode 100644
index 0000000000..25e2af8d9c
--- /dev/null
+++ b/nym-connect/src/components/ConntectionTimer.tsx
@@ -0,0 +1,26 @@
+import React, { useEffect } from 'react';
+import { Stack, Typography } from '@mui/material';
+import { DateTime } from 'luxon';
+
+export const ConnectionTimer = ({ connectedSince }: { connectedSince?: DateTime }) => {
+ const [duration, setDuration] = React.useState();
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ if (connectedSince) {
+ setDuration(DateTime.now().diff(connectedSince).toFormat('hh:mm:ss'));
+ }
+ }, 500);
+ return () => {
+ clearInterval(intervalId);
+ };
+ }, [connectedSince]);
+
+ return (
+
+
+ Connection time
+
+ {duration || '00:00:00'}
+
+ );
+};
diff --git a/nym-connect/src/components/CustomTitleBar.tsx b/nym-connect/src/components/CustomTitleBar.tsx
new file mode 100644
index 0000000000..dc4e8c0061
--- /dev/null
+++ b/nym-connect/src/components/CustomTitleBar.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { ArrowBack, Close, HelpOutline } from '@mui/icons-material';
+import { Box, IconButton } from '@mui/material';
+import { NymWordmark } from '@nymproject/react/logo/NymWordmark';
+import { appWindow } from '@tauri-apps/api/window';
+import { useClientContext } from 'src/context/main';
+
+const customTitleBarStyles = {
+ titlebar: {
+ background: '#1D2125',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: '16px',
+ paddingBottom: '0px',
+ borderTopLeftRadius: '12px',
+ borderTopRightRadius: '12px',
+ },
+};
+
+const CustomButton = ({ Icon, onClick }: { Icon: React.JSXElementConstructor; onClick: () => void }) => (
+
+
+
+);
+
+export const CustomTitleBar = () => {
+ const { showHelp, handleShowHelp } = useClientContext();
+ return (
+
+ {
+ handleShowHelp();
+ }}
+ />
+
+ appWindow.close()} />
+
+ );
+};
diff --git a/nym-connect/src/components/HelpPage.tsx b/nym-connect/src/components/HelpPage.tsx
new file mode 100644
index 0000000000..788f7264d5
--- /dev/null
+++ b/nym-connect/src/components/HelpPage.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { Stack, Typography } from '@mui/material';
+import { HelpPageActions } from './HelpPageActions';
+import { HelpImage } from './HelpPageImage';
+import { StepIndicator } from './HelpPageStepIndicator';
+
+export const HelpPage = ({
+ step,
+ description,
+ img,
+ onNext,
+ onPrev,
+}: {
+ step: number;
+ description: string;
+ img: any;
+ onNext?: () => void;
+ onPrev?: () => void;
+}) => (
+
+
+
+
+ How to connect guide {step}/4
+
+
+ {description}
+
+
+
+
+
+);
diff --git a/nym-connect/src/components/HelpPageActions.tsx b/nym-connect/src/components/HelpPageActions.tsx
new file mode 100644
index 0000000000..17e513120f
--- /dev/null
+++ b/nym-connect/src/components/HelpPageActions.tsx
@@ -0,0 +1,20 @@
+import { ArrowBack, ArrowForward } from '@mui/icons-material';
+import { Box, Button } from '@mui/material';
+import React from 'react';
+
+export const HelpPageActions = ({ onNext, onPrev }: { onNext?: () => void; onPrev?: () => void }) => (
+
+ {onPrev ? (
+ }>
+ Back
+
+ ) : (
+
+ )}
+ {onNext && (
+ }>
+ Next
+
+ )}
+
+);
diff --git a/nym-connect/src/components/HelpPageImage.tsx b/nym-connect/src/components/HelpPageImage.tsx
new file mode 100644
index 0000000000..27a8dcdca3
--- /dev/null
+++ b/nym-connect/src/components/HelpPageImage.tsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export const HelpImage = ({ img, imageDescription }: { img: any; imageDescription: string }) => (
+
+);
diff --git a/nym-connect/src/components/HelpPageStepIndicator.tsx b/nym-connect/src/components/HelpPageStepIndicator.tsx
new file mode 100644
index 0000000000..b5dff42b34
--- /dev/null
+++ b/nym-connect/src/components/HelpPageStepIndicator.tsx
@@ -0,0 +1,15 @@
+import { Box } from '@mui/material';
+import React from 'react';
+
+const Step = ({ highlight }: { highlight: boolean }) => (
+
+);
+
+export const StepIndicator = ({ step }: { step: number }) => (
+
+
+ = 2} />
+ = 3} />
+ = 4} />
+
+);
diff --git a/nym-connect/src/components/InfoModal.tsx b/nym-connect/src/components/InfoModal.tsx
new file mode 100644
index 0000000000..ed628de7eb
--- /dev/null
+++ b/nym-connect/src/components/InfoModal.tsx
@@ -0,0 +1,69 @@
+import { Close, ErrorOutline } from '@mui/icons-material';
+import { Box, IconButton, Modal, Theme, Typography } from '@mui/material';
+import React from 'react';
+
+const styles = {
+ position: 'absolute',
+ top: '50%',
+ left: '50%',
+ transform: 'translate(-50%, -50%)',
+ width: 200,
+ bgcolor: '#292E34',
+ p: 1.5,
+ borderRadius: 0.5,
+ height: 'fit-content',
+ border: (theme: Theme) => `1px solid ${theme.palette.grey[700]}`,
+};
+
+const ModalTitle = ({ title, withCloseIcon }: { title: string; withCloseIcon: boolean }) => (
+
+
+
+ {title}
+
+
+);
+
+const ModalBody = ({ description, children }: { description: string; children?: React.ReactElement }) => (
+
+ {children}
+
+ {description}
+
+
+);
+
+export const InfoModal = ({
+ title,
+ description,
+ show,
+ children,
+ Action,
+ onClose,
+}: {
+ title: string;
+ description: string;
+ show: boolean;
+ children?: React.ReactElement;
+ Action?: React.ReactNode;
+ onClose?: () => void;
+}) => (
+
+
+ {onClose && (
+
+
+
+
+
+ )}
+
+ {children}
+ {Action && (
+
+ {Action}
+
+ )}
+
+
+);
diff --git a/nym-connect/src/components/IpAddressAndPort.tsx b/nym-connect/src/components/IpAddressAndPort.tsx
index 435cf926fb..8d7d79c1f2 100644
--- a/nym-connect/src/components/IpAddressAndPort.tsx
+++ b/nym-connect/src/components/IpAddressAndPort.tsx
@@ -37,8 +37,12 @@ export const IpAddressAndPort: React.FC<{
return (
- {label}
- Port
+
+ {label}
+
+
+ Port
+
void;
+}) => (
+ Done}
+ >
+
+
+ Socks5 address
+
+
+ {ipAddress}
+
+
+
+
+ Port
+
+
+ {port}
+
+
+
+
+);
diff --git a/nym-connect/src/components/ServiceProviderSelector.tsx b/nym-connect/src/components/ServiceProviderSelector.tsx
index 08cda8eb8a..feb94d1e4a 100644
--- a/nym-connect/src/components/ServiceProviderSelector.tsx
+++ b/nym-connect/src/components/ServiceProviderSelector.tsx
@@ -1,11 +1,6 @@
import React, { useEffect, useMemo } from 'react';
-import IconButton from '@mui/material/IconButton';
-import Menu from '@mui/material/Menu';
-import MenuItem from '@mui/material/MenuItem';
-import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded';
-import KeyboardArrowUpRoundedIcon from '@mui/icons-material/KeyboardArrowUpRounded';
-import { Box, CircularProgress, Stack, Tooltip, Typography, ListItemIcon } from '@mui/material';
-import Check from '@mui/icons-material/Check';
+import { Box, CircularProgress, Input, Stack, TextField, Tooltip, Typography, MenuItem, ListItemIcon } from '@mui/material';
+import { Check } from '@mui/icons-material';
import { ServiceProvider, Service, Services } from '../types/directory';
type ServiceWithRandomSp = {
@@ -19,11 +14,8 @@ export const ServiceProviderSelector: React.FC<{
services?: Services;
currentSp?: ServiceProvider;
}> = ({ services, currentSp, onChange }) => {
- const [service, setService] = React.useState();
+ const [service, setService] = React.useState({ id: '', description: '', items: [] });
const [serviceProvider, setServiceProvider] = React.useState(currentSp);
- const textEl = React.useRef(null);
- const [anchorEl, setAnchorEl] = React.useState(null);
- const open = Boolean(anchorEl);
useEffect(() => {
if (!serviceProvider && currentSp) {
@@ -34,38 +26,34 @@ export const ServiceProviderSelector: React.FC<{
useEffect(() => {
if (services && serviceProvider) {
// retrieve the service corresponding to this service provider
- setService(
- services.find((s) =>
- s.items.some(
- ({ id, address, gateway }) =>
- id === serviceProvider.id && address === serviceProvider.address && gateway === serviceProvider.gateway,
- ),
+
+ const match = services.find((s) =>
+ s.items.some(
+ ({ id, address, gateway }) =>
+ id === serviceProvider.id && address === serviceProvider.address && gateway === serviceProvider.gateway,
),
);
+
+ if (match) {
+ setService(match);
+ }
}
}, [serviceProvider, services]);
- const handleClick = () => {
- setAnchorEl(textEl.current);
- };
- const handleClose = (newServiceProvider?: ServiceProvider) => {
+ const handleSelectSp = (newServiceProvider?: ServiceProvider) => {
if (newServiceProvider && newServiceProvider !== currentSp) {
setServiceProvider(newServiceProvider);
onChange?.(newServiceProvider);
}
- setAnchorEl(null);
};
if (!services) {
return (
-
+
theme.palette.common.white}>
Loading services...
-
- {open ? : }
-
);
}
@@ -80,68 +68,44 @@ export const ServiceProviderSelector: React.FC<{
[services],
);
+ if (!service) return null;
+
return (
- <>
- `1px solid ${theme.palette.info.main}` }}
- >
- theme.palette.info.main}>
- {!service ? 'Select a service' : service.description}
-
-
- {open ? : }
-
-
-
- >
+
+
);
};
diff --git a/nym-connect/src/context/main.tsx b/nym-connect/src/context/main.tsx
index 42bfed638e..5d2b049bf9 100644
--- a/nym-connect/src/context/main.tsx
+++ b/nym-connect/src/context/main.tsx
@@ -7,20 +7,26 @@ import { forage } from '@tauri-apps/tauri-forage';
import { ConnectionStatusKind } from '../types';
import { ConnectionStatsItem } from '../components/ConnectionStats';
import { ServiceProvider, Services } from '../types/directory';
+import { Error } from 'src/types/error';
+import { TauriEvent } from 'src/types/event';
const TAURI_EVENT_STATUS_CHANGED = 'app:connection-status-changed';
type ModeType = 'light' | 'dark';
-type TClientContext = {
+export type TClientContext = {
mode: ModeType;
connectionStatus: ConnectionStatusKind;
connectionStats?: ConnectionStatsItem[];
connectedSince?: DateTime;
services?: Services;
serviceProvider?: ServiceProvider;
+ showHelp: boolean;
+ error?: Error;
setMode: (mode: ModeType) => void;
+ clearError: () => void;
+ handleShowHelp: () => void;
setConnectionStatus: (connectionStatus: ConnectionStatusKind) => void;
setConnectionStats: (connectionStats: ConnectionStatsItem[] | undefined) => void;
setConnectedSince: (connectedSince: DateTime | undefined) => void;
@@ -39,6 +45,8 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
const [connectedSince, setConnectedSince] = useState();
const [services, setServices] = React.useState([]);
const [serviceProvider, setRawServiceProvider] = React.useState();
+ const [showHelp, setShowHelp] = useState(false);
+ const [error, setError] = useState();
useEffect(() => {
invoke('get_services').then((result) => {
@@ -47,30 +55,45 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
}, []);
useEffect(() => {
- let unlisten: UnlistenFn | undefined;
+ const unlisten: UnlistenFn[] = [];
// TODO: fix typings
listen(TAURI_EVENT_STATUS_CHANGED, (event) => {
const { status } = event.payload as any;
console.log(TAURI_EVENT_STATUS_CHANGED, { status, event });
setConnectionStatus(status);
+ })
+ .then((result) => {
+ unlisten.push(result);
+ })
+ .catch((e) => console.log(e));
+
+ listen('socks5-event', (e: TauriEvent) => {
+ setError(e.payload);
}).then((result) => {
- unlisten = result;
+ unlisten.push(result);
});
return () => {
- if (unlisten) {
- unlisten();
- }
+ unlisten.forEach((unsubscribe) => unsubscribe());
};
}, []);
const startConnecting = useCallback(async () => {
- await invoke('start_connecting');
+ try {
+ await invoke('start_connecting');
+ } catch (e) {
+ setError({ title: 'Could not connect', message: e as string });
+ console.log(e);
+ }
}, []);
const startDisconnecting = useCallback(async () => {
- await invoke('start_disconnecting');
+ try {
+ await invoke('start_disconnecting');
+ } catch (e) {
+ console.log(e);
+ }
}, []);
const setSpInStorage = async (sp: ServiceProvider) => {
@@ -92,12 +115,17 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
const spFromStorage = await forage.getItem({ key: 'nym-connect-sp' })();
if (spFromStorage) {
setRawServiceProvider(spFromStorage);
+ setServiceProvider(spFromStorage);
}
} catch (e) {
console.warn(e);
}
};
+ const handleShowHelp = () => setShowHelp((show) => !show);
+
+ const clearError = () => setError(undefined);
+
useEffect(() => {
const validityCheck = async () => {
if (services.length > 0 && serviceProvider) {
@@ -122,6 +150,8 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
() => ({
mode,
setMode,
+ error,
+ clearError,
connectionStatus,
setConnectionStatus,
connectionStats,
@@ -133,8 +163,20 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
services,
serviceProvider,
setServiceProvider,
+ showHelp,
+ handleShowHelp,
}),
- [mode, connectedSince, connectionStatus, connectionStats, connectedSince, services, serviceProvider],
+ [
+ mode,
+ error,
+ connectedSince,
+ showHelp,
+ connectionStatus,
+ connectionStats,
+ connectedSince,
+ services,
+ serviceProvider,
+ ],
);
return {children};
diff --git a/nym-connect/src/context/mocks/main.tsx b/nym-connect/src/context/mocks/main.tsx
new file mode 100644
index 0000000000..e2974413eb
--- /dev/null
+++ b/nym-connect/src/context/mocks/main.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { ConnectionStatusKind } from 'src/types';
+import { ClientContext, TClientContext } from '../main';
+
+const mockValues: TClientContext = {
+ mode: 'dark',
+ connectionStatus: ConnectionStatusKind.disconnected,
+ services: [],
+ showHelp: false,
+ serviceProvider: { id: '1', description: 'Keybase service provider', gateway: 'abc123', address: '123abc' },
+ setMode: () => {},
+ clearError: () => {},
+ handleShowHelp: () => {},
+ setConnectedSince: () => {},
+ setConnectionStats: () => {},
+ setConnectionStatus: () => {},
+ setServiceProvider: () => {},
+ startConnecting: async () => {},
+ startDisconnecting: async () => {},
+};
+
+export const MockProvider = ({ children }: { children: React.ReactNode }) => {
+ return {children};
+};
diff --git a/nym-connect/src/index.tsx b/nym-connect/src/index.tsx
index 2938840fff..66a3e11d17 100644
--- a/nym-connect/src/index.tsx
+++ b/nym-connect/src/index.tsx
@@ -4,16 +4,18 @@ import { ErrorBoundary } from 'react-error-boundary';
import { ClientContextProvider } from './context/main';
import { ErrorFallback } from './components/Error';
import { NymMixnetTheme } from './theme';
-import './fonts/fonts.css';
import { App } from './App';
+import { AppWindowFrame } from './components/AppWindowFrame';
const root = document.getElementById('root');
ReactDOM.render(
-
-
+
+
+
+
,
diff --git a/nym-connect/src/layouts/ConnectedLayout.tsx b/nym-connect/src/layouts/ConnectedLayout.tsx
index a031f18a56..8e34415b39 100644
--- a/nym-connect/src/layouts/ConnectedLayout.tsx
+++ b/nym-connect/src/layouts/ConnectedLayout.tsx
@@ -1,11 +1,11 @@
import React from 'react';
-import { Box } from '@mui/material';
+import { Box, Divider } from '@mui/material';
import { DateTime } from 'luxon';
-import { AppWindowFrame } from '../components/AppWindowFrame';
+import { IpAddressAndPortModal } from 'src/components/IpAddressAndPortModal';
+import { ConnectionTimer } from 'src/components/ConntectionTimer';
import { ConnectionStatus } from '../components/ConnectionStatus';
import { ConnectionStatusKind } from '../types';
-import { ConnectionStats, ConnectionStatsItem } from '../components/ConnectionStats';
-import { NeedHelp } from '../components/NeedHelp';
+import { ConnectionStatsItem } from '../components/ConnectionStats';
import { ConnectionButton } from '../components/ConnectionButton';
import { IpAddressAndPort } from '../components/IpAddressAndPort';
import { ServiceProvider } from '../types/directory';
@@ -17,19 +17,32 @@ export const ConnectedLayout: React.FC<{
port: number;
connectedSince?: DateTime;
busy?: boolean;
+ showInfoModal: boolean;
isError?: boolean;
+ handleCloseInfoModal: () => void;
onConnectClick?: (status: ConnectionStatusKind) => void;
serviceProvider?: ServiceProvider;
-}> = ({ status, stats, ipAddress, port, connectedSince, busy, isError, serviceProvider, onConnectClick }) => (
-
+}> = ({
+ status,
+ showInfoModal,
+ handleCloseInfoModal,
+ ipAddress,
+ port,
+ connectedSince,
+ busy,
+ isError,
+ serviceProvider,
+ onConnectClick,
+}) => (
+ <>
+
-
-
-
-
+
+
+
{/* */}
+
-
-
+ >
);
diff --git a/nym-connect/src/layouts/DefaultLayout.tsx b/nym-connect/src/layouts/DefaultLayout.tsx
index ba59b7f95d..ed42aad5fe 100644
--- a/nym-connect/src/layouts/DefaultLayout.tsx
+++ b/nym-connect/src/layouts/DefaultLayout.tsx
@@ -1,48 +1,48 @@
import React from 'react';
import { Typography } from '@mui/material';
-import { AppWindowFrame } from '../components/AppWindowFrame';
+import { Box } from '@mui/material';
+import { ConnectionStatus } from 'src/components/ConnectionStatus';
+import { ConnectionTimer } from 'src/components/ConntectionTimer';
+import { InfoModal } from 'src/components/InfoModal';
+import { Error } from 'src/types/error';
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';
import { useClientContext } from '../context/main';
+import { ConnectionStatusKind } from '../types';
+import { ServiceProvider, Services } from '../types/directory';
export const DefaultLayout: React.FC<{
+ error?: Error;
status: ConnectionStatusKind;
services?: Services;
busy?: boolean;
isError?: boolean;
+ clearError: () => void;
onConnectClick?: (status: ConnectionStatusKind) => void;
onServiceProviderChange?: (serviceProvider: ServiceProvider) => void;
-}> = ({ status, services, busy, isError, onConnectClick, onServiceProviderChange }) => {
- const [serviceProvider, setServiceProvider] = React.useState();
+}> = ({ status, error, services, busy, isError, onConnectClick, onServiceProviderChange, clearError }) => {
const handleServiceProviderChange = (newServiceProvider: ServiceProvider) => {
- setServiceProvider(newServiceProvider);
onServiceProviderChange?.(newServiceProvider);
};
+
const { serviceProvider: currentSp } = useClientContext();
return (
-
-
- This is experimental software.
- Do not rely on it for strong anonymity (yet).
-
-
- Connect to the
-
- Nym mixnet for privacy.
+
+ {error && }
+
+
+ Connect to the Nym
mixnet for privacy.
+
-
-
+
);
};
diff --git a/nym-connect/src/layouts/HelpGuideLayout.tsx b/nym-connect/src/layouts/HelpGuideLayout.tsx
new file mode 100644
index 0000000000..d56fd6b0ae
--- /dev/null
+++ b/nym-connect/src/layouts/HelpGuideLayout.tsx
@@ -0,0 +1,70 @@
+import React, { useState } from 'react';
+import { HelpPage } from 'src/components/HelpPage';
+import { Button, Link, Stack } from '@mui/material';
+import Image1 from '../assets/help-step-one.png';
+import Image2 from '../assets/help-step-two.png';
+import Image3 from '../assets/help-step-three.png';
+import Image4 from '../assets/help-step-four.png';
+
+export const HelpGuideLayout = () => {
+ const [step, setStep] = useState(0);
+
+ if (step === 1)
+ return (
+ setStep(2)}
+ />
+ );
+
+ if (step === 2)
+ return (
+ setStep(1)}
+ onNext={() => setStep(3)}
+ />
+ );
+
+ if (step === 3)
+ return (
+ setStep(2)}
+ onNext={() => setStep(4)}
+ />
+ );
+
+ if (step === 4)
+ return (
+ setStep(3)}
+ />
+ );
+
+ return (
+
+
+
+
+ );
+};
diff --git a/nym-connect/src/stories/AppFlow.stories.tsx b/nym-connect/src/stories/AppFlow.stories.tsx
index ef7a1ea434..34064e6202 100644
--- a/nym-connect/src/stories/AppFlow.stories.tsx
+++ b/nym-connect/src/stories/AppFlow.stories.tsx
@@ -67,20 +67,25 @@ export const Mock: ComponentStory = () => {
context.connectionStatus === ConnectionStatusKind.connecting
) {
return (
-
+
{}}
/>
-
+
);
}
return (
-
+
{
+ return undefined;
+ }}
status={context.connectionStatus}
busy={busy}
onConnectClick={handleConnectClick}
@@ -101,6 +106,6 @@ export const Mock: ComponentStory = () => {
},
]}
/>
-
+
);
};
diff --git a/nym-connect/src/stories/ConnectedLayout.stories.tsx b/nym-connect/src/stories/ConnectedLayout.stories.tsx
index 6063f83c28..53538457bf 100644
--- a/nym-connect/src/stories/ConnectedLayout.stories.tsx
+++ b/nym-connect/src/stories/ConnectedLayout.stories.tsx
@@ -11,8 +11,12 @@ export default {
} as ComponentMeta;
export const Default: ComponentStory = () => (
-
+
{
+ return undefined;
+ }}
status={ConnectionStatusKind.connected}
connectedSince={DateTime.now()}
ipAddress="127.0.0.1"
diff --git a/nym-connect/src/stories/DefaultLayout.stories.tsx b/nym-connect/src/stories/DefaultLayout.stories.tsx
index 2c4ef513c2..ba311a410b 100644
--- a/nym-connect/src/stories/DefaultLayout.stories.tsx
+++ b/nym-connect/src/stories/DefaultLayout.stories.tsx
@@ -10,7 +10,24 @@ export default {
} as ComponentMeta;
export const Default: ComponentStory = () => (
-
-
+
+ {}} error={undefined} />
+
+);
+
+export const WithServices: ComponentStory = () => (
+
+ {}}
+ error={undefined}
+ />
);
diff --git a/nym-connect/src/theme/index.tsx b/nym-connect/src/theme/index.tsx
index 63f9db42fa..075aa7920a 100644
--- a/nym-connect/src/theme/index.tsx
+++ b/nym-connect/src/theme/index.tsx
@@ -3,12 +3,12 @@ import { createTheme, ThemeProvider } from '@mui/material/styles';
import { CssBaseline } from '@mui/material';
import { getDesignTokens } from './theme';
import { ClientContext } from '../context/main';
+import '../../../assets/fonts/non-variable/fonts.css';
/**
- * Provides the theme for the Network Explorer by reacting to the light/dark mode choice stored in the app context.
+ * Provides the theme for Nym Connect by reacting to the light/dark mode choice stored in the app context.
*/
-export const NymMixnetTheme: React.FC = ({ children }) => {
- const { mode } = useContext(ClientContext);
+export const NymMixnetTheme: React.FC<{ mode: 'light' | 'dark' }> = ({ children, mode }) => {
const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]);
return (
diff --git a/nym-connect/src/theme/mui-theme.d.ts b/nym-connect/src/theme/mui-theme.d.ts
index a5b92073b3..ef042f6305 100644
--- a/nym-connect/src/theme/mui-theme.d.ts
+++ b/nym-connect/src/theme/mui-theme.d.ts
@@ -30,6 +30,7 @@ declare module '@mui/material/styles' {
interface NymPalette {
highlight: string;
success: string;
+ warning: string;
info: string;
fee: string;
background: { light: string; dark: string };
diff --git a/nym-connect/src/theme/theme.tsx b/nym-connect/src/theme/theme.tsx
index e0153e6824..5b70f5118b 100644
--- a/nym-connect/src/theme/theme.tsx
+++ b/nym-connect/src/theme/theme.tsx
@@ -23,6 +23,7 @@ const nymPalette: NymPalette = {
highlight: '#FB6E4E',
success: '#21D073',
info: '#60D7EF',
+ warning: '#FFE600',
fee: '#967FF0',
background: { light: '#F4F6F8', dark: '#1D2125' },
text: {
@@ -93,6 +94,9 @@ const variantToMUIPalette = (variant: NymPaletteVariant): PaletteOptions => ({
info: {
main: nymPalette.info,
},
+ warning: {
+ main: nymPalette.warning,
+ },
background: {
default: variant.background.main,
paper: variant.background.paper,
diff --git a/nym-connect/src/types/error.ts b/nym-connect/src/types/error.ts
new file mode 100644
index 0000000000..599b959c39
--- /dev/null
+++ b/nym-connect/src/types/error.ts
@@ -0,0 +1,4 @@
+export type Error = {
+ title: string;
+ message: string;
+};
diff --git a/nym-connect/src/types/event.ts b/nym-connect/src/types/event.ts
new file mode 100644
index 0000000000..66d1e514a6
--- /dev/null
+++ b/nym-connect/src/types/event.ts
@@ -0,0 +1,6 @@
+export type TauriEvent = {
+ payload: {
+ title: string;
+ message: string;
+ };
+};