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 ? ( + + ) : ( +
+ )} + {onNext && ( + + )} + +); 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 }) => ( + {imageDescription} +); 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 ? : } - - - handleClose()} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'right', - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'left', - }} - PaperProps={{ + + {servicesWithRandomSp.map(({ id, description, sp }) => ( - handleClose(sp)} - > + handleSelectSp(sp)}> @@ -176,7 +140,7 @@ export const ServiceProviderSelector: React.FC<{ )} ))} - - + + ); }; 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; + }; +};