refactor(gui): Update MUI to v7 (#383)

* task(gui): update to mui v5

* task(gui): use sx prop instead of system props

* task(gui): update to mui v6 and replace makeStyles with sx prop

* task(gui): update to mui v7

* task(gui): update react

* fix(gui): fix import

* task(gui): adjust theme and few components to fix migration introduced styling errors

* fix(gui): animation issues with text field animations

* fix(gui): remove 'darker' theme and make 'dark' theme the default

- with the new update 'dark' theme is already quite dark and therefore a 'darker' theme not necessary
- the default theme is set to 'dark' now in settings initialization

* feat(tooling): Upgrade dprint to 0.50.0, eslint config, prettier, justfile commands

- Upgrade dprint to 0.50.0
- Use sane default eslint config (fairly permissive)
- `dprint fmt` now runs prettier for the `src-gui` folder
- Added `check_gui_eslint`, `check_gui_tsc` and `check_gui` commands

* refactor: fix a few eslint errors

* dprint fmt

* fix tsc complains

* nitpick: small spacing issue

---------

Co-authored-by: Binarybaron <binarybaron@protonmail.com>
Co-authored-by: Mohan <86064887+binarybaron@users.noreply.github.com>
This commit is contained in:
b-enedict 2025-06-06 22:31:33 +02:00 committed by GitHub
parent 2ba69ba340
commit 430a22fbf6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
169 changed files with 12883 additions and 3950 deletions

View file

@ -101,7 +101,7 @@ jobs:
- name: install dprint globally - name: install dprint globally
uses: taiki-e/cache-cargo-install-action@v2 uses: taiki-e/cache-cargo-install-action@v2
with: with:
tool: dprint@0.39.1 tool: dprint@0.50.0
- name: Build Tauri App - name: Build Tauri App
env: env:

View file

@ -87,7 +87,7 @@ jobs:
- name: install dprint globally - name: install dprint globally
uses: taiki-e/cache-cargo-install-action@v2 uses: taiki-e/cache-cargo-install-action@v2
with: with:
tool: dprint@0.39.1 tool: dprint@0.50.0
- uses: tauri-apps/tauri-action@v0 - uses: tauri-apps/tauri-action@v0
env: env:

View file

@ -40,7 +40,7 @@ jobs:
- name: Check formatting - name: Check formatting
uses: dprint/check@v2.2 uses: dprint/check@v2.2
with: with:
dprint-version: 0.39.1 dprint-version: 0.50.0
- name: Run clippy with default features - name: Run clippy with default features
run: cargo clippy --workspace --all-targets -- -D warnings run: cargo clippy --workspace --all-targets -- -D warnings

View file

@ -52,7 +52,7 @@ jobs:
- name: Commit changelog and manifest files - name: Commit changelog and manifest files
id: make-commit id: make-commit
env: env:
DPRINT_VERSION: "0.39.1" DPRINT_VERSION: "0.50.0"
RUST_TOOLCHAIN: "1.82" RUST_TOOLCHAIN: "1.82"
run: | run: |
rustup component add rustfmt --toolchain "$RUST_TOOLCHAIN-x86_64-unknown-linux-gnu" rustup component add rustfmt --toolchain "$RUST_TOOLCHAIN-x86_64-unknown-linux-gnu"

View file

@ -1,6 +1,6 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ "monero-rpc", "swap", "monero-wallet", "src-tauri" ] members = ["monero-rpc", "monero-wallet", "src-tauri", "swap"]
[patch.crates-io] [patch.crates-io]
# patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51 # patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51

View file

@ -59,9 +59,9 @@ For example:
```toml ```toml
[network] [network]
rendezvous_point = [ rendezvous_point = [
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw", "/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw",
"/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU" "/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU",
] ]
external_addresses = ["/dns4/example.com/tcp/9939"] external_addresses = ["/dns4/example.com/tcp/9939"]
``` ```

View file

@ -1,8 +1,16 @@
import Image from 'next/image'; import Image from "next/image";
export default function Logo() { export default function Logo() {
return <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> return (
<Image src="/favicon.svg" alt="UnstoppableSwap" width={32} height={32} style={{ borderRadius: '20%' }}/> <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span>UnstoppableSwap</span> <Image
</div>; src="/favicon.svg"
} alt="UnstoppableSwap"
width={32}
height={32}
style={{ borderRadius: "20%" }}
/>
<span>UnstoppableSwap</span>
</div>
);
}

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Table, Td, Th, Tr } from 'nextra/components' import { Table, Td, Th, Tr } from "nextra/components";
export default function SwapMakerTable() { export default function SwapMakerTable() {
function satsToBtc(sats) { function satsToBtc(sats) {
@ -40,9 +40,7 @@ export default function SwapMakerTable() {
<tbody> <tbody>
{makers.map((maker) => ( {makers.map((maker) => (
<Tr key={maker.peerId}> <Tr key={maker.peerId}>
<Td> <Td>{maker.testnet ? "Testnet" : "Mainnet"}</Td>
{maker.testnet ? "Testnet" : "Mainnet"}
</Td>
<Td>{maker.multiAddr}</Td> <Td>{maker.multiAddr}</Td>
<Td>{maker.peerId}</Td> <Td>{maker.peerId}</Td>
<Td>{satsToBtc(maker.minSwapAmount)} BTC</Td> <Td>{satsToBtc(maker.minSwapAmount)} BTC</Td>

View file

@ -6,4 +6,4 @@
"becoming_a_maker": "Becoming a Maker", "becoming_a_maker": "Becoming a Maker",
"send_feedback": "Send Feedback", "send_feedback": "Send Feedback",
"donate": "Donate" "donate": "Donate"
} }

View file

@ -1,3 +1,3 @@
{ {
"overview": "Overview" "overview": "Overview"
} }

View file

@ -1,3 +1,3 @@
{ {
"install_instructions": "Installation" "install_instructions": "Installation"
} }

View file

@ -48,7 +48,7 @@ If you want to build the application from source you'll need to have the followi
- `cargo` ([installation](https://www.rust-lang.org/tools/install)) and `cargo tauri` ([installation](https://v2.tauri.app/reference/cli/) and [prerequisites](https://v2.tauri.app/start/prerequisites/)) - `cargo` ([installation](https://www.rust-lang.org/tools/install)) and `cargo tauri` ([installation](https://v2.tauri.app/reference/cli/) and [prerequisites](https://v2.tauri.app/start/prerequisites/))
- `node` ([installation](https://nodejs.org/en/download/)) and `yarn` (version 1.22, not 4.x) - `node` ([installation](https://nodejs.org/en/download/)) and `yarn` (version 1.22, not 4.x)
- `dprint` (`cargo install dprint@0.39.1`) - `dprint` (`cargo install dprint@0.50.0`)
- `typeshare` (`cargo install typeshare-cli`) - `typeshare` (`cargo install typeshare-cli`)
After that you only need to clone the repository and run the following commands: After that you only need to clone the repository and run the following commands:

View file

@ -2,4 +2,4 @@
"first_swap": "Complete your first swap", "first_swap": "Complete your first swap",
"market_maker_discovery": "Maker discovery", "market_maker_discovery": "Maker discovery",
"refund_punish": "Cancel, Refund and Punish explained" "refund_punish": "Cancel, Refund and Punish explained"
} }

View file

@ -1,10 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": false, "strict": false,
@ -18,12 +14,6 @@
"jsx": "preserve", "jsx": "preserve",
"target": "ES2017" "target": "ES2017"
}, },
"include": [ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"next-env.d.ts", "exclude": ["node_modules"]
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
} }

View file

@ -4,25 +4,31 @@
"incremental": true, "incremental": true,
"markdown": {}, "markdown": {},
"exec": { "exec": {
"associations": "**/*.{rs}", "commands": [
"rustfmt": "rustfmt --edition 2021", {
"rustfmt.associations": "**/*.rs" "command": "rustfmt --edition 2021",
"exts": ["rs"]
}
]
}, },
"includes": [ "includes": [
"**/*.{md}", "**/*.{md}",
"**/*.{toml}", "**/*.{toml}",
"**/*.{rs}" "**/*.{rs}",
"**/*.{js,jsx,ts,tsx,json,css,scss,html}"
], ],
"excludes": [ "excludes": [
"target/", "target/",
"src-tauri/Cargo.toml", "src-tauri/Cargo.toml",
"monero-sys/monero/", "monero-sys/monero/",
".git/**" ".git/**",
"**/node_modules/**",
"**/dist/**"
], ],
"plugins": [ "plugins": [
"https://plugins.dprint.dev/markdown-0.13.1.wasm", "https://plugins.dprint.dev/markdown-0.18.0.wasm",
"https://github.com/thomaseizinger/dprint-plugin-cargo-toml/releases/download/0.1.0/cargo-toml-0.1.0.wasm", "https://plugins.dprint.dev/toml-0.7.0.wasm",
"https://plugins.dprint.dev/exec-0.3.5.json@d687dda57be0fe9a0088ccdaefa5147649ff24127d8b3ea227536c68ee7abeab", "https://plugins.dprint.dev/exec-0.5.1.json@492414e39dea4dccc07b4af796d2f4efdb89e84bae2bd4e1e924c0cc050855bf",
"https://plugins.dprint.dev/prettier-0.26.6.json@0118376786f37496e41bb19dbcfd1e7214e2dc859a55035c5e54d1107b4c9c57" "https://plugins.dprint.dev/prettier-0.57.0.json@1bc6b449e982d5b91a25a7c59894102d40c5748651a08a095fb3926e64d55a31"
] ]
} }

View file

@ -69,6 +69,19 @@ kill_monero_wallet_rpc:
fmt: fmt:
dprint fmt dprint fmt
# Run eslint for the GUI frontend
check_gui_eslint:
cd src-gui && yarn run eslint
# Run the typescript type checker for the GUI frontend
check_gui_tsc:
cd src-gui && yarn run tsc --noEmit
# Run the checks for the GUI frontend
check_gui:
just check_gui_eslint || true
just check_gui_tsc
# Sometimes you have to prune the docker network to get the integration tests to work # Sometimes you have to prune the docker network to get the integration tests to work
docker-prune-network: docker-prune-network:
docker network prune -f docker network prune -f

View file

@ -1,7 +1,7 @@
[package] [package]
name = "monero-harness" name = "monero-harness"
version = "0.1.0" version = "0.1.0"
authors = [ "CoBloX Team <team@coblox.tech>" ] authors = ["CoBloX Team <team@coblox.tech>"]
edition = "2021" edition = "2021"
publish = false publish = false
@ -11,6 +11,6 @@ futures = "0.3"
monero-rpc = { path = "../monero-rpc" } monero-rpc = { path = "../monero-rpc" }
rand = "0.7" rand = "0.7"
testcontainers = "0.15" testcontainers = "0.15"
tokio = { version = "1", default-features = false, features = [ "rt-multi-thread", "time", "macros" ] } tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "time", "macros"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", "ansi", "env-filter", "tracing-log" ] } tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "ansi", "env-filter", "tracing-log"] }

View file

@ -1,23 +1,23 @@
[package] [package]
name = "monero-rpc" name = "monero-rpc"
version = "0.1.0" version = "0.1.0"
authors = [ "CoBloX Team <team@coblox.tech>" ] authors = ["CoBloX Team <team@coblox.tech>"]
edition = "2021" edition = "2021"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
curve25519-dalek = "3.1" curve25519-dalek = "3.1"
hex = "0.4" hex = "0.4"
jsonrpc_client = { version = "0.7", features = [ "reqwest" ] } jsonrpc_client = { version = "0.7", features = ["reqwest"] }
monero = "0.12" monero = "0.12"
monero-epee-bin-serde = "1" monero-epee-bin-serde = "1"
rand = "0.7" rand = "0.7"
reqwest = { version = "0.12", default-features = false, features = [ "json" ] } reqwest = { version = "0.12", default-features = false, features = ["json"] }
rust_decimal = { version = "1", features = [ "serde-float" ] } rust_decimal = { version = "1", features = ["serde-float"] }
serde = { version = "1.0", features = [ "derive" ] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tracing = "0.1" tracing = "0.1"
[dev-dependencies] [dev-dependencies]
hex-literal = "0.4" hex-literal = "0.4"
tokio = { version = "1", features = [ "full" ] } tokio = { version = "1", features = ["full"] }

View file

@ -1,7 +1,7 @@
[package] [package]
name = "monero-wallet" name = "monero-wallet"
version = "0.1.0" version = "0.1.0"
authors = [ "CoBloX Team <team@coblox.tech>" ] authors = ["CoBloX Team <team@coblox.tech>"]
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -15,5 +15,5 @@ curve25519-dalek = "3"
monero-harness = { path = "../monero-harness" } monero-harness = { path = "../monero-harness" }
rand = "0.7" rand = "0.7"
testcontainers = "0.15" testcontainers = "0.15"
tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs" ] } tokio = { version = "1", features = ["rt-multi-thread", "time", "macros", "sync", "process", "fs"] }
tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", "ansi", "env-filter", "chrono", "tracing-log" ] } tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "ansi", "env-filter", "chrono", "tracing-log"] }

View file

@ -6,7 +6,7 @@
- For compiling the Rust code: `cargo` and `cargo tauri` ([installation](https://v2.tauri.app/reference/cli/)) - For compiling the Rust code: `cargo` and `cargo tauri` ([installation](https://v2.tauri.app/reference/cli/))
- For running the Typescript code: `node` and `yarn` - For running the Typescript code: `node` and `yarn`
- For formatting and bindings: `dprint` (`cargo install dprint@0.39.1`) and `typeshare` (`cargo install typeshare-cli`) - For formatting and bindings: `dprint` (`cargo install dprint@0.50.0`) and `typeshare` (`cargo install typeshare-cli`)
- If you are on Windows and you want to use the `check-bindings` command you'll need to manually install the GNU DiffUtils ([installation](https://gnuwin32.sourceforge.net/packages/diffutils.htm)) and GNU CoreUtils ([installtion](https://gnuwin32.sourceforge.net/packages/coreutils.htm)). Remember to add the installation path (probably `C:\Program Files (x86)\GnuWin32\bin`) to the `PATH` in your enviroment variables. - If you are on Windows and you want to use the `check-bindings` command you'll need to manually install the GNU DiffUtils ([installation](https://gnuwin32.sourceforge.net/packages/diffutils.htm)) and GNU CoreUtils ([installtion](https://gnuwin32.sourceforge.net/packages/coreutils.htm)). Remember to add the installation path (probably `C:\Program Files (x86)\GnuWin32\bin`) to the `PATH` in your enviroment variables.
## Start development servers ## Start development servers

View file

@ -1,21 +1,23 @@
import globals from "globals"; import globals from "globals";
import pluginJs from "@eslint/js"; import js from "@eslint/js";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react"; import pluginReact from "eslint-plugin-react";
export default [ export default [
{ { ignores: ["node_modules", "dist"] },
ignores: ["node_modules", "dist"], js.configs.recommended,
},
pluginJs.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,
pluginReact.configs.flat.recommended, pluginReact.configs.flat.recommended,
{ {
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], languageOptions: {
languageOptions: { globals: globals.browser }, globals: globals.browser,
},
rules: { rules: {
"react/react-in-jsx-scope": "off", "react/react-in-jsx-scope": "off",
// Disallow the use of the `open` on the gloal object "react/no-unescaped-entities": "off",
"react/no-children-prop": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-empty-object-type": "off",
"no-restricted-globals": [ "no-restricted-globals": [
"warn", "warn",
{ {
@ -24,7 +26,6 @@ export default [
"Use the open(...) function from @tauri-apps/plugin-shell instead", "Use the open(...) function from @tauri-apps/plugin-shell instead",
}, },
], ],
// Disallow the use of the `open` on the `window` object
"no-restricted-properties": [ "no-restricted-properties": [
"warn", "warn",
{ {

View file

@ -1,35 +1,33 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<head> <body>
<meta charset="UTF-8" /> <div id="root"></div>
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <script type="module" src="/src/renderer/index.tsx"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <style>
</head> ::-webkit-scrollbar {
display: none;
}
<body> *,
<div id="root"></div> *::after,
<script type="module" src="/src/renderer/index.tsx"></script> *::before {
<style> -webkit-user-select: none;
::-webkit-scrollbar { -webkit-user-drag: none;
display: none; -webkit-app-region: no-drag;
} }
*, html,
*::after, body {
*::before { height: 100%;
-webkit-user-select: none; margin: 0;
-webkit-user-drag: none; overflow: auto;
-webkit-app-region: no-drag; }
} </style>
</body>
html, </html>
body {
height: 100%;
margin: 0;
overflow: auto;
}
</style>
</body>
</html>

8284
src-gui/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -16,10 +16,12 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.1.0", "@fontsource/roboto": "^5.1.0",
"@material-ui/core": "^4.12.4", "@mui/icons-material": "^7.1.1",
"@material-ui/icons": "^4.11.3", "@mui/lab": "^7.0.0-beta.13",
"@material-ui/lab": "^4.0.0-alpha.61", "@mui/material": "^7.1.1",
"@reduxjs/toolkit": "^2.3.0", "@reduxjs/toolkit": "^2.3.0",
"@tauri-apps/api": "^2.0.0", "@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-cli": "^2.0.0", "@tauri-apps/plugin-cli": "^2.0.0",
@ -37,11 +39,11 @@
"notistack": "^3.0.1", "notistack": "^3.0.1",
"pino": "^9.2.0", "pino": "^9.2.0",
"pino-pretty": "^11.2.1", "pino-pretty": "^11.2.1",
"react": "^18.2.0", "react": "^19.1.0",
"react-dom": "^18.2.0", "react-dom": "^19.1.0",
"react-qr-code": "^2.0.15", "react-qr-code": "^2.0.15",
"react-redux": "^9.1.2", "react-redux": "^9.2.0",
"react-router-dom": "^6.28.0", "react-router-dom": "^7.6.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"semver": "^7.6.2", "semver": "^7.6.2",
"virtua": "^0.33.2" "virtua": "^0.33.2"
@ -54,9 +56,10 @@
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
"@types/humanize-duration": "^3.27.4", "@types/humanize-duration": "^3.27.4",
"@types/lodash": "^4.17.6", "@types/lodash": "^4.17.6",
"@types/node": "^20.14.10", "@types/node": "^22.15.29",
"@types/react": "^18.2.15", "@types/react": "^19.1.6",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^19.1.5",
"@types/react-is": "^19.0.0",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"eslint": "^9.9.0", "eslint": "^9.9.0",

View file

@ -6,7 +6,7 @@ export interface ExtendedMakerStatus extends MakerStatus {
recommended?: boolean; recommended?: boolean;
} }
export interface MakerStatus extends MakerQuote, Maker { } export interface MakerStatus extends MakerQuote, Maker {}
export interface MakerQuote { export interface MakerQuote {
price: number; price: number;
@ -29,16 +29,16 @@ export interface Alert {
// Define the correct 9-element tuple type for PrimitiveDateTime // Define the correct 9-element tuple type for PrimitiveDateTime
export type PrimitiveDateTimeString = [ export type PrimitiveDateTimeString = [
number, // Year number, // Year
number, // Day of Year number, // Day of Year
number, // Hour number, // Hour
number, // Minute number, // Minute
number, // Second number, // Second
number, // Nanosecond number, // Nanosecond
number, // Offset Hour number, // Offset Hour
number, // Offset Minute number, // Offset Minute
number // Offset Second number, // Offset Second
]; ];
export interface Feedback { export interface Feedback {
id: string; id: string;
@ -46,7 +46,7 @@ export interface Feedback {
} }
export interface Attachment { export interface Attachment {
id: number; id: number;
message_id: number; message_id: number;
key: string; key: string;
content: string; content: string;

View file

@ -61,4 +61,3 @@ export function parseCliLogString(log: string): CliLog | string {
return log; return log;
} }
} }

View file

@ -14,7 +14,8 @@ export type TauriSwapProgressEventContent<
T extends TauriSwapProgressEventType, T extends TauriSwapProgressEventType,
> = Extract<TauriSwapProgressEvent, { type: T }>["content"]; > = Extract<TauriSwapProgressEvent, { type: T }>["content"];
export type TauriSwapProgressEventExt<T extends TauriSwapProgressEventType> = Extract<TauriSwapProgressEvent, { type: T }>; export type TauriSwapProgressEventExt<T extends TauriSwapProgressEventType> =
Extract<TauriSwapProgressEvent, { type: T }>;
// See /swap/src/protocol/bob/state.rs#L57 // See /swap/src/protocol/bob/state.rs#L57
// TODO: Replace this with a typeshare definition // TODO: Replace this with a typeshare definition
@ -36,19 +37,32 @@ export enum BobStateName {
export function bobStateNameToHumanReadable(stateName: BobStateName): string { export function bobStateNameToHumanReadable(stateName: BobStateName): string {
switch (stateName) { switch (stateName) {
case BobStateName.Started: return "Started"; case BobStateName.Started:
case BobStateName.SwapSetupCompleted: return "Setup completed"; return "Started";
case BobStateName.BtcLocked: return "Bitcoin locked"; case BobStateName.SwapSetupCompleted:
case BobStateName.XmrLockProofReceived: return "Monero locked"; return "Setup completed";
case BobStateName.XmrLocked: return "Monero locked and fully confirmed"; case BobStateName.BtcLocked:
case BobStateName.EncSigSent: return "Encrypted signature sent"; return "Bitcoin locked";
case BobStateName.BtcRedeemed: return "Bitcoin redeemed"; case BobStateName.XmrLockProofReceived:
case BobStateName.CancelTimelockExpired: return "Cancel timelock expired"; return "Monero locked";
case BobStateName.BtcCancelled: return "Bitcoin cancelled"; case BobStateName.XmrLocked:
case BobStateName.BtcRefunded: return "Bitcoin refunded"; return "Monero locked and fully confirmed";
case BobStateName.XmrRedeemed: return "Monero redeemed"; case BobStateName.EncSigSent:
case BobStateName.BtcPunished: return "Bitcoin punished"; return "Encrypted signature sent";
case BobStateName.SafelyAborted: return "Safely aborted"; case BobStateName.BtcRedeemed:
return "Bitcoin redeemed";
case BobStateName.CancelTimelockExpired:
return "Cancel timelock expired";
case BobStateName.BtcCancelled:
return "Bitcoin cancelled";
case BobStateName.BtcRefunded:
return "Bitcoin refunded";
case BobStateName.XmrRedeemed:
return "Monero redeemed";
case BobStateName.BtcPunished:
return "Bitcoin punished";
case BobStateName.SafelyAborted:
return "Safely aborted";
default: default:
return exhaustiveGuard(stateName); return exhaustiveGuard(stateName);
} }
@ -64,7 +78,11 @@ export type TimelockCancel = Extract<ExpiredTimelocks, { type: "Cancel" }>;
export type TimelockPunish = Extract<ExpiredTimelocks, { type: "Punish" }>; export type TimelockPunish = Extract<ExpiredTimelocks, { type: "Punish" }>;
// This function returns the absolute block number of the timelock relative to the block the tx_lock was included in // This function returns the absolute block number of the timelock relative to the block the tx_lock was included in
export function getAbsoluteBlock(timelock: ExpiredTimelocks, cancelTimelock: number, punishTimelock: number): number { export function getAbsoluteBlock(
timelock: ExpiredTimelocks,
cancelTimelock: number,
punishTimelock: number,
): number {
if (timelock.type === "None") { if (timelock.type === "None") {
return cancelTimelock - timelock.content.blocks_left; return cancelTimelock - timelock.content.blocks_left;
} }
@ -208,12 +226,15 @@ export function isGetSwapInfoResponseRunningSwap(
* @returns True if the timelock exists, false otherwise * @returns True if the timelock exists, false otherwise
*/ */
export function isGetSwapInfoResponseWithTimelock( export function isGetSwapInfoResponseWithTimelock(
response: GetSwapInfoResponseExt response: GetSwapInfoResponseExt,
): response is GetSwapInfoResponseExtWithTimelock { ): response is GetSwapInfoResponseExtWithTimelock {
return response.timelock !== null; return response.timelock !== null;
} }
export type PendingApprovalRequest = Extract<ApprovalRequest, { state: "Pending" }>; export type PendingApprovalRequest = Extract<
ApprovalRequest,
{ state: "Pending" }
>;
export type PendingLockBitcoinApprovalRequest = PendingApprovalRequest & { export type PendingLockBitcoinApprovalRequest = PendingApprovalRequest & {
content: { content: {
@ -239,7 +260,10 @@ export function isPendingBackgroundProcess(
return process.progress.type === "Pending"; return process.progress.type === "Pending";
} }
export type TauriBitcoinSyncProgress = Extract<TauriBackgroundProgress, { componentName: "SyncingBitcoinWallet" }>; export type TauriBitcoinSyncProgress = Extract<
TauriBackgroundProgress,
{ componentName: "SyncingBitcoinWallet" }
>;
export function isBitcoinSyncProgress( export function isBitcoinSyncProgress(
progress: TauriBackgroundProgress, progress: TauriBackgroundProgress,

View file

@ -5,20 +5,34 @@
// - and to submit feedback // - and to submit feedback
// - fetch currency rates from CoinGecko // - fetch currency rates from CoinGecko
import { Alert, Attachment, AttachmentInput, ExtendedMakerStatus, Feedback, Message, MessageWithAttachments, PrimitiveDateTimeString } from "models/apiModel"; import {
Alert,
Attachment,
AttachmentInput,
ExtendedMakerStatus,
Feedback,
Message,
MessageWithAttachments,
PrimitiveDateTimeString,
} from "models/apiModel";
import { store } from "./store/storeRenderer"; import { store } from "./store/storeRenderer";
import { setBtcPrice, setXmrBtcRate, setXmrPrice } from "store/features/ratesSlice"; import {
setBtcPrice,
setXmrBtcRate,
setXmrPrice,
} from "store/features/ratesSlice";
import { FiatCurrency } from "store/features/settingsSlice"; import { FiatCurrency } from "store/features/settingsSlice";
import { setAlerts } from "store/features/alertsSlice"; import { setAlerts } from "store/features/alertsSlice";
import { registryConnectionFailed, setRegistryMakers } from "store/features/makersSlice"; import {
registryConnectionFailed,
setRegistryMakers,
} from "store/features/makersSlice";
import logger from "utils/logger"; import logger from "utils/logger";
import { setConversation } from "store/features/conversationsSlice"; import { setConversation } from "store/features/conversationsSlice";
const PUBLIC_REGISTRY_API_BASE_URL = "https://api.unstoppableswap.net"; const PUBLIC_REGISTRY_API_BASE_URL = "https://api.unstoppableswap.net";
async function fetchMakersViaHttp(): Promise< async function fetchMakersViaHttp(): Promise<ExtendedMakerStatus[]> {
ExtendedMakerStatus[]
> {
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/list`); const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/list`);
return (await response.json()) as ExtendedMakerStatus[]; return (await response.json()) as ExtendedMakerStatus[];
} }
@ -30,7 +44,7 @@ async function fetchAlertsViaHttp(): Promise<Alert[]> {
export async function submitFeedbackViaHttp( export async function submitFeedbackViaHttp(
content: string, content: string,
attachments?: AttachmentInput[] attachments?: AttachmentInput[],
): Promise<string> { ): Promise<string> {
type Response = string; type Response = string;
@ -39,64 +53,83 @@ export async function submitFeedbackViaHttp(
attachments: attachments || [], // Ensure attachments is always an array attachments: attachments || [], // Ensure attachments is always an array
}; };
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/submit-feedback`, { const response = await fetch(
method: "POST", `${PUBLIC_REGISTRY_API_BASE_URL}/api/submit-feedback`,
headers: { {
"Content-Type": "application/json", method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestPayload), // Send the corrected structure
}, },
body: JSON.stringify(requestPayload), // Send the corrected structure );
});
if (!response.ok) { if (!response.ok) {
const errorBody = await response.text(); const errorBody = await response.text();
throw new Error(`Failed to submit feedback. Status: ${response.status}. Body: ${errorBody}`); throw new Error(
`Failed to submit feedback. Status: ${response.status}. Body: ${errorBody}`,
);
} }
const responseBody = (await response.json()) as Response; const responseBody = (await response.json()) as Response;
return responseBody; return responseBody;
} }
export async function fetchFeedbackMessagesViaHttp(feedbackId: string): Promise<Message[]> { export async function fetchFeedbackMessagesViaHttp(
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/feedback/${feedbackId}/messages`); feedbackId: string,
): Promise<Message[]> {
const response = await fetch(
`${PUBLIC_REGISTRY_API_BASE_URL}/api/feedback/${feedbackId}/messages`,
);
if (!response.ok) { if (!response.ok) {
const errorBody = await response.text(); const errorBody = await response.text();
throw new Error(`Failed to fetch messages for feedback ${feedbackId}. Status: ${response.status}. Body: ${errorBody}`); throw new Error(
`Failed to fetch messages for feedback ${feedbackId}. Status: ${response.status}. Body: ${errorBody}`,
);
} }
// Assuming the response is directly the Message[] array including attachments // Assuming the response is directly the Message[] array including attachments
return (await response.json()) as Message[]; return (await response.json()) as Message[];
} }
export async function appendFeedbackMessageViaHttp( export async function appendFeedbackMessageViaHttp(
feedbackId: string, feedbackId: string,
content: string, content: string,
attachments?: AttachmentInput[] attachments?: AttachmentInput[],
): Promise<number> { ): Promise<number> {
type Response = number; type Response = number;
const body = { const body = {
feedback_id: feedbackId, feedback_id: feedbackId,
content, content,
attachments: attachments || [], // Ensure attachments is always an array attachments: attachments || [], // Ensure attachments is always an array
}; };
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/append-feedback-message`, { const response = await fetch(
method: "POST", `${PUBLIC_REGISTRY_API_BASE_URL}/api/append-feedback-message`,
headers: { {
"Content-Type": "application/json", method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body), // Send new structure
}, },
body: JSON.stringify(body), // Send new structure );
});
if (!response.ok) { if (!response.ok) {
const errorBody = await response.text(); const errorBody = await response.text();
throw new Error(`Failed to append message for feedback ${feedbackId}. Status: ${response.status}. Body: ${errorBody}`); throw new Error(
`Failed to append message for feedback ${feedbackId}. Status: ${response.status}. Body: ${errorBody}`,
);
} }
const responseBody = (await response.json()) as Response; const responseBody = (await response.json()) as Response;
return responseBody; return responseBody;
} }
async function fetchCurrencyPrice(currency: string, fiatCurrency: FiatCurrency): Promise<number> { async function fetchCurrencyPrice(
currency: string,
fiatCurrency: FiatCurrency,
): Promise<number> {
const response = await fetch( const response = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=${fiatCurrency.toLowerCase()}`, `https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=${fiatCurrency.toLowerCase()}`,
); );
@ -105,7 +138,9 @@ async function fetchCurrencyPrice(currency: string, fiatCurrency: FiatCurrency):
} }
async function fetchXmrBtcRate(): Promise<number> { async function fetchXmrBtcRate(): Promise<number> {
const response = await fetch('https://api.kraken.com/0/public/Ticker?pair=XMRXBT'); const response = await fetch(
"https://api.kraken.com/0/public/Ticker?pair=XMRXBT",
);
const data = await response.json(); const data = await response.json();
if (data.error && data.error.length > 0) { if (data.error && data.error.length > 0) {
@ -127,13 +162,12 @@ async function fetchXmrPrice(fiatCurrency: FiatCurrency): Promise<number> {
} }
/** /**
* If enabled by the user, fetch the XMR, BTC and XMR/BTC rates * If enabled by the user, fetch the XMR, BTC and XMR/BTC rates
* and store them in the Redux store. * and store them in the Redux store.
*/ */
export async function updateRates(): Promise<void> { export async function updateRates(): Promise<void> {
const settings = store.getState().settings; const settings = store.getState().settings;
if (!settings.fetchFiatPrices) if (!settings.fetchFiatPrices) return;
return;
try { try {
const xmrBtcRate = await fetchXmrBtcRate(); const xmrBtcRate = await fetchXmrBtcRate();
@ -191,8 +225,12 @@ export async function fetchAllConversations(): Promise<void> {
const messages = await fetchFeedbackMessagesViaHttp(feedbackId); const messages = await fetchFeedbackMessagesViaHttp(feedbackId);
console.log("Fetched messages for feedback id", feedbackId, messages); console.log("Fetched messages for feedback id", feedbackId, messages);
store.dispatch(setConversation({ feedbackId, messages })); store.dispatch(setConversation({ feedbackId, messages }));
} catch (error) { } catch (error) {
logger.error(error, "Error fetching messages for feedback id", feedbackId); logger.error(
error,
"Error fetching messages for feedback id",
feedbackId,
);
} }
} }
} }

View file

@ -1,10 +1,27 @@
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { TauriContextStatusEvent, TauriEvent } from "models/tauriModel"; import { TauriContextStatusEvent, TauriEvent } from "models/tauriModel";
import { contextStatusEventReceived, receivedCliLog, rpcSetBalance, timelockChangeEventReceived, approvalEventReceived, backgroundProgressEventReceived } from "store/features/rpcSlice"; import {
contextStatusEventReceived,
receivedCliLog,
rpcSetBalance,
timelockChangeEventReceived,
approvalEventReceived,
backgroundProgressEventReceived,
} from "store/features/rpcSlice";
import { swapProgressEventReceived } from "store/features/swapSlice"; import { swapProgressEventReceived } from "store/features/swapSlice";
import logger from "utils/logger"; import logger from "utils/logger";
import { fetchAllConversations, updateAlerts, updatePublicRegistry, updateRates } from "./api"; import {
import { checkContextAvailability, getSwapInfo, initializeContext, updateAllNodeStatuses } from "./rpc"; fetchAllConversations,
updateAlerts,
updatePublicRegistry,
updateRates,
} from "./api";
import {
checkContextAvailability,
getSwapInfo,
initializeContext,
updateAllNodeStatuses,
} from "./rpc";
import { store } from "./store/storeRenderer"; import { store } from "./store/storeRenderer";
import { exhaustiveGuard } from "utils/typescriptUtils"; import { exhaustiveGuard } from "utils/typescriptUtils";
@ -23,81 +40,86 @@ const UPDATE_RATE_INTERVAL = 5 * 60 * 1_000;
const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000; const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000;
function setIntervalImmediate(callback: () => void, interval: number): void { function setIntervalImmediate(callback: () => void, interval: number): void {
callback(); callback();
setInterval(callback, interval); setInterval(callback, interval);
} }
export async function setupBackgroundTasks(): Promise<void> { export async function setupBackgroundTasks(): Promise<void> {
// Setup periodic fetch tasks // Setup periodic fetch tasks
setIntervalImmediate(updatePublicRegistry, PROVIDER_UPDATE_INTERVAL); setIntervalImmediate(updatePublicRegistry, PROVIDER_UPDATE_INTERVAL);
setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL); setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
setIntervalImmediate(updateRates, UPDATE_RATE_INTERVAL); setIntervalImmediate(updateRates, UPDATE_RATE_INTERVAL);
setIntervalImmediate(fetchAllConversations, FETCH_CONVERSATIONS_INTERVAL); setIntervalImmediate(fetchAllConversations, FETCH_CONVERSATIONS_INTERVAL);
// Fetch all alerts // Fetch all alerts
updateAlerts(); updateAlerts();
// Setup Tauri event listeners // Setup Tauri event listeners
// Check if the context is already available. This is to prevent unnecessary re-initialization // Check if the context is already available. This is to prevent unnecessary re-initialization
if (await checkContextAvailability()) { if (await checkContextAvailability()) {
store.dispatch(contextStatusEventReceived(TauriContextStatusEvent.Available)); store.dispatch(
} else { contextStatusEventReceived(TauriContextStatusEvent.Available),
// Warning: If we reload the page while the Context is being initialized, this function will throw an error );
} else {
// Warning: If we reload the page while the Context is being initialized, this function will throw an error
initializeContext().catch((e) => {
logger.error(
e,
"Failed to initialize context on page load. This might be because we reloaded the page while the context was being initialized",
);
// Wait a short time before retrying
setTimeout(() => {
initializeContext().catch((e) => { initializeContext().catch((e) => {
logger.error(e, "Failed to initialize context on page load. This might be because we reloaded the page while the context was being initialized"); logger.error(e, "Failed to initialize context even after retry");
// Wait a short time before retrying
setTimeout(() => {
initializeContext().catch((e) => {
logger.error(e, "Failed to initialize context even after retry");
});
}, 2000);
}); });
} }, 2000);
// Listen for the unified event
listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
const { channelName, event: eventData } = event.payload;
switch (channelName) {
case "SwapProgress":
store.dispatch(swapProgressEventReceived(eventData));
break;
case "ContextInitProgress":
store.dispatch(contextStatusEventReceived(eventData));
break;
case "CliLog":
store.dispatch(receivedCliLog(eventData));
break;
case "BalanceChange":
store.dispatch(rpcSetBalance((eventData).balance));
break;
case "SwapDatabaseStateUpdate":
getSwapInfo(eventData.swap_id);
// This is ugly but it's the best we can do for now
// Sometimes we are too quick to fetch the swap info and the new state is not yet reflected
// in the database. So we wait a bit before fetching the new state
setTimeout(() => getSwapInfo(eventData.swap_id), 3000);
break;
case "TimelockChange":
store.dispatch(timelockChangeEventReceived(eventData));
break;
case "Approval":
store.dispatch(approvalEventReceived(eventData));
break;
case "BackgroundProgress":
store.dispatch(backgroundProgressEventReceived(eventData));
break;
default:
exhaustiveGuard(channelName);
}
}); });
} }
// Listen for the unified event
listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
const { channelName, event: eventData } = event.payload;
switch (channelName) {
case "SwapProgress":
store.dispatch(swapProgressEventReceived(eventData));
break;
case "ContextInitProgress":
store.dispatch(contextStatusEventReceived(eventData));
break;
case "CliLog":
store.dispatch(receivedCliLog(eventData));
break;
case "BalanceChange":
store.dispatch(rpcSetBalance(eventData.balance));
break;
case "SwapDatabaseStateUpdate":
getSwapInfo(eventData.swap_id);
// This is ugly but it's the best we can do for now
// Sometimes we are too quick to fetch the swap info and the new state is not yet reflected
// in the database. So we wait a bit before fetching the new state
setTimeout(() => getSwapInfo(eventData.swap_id), 3000);
break;
case "TimelockChange":
store.dispatch(timelockChangeEventReceived(eventData));
break;
case "Approval":
store.dispatch(approvalEventReceived(eventData));
break;
case "BackgroundProgress":
store.dispatch(backgroundProgressEventReceived(eventData));
break;
default:
exhaustiveGuard(channelName);
}
});
}

View file

@ -1,5 +1,9 @@
import { Box, CssBaseline, makeStyles } from "@material-ui/core"; import { Box, CssBaseline } from "@mui/material";
import { ThemeProvider } from "@material-ui/core/styles"; import {
ThemeProvider,
Theme,
StyledEngineProvider,
} from "@mui/material/styles";
import "@tauri-apps/plugin-shell"; import "@tauri-apps/plugin-shell";
import { Route, MemoryRouter as Router, Routes } from "react-router-dom"; import { Route, MemoryRouter as Router, Routes } from "react-router-dom";
import Navigation, { drawerWidth } from "./navigation/Navigation"; import Navigation, { drawerWidth } from "./navigation/Navigation";
@ -10,21 +14,21 @@ import WalletPage from "./pages/wallet/WalletPage";
import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider"; import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider";
import UpdaterDialog from "./modal/updater/UpdaterDialog"; import UpdaterDialog from "./modal/updater/UpdaterDialog";
import { useSettings } from "store/hooks"; import { useSettings } from "store/hooks";
import { themes } from "./theme"; import { Theme as ThemeEnum, themes } from "./theme";
import { useEffect } from "react"; import { useEffect } from "react";
import { setupBackgroundTasks } from "renderer/background"; import { setupBackgroundTasks } from "renderer/background";
import "@fontsource/roboto"; import "@fontsource/roboto";
import FeedbackPage from "./pages/feedback/FeedbackPage"; import FeedbackPage from "./pages/feedback/FeedbackPage";
import IntroductionModal from "./modal/introduction/IntroductionModal"; import IntroductionModal from "./modal/introduction/IntroductionModal";
const useStyles = makeStyles((theme) => ({ declare module "@mui/material/styles" {
innerContent: { interface Theme {
padding: theme.spacing(4), // Add your custom theme properties here if needed
marginLeft: drawerWidth, }
maxHeight: `100vh`, interface ThemeOptions {
flex: 1, // Add your custom theme options here if needed
}, }
})); }
export default function App() { export default function App() {
useEffect(() => { useEffect(() => {
@ -32,27 +36,37 @@ export default function App() {
}, []); }, []);
const theme = useSettings((s) => s.theme); const theme = useSettings((s) => s.theme);
const currentTheme = themes[theme] || themes[ThemeEnum.Dark];
console.log("Current theme:", { theme, currentTheme });
return ( return (
<ThemeProvider theme={themes[theme]}> <StyledEngineProvider injectFirst>
<GlobalSnackbarProvider> <ThemeProvider theme={currentTheme}>
<CssBaseline /> <CssBaseline />
<IntroductionModal/> <GlobalSnackbarProvider>
<Router> <IntroductionModal />
<Navigation /> <Router>
<InnerContent /> <Navigation />
<UpdaterDialog /> <InnerContent />
</Router> <UpdaterDialog />
</GlobalSnackbarProvider> </Router>
</ThemeProvider> </GlobalSnackbarProvider>
</ThemeProvider>
</StyledEngineProvider>
); );
} }
function InnerContent() { function InnerContent() {
const classes = useStyles();
return ( return (
<Box className={classes.innerContent}> <Box
sx={{
padding: 4,
marginLeft: drawerWidth,
maxHeight: `100vh`,
flex: 1,
}}
>
<Routes> <Routes>
<Route path="/swap" element={<SwapPage />} /> <Route path="/swap" element={<SwapPage />} />
<Route path="/history" element={<HistoryPage />} /> <Route path="/history" element={<HistoryPage />} />
@ -63,4 +77,4 @@ function InnerContent() {
</Routes> </Routes>
</Box> </Box>
); );
} }

View file

@ -4,8 +4,8 @@ import {
IconButton, IconButton,
IconButtonProps, IconButtonProps,
Tooltip, Tooltip,
} from "@material-ui/core"; } from "@mui/material";
import CircularProgress from "@material-ui/core/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react";
import { useIsContextAvailable } from "store/hooks"; import { useIsContextAvailable } from "store/hooks";
@ -84,6 +84,10 @@ export default function PromiseInvokeButton<T>({
onClick={handleClick} onClick={handleClick}
disabled={isDisabled} disabled={isDisabled}
{...(rest as IconButtonProps)} {...(rest as IconButtonProps)}
size="large"
sx={{
padding: "0.25rem",
}}
> >
{actualEndIcon} {actualEndIcon}
</IconButton> </IconButton>

View file

@ -1,42 +1,49 @@
import { BackgroundRefundState } from "models/tauriModel"; import { BackgroundRefundState } from "models/tauriModel";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert"; import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
import { AlertTitle } from "@material-ui/lab"; import { AlertTitle } from "@mui/material";
import TruncatedText from "../other/TruncatedText"; import TruncatedText from "../other/TruncatedText";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { useEffect } from "react"; import { useEffect } from "react";
export default function BackgroundRefundAlert() { export default function BackgroundRefundAlert() {
const backgroundRefund = useAppSelector(state => state.rpc.state.backgroundRefund); const backgroundRefund = useAppSelector(
const notistack = useSnackbar(); (state) => state.rpc.state.backgroundRefund,
);
const notistack = useSnackbar();
useEffect(() => { useEffect(() => {
// If we failed to refund, show a notification // If we failed to refund, show a notification
if (backgroundRefund?.state.type === "Failed") { if (backgroundRefund?.state.type === "Failed") {
notistack.enqueueSnackbar( notistack.enqueueSnackbar(
<> <>
Our attempt to refund {backgroundRefund.swapId} in the background failed. Our attempt to refund {backgroundRefund.swapId} in the background
<br /> failed.
Error: {backgroundRefund.state.content.error} <br />
</>, Error: {backgroundRefund.state.content.error}
{ variant: "error", autoHideDuration: 60 * 1000 } </>,
); { variant: "error", autoHideDuration: 60 * 1000 },
} );
// If we successfully refunded, show a notification as well
if (backgroundRefund?.state.type === "Completed") {
notistack.enqueueSnackbar(`The swap ${backgroundRefund.swapId} has been refunded in the background.`, { variant: "success", persist: true });
}
}, [backgroundRefund]);
if (backgroundRefund?.state.type === "Started") {
return <LoadingSpinnerAlert>
<AlertTitle>
Refund in progress
</AlertTitle>
The swap <TruncatedText>{backgroundRefund.swapId}</TruncatedText> is being refunded in the background.
</LoadingSpinnerAlert>
} }
return null; // If we successfully refunded, show a notification as well
} if (backgroundRefund?.state.type === "Completed") {
notistack.enqueueSnackbar(
`The swap ${backgroundRefund.swapId} has been refunded in the background.`,
{ variant: "success", persist: true },
);
}
}, [backgroundRefund]);
if (backgroundRefund?.state.type === "Started") {
return (
<LoadingSpinnerAlert>
<AlertTitle>Refund in progress</AlertTitle>
The swap <TruncatedText>{backgroundRefund.swapId}</TruncatedText> is
being refunded in the background.
</LoadingSpinnerAlert>
);
}
return null;
}

View file

@ -1,34 +1,36 @@
import { Box, Button, LinearProgress, makeStyles, Badge } from "@material-ui/core"; import { Box, Button, LinearProgress, Badge } from "@mui/material";
import { Alert } from "@material-ui/lab"; import { Alert } from "@mui/material";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useAppSelector, usePendingBackgroundProcesses } from "store/hooks"; import { useAppSelector, usePendingBackgroundProcesses } from "store/hooks";
import { exhaustiveGuard } from "utils/typescriptUtils"; import { exhaustiveGuard } from "utils/typescriptUtils";
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert"; import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
import { bytesToMb } from "utils/conversionUtils"; import { bytesToMb } from "utils/conversionUtils";
import { TauriBackgroundProgress, TauriContextStatusEvent } from "models/tauriModel"; import {
TauriBackgroundProgress,
TauriContextStatusEvent,
} from "models/tauriModel";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import TruncatedText from "../other/TruncatedText"; import TruncatedText from "../other/TruncatedText";
import BitcoinIcon from "../icons/BitcoinIcon"; import BitcoinIcon from "../icons/BitcoinIcon";
import MoneroIcon from "../icons/MoneroIcon"; import MoneroIcon from "../icons/MoneroIcon";
import TorIcon from "../icons/TorIcon"; import TorIcon from "../icons/TorIcon";
const useStyles = makeStyles((theme) => ({ function AlertWithLinearProgress({
innerAlert: { title,
display: "flex", progress,
flexDirection: "column", icon,
gap: theme.spacing(2), count,
}, }: {
})); title: React.ReactNode;
progress: number | null;
function AlertWithLinearProgress({ title, progress, icon, count }: { icon?: React.ReactNode | null;
title: React.ReactNode, count?: number;
progress: number | null,
icon?: React.ReactNode | null,
count?: number
}) { }) {
const BUFFER_PROGRESS_ADDITION_MAX = 20; const BUFFER_PROGRESS_ADDITION_MAX = 20;
const [bufferProgressAddition, setBufferProgressAddition] = useState(Math.random() * BUFFER_PROGRESS_ADDITION_MAX); const [bufferProgressAddition, setBufferProgressAddition] = useState(
Math.random() * BUFFER_PROGRESS_ADDITION_MAX,
);
useEffect(() => { useEffect(() => {
setBufferProgressAddition(Math.random() * BUFFER_PROGRESS_ADDITION_MAX); setBufferProgressAddition(Math.random() * BUFFER_PROGRESS_ADDITION_MAX);
@ -45,22 +47,30 @@ function AlertWithLinearProgress({ title, progress, icon, count }: {
// If the progress is already at 100%, but not finished yet we show an indeterminate progress bar // If the progress is already at 100%, but not finished yet we show an indeterminate progress bar
// as it'd be confusing to show a 100% progress bar for longer than a second or so. // as it'd be confusing to show a 100% progress bar for longer than a second or so.
return <Alert severity="info" icon={displayIcon}> return (
<Box style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}> <Alert severity="info" icon={displayIcon}>
{title} <Box style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
{(progress === null || progress === 0 || progress >= 100) ? ( {title}
<LinearProgress variant="indeterminate" /> {progress === null || progress === 0 || progress >= 100 ? (
) : ( <LinearProgress variant="indeterminate" />
<LinearProgress variant="buffer" value={progress} valueBuffer={Math.min(progress + bufferProgressAddition, 100)} /> ) : (
)} <LinearProgress
</Box> variant="buffer"
</Alert> value={progress}
valueBuffer={Math.min(progress + bufferProgressAddition, 100)}
/>
)}
</Box>
</Alert>
);
} }
function PartialInitStatus({ status, totalOfType, classes }: { function PartialInitStatus({
status: TauriBackgroundProgress, status,
totalOfType: number, totalOfType,
classes: ReturnType<typeof useStyles> }: {
status: TauriBackgroundProgress;
totalOfType: number;
}) { }) {
if (status.progress.type === "Completed") { if (status.progress.type === "Completed") {
return null; return null;
@ -70,90 +80,82 @@ function PartialInitStatus({ status, totalOfType, classes }: {
case "EstablishingTorCircuits": case "EstablishingTorCircuits":
return ( return (
<AlertWithLinearProgress <AlertWithLinearProgress
title={ title={<>Establishing Tor circuits</>}
<>
Establishing Tor circuits
</>
}
progress={status.progress.content.frac * 100} progress={status.progress.content.frac * 100}
count={totalOfType} count={totalOfType}
icon={<TorIcon />} icon={<TorIcon />}
/> />
); );
case "SyncingBitcoinWallet": case "SyncingBitcoinWallet": {
const progressValue = const progressValue =
status.progress.content?.type === "Known" ? status.progress.content?.type === "Known"
(status.progress.content?.content?.consumed / status.progress.content?.content?.total) * 100 : null; ? (status.progress.content?.content?.consumed /
status.progress.content?.content?.total) *
100
: null;
return ( return (
<AlertWithLinearProgress <AlertWithLinearProgress
title={ title={<>Syncing Bitcoin wallet</>}
<>
Syncing Bitcoin wallet
</>
}
progress={progressValue} progress={progressValue}
icon={<BitcoinIcon />} icon={<BitcoinIcon />}
count={totalOfType} count={totalOfType}
/> />
); );
case "FullScanningBitcoinWallet": }
const fullScanProgressValue = status.progress.content?.type === "Known" ? (status.progress.content?.content?.current_index / status.progress.content?.content?.assumed_total) * 100 : null; case "FullScanningBitcoinWallet": {
const fullScanProgressValue =
status.progress.content?.type === "Known"
? (status.progress.content?.content?.current_index /
status.progress.content?.content?.assumed_total) *
100
: null;
return ( return (
<AlertWithLinearProgress <AlertWithLinearProgress
title={ title={<>Full scan of Bitcoin wallet (one time operation)</>}
<>
Full scan of Bitcoin wallet (one time operation)
</>
}
progress={fullScanProgressValue} progress={fullScanProgressValue}
icon={<BitcoinIcon />} icon={<BitcoinIcon />}
count={totalOfType} count={totalOfType}
/> />
); );
}
case "OpeningBitcoinWallet": case "OpeningBitcoinWallet":
return ( return (
<LoadingSpinnerAlert severity="info"> <LoadingSpinnerAlert severity="info">
<> <>Opening Bitcoin wallet</>
Opening Bitcoin wallet
</>
</LoadingSpinnerAlert> </LoadingSpinnerAlert>
); );
case "DownloadingMoneroWalletRpc": case "DownloadingMoneroWalletRpc": {
const moneroRpcTitle = `Downloading and verifying the Monero wallet RPC (${bytesToMb(status.progress.content.size).toFixed(2)} MB)`; const moneroRpcTitle = `Downloading and verifying the Monero wallet RPC (${bytesToMb(status.progress.content.size).toFixed(2)} MB)`;
return ( return (
<AlertWithLinearProgress <AlertWithLinearProgress
title={ title={<>{moneroRpcTitle}</>}
<>
{moneroRpcTitle}
</>
}
progress={status.progress.content.progress} progress={status.progress.content.progress}
icon={<MoneroIcon />} icon={<MoneroIcon />}
count={totalOfType} count={totalOfType}
/> />
); );
}
case "OpeningMoneroWallet": case "OpeningMoneroWallet":
return ( return (
<LoadingSpinnerAlert severity="info"> <LoadingSpinnerAlert severity="info">
<> <>Opening the Monero wallet</>
Opening the Monero wallet
</>
</LoadingSpinnerAlert> </LoadingSpinnerAlert>
); );
case "OpeningDatabase": case "OpeningDatabase":
return ( return (
<LoadingSpinnerAlert severity="info"> <LoadingSpinnerAlert severity="info">
<> <>Opening the local database</>
Opening the local database
</>
</LoadingSpinnerAlert> </LoadingSpinnerAlert>
); );
case "BackgroundRefund": case "BackgroundRefund":
return ( return (
<LoadingSpinnerAlert severity="info"> <LoadingSpinnerAlert severity="info">
<> <>
Refunding swap <TruncatedText limit={10}>{status.progress.content.swap_id}</TruncatedText> Refunding swap{" "}
<TruncatedText limit={10}>
{status.progress.content.swap_id}
</TruncatedText>
</> </>
</LoadingSpinnerAlert> </LoadingSpinnerAlert>
); );
@ -166,13 +168,24 @@ export default function DaemonStatusAlert() {
const contextStatus = useAppSelector((s) => s.rpc.status); const contextStatus = useAppSelector((s) => s.rpc.status);
const navigate = useNavigate(); const navigate = useNavigate();
if (contextStatus === null || contextStatus === TauriContextStatusEvent.NotInitialized) { if (
return <LoadingSpinnerAlert severity="warning">Checking for available remote nodes</LoadingSpinnerAlert>; contextStatus === null ||
contextStatus === TauriContextStatusEvent.NotInitialized
) {
return (
<LoadingSpinnerAlert severity="warning">
Checking for available remote nodes
</LoadingSpinnerAlert>
);
} }
switch (contextStatus) { switch (contextStatus) {
case TauriContextStatusEvent.Initializing: case TauriContextStatusEvent.Initializing:
return <LoadingSpinnerAlert severity="warning">Core components are loading</LoadingSpinnerAlert>; return (
<LoadingSpinnerAlert severity="warning">
Core components are loading
</LoadingSpinnerAlert>
);
case TauriContextStatusEvent.Available: case TauriContextStatusEvent.Available:
return <Alert severity="success">The daemon is running</Alert>; return <Alert severity="success">The daemon is running</Alert>;
case TauriContextStatusEvent.Failed: case TauriContextStatusEvent.Failed:
@ -199,7 +212,6 @@ export default function DaemonStatusAlert() {
export function BackgroundProgressAlerts() { export function BackgroundProgressAlerts() {
const backgroundProgress = usePendingBackgroundProcesses(); const backgroundProgress = usePendingBackgroundProcesses();
const classes = useStyles();
if (backgroundProgress.length === 0) { if (backgroundProgress.length === 0) {
return null; return null;
@ -207,7 +219,8 @@ export function BackgroundProgressAlerts() {
const componentCounts: Record<string, number> = {}; const componentCounts: Record<string, number> = {};
backgroundProgress.forEach(([, status]) => { backgroundProgress.forEach(([, status]) => {
componentCounts[status.componentName] = (componentCounts[status.componentName] || 0) + 1; componentCounts[status.componentName] =
(componentCounts[status.componentName] || 0) + 1;
}); });
const renderedComponentNames = new Set<string>(); const renderedComponentNames = new Set<string>();
@ -219,12 +232,15 @@ export function BackgroundProgressAlerts() {
return false; return false;
}); });
return uniqueBackgroundProcesses.map(([id, status]) => ( return (
<PartialInitStatus <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
key={id} {uniqueBackgroundProcesses.map(([id, status]) => (
status={status} <PartialInitStatus
classes={classes} key={id}
totalOfType={componentCounts[status.componentName]} status={status}
/> totalOfType={componentCounts[status.componentName]}
)); />
} ))}
</Box>
);
}

View file

@ -1,5 +1,5 @@
import { Button } from "@material-ui/core"; import { Button } from "@mui/material";
import Alert from "@material-ui/lab/Alert"; import Alert from "@mui/material/Alert";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";

View file

@ -1,6 +1,7 @@
import { CircularProgress } from "@material-ui/core"; import { CircularProgress } from "@mui/material";
import { AlertProps, Alert } from "@material-ui/lab"; import { Alert } from "@mui/material";
import { AlertProps } from "@mui/material";
export function LoadingSpinnerAlert({ ...rest }: AlertProps) { export function LoadingSpinnerAlert({ ...rest }: AlertProps) {
return <Alert icon={<CircularProgress size={22} />} {...rest} />; return <Alert icon={<CircularProgress size={22} />} {...rest} />;
} }

View file

@ -1,33 +0,0 @@
import { Box, LinearProgress } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import { useAppSelector } from "store/hooks";
export default function MoneroWalletRpcUpdatingAlert() {
// TODO: Reimplement this using Tauri Events
return <></>;
const updateState = useAppSelector(
(s) => s.rpc.state.moneroWalletRpc.updateState,
);
if (updateState === false) {
return null;
}
const progress = Number.parseFloat(
updateState.progress.substring(0, updateState.progress.length - 1),
);
return (
<Alert severity="info">
<Box style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
<span>The Monero wallet is updating. This may take a few moments</span>
<LinearProgress
variant="determinate"
value={progress}
title="Download progress"
/>
</Box>
</Alert>
);
}

View file

@ -1,33 +1,26 @@
import { Box, makeStyles } from "@material-ui/core"; import { Box } from "@mui/material";
import { Alert } from "@material-ui/lab"; import { Alert } from "@mui/material";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";
import { SatsAmount } from "../other/Units"; import { SatsAmount } from "../other/Units";
import WalletRefreshButton from "../pages/wallet/WalletRefreshButton"; import WalletRefreshButton from "../pages/wallet/WalletRefreshButton";
const useStyles = makeStyles((theme) => ({
outer: {
paddingBottom: theme.spacing(1),
},
}));
export default function RemainingFundsWillBeUsedAlert() { export default function RemainingFundsWillBeUsedAlert() {
const classes = useStyles(); const balance = useAppSelector((s) => s.rpc.state.balance);
const balance = useAppSelector((s) => s.rpc.state.balance);
if (balance == null || balance <= 0) { if (balance == null || balance <= 0) {
return <></>; return <></>;
} }
return ( return (
<Box className={classes.outer}> <Box sx={{ paddingBottom: 1 }}>
<Alert <Alert
severity="warning" severity="warning"
action={<WalletRefreshButton />} action={<WalletRefreshButton />}
variant="filled" variant="filled"
> >
The remaining funds of <SatsAmount amount={balance} /> in the wallet The remaining funds of <SatsAmount amount={balance} /> in the wallet
will be used for the next swap will be used for the next swap
</Alert> </Alert>
</Box> </Box>
); );
} }

View file

@ -1,5 +1,4 @@
import { Box, makeStyles } from "@material-ui/core"; import { Box, Alert, AlertTitle } from "@mui/material";
import { Alert, AlertTitle } from "@material-ui/lab/";
import { import {
BobStateName, BobStateName,
GetSwapInfoResponseExt, GetSwapInfoResponseExt,
@ -16,41 +15,32 @@ import TruncatedText from "../../other/TruncatedText";
import { SwapMoneroRecoveryButton } from "../../pages/history/table/SwapMoneroRecoveryButton"; import { SwapMoneroRecoveryButton } from "../../pages/history/table/SwapMoneroRecoveryButton";
import { TimelockTimeline } from "./TimelockTimeline"; import { TimelockTimeline } from "./TimelockTimeline";
const useStyles = makeStyles((theme) => ({
box: {
display: "flex",
flexDirection: "column",
gap: theme.spacing(1),
},
list: {
padding: "0px",
margin: "0px",
"& li": {
marginBottom: theme.spacing(0.5),
"&:last-child": {
marginBottom: 0
}
},
},
alertMessage: {
flexGrow: 1,
},
}));
/** /**
* Component for displaying a list of messages. * Component for displaying a list of messages.
* @param messages - Array of messages to display. * @param messages - Array of messages to display.
* @returns JSX.Element * @returns JSX.Element
*/ */
function MessageList({ messages }: { messages: ReactNode[]; }) { function MessageList({ messages }: { messages: ReactNode[] }) {
const classes = useStyles();
return ( return (
<ul className={classes.list}> <Box
{messages.filter(msg => msg != null).map((msg, i) => ( component="ul"
<li key={i}>{msg}</li> sx={{
))} padding: "0px",
</ul> margin: "0px",
"& li": {
marginBottom: 0.5,
"&:last-child": {
marginBottom: 0,
},
},
}}
>
{messages
.filter((msg) => msg != null)
.map((msg, i) => (
<li key={i}>{msg}</li>
))}
</Box>
); );
} }
@ -59,17 +49,23 @@ function MessageList({ messages }: { messages: ReactNode[]; }) {
* @param swap - The swap information. * @param swap - The swap information.
* @returns JSX.Element * @returns JSX.Element
*/ */
function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt; }) { function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt }) {
const classes = useStyles();
return ( return (
<Box className={classes.box}> <Box
sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<MessageList <MessageList
messages={[ messages={[
"The Bitcoin has been redeemed by the other party", "The Bitcoin has been redeemed by the other party",
"There is no risk of losing funds. Take as much time as you need", "There is no risk of losing funds. Take as much time as you need",
"The Monero will automatically be redeemed to your provided address once you resume the swap", "The Monero will automatically be redeemed to your provided address once you resume the swap",
"If this step fails, you can manually redeem your funds", "If this step fails, you can manually redeem your funds",
]} /> ]}
/>
<SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" /> <SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" />
</Box> </Box>
); );
@ -82,7 +78,10 @@ function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt; })
* @returns JSX.Element * @returns JSX.Element
*/ */
function BitcoinLockedNoTimelockExpiredStateAlert({ function BitcoinLockedNoTimelockExpiredStateAlert({
timelock, cancelTimelockOffset, punishTimelockOffset, isRunning, timelock,
cancelTimelockOffset,
punishTimelockOffset,
isRunning,
}: { }: {
timelock: TimelockNone; timelock: TimelockNone;
cancelTimelockOffset: number; cancelTimelockOffset: number;
@ -92,20 +91,25 @@ function BitcoinLockedNoTimelockExpiredStateAlert({
return ( return (
<MessageList <MessageList
messages={[ messages={[
isRunning ? "We are waiting for the other party to lock their Monero" : null, isRunning
? "We are waiting for the other party to lock their Monero"
: null,
<> <>
If the swap isn't completed in {" "} If the swap isn't completed in{" "}
<HumanizedBitcoinBlockDuration <HumanizedBitcoinBlockDuration
blocks={timelock.content.blocks_left} blocks={timelock.content.blocks_left}
displayBlocks={false} displayBlocks={false}
/>, it needs to be refunded />
, it needs to be refunded
</>, </>,
"For that, you need to have the app open sometime within the refund period", "For that, you need to have the app open sometime within the refund period",
<> <>
After that, cooperation from the other party would be required to recover the funds After that, cooperation from the other party would be required to
recover the funds
</>, </>,
isRunning ? null : "Please resume the swap to continue" isRunning ? null : "Please resume the swap to continue",
]} /> ]}
/>
); );
} }
@ -117,7 +121,8 @@ function BitcoinLockedNoTimelockExpiredStateAlert({
* @returns JSX.Element * @returns JSX.Element
*/ */
function BitcoinPossiblyCancelledAlert({ function BitcoinPossiblyCancelledAlert({
swap, timelock, swap,
timelock,
}: { }: {
swap: GetSwapInfoResponseExt; swap: GetSwapInfoResponseExt;
timelock: TimelockCancel; timelock: TimelockCancel;
@ -130,10 +135,13 @@ function BitcoinPossiblyCancelledAlert({
<> <>
If we haven't refunded in{" "} If we haven't refunded in{" "}
<HumanizedBitcoinBlockDuration <HumanizedBitcoinBlockDuration
blocks={timelock.content.blocks_left} /> blocks={timelock.content.blocks_left}
, cooperation from the other party will be required to recover the funds />
</> , cooperation from the other party will be required to recover the
]} /> funds
</>,
]}
/>
); );
} }
@ -148,7 +156,8 @@ function PunishTimelockExpiredAlert() {
"We couldn't refund within the refund period", "We couldn't refund within the refund period",
"We might still be able to redeem the Monero. However, this will require cooperation from the other party", "We might still be able to redeem the Monero. However, this will require cooperation from the other party",
"Resume the swap as soon as possible", "Resume the swap as soon as possible",
]} /> ]}
/>
); );
} }
@ -157,8 +166,13 @@ function PunishTimelockExpiredAlert() {
* @param swap - The swap information. * @param swap - The swap information.
* @returns JSX.Element | null * @returns JSX.Element | null
*/ */
export function StateAlert({ swap, isRunning }: { swap: GetSwapInfoResponseExtRunningSwap; isRunning: boolean; }) { export function StateAlert({
swap,
isRunning,
}: {
swap: GetSwapInfoResponseExtRunningSwap;
isRunning: boolean;
}) {
switch (swap.state_name) { switch (swap.state_name) {
// This is the state where the swap is safe because the other party has redeemed the Bitcoin // This is the state where the swap is safe because the other party has redeemed the Bitcoin
// It cannot be punished anymore // It cannot be punished anymore
@ -218,8 +232,6 @@ export default function SwapStatusAlert({
swap: GetSwapInfoResponseExt; swap: GetSwapInfoResponseExt;
isRunning: boolean; isRunning: boolean;
}): JSX.Element | null { }): JSX.Element | null {
const classes = useStyles();
// If the swap is completed, we do not need to display anything // If the swap is completed, we do not need to display anything
if (!isGetSwapInfoResponseRunningSwap(swap)) { if (!isGetSwapInfoResponseRunningSwap(swap)) {
return null; return null;
@ -235,12 +247,29 @@ export default function SwapStatusAlert({
key={swap.swap_id} key={swap.swap_id}
severity="warning" severity="warning"
variant="filled" variant="filled"
classes={{ message: classes.alertMessage }} classes={{ message: "alert-message-flex-grow" }}
sx={{
"& .alert-message-flex-grow": {
flexGrow: 1,
},
}}
> >
<AlertTitle> <AlertTitle>
{isRunning ? "Swap has been running for a while" : <>Swap <TruncatedText>{swap.swap_id}</TruncatedText> is not running</>} {isRunning ? (
"Swap has been running for a while"
) : (
<>
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is not running
</>
)}
</AlertTitle> </AlertTitle>
<Box className={classes.box}> <Box
sx={{
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<StateAlert swap={swap} isRunning={isRunning} /> <StateAlert swap={swap} isRunning={isRunning} />
<TimelockTimeline swap={swap} /> <TimelockTimeline swap={swap} />
</Box> </Box>

View file

@ -1,176 +1,214 @@
import { useTheme, Tooltip, Typography, Box, LinearProgress, Paper } from "@material-ui/core"; import {
useTheme,
Tooltip,
Typography,
Box,
LinearProgress,
Paper,
} from "@mui/material";
import { ExpiredTimelocks } from "models/tauriModel"; import { ExpiredTimelocks } from "models/tauriModel";
import { GetSwapInfoResponseExt, getAbsoluteBlock } from "models/tauriModelExt"; import { GetSwapInfoResponseExt, getAbsoluteBlock } from "models/tauriModelExt";
import HumanizedBitcoinBlockDuration from "renderer/components/other/HumanizedBitcoinBlockDuration"; import HumanizedBitcoinBlockDuration from "renderer/components/other/HumanizedBitcoinBlockDuration";
interface TimelineSegment { interface TimelineSegment {
title: string; title: string;
label: string; label: string;
bgcolor: string; bgcolor: string;
startBlock: number; startBlock: number;
} }
interface TimelineSegmentProps { interface TimelineSegmentProps {
segment: TimelineSegment; segment: TimelineSegment;
isActive: boolean; isActive: boolean;
absoluteBlock: number; absoluteBlock: number;
durationOfSegment: number | null; durationOfSegment: number | null;
totalBlocks: number; totalBlocks: number;
} }
function TimelineSegment({ function TimelineSegment({
segment, segment,
isActive, isActive,
absoluteBlock, absoluteBlock,
durationOfSegment, durationOfSegment,
totalBlocks totalBlocks,
}: TimelineSegmentProps) { }: TimelineSegmentProps) {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Tooltip title={<Typography variant="caption">{segment.title}</Typography>}> <Tooltip title={<Typography variant="caption">{segment.title}</Typography>}>
<Box sx={{ <Box
display: 'flex', sx={{
flexDirection: 'column', display: "flex",
alignItems: 'center', flexDirection: "column",
justifyContent: 'center', alignItems: "center",
bgcolor: segment.bgcolor, justifyContent: "center",
width: `${durationOfSegment ? ((durationOfSegment / totalBlocks) * 85) : 15}%`, bgcolor: segment.bgcolor,
position: 'relative', width: `${durationOfSegment ? (durationOfSegment / totalBlocks) * 85 : 15}%`,
}} style={{ position: "relative",
opacity: isActive ? 1 : 0.3 }}
}}> style={{
{isActive && ( opacity: isActive ? 1 : 0.3,
<Box sx={{ }}
position: 'absolute', >
top: 0, {isActive && (
left: 0, <Box
height: '100%', sx={{
width: `${Math.max(5, ((absoluteBlock - segment.startBlock) / durationOfSegment) * 100)}%`, position: "absolute",
zIndex: 1, top: 0,
}}> left: 0,
<LinearProgress height: "100%",
variant="indeterminate" width: `${Math.max(5, ((absoluteBlock - segment.startBlock) / durationOfSegment) * 100)}%`,
color="primary" zIndex: 1,
style={{ }}
height: '100%', >
backgroundColor: theme.palette.primary.dark, <LinearProgress
opacity: 0.3, variant="indeterminate"
}} color="primary"
/> style={{
</Box> height: "100%",
)} backgroundColor: theme.palette.primary.dark,
<Typography variant="subtitle2" color="inherit" align="center" style={{ zIndex: 2 }}> opacity: 0.3,
{segment.label} }}
</Typography> />
{durationOfSegment && ( </Box>
<Typography )}
variant="caption" <Typography
color="inherit" variant="subtitle2"
align="center" color="inherit"
style={{ align="center"
zIndex: 2, style={{ zIndex: 2 }}
opacity: 0.8 >
}} {segment.label}
> </Typography>
{isActive && ( {durationOfSegment && (
<> <Typography
<HumanizedBitcoinBlockDuration variant="caption"
blocks={durationOfSegment - (absoluteBlock - segment.startBlock)} color="inherit"
/>{" "}left align="center"
</> style={{
)} zIndex: 2,
{!isActive && ( opacity: 0.8,
<HumanizedBitcoinBlockDuration }}
blocks={durationOfSegment} >
/> {isActive && (
)} <>
</Typography> <HumanizedBitcoinBlockDuration
)} blocks={
</Box> durationOfSegment - (absoluteBlock - segment.startBlock)
</Tooltip> }
); />{" "}
left
</>
)}
{!isActive && (
<HumanizedBitcoinBlockDuration blocks={durationOfSegment} />
)}
</Typography>
)}
</Box>
</Tooltip>
);
} }
export function TimelockTimeline({ swap }: { export function TimelockTimeline({
// This forces the timelock to not be null swap,
swap: GetSwapInfoResponseExt & { timelock: ExpiredTimelocks } }: {
// This forces the timelock to not be null
swap: GetSwapInfoResponseExt & { timelock: ExpiredTimelocks };
}) { }) {
const theme = useTheme(); const theme = useTheme();
const timelineSegments: TimelineSegment[] = [ const timelineSegments: TimelineSegment[] = [
{ {
title: "Normally a swap is completed during this period", title: "Normally a swap is completed during this period",
label: "Normal", label: "Normal",
bgcolor: theme.palette.success.main, bgcolor: theme.palette.success.main,
startBlock: 0, startBlock: 0,
}, },
{ {
title: "If the swap hasn't been completed before we reach this period, the Bitcoin needs to be refunded. For that, you need to have the app open sometime within the refund period", title:
label: "Refund", "If the swap hasn't been completed before we reach this period, the Bitcoin needs to be refunded. For that, you need to have the app open sometime within the refund period",
bgcolor: theme.palette.warning.main, label: "Refund",
startBlock: swap.cancel_timelock, bgcolor: theme.palette.warning.main,
}, startBlock: swap.cancel_timelock,
{ },
title: "If you didn't refund within the refund window, you will enter this period. At this point, the Bitcoin can no longer be refunded. It may still be possible to redeem the Monero with cooperation from the other party but this cannot be guaranteed.", {
label: "Danger", title:
bgcolor: theme.palette.error.main, "If you didn't refund within the refund window, you will enter this period. At this point, the Bitcoin can no longer be refunded. It may still be possible to redeem the Monero with cooperation from the other party but this cannot be guaranteed.",
startBlock: swap.cancel_timelock + swap.punish_timelock, label: "Danger",
} bgcolor: theme.palette.error.main,
]; startBlock: swap.cancel_timelock + swap.punish_timelock,
},
];
const totalBlocks = swap.cancel_timelock + swap.punish_timelock; const totalBlocks = swap.cancel_timelock + swap.punish_timelock;
const absoluteBlock = getAbsoluteBlock(swap.timelock, swap.cancel_timelock, swap.punish_timelock); const absoluteBlock = getAbsoluteBlock(
swap.timelock,
swap.cancel_timelock,
swap.punish_timelock,
);
// This calculates the duration of a segment // This calculates the duration of a segment
// by getting the the difference to the next segment // by getting the the difference to the next segment
function durationOfSegment(index: number): number | null { function durationOfSegment(index: number): number | null {
const nextSegment = timelineSegments[index + 1]; const nextSegment = timelineSegments[index + 1];
if (nextSegment == null) { if (nextSegment == null) {
return null; return null;
}
return nextSegment.startBlock - timelineSegments[index].startBlock;
}
// This function returns the index of the active segment based on the current block
// We iterate in reverse to find the first segment that has a start block less than the current block
function getActiveSegmentIndex() {
return Array.from(timelineSegments
.slice()
// We use .entries() to keep the indexes despite reversing
.entries())
.reverse()
.find(([_, segment]) => absoluteBlock >= segment.startBlock)?.[0] ?? 0;
} }
return nextSegment.startBlock - timelineSegments[index].startBlock;
}
// This function returns the index of the active segment based on the current block
// We iterate in reverse to find the first segment that has a start block less than the current block
function getActiveSegmentIndex() {
return ( return (
<Box sx={{ Array.from(
width: '100%', timelineSegments
minWidth: '100%', .slice()
flexGrow: 1 // We use .entries() to keep the indexes despite reversing
}}> .entries(),
<Paper style={{ )
position: 'relative', .reverse()
height: '5rem', .find(([_, segment]) => absoluteBlock >= segment.startBlock)?.[0] ?? 0
overflow: 'hidden',
}} elevation={3} variant="outlined">
<Box sx={{
position: 'relative',
height: '100%',
display: 'flex'
}}>
{timelineSegments.map((segment, index) => (
<TimelineSegment
key={index}
segment={segment}
isActive={getActiveSegmentIndex() === index}
absoluteBlock={absoluteBlock}
durationOfSegment={durationOfSegment(index)}
totalBlocks={totalBlocks}
/>
))}
</Box>
</Paper>
</Box>
); );
} }
return (
<Box
sx={{
width: "100%",
minWidth: "100%",
flexGrow: 1,
}}
>
<Paper
style={{
position: "relative",
height: "5rem",
overflow: "hidden",
}}
elevation={3}
variant="outlined"
>
<Box
sx={{
position: "relative",
height: "100%",
display: "flex",
}}
>
{timelineSegments.map((segment, index) => (
<TimelineSegment
key={index}
segment={segment}
isActive={getActiveSegmentIndex() === index}
absoluteBlock={absoluteBlock}
durationOfSegment={durationOfSegment(index)}
totalBlocks={totalBlocks}
/>
))}
</Box>
</Paper>
</Box>
);
}

View file

@ -1,25 +1,15 @@
import { Box, makeStyles } from "@material-ui/core"; import { Box } from "@mui/material";
import { useSwapInfosSortedByDate } from "store/hooks"; import { useSwapInfosSortedByDate } from "store/hooks";
import SwapStatusAlert from "./SwapStatusAlert/SwapStatusAlert"; import SwapStatusAlert from "./SwapStatusAlert/SwapStatusAlert";
const useStyles = makeStyles((theme) => ({
outer: {
display: "flex",
flexDirection: "column",
gap: theme.spacing(1),
},
}));
export default function SwapTxLockAlertsBox() { export default function SwapTxLockAlertsBox() {
const classes = useStyles();
// We specifically choose ALL swaps here // We specifically choose ALL swaps here
// If a swap is in a state where an Alert is not needed (becaue no Bitcoin have been locked or because the swap has been completed) // If a swap is in a state where an Alert is not needed (becaue no Bitcoin have been locked or because the swap has been completed)
// the SwapStatusAlert component will not render an Alert // the SwapStatusAlert component will not render an Alert
const swaps = useSwapInfosSortedByDate(); const swaps = useSwapInfosSortedByDate();
return ( return (
<Box className={classes.outer}> <Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
{swaps.map((swap) => ( {swaps.map((swap) => (
<SwapStatusAlert key={swap.swap_id} swap={swap} isRunning={false} /> <SwapStatusAlert key={swap.swap_id} swap={swap} isRunning={false} />
))} ))}

View file

@ -1,5 +1,5 @@
import { Button } from "@material-ui/core"; import { Button } from "@mui/material";
import Alert from "@material-ui/lab/Alert"; import Alert from "@mui/material/Alert";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useResumeableSwapsCountExcludingPunished } from "store/hooks"; import { useResumeableSwapsCountExcludingPunished } from "store/hooks";

View file

@ -1,5 +1,4 @@
import { SvgIcon } from "@material-ui/core"; import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function BitcoinIcon(props: SvgIconProps) { export default function BitcoinIcon(props: SvgIconProps) {
return ( return (

View file

@ -1,5 +1,4 @@
import { SvgIcon } from "@material-ui/core"; import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function DiscordIcon(props: SvgIconProps) { export default function DiscordIcon(props: SvgIconProps) {
return ( return (

View file

@ -1,29 +1,30 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from "react";
import * as jdenticon from 'jdenticon'; import * as jdenticon from "jdenticon";
interface IdentIconProps { interface IdentIconProps {
value: string; value: string;
size?: number | string; size?: number | string;
className?: string; className?: string;
} }
function IdentIcon({ value, size = 40, className = '' }: IdentIconProps) { function IdentIcon({ value, size = 40, className = "" }: IdentIconProps) {
const iconRef = useRef<SVGSVGElement>(null); const iconRef = useRef<SVGSVGElement>(null);
useEffect(() => { useEffect(() => {
if (iconRef.current) { if (iconRef.current) {
jdenticon.update(iconRef.current, value); jdenticon.update(iconRef.current, value);
} }
}, [value]); }, [value]);
return ( return (
<svg <svg
ref={iconRef} ref={iconRef}
width={size} width={size}
height={size} height={size}
className={className} className={className}
data-jdenticon-value={value} /> data-jdenticon-value={value}
); />
);
} }
export default IdentIcon; export default IdentIcon;

View file

@ -1,4 +1,4 @@
import { IconButton } from "@material-ui/core"; import { IconButton } from "@mui/material";
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
import { ReactNode } from "react"; import { ReactNode } from "react";
@ -10,7 +10,7 @@ export default function LinkIconButton({
children: ReactNode; children: ReactNode;
}) { }) {
return ( return (
<IconButton component="span" onClick={() => open(url)}> <IconButton component="span" onClick={() => open(url)} size="large">
{children} {children}
</IconButton> </IconButton>
); );

View file

@ -1,12 +1,11 @@
import { SvgIcon } from "@material-ui/core"; import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function MatrixIcon(props: SvgIconProps) { export default function MatrixIcon(props: SvgIconProps) {
return ( return (
<SvgIcon viewBox="0 0 27.9 32" {...props}> <SvgIcon viewBox="0 0 27.9 32" {...props}>
<path d="M27.1 31.2V0.7h-2.19V0h3.04v32h-3.04v-0.732z" /> <path d="M27.1 31.2V0.7h-2.19V0h3.04v32h-3.04v-0.732z" />
<path d="M8.23 10.4v1.54h0.044c0.385-0.564 0.893-1.03 1.49-1.37 0.58-0.323 1.25-0.485 1.99-0.485 0.72 0 1.38 0.14 1.97 0.42 0.595 0.279 1.05 0.771 1.36 1.48 0.338-0.5 0.796-0.941 1.38-1.32 0.58-0.383 1.27-0.574 2.06-0.574 0.602 0 1.16 0.074 1.67 0.22 0.514 0.148 0.954 0.383 1.32 0.707 0.366 0.323 0.653 0.746 0.859 1.27 0.205 0.522 0.308 1.15 0.308 1.89v7.63h-3.13v-6.46c0-0.383-0.015-0.743-0.044-1.08-0.0209-0.307-0.103-0.607-0.242-0.882-0.133-0.251-0.336-0.458-0.584-0.596-0.257-0.146-0.606-0.22-1.05-0.22-0.44 0-0.796 0.085-1.07 0.253-0.272 0.17-0.485 0.39-0.639 0.662-0.159 0.287-0.264 0.602-0.308 0.927-0.052 0.347-0.078 0.697-0.078 1.05v6.35h-3.13v-6.4c0-0.338-7e-3-0.673-0.021-1-0.0114-0.314-0.0749-0.623-0.188-0.916-0.108-0.277-0.3-0.512-0.55-0.673-0.258-0.168-0.636-0.253-1.14-0.253-0.198 0.0083-0.394 0.042-0.584 0.1-0.258 0.0745-0.498 0.202-0.705 0.374-0.228 0.184-0.422 0.449-0.584 0.794-0.161 0.346-0.242 0.798-0.242 1.36v6.62h-3.13v-11.4z" /> <path d="M8.23 10.4v1.54h0.044c0.385-0.564 0.893-1.03 1.49-1.37 0.58-0.323 1.25-0.485 1.99-0.485 0.72 0 1.38 0.14 1.97 0.42 0.595 0.279 1.05 0.771 1.36 1.48 0.338-0.5 0.796-0.941 1.38-1.32 0.58-0.383 1.27-0.574 2.06-0.574 0.602 0 1.16 0.074 1.67 0.22 0.514 0.148 0.954 0.383 1.32 0.707 0.366 0.323 0.653 0.746 0.859 1.27 0.205 0.522 0.308 1.15 0.308 1.89v7.63h-3.13v-6.46c0-0.383-0.015-0.743-0.044-1.08-0.0209-0.307-0.103-0.607-0.242-0.882-0.133-0.251-0.336-0.458-0.584-0.596-0.257-0.146-0.606-0.22-1.05-0.22-0.44 0-0.796 0.085-1.07 0.253-0.272 0.17-0.485 0.39-0.639 0.662-0.159 0.287-0.264 0.602-0.308 0.927-0.052 0.347-0.078 0.697-0.078 1.05v6.35h-3.13v-6.4c0-0.338-7e-3-0.673-0.021-1-0.0114-0.314-0.0749-0.623-0.188-0.916-0.108-0.277-0.3-0.512-0.55-0.673-0.258-0.168-0.636-0.253-1.14-0.253-0.198 0.0083-0.394 0.042-0.584 0.1-0.258 0.0745-0.498 0.202-0.705 0.374-0.228 0.184-0.422 0.449-0.584 0.794-0.161 0.346-0.242 0.798-0.242 1.36v6.62h-3.13v-11.4z" />
<path d="M0.936 0.732v30.5h2.19v0.732h-3.04v-32h3.03v0.732z" /> <path d="M0.936 0.732v30.5h2.19v0.732h-3.04v-32h3.03v0.732z" />
</SvgIcon> </SvgIcon>
); );
} }

View file

@ -1,5 +1,4 @@
import { SvgIcon } from "@material-ui/core"; import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function MoneroIcon(props: SvgIconProps) { export default function MoneroIcon(props: SvgIconProps) {
return ( return (

View file

@ -1,5 +1,4 @@
import { SvgIcon } from "@material-ui/core"; import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function TorIcon(props: SvgIconProps) { export default function TorIcon(props: SvgIconProps) {
return ( return (

View file

@ -1,5 +1,4 @@
import { TextField } from "@material-ui/core"; import TextField, { TextFieldProps } from "@mui/material/TextField";
import { TextFieldProps } from "@material-ui/core/TextField/TextField";
import { useEffect } from "react"; import { useEffect } from "react";
import { isTestnet } from "store/config"; import { isTestnet } from "store/config";
import { isBtcAddressValid } from "utils/conversionUtils"; import { isBtcAddressValid } from "utils/conversionUtils";

View file

@ -1,39 +1,45 @@
import { createContext, useContext, useState, ReactNode } from 'react' import { createContext, useContext, useState, ReactNode } from "react";
interface CardSelectionContextType { interface CardSelectionContextType {
selectedValue: string selectedValue: string;
setSelectedValue: (value: string) => void setSelectedValue: (value: string) => void;
} }
const CardSelectionContext = createContext<CardSelectionContextType | undefined>(undefined) const CardSelectionContext = createContext<
CardSelectionContextType | undefined
>(undefined);
export function CardSelectionProvider({ export function CardSelectionProvider({
children, children,
initialValue, initialValue,
onChange onChange,
}: { }: {
children: ReactNode children: ReactNode;
initialValue: string initialValue: string;
onChange?: (value: string) => void onChange?: (value: string) => void;
}) { }) {
const [selectedValue, setSelectedValue] = useState(initialValue) const [selectedValue, setSelectedValue] = useState(initialValue);
const handleValueChange = (value: string) => { const handleValueChange = (value: string) => {
setSelectedValue(value) setSelectedValue(value);
onChange?.(value) onChange?.(value);
} };
return ( return (
<CardSelectionContext.Provider value={{ selectedValue, setSelectedValue: handleValueChange }}> <CardSelectionContext.Provider
{children} value={{ selectedValue, setSelectedValue: handleValueChange }}
</CardSelectionContext.Provider> >
) {children}
</CardSelectionContext.Provider>
);
} }
export function useCardSelection() { export function useCardSelection() {
const context = useContext(CardSelectionContext) const context = useContext(CardSelectionContext);
if (context === undefined) { if (context === undefined) {
throw new Error('useCardSelection must be used within a CardSelectionProvider') throw new Error(
} "useCardSelection must be used within a CardSelectionProvider",
return context );
} }
return context;
}

View file

@ -1,23 +1,30 @@
import { Box } from '@material-ui/core' import { Box } from "@mui/material";
import CheckIcon from '@material-ui/icons/Check' import CheckIcon from "@mui/icons-material/Check";
import { CardSelectionProvider } from './CardSelectionContext' import { CardSelectionProvider } from "./CardSelectionContext";
interface CardSelectionGroupProps { interface CardSelectionGroupProps {
children: React.ReactElement<{ value: string }>[] children: React.ReactElement<{ value: string }>[];
value: string value: string;
onChange: (value: string) => void onChange: (value: string) => void;
} }
export default function CardSelectionGroup({ export default function CardSelectionGroup({
children, children,
value, value,
onChange, onChange,
}: CardSelectionGroupProps) { }: CardSelectionGroupProps) {
return ( return (
<CardSelectionProvider initialValue={value} onChange={onChange}> <CardSelectionProvider initialValue={value} onChange={onChange}>
<Box style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 12 }}> <Box
{children} style={{
</Box> display: "flex",
</CardSelectionProvider> flexDirection: "column",
) gap: 12,
marginTop: 12,
}}
>
{children}
</Box>
</CardSelectionProvider>
);
} }

View file

@ -1,53 +1,65 @@
import { Box } from "@material-ui/core"; import { Box } from "@mui/material";
import CheckIcon from '@material-ui/icons/Check' import CheckIcon from "@mui/icons-material/Check";
import { useCardSelection } from './CardSelectionContext' import { useCardSelection } from "./CardSelectionContext";
// The value prop is used by the parent CardSelectionGroup to determine which option is selected // The value prop is used by the parent CardSelectionGroup to determine which option is selected
export default function CardSelectionOption({children, value}: {children: React.ReactNode, value: string}) { export default function CardSelectionOption({
const { selectedValue, setSelectedValue } = useCardSelection() children,
const selected = value === selectedValue value,
}: {
children: React.ReactNode;
value: string;
}) {
const { selectedValue, setSelectedValue } = useCardSelection();
const selected = value === selectedValue;
return ( return (
<Box <Box
onClick={() => setSelectedValue(value)} onClick={() => setSelectedValue(value)}
style={{
display: "flex",
alignItems: "flex-start",
gap: 16,
border: selected ? "2px solid #FF5C1B" : "2px solid #555",
borderRadius: 16,
padding: "1em",
cursor: "pointer",
transition: "all 0.2s ease-in-out",
}}
>
<Box
style={{
border: selected ? "2px solid #FF5C1B" : "2px solid #555",
borderRadius: 99999,
width: 28,
height: 28,
background: selected ? "#FF5C1B" : "transparent",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s ease-in-out",
transform: selected ? "scale(1.1)" : "scale(1)",
flexShrink: 0,
}}
>
{selected ? (
<CheckIcon
style={{ style={{
display: 'flex', transition: "all 0.2s ease-in-out",
alignItems: 'flex-start', transform: "scale(1)",
gap: 16, animation: "checkIn 0.2s ease-in-out",
border: selected ? '2px solid #FF5C1B' : '2px solid #555',
borderRadius: 16,
padding: '1em',
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
}} }}
> />
<Box ) : null}
style={{ </Box>
border: selected ? '2px solid #FF5C1B' : '2px solid #555', <Box
borderRadius: 99999, sx={{
width: 28, pt: 0.5,
height: 28, }}
background: selected ? '#FF5C1B' : 'transparent', >
overflow: 'hidden', {children}
display: 'flex', </Box>
alignItems: 'center', </Box>
justifyContent: 'center', );
transition: 'all 0.2s ease-in-out', }
transform: selected ? 'scale(1.1)' : 'scale(1)',
flexShrink: 0,
}}
>
{selected ? (
<CheckIcon
style={{
transition: 'all 0.2s ease-in-out',
transform: 'scale(1)',
animation: 'checkIn 0.2s ease-in-out'
}}
/>
) : null}
</Box>
<Box pt={0.5}>{children}</Box>
</Box>
)
}

View file

@ -1,18 +1,30 @@
import { Box, Button, Dialog, DialogActions, DialogContent, IconButton, List, ListItem, ListItemText, TextField } from "@material-ui/core"; import {
import { TextFieldProps } from "@material-ui/core/TextField/TextField"; Box,
Button,
Dialog,
DialogActions,
DialogContent,
IconButton,
List,
ListItemText,
TextField,
} from "@mui/material";
import { TextFieldProps } from "@mui/material";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getMoneroAddresses } from "renderer/rpc"; import { getMoneroAddresses } from "renderer/rpc";
import { isTestnet } from "store/config"; import { isTestnet } from "store/config";
import { isXmrAddressValid } from "utils/conversionUtils"; import { isXmrAddressValid } from "utils/conversionUtils";
import ImportContactsIcon from '@material-ui/icons/ImportContacts'; import ImportContactsIcon from "@mui/icons-material/ImportContacts";
import TruncatedText from "../other/TruncatedText"; import TruncatedText from "../other/TruncatedText";
import ListItemButton from "@mui/material/ListItemButton";
type MoneroAddressTextFieldProps = TextFieldProps & { type MoneroAddressTextFieldProps = TextFieldProps & {
address: string; address: string;
onAddressChange: (address: string) => void; onAddressChange: (address: string) => void;
onAddressValidityChange: (valid: boolean) => void; onAddressValidityChange: (valid: boolean) => void;
helperText: string; helperText: string;
} };
export default function MoneroAddressTextField({ export default function MoneroAddressTextField({
address, address,
@ -59,12 +71,14 @@ export default function MoneroAddressTextField({
helperText={address.length > 0 ? errorText || helperText : helperText} helperText={address.length > 0 ? errorText || helperText : helperText}
placeholder={placeholder} placeholder={placeholder}
variant="outlined" variant="outlined"
InputProps={{ slotProps={{
endAdornment: addresses?.length > 0 && ( input: {
<IconButton onClick={() => setShowDialog(true)} size="small"> endAdornment: addresses?.length > 0 && (
<ImportContactsIcon /> <IconButton onClick={() => setShowDialog(true)} size="small">
</IconButton> <ImportContactsIcon />
) </IconButton>
),
},
}} }}
{...props} {...props}
/> />
@ -90,26 +104,21 @@ function RecentlyUsedAddressesDialog({
open, open,
onClose, onClose,
addresses, addresses,
onAddressSelect onAddressSelect,
}: RecentlyUsedAddressesDialogProps) { }: RecentlyUsedAddressesDialogProps) {
return ( return (
<Dialog <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
>
<DialogContent> <DialogContent>
<List> <List>
{addresses.map((addr) => ( {addresses.map((addr) => (
<ListItem <ListItemButton key={addr} onClick={() => onAddressSelect(addr)}>
button <ListItemText
key={addr}
onClick={() => onAddressSelect(addr)}
>
<ListItemText
primary={ primary={
<Box fontFamily="monospace"> <Box
sx={{
fontFamily: "monospace",
}}
>
<TruncatedText limit={40} truncateMiddle> <TruncatedText limit={40} truncateMiddle>
{addr} {addr}
</TruncatedText> </TruncatedText>
@ -117,16 +126,12 @@ function RecentlyUsedAddressesDialog({
} }
secondary="Recently used as a redeem address" secondary="Recently used as a redeem address"
/> />
</ListItem> </ListItemButton>
))} ))}
</List> </List>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button onClick={onClose} variant="contained" color="primary">
onClick={onClose}
variant="contained"
color="primary"
>
Close Close
</Button> </Button>
</DialogActions> </DialogActions>

View file

@ -1,22 +1,18 @@
import { DialogTitle, makeStyles, Typography } from "@material-ui/core"; import { DialogTitle, Typography } from "@mui/material";
import { ReactNode } from "react"; import { ReactNode } from "react";
const useStyles = makeStyles({
root: {
display: "flex",
justifyContent: "space-between",
},
});
type DialogTitleProps = { type DialogTitleProps = {
title: ReactNode; title: ReactNode;
}; };
export default function DialogHeader({ title }: DialogTitleProps) { export default function DialogHeader({ title }: DialogTitleProps) {
const classes = useStyles();
return ( return (
<DialogTitle disableTypography className={classes.root}> <DialogTitle
sx={{
display: "flex",
justifyContent: "space-between",
}}
>
<Typography variant="h6">{title}</Typography> <Typography variant="h6">{title}</Typography>
</DialogTitle> </DialogTitle>
); );

View file

@ -1,31 +1,25 @@
import { Button, makeStyles, Paper, Typography } from "@material-ui/core"; import { Button, Paper, Typography } from "@mui/material";
const useStyles = makeStyles((theme) => ({
logsOuter: {
overflow: "auto",
padding: theme.spacing(1),
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
maxHeight: "10rem",
},
copyButton: {
marginTop: theme.spacing(1),
},
}));
export default function PaperTextBox({ stdOut }: { stdOut: string }) { export default function PaperTextBox({ stdOut }: { stdOut: string }) {
const classes = useStyles();
function handleCopyLogs() { function handleCopyLogs() {
throw new Error("Not implemented"); throw new Error("Not implemented");
} }
return ( return (
<Paper className={classes.logsOuter} variant="outlined"> <Paper
variant="outlined"
sx={{
overflow: "auto",
padding: 1,
marginTop: 1,
marginBottom: 1,
maxHeight: "10rem",
}}
>
<Typography component="pre" variant="body2"> <Typography component="pre" variant="body2">
{stdOut} {stdOut}
</Typography> </Typography>
<Button onClick={handleCopyLogs} className={classes.copyButton}> <Button onClick={handleCopyLogs} sx={{ marginTop: 1 }}>
Copy Copy
</Button> </Button>
</Paper> </Paper>

View file

@ -5,7 +5,7 @@ import {
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
} from "@material-ui/core"; } from "@mui/material";
import { suspendCurrentSwap } from "renderer/rpc"; import { suspendCurrentSwap } from "renderer/rpc";
import PromiseInvokeButton from "../PromiseInvokeButton"; import PromiseInvokeButton from "../PromiseInvokeButton";

View file

@ -1,247 +1,241 @@
import { import {
Box, Box,
Button, Button,
Checkbox, Checkbox,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
FormControlLabel, FormControlLabel,
IconButton, IconButton,
Paper, Paper,
TextField, TextField,
Tooltip, Tooltip,
Typography, Typography,
} from '@material-ui/core' } from "@mui/material";
import { ErrorOutline, Visibility } from '@material-ui/icons' import { ErrorOutline, Visibility } from "@mui/icons-material";
import ExternalLink from 'renderer/components/other/ExternalLink' import ExternalLink from "renderer/components/other/ExternalLink";
import SwapSelectDropDown from './SwapSelectDropDown' import SwapSelectDropDown from "./SwapSelectDropDown";
import LogViewer from './LogViewer' import LogViewer from "./LogViewer";
import { useFeedback, MAX_FEEDBACK_LENGTH } from './useFeedback' import { useFeedback, MAX_FEEDBACK_LENGTH } from "./useFeedback";
import { useState } from 'react' import { useState } from "react";
import PromiseInvokeButton from 'renderer/components/PromiseInvokeButton' import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
export default function FeedbackDialog({ export default function FeedbackDialog({
open, open,
onClose, onClose,
}: { }: {
open: boolean open: boolean;
onClose: () => void onClose: () => void;
}) { }) {
const [swapLogsEditorOpen, setSwapLogsEditorOpen] = useState(false) const [swapLogsEditorOpen, setSwapLogsEditorOpen] = useState(false);
const [daemonLogsEditorOpen, setDaemonLogsEditorOpen] = useState(false) const [daemonLogsEditorOpen, setDaemonLogsEditorOpen] = useState(false);
const { input, setInputState, logs, error, clearState, submitFeedback } = const { input, setInputState, logs, error, clearState, submitFeedback } =
useFeedback() useFeedback();
const handleClose = () => { const handleClose = () => {
clearState() clearState();
onClose() onClose();
} };
const bodyTooLong = input.bodyText.length > MAX_FEEDBACK_LENGTH const bodyTooLong = input.bodyText.length > MAX_FEEDBACK_LENGTH;
return ( return (
<Dialog open={open} onClose={handleClose}> <Dialog open={open} onClose={handleClose}>
<DialogTitle style={{ paddingBottom: '0.5rem' }}> <DialogTitle style={{ paddingBottom: "0.5rem" }}>
Submit Feedback Submit Feedback
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<Box
style={{
display: "flex",
flexDirection: "column",
gap: "1.5rem",
}}
>
{error && (
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "start",
gap: "0.5rem",
width: "100%",
backgroundColor: "hsla(0, 45%, 17%, 1)",
padding: "0.5rem",
borderRadius: "0.5rem",
border: "1px solid hsla(0, 61%, 32%, 1)",
}}
>
<ErrorOutline style={{ color: "hsla(0, 77%, 75%, 1)" }} />
<Typography style={{ color: "hsla(0, 83%, 91%, 1)" }} noWrap>
{error}
</Typography>
</Box>
)}
<Box>
<Typography style={{ marginBottom: "0.5rem" }}>
Have a question or need assistance? Message us below or{" "}
<ExternalLink href="https://docs.unstoppableswap.net/send_feedback#email-support">
email us
</ExternalLink>
!
</Typography>
<TextField
variant="outlined"
value={input.bodyText}
onChange={(e) =>
setInputState((prev) => ({
...prev,
bodyText: e.target.value,
}))
}
label={
bodyTooLong
? `Text is too long (${input.bodyText.length}/${MAX_FEEDBACK_LENGTH})`
: "Message"
}
multiline
minRows={4}
maxRows={4}
fullWidth
error={bodyTooLong}
/>
</Box>
<Box>
<Typography style={{ marginBottom: "0.5rem" }}>
Attach logs with your feedback for better support.
</Typography>
<Box
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: "1rem",
paddingBottom: "0.5rem",
}}
>
<SwapSelectDropDown
selectedSwap={input.selectedSwap}
setSelectedSwap={(swapId) =>
setInputState((prev) => ({
...prev,
selectedSwap: swapId,
}))
}
/>
<Tooltip title="View the logs">
<Box <Box
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'column', alignItems: "center",
gap: '1.5rem', justifyContent: "center",
}} }}
> >
{error && ( <IconButton
<Box onClick={() => setSwapLogsEditorOpen(true)}
style={{ disabled={input.selectedSwap === null}
display: 'flex', size="large"
alignItems: 'center', >
justifyContent: 'start', <Visibility />
gap: '0.5rem', </IconButton>
width: '100%',
backgroundColor: 'hsla(0, 45%, 17%, 1)',
padding: '0.5rem',
borderRadius: '0.5rem',
border: '1px solid hsla(0, 61%, 32%, 1)',
}}
>
<ErrorOutline style={{ color: 'hsla(0, 77%, 75%, 1)' }} />
<Typography style={{ color: 'hsla(0, 83%, 91%, 1)' }} noWrap>
{error}
</Typography>
</Box>
)}
<Box>
<Typography style={{ marginBottom: '0.5rem' }}>
Have a question or need assistance? Message us below
or{' '}
<ExternalLink href="https://docs.unstoppableswap.net/send_feedback#email-support">
email us
</ExternalLink>
!
</Typography>
<TextField
variant="outlined"
value={input.bodyText}
onChange={(e) =>
setInputState((prev) => ({
...prev,
bodyText: e.target.value,
}))
}
label={
bodyTooLong
? `Text is too long (${input.bodyText.length}/${MAX_FEEDBACK_LENGTH})`
: 'Message'
}
multiline
minRows={4}
maxRows={4}
fullWidth
error={bodyTooLong}
/>
</Box>
<Box>
<Typography style={{ marginBottom: '0.5rem' }}>
Attach logs with your feedback for better support.
</Typography>
<Box
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
gap: '1rem',
paddingBottom: '0.5rem',
}}
>
<SwapSelectDropDown
selectedSwap={input.selectedSwap}
setSelectedSwap={(swapId) =>
setInputState((prev) => ({
...prev,
selectedSwap: swapId,
}))
}
/>
<Tooltip title="View the logs">
<Box
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<IconButton
onClick={() =>
setSwapLogsEditorOpen(true)
}
disabled={input.selectedSwap === null}
>
<Visibility />
</IconButton>
</Box>
</Tooltip>
</Box>
<LogViewer
open={swapLogsEditorOpen}
setOpen={setSwapLogsEditorOpen}
logs={logs.swapLogs}
setIsRedacted={(redact) =>
setInputState((prev) => ({
...prev,
isSwapLogsRedacted: redact,
}))
}
isRedacted={input.isSwapLogsRedacted}
/>
<Box
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
gap: '1rem',
}}
>
<Paper
variant="outlined"
style={{ padding: '0.5rem', width: '100%' }}
>
<FormControlLabel
control={
<Checkbox
color="primary"
checked={input.attachDaemonLogs}
onChange={(e) =>
setInputState((prev) => ({
...prev,
attachDaemonLogs:
e.target.checked,
}))
}
/>
}
label="Attach logs from the current session"
/>
</Paper>
<Tooltip title="View the logs">
<Box
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<IconButton
onClick={() =>
setDaemonLogsEditorOpen(true)
}
disabled={
input.attachDaemonLogs === false
}
>
<Visibility />
</IconButton>
</Box>
</Tooltip>
</Box>
</Box>
<Typography
variant="caption"
color="textSecondary"
style={{ marginBottom: '0.5rem' }}
>
Your feedback will be answered in the app and can be
found in the Feedback tab
</Typography>
<LogViewer
open={daemonLogsEditorOpen}
setOpen={setDaemonLogsEditorOpen}
logs={logs.daemonLogs}
setIsRedacted={(redact) =>
setInputState((prev) => ({
...prev,
isDaemonLogsRedacted: redact,
}))
}
isRedacted={input.isDaemonLogsRedacted}
/>
</Box> </Box>
</DialogContent> </Tooltip>
<DialogActions> </Box>
<Button onClick={handleClose}>Cancel</Button> <LogViewer
<PromiseInvokeButton open={swapLogsEditorOpen}
requiresContext={false} setOpen={setSwapLogsEditorOpen}
color="primary" logs={logs.swapLogs}
variant="contained" setIsRedacted={(redact) =>
onInvoke={submitFeedback} setInputState((prev) => ({
onSuccess={handleClose} ...prev,
isSwapLogsRedacted: redact,
}))
}
isRedacted={input.isSwapLogsRedacted}
/>
<Box
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: "1rem",
}}
>
<Paper
variant="outlined"
style={{ padding: "0.5rem", width: "100%" }}
>
<FormControlLabel
control={
<Checkbox
color="primary"
checked={input.attachDaemonLogs}
onChange={(e) =>
setInputState((prev) => ({
...prev,
attachDaemonLogs: e.target.checked,
}))
}
/>
}
label="Attach logs from the current session"
/>
</Paper>
<Tooltip title="View the logs">
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
> >
Submit <IconButton
</PromiseInvokeButton> onClick={() => setDaemonLogsEditorOpen(true)}
</DialogActions> disabled={input.attachDaemonLogs === false}
</Dialog> size="large"
) >
<Visibility />
</IconButton>
</Box>
</Tooltip>
</Box>
</Box>
<Typography
variant="caption"
color="textSecondary"
style={{ marginBottom: "0.5rem" }}
>
Your feedback will be answered in the app and can be found in the
Feedback tab
</Typography>
<LogViewer
open={daemonLogsEditorOpen}
setOpen={setDaemonLogsEditorOpen}
logs={logs.daemonLogs}
setIsRedacted={(redact) =>
setInputState((prev) => ({
...prev,
isDaemonLogsRedacted: redact,
}))
}
isRedacted={input.isDaemonLogsRedacted}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<PromiseInvokeButton
requiresContext={false}
color="primary"
variant="contained"
onInvoke={submitFeedback}
onSuccess={handleClose}
>
Submit
</PromiseInvokeButton>
</DialogActions>
</Dialog>
);
} }

View file

@ -8,7 +8,7 @@ import {
Paper, Paper,
Switch, Switch,
Typography, Typography,
} from "@material-ui/core"; } from "@mui/material";
import { CliLog } from "models/cliModel"; import { CliLog } from "models/cliModel";
import CliLogsBox from "renderer/components/other/RenderedCliLog"; import CliLogsBox from "renderer/components/other/RenderedCliLog";
@ -25,39 +25,58 @@ export default function LogViewer({
setOpen, setOpen,
logs, logs,
setIsRedacted, setIsRedacted,
isRedacted isRedacted,
}: LogViewerProps) { }: LogViewerProps) {
return ( return (
<Dialog open={open} onClose={() => setOpen(false)} fullWidth> <Dialog open={open} onClose={() => setOpen(false)} fullWidth>
<DialogContent> <DialogContent>
<Box> <Box>
<DialogContentText> <DialogContentText>
<Box style={{ display: "flex", flexDirection: "row", alignItems: "center" }}> <Box
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
}}
>
<Typography> <Typography>
These are the logs that would be attached to your feedback message and provided to us developers. These are the logs that would be attached to your feedback
They help us narrow down the problem you encountered. message and provided to us developers. They help us narrow down
the problem you encountered.
</Typography> </Typography>
</Box> </Box>
</DialogContentText> </DialogContentText>
<CliLogsBox <CliLogsBox
label="Logs" label="Logs"
logs={logs} logs={logs}
topRightButton={ topRightButton={
<Paper style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', paddingLeft: "0.5rem" }} variant="outlined"> <Paper
style={{
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
paddingLeft: "0.5rem",
}}
variant="outlined"
>
Redact Redact
<Switch <Switch
color="primary" color="primary"
checked={isRedacted} checked={isRedacted}
onChange={(_, checked: boolean) => setIsRedacted(checked)} onChange={(_, checked: boolean) => setIsRedacted(checked)}
/> />
</Paper> </Paper>
} }
/> />
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button variant="contained" color="primary" onClick={() => setOpen(false)}> <Button
variant="contained"
color="primary"
onClick={() => setOpen(false)}
>
Close Close
</Button> </Button>
</DialogActions> </DialogActions>

View file

@ -1,4 +1,4 @@
import { MenuItem, Select, Box } from "@material-ui/core"; import { MenuItem, Select, Box } from "@mui/material";
import TruncatedText from "renderer/components/other/TruncatedText"; import TruncatedText from "renderer/components/other/TruncatedText";
import { PiconeroAmount } from "../../other/Units"; import { PiconeroAmount } from "../../other/Units";
import { parseDateString } from "utils/parseUtils"; import { parseDateString } from "utils/parseUtils";
@ -26,20 +26,20 @@ export default function SwapSelectDropDown({
<Select <Select
value={selectedSwap ?? ""} value={selectedSwap ?? ""}
variant="outlined" variant="outlined"
onChange={(e) => setSelectedSwap(e.target.value as string || null)} onChange={(e) => setSelectedSwap((e.target.value as string) || null)}
style={{ width: "100%" }} style={{ width: "100%" }}
displayEmpty displayEmpty
> >
{swaps.map((swap) => ( {swaps.map((swap) => (
<MenuItem value={swap.swap_id} key={swap.swap_id}> <MenuItem value={swap.swap_id} key={swap.swap_id}>
<Box component="span" style={{ whiteSpace: 'pre' }}> <Box component="span" style={{ whiteSpace: "pre" }}>
Swap <TruncatedText>{swap.swap_id}</TruncatedText> from{' '} Swap <TruncatedText>{swap.swap_id}</TruncatedText> from{" "}
{new Date(parseDateString(swap.start_date)).toDateString()} ( {new Date(parseDateString(swap.start_date)).toDateString()} (
<PiconeroAmount amount={swap.xmr_amount} />) <PiconeroAmount amount={swap.xmr_amount} />)
</Box> </Box>
</MenuItem> </MenuItem>
))} ))}
<MenuItem value="">Do not attach a swap</MenuItem> <MenuItem value="">Do not attach a swap</MenuItem>
</Select> </Select>
); );
} }

View file

@ -1,163 +1,163 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from "react";
import { store } from 'renderer/store/storeRenderer' import { store } from "renderer/store/storeRenderer";
import { useActiveSwapInfo } from 'store/hooks' import { useActiveSwapInfo } from "store/hooks";
import { logsToRawString } from 'utils/parseUtils' import { logsToRawString } from "utils/parseUtils";
import { getLogsOfSwap, redactLogs } from 'renderer/rpc' import { getLogsOfSwap, redactLogs } from "renderer/rpc";
import { CliLog, parseCliLogString } from 'models/cliModel' import { CliLog, parseCliLogString } from "models/cliModel";
import logger from 'utils/logger' import logger from "utils/logger";
import { submitFeedbackViaHttp } from 'renderer/api' import { submitFeedbackViaHttp } from "renderer/api";
import { addFeedbackId } from 'store/features/conversationsSlice' import { addFeedbackId } from "store/features/conversationsSlice";
import { AttachmentInput } from 'models/apiModel' import { AttachmentInput } from "models/apiModel";
import { useSnackbar } from 'notistack' import { useSnackbar } from "notistack";
export const MAX_FEEDBACK_LENGTH = 4000 export const MAX_FEEDBACK_LENGTH = 4000;
interface FeedbackInputState { interface FeedbackInputState {
bodyText: string bodyText: string;
selectedSwap: string | null selectedSwap: string | null;
attachDaemonLogs: boolean attachDaemonLogs: boolean;
isSwapLogsRedacted: boolean isSwapLogsRedacted: boolean;
isDaemonLogsRedacted: boolean isDaemonLogsRedacted: boolean;
} }
interface FeedbackLogsState { interface FeedbackLogsState {
swapLogs: (string | CliLog)[] | null swapLogs: (string | CliLog)[] | null;
daemonLogs: (string | CliLog)[] | null daemonLogs: (string | CliLog)[] | null;
} }
const initialInputState: FeedbackInputState = { const initialInputState: FeedbackInputState = {
bodyText: '', bodyText: "",
selectedSwap: null, selectedSwap: null,
attachDaemonLogs: true, attachDaemonLogs: true,
isSwapLogsRedacted: false, isSwapLogsRedacted: false,
isDaemonLogsRedacted: false, isDaemonLogsRedacted: false,
} };
const initialLogsState: FeedbackLogsState = { const initialLogsState: FeedbackLogsState = {
swapLogs: null, swapLogs: null,
daemonLogs: null, daemonLogs: null,
} };
export function useFeedback() { export function useFeedback() {
const currentSwapId = useActiveSwapInfo() const currentSwapId = useActiveSwapInfo();
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar();
const [inputState, setInputState] = useState<FeedbackInputState>({ const [inputState, setInputState] = useState<FeedbackInputState>({
...initialInputState, ...initialInputState,
selectedSwap: currentSwapId?.swap_id || null, selectedSwap: currentSwapId?.swap_id || null,
}) });
const [logsState, setLogsState] = const [logsState, setLogsState] =
useState<FeedbackLogsState>(initialLogsState) useState<FeedbackLogsState>(initialLogsState);
const [isPending, setIsPending] = useState(false) const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null);
const bodyTooLong = inputState.bodyText.length > MAX_FEEDBACK_LENGTH const bodyTooLong = inputState.bodyText.length > MAX_FEEDBACK_LENGTH;
useEffect(() => { useEffect(() => {
if (inputState.selectedSwap === null) { if (inputState.selectedSwap === null) {
setLogsState((prev) => ({ ...prev, swapLogs: null })) setLogsState((prev) => ({ ...prev, swapLogs: null }));
return return;
}
getLogsOfSwap(inputState.selectedSwap, inputState.isSwapLogsRedacted)
.then((response) => {
setLogsState((prev) => ({
...prev,
swapLogs: response.logs.map(parseCliLogString),
}))
setError(null)
})
.catch((e) => {
logger.error(`Failed to fetch swap logs: ${e}`)
setLogsState((prev) => ({ ...prev, swapLogs: null }))
setError(`Failed to fetch swap logs: ${e}`)
})
}, [inputState.selectedSwap, inputState.isSwapLogsRedacted])
useEffect(() => {
if (!inputState.attachDaemonLogs) {
setLogsState((prev) => ({ ...prev, daemonLogs: null }))
return
}
try {
if (inputState.isDaemonLogsRedacted) {
redactLogs(store.getState().rpc?.logs)
.then((redactedLogs) => {
setLogsState((prev) => ({
...prev,
daemonLogs: redactedLogs,
}))
setError(null)
})
.catch((e) => {
logger.error(`Failed to redact daemon logs: ${e}`)
setLogsState((prev) => ({ ...prev, daemonLogs: null }))
setError(`Failed to redact daemon logs: ${e}`)
})
} else {
setLogsState((prev) => ({
...prev,
daemonLogs: store.getState().rpc?.logs,
}))
setError(null)
}
} catch (e) {
logger.error(`Failed to fetch daemon logs: ${e}`)
setLogsState((prev) => ({ ...prev, daemonLogs: null }))
setError(`Failed to fetch daemon logs: ${e}`)
}
}, [inputState.attachDaemonLogs, inputState.isDaemonLogsRedacted])
const clearState = () => {
setInputState(initialInputState)
setLogsState(initialLogsState)
setError(null)
} }
const submitFeedback = async () => { getLogsOfSwap(inputState.selectedSwap, inputState.isSwapLogsRedacted)
if (inputState.bodyText.length === 0) { .then((response) => {
setError('Please enter a message') setLogsState((prev) => ({
throw new Error('User did not enter a message') ...prev,
} swapLogs: response.logs.map(parseCliLogString),
}));
setError(null);
})
.catch((e) => {
logger.error(`Failed to fetch swap logs: ${e}`);
setLogsState((prev) => ({ ...prev, swapLogs: null }));
setError(`Failed to fetch swap logs: ${e}`);
});
}, [inputState.selectedSwap, inputState.isSwapLogsRedacted]);
const attachments: AttachmentInput[] = [] useEffect(() => {
// Add swap logs as an attachment if (!inputState.attachDaemonLogs) {
if (logsState.swapLogs) { setLogsState((prev) => ({ ...prev, daemonLogs: null }));
attachments.push({ return;
key: `swap_logs_${inputState.selectedSwap}.txt`,
content: logsToRawString(logsState.swapLogs),
})
}
// Handle daemon logs
if (logsState.daemonLogs) {
attachments.push({
key: 'daemon_logs.txt',
content: logsToRawString(logsState.daemonLogs),
})
}
// Call the updated API function
const feedbackId = await submitFeedbackViaHttp(
inputState.bodyText,
attachments
)
enqueueSnackbar('Feedback submitted successfully', {
variant: 'success',
})
// Dispatch only the ID
store.dispatch(addFeedbackId(feedbackId))
} }
return { try {
input: inputState, if (inputState.isDaemonLogsRedacted) {
setInputState, redactLogs(store.getState().rpc?.logs)
logs: logsState, .then((redactedLogs) => {
error, setLogsState((prev) => ({
clearState, ...prev,
submitFeedback, daemonLogs: redactedLogs,
}));
setError(null);
})
.catch((e) => {
logger.error(`Failed to redact daemon logs: ${e}`);
setLogsState((prev) => ({ ...prev, daemonLogs: null }));
setError(`Failed to redact daemon logs: ${e}`);
});
} else {
setLogsState((prev) => ({
...prev,
daemonLogs: store.getState().rpc?.logs,
}));
setError(null);
}
} catch (e) {
logger.error(`Failed to fetch daemon logs: ${e}`);
setLogsState((prev) => ({ ...prev, daemonLogs: null }));
setError(`Failed to fetch daemon logs: ${e}`);
} }
}, [inputState.attachDaemonLogs, inputState.isDaemonLogsRedacted]);
const clearState = () => {
setInputState(initialInputState);
setLogsState(initialLogsState);
setError(null);
};
const submitFeedback = async () => {
if (inputState.bodyText.length === 0) {
setError("Please enter a message");
throw new Error("User did not enter a message");
}
const attachments: AttachmentInput[] = [];
// Add swap logs as an attachment
if (logsState.swapLogs) {
attachments.push({
key: `swap_logs_${inputState.selectedSwap}.txt`,
content: logsToRawString(logsState.swapLogs),
});
}
// Handle daemon logs
if (logsState.daemonLogs) {
attachments.push({
key: "daemon_logs.txt",
content: logsToRawString(logsState.daemonLogs),
});
}
// Call the updated API function
const feedbackId = await submitFeedbackViaHttp(
inputState.bodyText,
attachments,
);
enqueueSnackbar("Feedback submitted successfully", {
variant: "success",
});
// Dispatch only the ID
store.dispatch(addFeedbackId(feedbackId));
};
return {
input: inputState,
setInputState,
logs: logsState,
error,
clearState,
submitFeedback,
};
} }

View file

@ -1,114 +1,108 @@
import { makeStyles, Modal } from '@material-ui/core' import { Modal } from "@mui/material";
import { useState } from 'react' import { useState } from "react";
import Slide01_GettingStarted from './slides/Slide01_GettingStarted' import Slide01_GettingStarted from "./slides/Slide01_GettingStarted";
import Slide02_ChooseAMaker from './slides/Slide02_ChooseAMaker' import Slide02_ChooseAMaker from "./slides/Slide02_ChooseAMaker";
import Slide03_PrepareSwap from './slides/Slide03_PrepareSwap' import Slide03_PrepareSwap from "./slides/Slide03_PrepareSwap";
import Slide04_ExecuteSwap from './slides/Slide04_ExecuteSwap' import Slide04_ExecuteSwap from "./slides/Slide04_ExecuteSwap";
import Slide05_KeepAnEyeOnYourSwaps from './slides/Slide05_KeepAnEyeOnYourSwaps' import Slide05_KeepAnEyeOnYourSwaps from "./slides/Slide05_KeepAnEyeOnYourSwaps";
import Slide06_FiatPricePreference from './slides/Slide06_FiatPricePreference' import Slide06_FiatPricePreference from "./slides/Slide06_FiatPricePreference";
import Slide07_ReachOut from './slides/Slide07_ReachOut' import Slide07_ReachOut from "./slides/Slide07_ReachOut";
import { import {
setFetchFiatPrices, setFetchFiatPrices,
setUserHasSeenIntroduction, setUserHasSeenIntroduction,
} from 'store/features/settingsSlice' } from "store/features/settingsSlice";
import { useAppDispatch, useSettings } from 'store/hooks' import { useAppDispatch, useSettings } from "store/hooks";
const useStyles = makeStyles({
modal: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
paper: {
width: '80%',
display: 'flex',
justifyContent: 'space-between',
},
})
export default function IntroductionModal() { export default function IntroductionModal() {
const userHasSeenIntroduction = useSettings( const userHasSeenIntroduction = useSettings((s) => s.userHasSeenIntroduction);
(s) => s.userHasSeenIntroduction
)
const dispatch = useAppDispatch() const dispatch = useAppDispatch();
// Handle Display State // Handle Display State
const [open, setOpen] = useState<boolean>(!userHasSeenIntroduction) const [open, setOpen] = useState<boolean>(!userHasSeenIntroduction);
const [showFiat, setShowFiat] = useState<boolean>(true) const [showFiat, setShowFiat] = useState<boolean>(true);
const handleClose = () => { const handleClose = () => {
setOpen(false) setOpen(false);
};
// Handle Slide Index
const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
const handleContinue = () => {
if (currentSlideIndex == slideComponents.length - 1) {
handleClose();
dispatch(setUserHasSeenIntroduction(true));
dispatch(setFetchFiatPrices(showFiat));
return;
} }
// Handle Slide Index setCurrentSlideIndex((i) => i + 1);
const [currentSlideIndex, setCurrentSlideIndex] = useState(0) };
const handleContinue = () => { const handlePrevious = () => {
if (currentSlideIndex == slideComponents.length - 1) { if (currentSlideIndex == 0) {
handleClose() return;
dispatch(setUserHasSeenIntroduction(true))
dispatch(setFetchFiatPrices(showFiat))
return
}
setCurrentSlideIndex((i) => i + 1)
} }
const handlePrevious = () => { setCurrentSlideIndex((i) => i - 1);
if (currentSlideIndex == 0) { };
return
}
setCurrentSlideIndex((i) => i - 1) const slideComponents = [
} <Slide01_GettingStarted
handleContinue={handleContinue}
handlePrevious={handlePrevious}
hidePreviousButton
key="slide-01"
/>,
<Slide02_ChooseAMaker
handleContinue={handleContinue}
handlePrevious={handlePrevious}
key="slide-02"
/>,
<Slide03_PrepareSwap
handleContinue={handleContinue}
handlePrevious={handlePrevious}
key="slide-03"
/>,
<Slide04_ExecuteSwap
handleContinue={handleContinue}
handlePrevious={handlePrevious}
key="slide-04"
/>,
<Slide05_KeepAnEyeOnYourSwaps
handleContinue={handleContinue}
handlePrevious={handlePrevious}
key="slide-05"
/>,
<Slide06_FiatPricePreference
handleContinue={handleContinue}
handlePrevious={handlePrevious}
showFiat={showFiat}
onChange={(showFiatSetting: string) =>
setShowFiat(showFiatSetting === "show")
}
key="slide-06"
/>,
<Slide07_ReachOut
handleContinue={handleContinue}
handlePrevious={handlePrevious}
key="slide-07"
/>,
];
const slideComponents = [ return (
<Slide01_GettingStarted <Modal
handleContinue={handleContinue} open={open}
handlePrevious={handlePrevious} onClose={handleClose}
hidePreviousButton sx={{
/>, display: "flex",
<Slide02_ChooseAMaker alignItems: "center",
handleContinue={handleContinue} justifyContent: "center",
handlePrevious={handlePrevious} }}
/>, disableAutoFocus
<Slide03_PrepareSwap closeAfterTransition
handleContinue={handleContinue} >
handlePrevious={handlePrevious} {slideComponents[currentSlideIndex]}
/>, </Modal>
<Slide04_ExecuteSwap );
handleContinue={handleContinue}
handlePrevious={handlePrevious}
/>,
<Slide05_KeepAnEyeOnYourSwaps
handleContinue={handleContinue}
handlePrevious={handlePrevious}
/>,
<Slide06_FiatPricePreference
handleContinue={handleContinue}
handlePrevious={handlePrevious}
showFiat={showFiat}
onChange={(showFiatSetting: string) =>
setShowFiat(showFiatSetting === 'show')
}
/>,
<Slide07_ReachOut
handleContinue={handleContinue}
handlePrevious={handlePrevious}
/>,
]
const classes = useStyles()
return (
<Modal
open={open}
onClose={handleClose}
className={classes.modal}
disableAutoFocus
closeAfterTransition
>
{slideComponents[currentSlideIndex]}
</Modal>
)
} }

View file

@ -1,23 +1,19 @@
import { Typography } from '@material-ui/core' import { Typography } from "@mui/material";
import SlideTemplate from './SlideTemplate' import SlideTemplate from "./SlideTemplate";
import imagePath from 'assets/walletWithBitcoinAndMonero.png' import imagePath from "assets/walletWithBitcoinAndMonero.png";
export default function Slide01_GettingStarted(props: slideProps) { export default function Slide01_GettingStarted(props: slideProps) {
return ( return (
<SlideTemplate <SlideTemplate title="Getting Started" {...props} imagePath={imagePath}>
title="Getting Started" <Typography variant="subtitle1">
{...props} To start swapping, you'll need:
imagePath={imagePath} </Typography>
> <Typography>
<Typography variant="subtitle1"> <ul>
To start swapping, you'll need: <li>A Bitcoin wallet with funds to swap</li>
</Typography> <li>A Monero wallet to receive your Monero</li>
<Typography> </ul>
<ul> </Typography>
<li>A Bitcoin wallet with funds to swap</li> </SlideTemplate>
<li>A Monero wallet to receive your Monero</li> );
</ul>
</Typography>
</SlideTemplate>
)
} }

View file

@ -1,18 +1,19 @@
import { Typography } from '@material-ui/core' import { Typography } from "@mui/material";
import SlideTemplate from './SlideTemplate' import SlideTemplate from "./SlideTemplate";
import imagePath from 'assets/mockMakerSelection.svg' import imagePath from "assets/mockMakerSelection.svg";
export default function Slide02_ChooseAMaker(props: slideProps) { export default function Slide02_ChooseAMaker(props: slideProps) {
return ( return (
<SlideTemplate <SlideTemplate
title="Choose a Maker" title="Choose a Maker"
stepLabel="Step 1" stepLabel="Step 1"
{...props} {...props}
imagePath={imagePath} imagePath={imagePath}
> >
<Typography variant="subtitle1"> <Typography variant="subtitle1">
To start a swap, choose a maker. Each maker offers different exchange rates and limits. To start a swap, choose a maker. Each maker offers different exchange
</Typography> rates and limits.
</SlideTemplate> </Typography>
) </SlideTemplate>
);
} }

View file

@ -1,13 +1,19 @@
import { Typography } from '@material-ui/core' import { Typography } from "@mui/material";
import SlideTemplate from './SlideTemplate' import SlideTemplate from "./SlideTemplate";
import imagePath from 'assets/mockConfigureSwap.svg' import imagePath from "assets/mockConfigureSwap.svg";
export default function Slide02_ChooseAMaker(props: slideProps) { export default function Slide02_ChooseAMaker(props: slideProps) {
return ( return (
<SlideTemplate title="Prepare Swap" stepLabel="Step 2" {...props} imagePath={imagePath}> <SlideTemplate
<Typography variant="subtitle1"> title="Prepare Swap"
To initiate a swap, provide a Monero address and optionally a Bitcoin refund address. stepLabel="Step 2"
</Typography> {...props}
</SlideTemplate> imagePath={imagePath}
) >
<Typography variant="subtitle1">
To initiate a swap, provide a Monero address and optionally a Bitcoin
refund address.
</Typography>
</SlideTemplate>
);
} }

View file

@ -1,26 +1,24 @@
import { Typography } from '@material-ui/core' import { Typography } from "@mui/material";
import SlideTemplate from './SlideTemplate' import SlideTemplate from "./SlideTemplate";
import imagePath from 'assets/simpleSwapFlowDiagram.svg' import imagePath from "assets/simpleSwapFlowDiagram.svg";
export default function Slide02_ChooseAMaker(props: slideProps) { export default function Slide02_ChooseAMaker(props: slideProps) {
return ( return (
<SlideTemplate <SlideTemplate
title="Execute Swap" title="Execute Swap"
stepLabel="Step 3" stepLabel="Step 3"
{...props} {...props}
imagePath={imagePath} imagePath={imagePath}
> >
<Typography variant="subtitle1"> <Typography variant="subtitle1">After confirming:</Typography>
After confirming: <Typography>
</Typography> <ol>
<Typography> <li>Your Bitcoin are locked</li>
<ol> <li>Maker locks the Monero</li>
<li>Your Bitcoin are locked</li> <li>Maker reedems the Bitcoin</li>
<li>Maker locks the Monero</li> <li>Monero is sent to your address</li>
<li>Maker reedems the Bitcoin</li> </ol>
<li>Monero is sent to your address</li> </Typography>
</ol> </SlideTemplate>
</Typography> );
</SlideTemplate>
)
} }

View file

@ -1,24 +1,24 @@
import { Link, Typography } from '@material-ui/core' import { Link, Typography } from "@mui/material";
import SlideTemplate from './SlideTemplate' import SlideTemplate from "./SlideTemplate";
import imagePath from 'assets/mockHistoryPage.svg' import imagePath from "assets/mockHistoryPage.svg";
import ExternalLink from 'renderer/components/other/ExternalLink' import ExternalLink from "renderer/components/other/ExternalLink";
export default function Slide05_KeepAnEyeOnYourSwaps(props: slideProps) { export default function Slide05_KeepAnEyeOnYourSwaps(props: slideProps) {
return ( return (
<SlideTemplate <SlideTemplate
title="Monitor Your Swaps" title="Monitor Your Swaps"
stepLabel="Step 3" stepLabel="Step 3"
{...props} {...props}
imagePath={imagePath} imagePath={imagePath}
> >
<Typography> <Typography>
Monitor active swaps to ensure everything proceeds smoothly. Monitor active swaps to ensure everything proceeds smoothly.
</Typography> </Typography>
<Typography> <Typography>
<ExternalLink href='https://docs.unstoppableswap.net/usage/first_swap'> <ExternalLink href="https://docs.unstoppableswap.net/usage/first_swap">
Learn more about atomic swaps Learn more about atomic swaps
</ExternalLink> </ExternalLink>
</Typography> </Typography>
</SlideTemplate> </SlideTemplate>
) );
} }

View file

@ -1,53 +1,54 @@
import { Box, Typography, Paper, Button, Slide } from '@material-ui/core' import { Box, Typography, Paper, Button, Slide } from "@mui/material";
import CardSelectionGroup from 'renderer/components/inputs/CardSelection/CardSelectionGroup' import CardSelectionGroup from "renderer/components/inputs/CardSelection/CardSelectionGroup";
import CardSelectionOption from 'renderer/components/inputs/CardSelection/CardSelectionOption' import CardSelectionOption from "renderer/components/inputs/CardSelection/CardSelectionOption";
import SlideTemplate from './SlideTemplate' import SlideTemplate from "./SlideTemplate";
import imagePath from 'assets/currencyFetching.svg' import imagePath from "assets/currencyFetching.svg";
const FiatPricePreferenceSlide = ({ const FiatPricePreferenceSlide = ({
handleContinue, handleContinue,
handlePrevious, handlePrevious,
showFiat, showFiat,
onChange, onChange,
}: slideProps & { }: slideProps & {
showFiat: boolean showFiat: boolean;
onChange: (value: string) => void onChange: (value: string) => void;
}) => { }) => {
return ( return (
<SlideTemplate handleContinue={handleContinue} handlePrevious={handlePrevious} title="Fiat Prices" imagePath={imagePath}> <SlideTemplate
<Typography variant="subtitle1" color="textSecondary"> handleContinue={handleContinue}
Do you want to show fiat prices? handlePrevious={handlePrevious}
</Typography> title="Fiat Prices"
<CardSelectionGroup imagePath={imagePath}
value={showFiat ? 'show' : 'hide'} >
onChange={onChange} <Typography variant="subtitle1" color="textSecondary">
> Do you want to show fiat prices?
<CardSelectionOption value="show"> </Typography>
<Typography>Show fiat prices</Typography> <CardSelectionGroup
<Typography value={showFiat ? "show" : "hide"}
variant="caption" onChange={onChange}
color="textSecondary" >
paragraph <CardSelectionOption value="show">
style={{ marginBottom: 4 }} <Typography>Show fiat prices</Typography>
> <Typography
We connect to CoinGecko to provide realtime currency variant="caption"
prices. color="textSecondary"
</Typography> paragraph
</CardSelectionOption> style={{ marginBottom: 4 }}
<CardSelectionOption value="hide"> >
<Typography>Don't show fiat prices</Typography> We connect to CoinGecko to provide realtime currency prices.
</CardSelectionOption> </Typography>
</CardSelectionGroup> </CardSelectionOption>
<Box style={{ marginTop: "0.5rem" }}> <CardSelectionOption value="hide">
<Typography <Typography>Don't show fiat prices</Typography>
variant="caption" </CardSelectionOption>
color="textSecondary" </CardSelectionGroup>
> <Box style={{ marginTop: "0.5rem" }}>
You can change your preference later in the settings <Typography variant="caption" color="textSecondary">
</Typography> You can change your preference later in the settings
</Box> </Typography>
</SlideTemplate> </Box>
) </SlideTemplate>
} );
};
export default FiatPricePreferenceSlide export default FiatPricePreferenceSlide;

View file

@ -1,25 +1,34 @@
import { Box, Typography } from '@material-ui/core' import { Box, Typography } from "@mui/material";
import SlideTemplate from './SlideTemplate' import SlideTemplate from "./SlideTemplate";
import imagePath from 'assets/groupWithChatbubbles.png' import imagePath from "assets/groupWithChatbubbles.png";
import GitHubIcon from "@material-ui/icons/GitHub" import GitHubIcon from "@mui/icons-material/GitHub";
import MatrixIcon from 'renderer/components/icons/MatrixIcon' import MatrixIcon from "renderer/components/icons/MatrixIcon";
import LinkIconButton from 'renderer/components/icons/LinkIconButton' import LinkIconButton from "renderer/components/icons/LinkIconButton";
export default function Slide02_ChooseAMaker(props: slideProps) { export default function Slide02_ChooseAMaker(props: slideProps) {
return ( return (
<SlideTemplate title="Reach out" {...props} imagePath={imagePath} customContinueButtonText="Get Started"> <SlideTemplate
<Typography variant="subtitle1"> title="Reach out"
We would love to hear about your experience with Unstoppable {...props}
Swap and invite you to join our community. imagePath={imagePath}
</Typography> customContinueButtonText="Get Started"
<Box mt={3}> >
<LinkIconButton url="https://github.com/UnstoppableSwap/core"> <Typography variant="subtitle1">
<GitHubIcon/> We would love to hear about your experience with Unstoppable Swap and
</LinkIconButton> invite you to join our community.
<LinkIconButton url="https://matrix.to/#/#unstoppableswap:matrix.org"> </Typography>
<MatrixIcon/> <Box
</LinkIconButton> sx={{
</Box> mt: 3,
</SlideTemplate> }}
) >
<LinkIconButton url="https://github.com/UnstoppableSwap/core">
<GitHubIcon />
</LinkIconButton>
<LinkIconButton url="https://matrix.to/#/#unstoppableswap:matrix.org">
<MatrixIcon />
</LinkIconButton>
</Box>
</SlideTemplate>
);
} }

View file

@ -1,94 +1,94 @@
import { makeStyles, Paper, Box, Typography, Button } from '@material-ui/core' import { Paper, Box, Typography, Button } from "@mui/material";
type slideTemplateProps = { type slideTemplateProps = {
handleContinue: () => void handleContinue: () => void;
handlePrevious: () => void handlePrevious: () => void;
hidePreviousButton?: boolean hidePreviousButton?: boolean;
stepLabel?: String stepLabel?: string;
title: String title: string;
children?: React.ReactNode children?: React.ReactNode;
imagePath?: string imagePath?: string;
imagePadded?: boolean imagePadded?: boolean;
customContinueButtonText?: String customContinueButtonText?: string;
} };
const useStyles = makeStyles({
paper: {
height: "80%",
width: "80%",
display: 'flex',
justifyContent: 'space-between',
},
stepLabel: {
textTransform: 'uppercase',
},
splitImage: {
height: '100%',
width: '100%',
objectFit: 'contain'
}
})
export default function SlideTemplate({ export default function SlideTemplate({
handleContinue, handleContinue,
handlePrevious, handlePrevious,
hidePreviousButton, hidePreviousButton,
stepLabel, stepLabel,
title, title,
children, children,
imagePath, imagePath,
imagePadded, imagePadded,
customContinueButtonText customContinueButtonText,
}: slideTemplateProps) { }: slideTemplateProps) {
const classes = useStyles() return (
<Paper
return ( sx={{
<Paper className={classes.paper}> height: "80%",
<Box m={3} flex alignContent="center" position="relative" width="50%" flexGrow={1}> width: "80%",
<Box> display: "flex",
{stepLabel && ( justifyContent: "space-between",
<Typography }}
variant="overline" >
className={classes.stepLabel} <Box
> sx={{
{stepLabel} m: 3,
</Typography> alignContent: "center",
)} position: "relative",
<Typography variant="h4" style={{ marginBottom: 16 }}>{title}</Typography> width: "50%",
{children} flexGrow: 1,
</Box> }}
<Box >
position="absolute" <Box>
bottom={0} {stepLabel && (
width="100%" <Typography variant="overline" sx={{ textTransform: "uppercase" }}>
display="flex" {stepLabel}
justifyContent={ </Typography>
hidePreviousButton ? 'flex-end' : 'space-between' )}
} <Typography variant="h4" style={{ marginBottom: 16 }}>
> {title}
{!hidePreviousButton && ( </Typography>
<Button onClick={handlePrevious}>Back</Button> {children}
)} </Box>
<Button <Box
onClick={handleContinue} sx={{
variant="contained" position: "absolute",
color="primary" bottom: 0,
> width: "100%",
{customContinueButtonText ? customContinueButtonText : 'Next' } display: "flex",
</Button> justifyContent: hidePreviousButton ? "flex-end" : "space-between",
</Box> }}
</Box> >
{imagePath && ( {!hidePreviousButton && (
<Box <Button onClick={handlePrevious}>Back</Button>
bgcolor="#212121" )}
width="50%" <Button onClick={handleContinue} variant="contained" color="primary">
display="flex" {customContinueButtonText ? customContinueButtonText : "Next"}
justifyContent="center" </Button>
p={imagePadded ? "1.5em" : 0} </Box>
> </Box>
<img src={imagePath} className={classes.splitImage} /> {imagePath && (
</Box> <Box
)} sx={{
</Paper> bgcolor: "#212121",
) width: "50%",
display: "flex",
justifyContent: "center",
p: imagePadded ? "1.5em" : 0,
}}
>
<img
src={imagePath}
style={{
height: "100%",
width: "100%",
objectFit: "contain",
}}
/>
</Box>
)}
</Paper>
);
} }

View file

@ -1,5 +1,5 @@
type slideProps = { type slideProps = {
handleContinue: () => void handleContinue: () => void;
handlePrevious: () => void handlePrevious: () => void;
hidePreviousButton?: boolean hidePreviousButton?: boolean;
} };

View file

@ -7,28 +7,21 @@ import {
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
makeStyles,
TextField, TextField,
Theme, } from "@mui/material";
} from "@material-ui/core";
import { ListSellersResponse } from "models/tauriModel"; import { ListSellersResponse } from "models/tauriModel";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
import TruncatedText from "renderer/components/other/TruncatedText"; import TruncatedText from "renderer/components/other/TruncatedText";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { listSellersAtRendezvousPoint, PRESET_RENDEZVOUS_POINTS } from "renderer/rpc"; import {
listSellersAtRendezvousPoint,
PRESET_RENDEZVOUS_POINTS,
} from "renderer/rpc";
import { discoveredMakersByRendezvous } from "store/features/makersSlice"; import { discoveredMakersByRendezvous } from "store/features/makersSlice";
import { useAppDispatch } from "store/hooks"; import { useAppDispatch } from "store/hooks";
import { isValidMultiAddressWithPeerId } from "utils/parseUtils"; import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
const useStyles = makeStyles((theme: Theme) => ({
chipOuter: {
display: "flex",
flexWrap: "wrap",
gap: theme.spacing(1),
},
}));
type ListSellersDialogProps = { type ListSellersDialogProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@ -38,7 +31,6 @@ export default function ListSellersDialog({
open, open,
onClose, onClose,
}: ListSellersDialogProps) { }: ListSellersDialogProps) {
const classes = useStyles();
const [rendezvousAddress, setRendezvousAddress] = useState(""); const [rendezvousAddress, setRendezvousAddress] = useState("");
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -101,7 +93,7 @@ export default function ListSellersDialog({
placeholder="/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE" placeholder="/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE"
error={!!getMultiAddressError()} error={!!getMultiAddressError()}
/> />
<Box className={classes.chipOuter}> <Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
{PRESET_RENDEZVOUS_POINTS.map((rAddress) => ( {PRESET_RENDEZVOUS_POINTS.map((rAddress) => (
<Chip <Chip
key={rAddress} key={rAddress}

View file

@ -1,5 +1,5 @@
import { Box, Chip, makeStyles, Paper, Tooltip, Typography } from "@material-ui/core"; import { Box, Chip, Paper, Tooltip, Typography } from "@mui/material";
import { VerifiedUser } from "@material-ui/icons"; import { VerifiedUser } from "@mui/icons-material";
import { ExtendedMakerStatus } from "models/apiModel"; import { ExtendedMakerStatus } from "models/apiModel";
import TruncatedText from "renderer/components/other/TruncatedText"; import TruncatedText from "renderer/components/other/TruncatedText";
import { import {
@ -7,44 +7,17 @@ import {
SatsAmount, SatsAmount,
} from "renderer/components/other/Units"; } from "renderer/components/other/Units";
import { getMarkup, satsToBtc, secondsToDays } from "utils/conversionUtils"; import { getMarkup, satsToBtc, secondsToDays } from "utils/conversionUtils";
import { isMakerOutdated, isMakerVersionOutdated } from 'utils/multiAddrUtils'; import { isMakerOutdated, isMakerVersionOutdated } from "utils/multiAddrUtils";
import WarningIcon from '@material-ui/icons/Warning'; import WarningIcon from "@mui/icons-material/Warning";
import { useAppSelector, useMakerVersion } from "store/hooks"; import { useAppSelector } from "store/hooks";
import IdentIcon from "renderer/components/icons/IdentIcon"; import IdentIcon from "renderer/components/icons/IdentIcon";
const useStyles = makeStyles((theme) => ({
content: {
flex: 1,
"& *": {
lineBreak: "anywhere",
},
display: "flex",
flexDirection: "column",
gap: theme.spacing(1),
},
chipsOuter: {
display: "flex",
flexWrap: "wrap",
gap: theme.spacing(0.5),
},
quoteOuter: {
display: "flex",
flexDirection: "column",
},
peerIdContainer: {
display: "flex",
alignItems: "center",
gap: theme.spacing(1),
},
}));
/** /**
* A chip that displays the markup of the maker's exchange rate compared to the market rate. * A chip that displays the markup of the maker's exchange rate compared to the market rate.
*/ */
function MakerMarkupChip({ maker }: { maker: ExtendedMakerStatus }) { function MakerMarkupChip({ maker }: { maker: ExtendedMakerStatus }) {
const marketExchangeRate = useAppSelector(s => s.rates?.xmrBtcRate); const marketExchangeRate = useAppSelector((s) => s.rates?.xmrBtcRate);
if (marketExchangeRate == null) if (marketExchangeRate == null) return null;
return null;
const makerExchangeRate = satsToBtc(maker.price); const makerExchangeRate = satsToBtc(maker.price);
/** The markup of the exchange rate compared to the market rate in percent */ /** The markup of the exchange rate compared to the market rate in percent */
@ -57,32 +30,44 @@ function MakerMarkupChip({ maker }: { maker: ExtendedMakerStatus }) {
); );
} }
export default function MakerInfo({ export default function MakerInfo({ maker }: { maker: ExtendedMakerStatus }) {
maker,
}: {
maker: ExtendedMakerStatus;
}) {
const classes = useStyles();
const isOutdated = isMakerOutdated(maker); const isOutdated = isMakerOutdated(maker);
return ( return (
<Box className={classes.content}> <Box
<Box className={classes.peerIdContainer}> sx={{
<Tooltip title={"This avatar is deterministically derived from the public key of the maker"} arrow> flex: 1,
<Box className={classes.peerIdContainer}> "& *": {
lineBreak: "anywhere",
},
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Tooltip
title={
"This avatar is deterministically derived from the public key of the maker"
}
arrow
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<IdentIcon value={maker.peerId} size={"3rem"} /> <IdentIcon value={maker.peerId} size={"3rem"} />
</Box> </Box>
</Tooltip> </Tooltip>
<Box> <Box>
<Typography variant="subtitle1"> <Typography variant="subtitle1">
<TruncatedText limit={16} truncateMiddle>{maker.peerId}</TruncatedText> <TruncatedText limit={16} truncateMiddle>
{maker.peerId}
</TruncatedText>
</Typography> </Typography>
<Typography color="textSecondary" variant="body2"> <Typography color="textSecondary" variant="body2">
{maker.multiAddr} {maker.multiAddr}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
<Box className={classes.quoteOuter}> <Box sx={{ display: "flex", flexDirection: "column" }}>
<Typography variant="caption"> <Typography variant="caption">
Exchange rate:{" "} Exchange rate:{" "}
<MoneroBitcoinExchangeRate rate={satsToBtc(maker.price)} /> <MoneroBitcoinExchangeRate rate={satsToBtc(maker.price)} />
@ -94,7 +79,7 @@ export default function MakerInfo({
Maximum amount: <SatsAmount amount={maker.maxSwapAmount} /> Maximum amount: <SatsAmount amount={maker.maxSwapAmount} />
</Typography> </Typography>
</Box> </Box>
<Box className={classes.chipsOuter}> <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{maker.testnet && <Chip label="Testnet" />} {maker.testnet && <Chip label="Testnet" />}
{maker.uptime && ( {maker.uptime && (
<Tooltip title="A high uptime (>90%) indicates reliability. Makers with very low uptime may be unreliable and cause swaps to take longer to complete or fail entirely."> <Tooltip title="A high uptime (>90%) indicates reliability. Makers with very low uptime may be unreliable and cause swaps to take longer to complete or fail entirely.">
@ -103,8 +88,9 @@ export default function MakerInfo({
)} )}
{maker.age ? ( {maker.age ? (
<Chip <Chip
label={`Went online ${Math.round(secondsToDays(maker.age))} ${maker.age === 1 ? "day" : "days" label={`Went online ${Math.round(secondsToDays(maker.age))} ${
} ago`} maker.age === 1 ? "day" : "days"
} ago`}
/> />
) : ( ) : (
<Chip label="Discovered via rendezvous point" /> <Chip label="Discovered via rendezvous point" />
@ -121,7 +107,6 @@ export default function MakerInfo({
)} )}
<MakerMarkupChip maker={maker} /> <MakerMarkupChip maker={maker} />
</Box> </Box>
</Box > </Box>
); );
} }

View file

@ -6,13 +6,11 @@ import {
DialogContent, DialogContent,
DialogTitle, DialogTitle,
List, List,
ListItem,
ListItemAvatar, ListItemAvatar,
ListItemText, ListItemText,
makeStyles, } from "@mui/material";
} from "@material-ui/core"; import AddIcon from "@mui/icons-material/Add";
import AddIcon from "@material-ui/icons/Add"; import SearchIcon from "@mui/icons-material/Search";
import SearchIcon from "@material-ui/icons/Search";
import { ExtendedMakerStatus } from "models/apiModel"; import { ExtendedMakerStatus } from "models/apiModel";
import { useState } from "react"; import { useState } from "react";
import { setSelectedMaker } from "store/features/makersSlice"; import { setSelectedMaker } from "store/features/makersSlice";
@ -21,11 +19,7 @@ import ListSellersDialog from "../listSellers/ListSellersDialog";
import MakerInfo from "./MakerInfo"; import MakerInfo from "./MakerInfo";
import MakerSubmitDialog from "./MakerSubmitDialog"; import MakerSubmitDialog from "./MakerSubmitDialog";
const useStyles = makeStyles({ import ListItemButton from "@mui/material/ListItemButton";
dialogContent: {
padding: 0,
},
});
type MakerSelectDialogProps = { type MakerSelectDialogProps = {
open: boolean; open: boolean;
@ -36,9 +30,8 @@ export function MakerSubmitDialogOpenButton() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<ListItem <ListItemButton
autoFocus autoFocus
button
onClick={() => { onClick={() => {
// Prevents background from being clicked and reopening dialog // Prevents background from being clicked and reopening dialog
if (!open) { if (!open) {
@ -53,7 +46,7 @@ export function MakerSubmitDialogOpenButton() {
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary="Add a new maker to public registry" /> <ListItemText primary="Add a new maker to public registry" />
</ListItem> </ListItemButton>
); );
} }
@ -61,9 +54,8 @@ export function ListSellersDialogOpenButton() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<ListItem <ListItemButton
autoFocus autoFocus
button
onClick={() => { onClick={() => {
// Prevents background from being clicked and reopening dialog // Prevents background from being clicked and reopening dialog
if (!open) { if (!open) {
@ -78,7 +70,7 @@ export function ListSellersDialogOpenButton() {
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary="Discover makers by connecting to a rendezvous point" /> <ListItemText primary="Discover makers by connecting to a rendezvous point" />
</ListItem> </ListItemButton>
); );
} }
@ -86,7 +78,6 @@ export default function MakerListDialog({
open, open,
onClose, onClose,
}: MakerSelectDialogProps) { }: MakerSelectDialogProps) {
const classes = useStyles();
const makers = useAllMakers(); const makers = useAllMakers();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -98,23 +89,20 @@ export default function MakerListDialog({
return ( return (
<Dialog onClose={onClose} open={open}> <Dialog onClose={onClose} open={open}>
<DialogTitle>Select a maker</DialogTitle> <DialogTitle>Select a maker</DialogTitle>
<DialogContent sx={{ padding: 0 }} dividers>
<DialogContent className={classes.dialogContent} dividers>
<List> <List>
{makers.map((maker) => ( {makers.map((maker) => (
<ListItem <ListItemButton
button
onClick={() => handleMakerChange(maker)} onClick={() => handleMakerChange(maker)}
key={maker.peerId} key={maker.peerId}
> >
<MakerInfo maker={maker} key={maker.peerId} /> <MakerInfo maker={maker} key={maker.peerId} />
</ListItem> </ListItemButton>
))} ))}
<ListSellersDialogOpenButton /> <ListSellersDialogOpenButton />
<MakerSubmitDialogOpenButton /> <MakerSubmitDialogOpenButton />
</List> </List>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose}>Cancel</Button> <Button onClick={onClose}>Cancel</Button>
</DialogActions> </DialogActions>

View file

@ -1,37 +1,13 @@
import { import { Paper, Card, CardContent, IconButton } from "@mui/material";
Box, import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
Card,
CardContent,
IconButton,
makeStyles,
} from "@material-ui/core";
import ArrowForwardIosIcon from "@material-ui/icons/ArrowForwardIos";
import { useState } from "react"; import { useState } from "react";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";
import MakerInfo from "./MakerInfo"; import MakerInfo from "./MakerInfo";
import MakerListDialog from "./MakerListDialog"; import MakerListDialog from "./MakerListDialog";
const useStyles = makeStyles({
inner: {
textAlign: "left",
width: "100%",
height: "100%",
},
makerCard: {
width: "100%",
},
makerCardContent: {
display: "flex",
alignItems: "center",
},
});
export default function MakerSelect() { export default function MakerSelect() {
const classes = useStyles();
const [selectDialogOpen, setSelectDialogOpen] = useState(false); const [selectDialogOpen, setSelectDialogOpen] = useState(false);
const selectedMaker = useAppSelector( const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
(state) => state.makers.selectedMaker,
);
if (!selectedMaker) return <>No maker selected</>; if (!selectedMaker) return <>No maker selected</>;
@ -44,19 +20,19 @@ export default function MakerSelect() {
} }
return ( return (
<Box> <Paper variant="outlined" elevation={4}>
<MakerListDialog <MakerListDialog
open={selectDialogOpen} open={selectDialogOpen}
onClose={handleSelectDialogClose} onClose={handleSelectDialogClose}
/> />
<Card variant="outlined" className={classes.makerCard}> <Card sx={{ width: "100%" }}>
<CardContent className={classes.makerCardContent}> <CardContent sx={{ display: "flex", alignItems: "center" }}>
<MakerInfo maker={selectedMaker} /> <MakerInfo maker={selectedMaker} />
<IconButton onClick={handleSelectDialogOpen} size="small"> <IconButton onClick={handleSelectDialogOpen} size="small">
<ArrowForwardIosIcon /> <ArrowForwardIosIcon />
</IconButton> </IconButton>
</CardContent> </CardContent>
</Card> </Card>
</Box> </Paper>
); );
} }

View file

@ -6,7 +6,7 @@ import {
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
TextField, TextField,
} from "@material-ui/core"; } from "@mui/material";
import { Multiaddr } from "multiaddr"; import { Multiaddr } from "multiaddr";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
@ -68,8 +68,8 @@ export default function MakerSubmitDialog({
<DialogTitle>Submit a maker to the public registry</DialogTitle> <DialogTitle>Submit a maker to the public registry</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<DialogContentText> <DialogContentText>
If the maker is valid and reachable, it will be displayed to all If the maker is valid and reachable, it will be displayed to all other
other users to trade with. users to trade with.
</DialogContentText> </DialogContentText>
<TextField <TextField
autoFocus autoFocus

View file

@ -1,4 +1,4 @@
import { Box } from "@material-ui/core"; import { Box } from "@mui/material";
import QRCode from "react-qr-code"; import QRCode from "react-qr-code";
export default function BitcoinQrCode({ address }: { address: string }) { export default function BitcoinQrCode({ address }: { address: string }) {

View file

@ -2,33 +2,26 @@ import {
Box, Box,
CircularProgress, CircularProgress,
LinearProgress, LinearProgress,
makeStyles,
Typography, Typography,
} from "@material-ui/core"; } from "@mui/material";
import { ReactNode } from "react"; import { ReactNode } from "react";
const useStyles = makeStyles((theme) => ({
subtitle: {
paddingTop: theme.spacing(1),
},
}));
export default function CircularProgressWithSubtitle({ export default function CircularProgressWithSubtitle({
description, description,
}: { }: {
description: string | ReactNode; description: string | ReactNode;
}) { }) {
const classes = useStyles();
return ( return (
<Box <Box
display="flex" sx={{
justifyContent="center" display: "flex",
alignItems="center" justifyContent: "center",
flexDirection="column" alignItems: "center",
flexDirection: "column",
}}
> >
<CircularProgress size={50} /> <CircularProgress size={50} />
<Typography variant="subtitle2" className={classes.subtitle}> <Typography variant="subtitle2" sx={{ paddingTop: 1 }}>
{description} {description}
</Typography> </Typography>
</Box> </Box>
@ -42,16 +35,26 @@ export function LinearProgressWithSubtitle({
description: string | ReactNode; description: string | ReactNode;
value: number; value: number;
}) { }) {
const classes = useStyles();
return ( return (
<Box display="flex" flexDirection="column" alignItems="center" justifyContent="center" style={{ gap: "0.5rem" }}> <Box
<Typography variant="subtitle2" className={classes.subtitle}> style={{ gap: "0.5rem" }}
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
<Typography variant="subtitle2" sx={{ paddingTop: 1 }}>
{description} {description}
</Typography> </Typography>
<Box width="10rem"> <Box
sx={{
width: "10rem",
}}
>
<LinearProgress variant="determinate" value={value} /> <LinearProgress variant="determinate" value={value} />
</Box> </Box>
</Box> </Box>
); );
} }

View file

@ -1,5 +1,4 @@
import { Button } from "@material-ui/core"; import Button, { ButtonProps } from "@mui/material/Button";
import { ButtonProps } from "@material-ui/core/Button/Button";
export default function ClipboardIconButton({ export default function ClipboardIconButton({
text, text,

View file

@ -1,8 +1,8 @@
import { Box } from "@material-ui/core"; import { Box } from "@mui/material";
import { ReactNode } from "react"; import { ReactNode } from "react";
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox"; import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
import InfoBox from "./InfoBox"; import InfoBox from "./InfoBox";
import { Alert } from "@material-ui/lab"; import { Alert } from "@mui/material";
type Props = { type Props = {
title: string; title: string;
@ -20,7 +20,13 @@ export default function DepositAddressInfoBox({
return ( return (
<InfoBox <InfoBox
title={title} title={title}
mainContent={<ActionableMonospaceTextBox content={address} displayCopyIcon={true} enableQrCode={true} />} mainContent={
<ActionableMonospaceTextBox
content={address}
displayCopyIcon={true}
enableQrCode={true}
/>
}
additionalContent={ additionalContent={
<Box <Box
style={{ style={{

View file

@ -1,10 +1,4 @@
import { import { Box, LinearProgress, Paper, Typography } from "@mui/material";
Box,
LinearProgress,
makeStyles,
Paper,
Typography,
} from "@material-ui/core";
import { ReactNode } from "react"; import { ReactNode } from "react";
type Props = { type Props = {
@ -16,21 +10,6 @@ type Props = {
icon: ReactNode; icon: ReactNode;
}; };
const useStyles = makeStyles((theme) => ({
outer: {
padding: theme.spacing(1.5),
overflow: "hidden",
display: "flex",
flexDirection: "column",
gap: theme.spacing(1),
},
upperContent: {
display: "flex",
alignItems: "center",
gap: theme.spacing(1),
},
}));
export default function InfoBox({ export default function InfoBox({
id = null, id = null,
title, title,
@ -39,12 +18,20 @@ export default function InfoBox({
icon, icon,
loading, loading,
}: Props) { }: Props) {
const classes = useStyles();
return ( return (
<Paper variant="outlined" className={classes.outer} id={id}> <Paper
variant="outlined"
id={id}
sx={{
padding: 1.5,
overflow: "hidden",
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<Typography variant="subtitle1">{title}</Typography> <Typography variant="subtitle1">{title}</Typography>
<Box className={classes.upperContent}> <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{icon} {icon}
{mainContent} {mainContent}
</Box> </Box>

View file

@ -1,10 +1,4 @@
import { import { Button, Dialog, DialogActions, DialogContent } from "@mui/material";
Button,
Dialog,
DialogActions,
DialogContent,
makeStyles,
} from "@material-ui/core";
import { useState } from "react"; import { useState } from "react";
import { swapReset } from "store/features/swapSlice"; import { swapReset } from "store/features/swapSlice";
import { useAppDispatch, useAppSelector, useIsSwapRunning } from "store/hooks"; import { useAppDispatch, useAppSelector, useIsSwapRunning } from "store/hooks";
@ -14,15 +8,6 @@ import SwapStatePage from "./pages/SwapStatePage";
import SwapDialogTitle from "./SwapDialogTitle"; import SwapDialogTitle from "./SwapDialogTitle";
import SwapStateStepper from "./SwapStateStepper"; import SwapStateStepper from "./SwapStateStepper";
const useStyles = makeStyles({
content: {
minHeight: "25rem",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
},
});
export default function SwapDialog({ export default function SwapDialog({
open, open,
onClose, onClose,
@ -30,8 +15,6 @@ export default function SwapDialog({
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
}) { }) {
const classes = useStyles();
const swap = useAppSelector((state) => state.swap); const swap = useAppSelector((state) => state.swap);
const isSwapRunning = useIsSwapRunning(); const isSwapRunning = useIsSwapRunning();
const [debug, setDebug] = useState(false); const [debug, setDebug] = useState(false);
@ -59,7 +42,15 @@ export default function SwapDialog({
title="Swap Bitcoin for Monero" title="Swap Bitcoin for Monero"
/> />
<DialogContent dividers className={classes.content}> <DialogContent
dividers
sx={{
minHeight: "25rem",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
{debug ? ( {debug ? (
<DebugPage /> <DebugPage />
) : ( ) : (

View file

@ -1,21 +1,8 @@
import { Box, DialogTitle, makeStyles, Typography } from "@material-ui/core"; import { Box, DialogTitle, Typography } from "@mui/material";
import DebugPageSwitchBadge from "./pages/DebugPageSwitchBadge"; import DebugPageSwitchBadge from "./pages/DebugPageSwitchBadge";
import FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge"; import FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge";
import TorStatusBadge from "./pages/TorStatusBadge"; import TorStatusBadge from "./pages/TorStatusBadge";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
rightSide: {
display: "flex",
alignItems: "center",
gridGap: theme.spacing(1),
},
}));
export default function SwapDialogTitle({ export default function SwapDialogTitle({
title, title,
debug, debug,
@ -25,12 +12,16 @@ export default function SwapDialogTitle({
debug: boolean; debug: boolean;
setDebug: (d: boolean) => void; setDebug: (d: boolean) => void;
}) { }) {
const classes = useStyles();
return ( return (
<DialogTitle disableTypography className={classes.root}> <DialogTitle
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography variant="h6">{title}</Typography> <Typography variant="h6">{title}</Typography>
<Box className={classes.rightSide}> <Box sx={{ display: "flex", alignItems: "center", gridGap: 1 }}>
<FeedbackSubmitBadge /> <FeedbackSubmitBadge />
<DebugPageSwitchBadge enabled={debug} setEnabled={setDebug} /> <DebugPageSwitchBadge enabled={debug} setEnabled={setDebug} />
<TorStatusBadge /> <TorStatusBadge />

View file

@ -1,4 +1,4 @@
import { Step, StepLabel, Stepper, Typography } from "@material-ui/core"; import { Step, StepLabel, Stepper, Typography } from "@mui/material";
import { SwapState } from "models/storeModel"; import { SwapState } from "models/storeModel";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";
import logger from "utils/logger"; import logger from "utils/logger";
@ -119,7 +119,7 @@ function getActiveStep(state: SwapState | null): PathStep | null {
default: default:
return fallbackStep("No step is assigned to the current state"); return fallbackStep("No step is assigned to the current state");
// TODO: Make this guard work. It should force the compiler to check if we have covered all possible cases. // TODO: Make this guard work. It should force the compiler to check if we have covered all possible cases.
// return exhaustiveGuard(latestState.type); // return exhaustiveGuard(latestState.type);
} }
} }

View file

@ -1,4 +1,4 @@
import { Link, Typography } from "@material-ui/core"; import { Link, Typography } from "@mui/material";
import { ReactNode } from "react"; import { ReactNode } from "react";
import InfoBox from "./InfoBox"; import InfoBox from "./InfoBox";

View file

@ -1,4 +1,4 @@
import { Box, DialogContentText } from "@material-ui/core"; import { Box, DialogContentText } from "@mui/material";
import { import {
useActiveSwapInfo, useActiveSwapInfo,
useActiveSwapLogs, useActiveSwapLogs,
@ -35,7 +35,10 @@ export default function DebugPage() {
data={cliState} data={cliState}
label="Swap Daemon State (exposed via API)" label="Swap Daemon State (exposed via API)"
/> />
<CliLogsBox label="Tor Daemon Logs" logs={(torStdOut || "").split("\n")} /> <CliLogsBox
label="Tor Daemon Logs"
logs={(torStdOut || "").split("\n")}
/>
</Box> </Box>
</DialogContentText> </DialogContentText>
</Box> </Box>

View file

@ -1,6 +1,6 @@
import { Tooltip } from "@material-ui/core"; import { Tooltip } from "@mui/material";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@mui/material/IconButton";
import DeveloperBoardIcon from "@material-ui/icons/DeveloperBoard"; import DeveloperBoardIcon from "@mui/icons-material/DeveloperBoard";
export default function DebugPageSwitchBadge({ export default function DebugPageSwitchBadge({
enabled, enabled,
@ -18,6 +18,7 @@ export default function DebugPageSwitchBadge({
<IconButton <IconButton
onClick={handleToggle} onClick={handleToggle}
color={enabled ? "primary" : "default"} color={enabled ? "primary" : "default"}
size="large"
> >
<DeveloperBoardIcon /> <DeveloperBoardIcon />
</IconButton> </IconButton>

View file

@ -1,5 +1,5 @@
import { IconButton } from "@material-ui/core"; import { IconButton } from "@mui/material";
import FeedbackIcon from "@material-ui/icons/Feedback"; import FeedbackIcon from "@mui/icons-material/Feedback";
import { useState } from "react"; import { useState } from "react";
import FeedbackDialog from "../../feedback/FeedbackDialog"; import FeedbackDialog from "../../feedback/FeedbackDialog";
@ -14,7 +14,7 @@ export default function FeedbackSubmitBadge() {
onClose={() => setShowFeedbackDialog(false)} onClose={() => setShowFeedbackDialog(false)}
/> />
)} )}
<IconButton onClick={() => setShowFeedbackDialog(true)}> <IconButton onClick={() => setShowFeedbackDialog(true)} size="large">
<FeedbackIcon /> <FeedbackIcon />
</IconButton> </IconButton>
</> </>

View file

@ -1,4 +1,4 @@
import { Box } from "@material-ui/core"; import { Box } from "@mui/material";
import { SwapState } from "models/storeModel"; import { SwapState } from "models/storeModel";
import CircularProgressWithSubtitle from "../CircularProgressWithSubtitle"; import CircularProgressWithSubtitle from "../CircularProgressWithSubtitle";
import BitcoinPunishedPage from "./done/BitcoinPunishedPage"; import BitcoinPunishedPage from "./done/BitcoinPunishedPage";

View file

@ -1,4 +1,4 @@
import { IconButton, Tooltip } from "@material-ui/core"; import { IconButton, Tooltip } from "@mui/material";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";
import TorIcon from "../../../icons/TorIcon"; import TorIcon from "../../../icons/TorIcon";
@ -8,7 +8,7 @@ export default function TorStatusBadge() {
if (tor.processRunning) { if (tor.processRunning) {
return ( return (
<Tooltip title="Tor is running in the background"> <Tooltip title="Tor is running in the background">
<IconButton> <IconButton size="large">
<TorIcon htmlColor="#7D4698" /> <TorIcon htmlColor="#7D4698" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>

View file

@ -1,11 +1,13 @@
import { Box, DialogContentText } from '@material-ui/core'; import { Box, DialogContentText } from "@mui/material";
import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox'; import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
import { TauriSwapProgressEventExt } from 'models/tauriModelExt'; import { TauriSwapProgressEventExt } from "models/tauriModelExt";
export default function BitcoinPunishedPage({ export default function BitcoinPunishedPage({
state, state,
}: { }: {
state: TauriSwapProgressEventExt<"BtcPunished"> | TauriSwapProgressEventExt<"CooperativeRedeemRejected"> state:
| TauriSwapProgressEventExt<"BtcPunished">
| TauriSwapProgressEventExt<"CooperativeRedeemRejected">;
}) { }) {
return ( return (
<Box> <Box>
@ -13,13 +15,13 @@ export default function BitcoinPunishedPage({
Unfortunately, the swap was unsuccessful. Since you did not refund in Unfortunately, the swap was unsuccessful. Since you did not refund in
time, the Bitcoin has been lost. However, with the cooperation of the time, the Bitcoin has been lost. However, with the cooperation of the
other party, you might still be able to redeem the Monero, although this other party, you might still be able to redeem the Monero, although this
is not guaranteed.{' '} is not guaranteed.{" "}
{state.type === "CooperativeRedeemRejected" && ( {state.type === "CooperativeRedeemRejected" && (
<> <>
<br /> <br />
We tried to redeem the Monero with the other party's help, but it We tried to redeem the Monero with the other party's help, but it
was unsuccessful (reason: {state.content.reason}). Attempting again at a was unsuccessful (reason: {state.content.reason}). Attempting again
later time might yield success. <br /> at a later time might yield success. <br />
</> </>
)} )}
</DialogContentText> </DialogContentText>

View file

@ -1,4 +1,4 @@
import { Box, DialogContentText } from "@material-ui/core"; import { Box, DialogContentText } from "@mui/material";
import { TauriSwapProgressEventContent } from "models/tauriModelExt"; import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import { useActiveSwapInfo } from "store/hooks"; import { useActiveSwapInfo } from "store/hooks";
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox"; import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";

View file

@ -1,4 +1,4 @@
import { Box, DialogContentText } from "@material-ui/core"; import { Box, DialogContentText } from "@mui/material";
import { TauriSwapProgressEventContent } from "models/tauriModelExt"; import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox"; import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox"; import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";

View file

@ -1,4 +1,4 @@
import { Box, DialogContentText } from "@material-ui/core"; import { Box, DialogContentText } from "@mui/material";
import { TauriSwapProgressEvent } from "models/tauriModel"; import { TauriSwapProgressEvent } from "models/tauriModel";
import CliLogsBox from "renderer/components/other/RenderedCliLog"; import CliLogsBox from "renderer/components/other/RenderedCliLog";
import { useActiveSwapInfo, useActiveSwapLogs } from "store/hooks"; import { useActiveSwapInfo, useActiveSwapLogs } from "store/hooks";

View file

@ -2,7 +2,7 @@ import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox"; import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
import SwapStatusAlert from "renderer/components/alert/SwapStatusAlert/SwapStatusAlert"; import SwapStatusAlert from "renderer/components/alert/SwapStatusAlert/SwapStatusAlert";
import { useActiveSwapInfo } from "store/hooks"; import { useActiveSwapInfo } from "store/hooks";
import { Box, DialogContentText } from "@material-ui/core"; import { Box, DialogContentText } from "@mui/material";
// This is the number of blocks after which we consider the swap to be at risk of being unsuccessful // This is the number of blocks after which we consider the swap to be at risk of being unsuccessful
const BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD = 2; const BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD = 2;
@ -17,17 +17,19 @@ export default function BitcoinLockTxInMempoolPage({
<Box> <Box>
{btc_lock_confirmations < BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && ( {btc_lock_confirmations < BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
<DialogContentText> <DialogContentText>
Your Bitcoin has been locked. {btc_lock_confirmations > 0 ? Your Bitcoin has been locked.{" "}
"We are waiting for the other party to lock their Monero." : {btc_lock_confirmations > 0
"We are waiting for the blockchain to confirm the transaction. Once confirmed, the other party will lock their Monero." ? "We are waiting for the other party to lock their Monero."
} : "We are waiting for the blockchain to confirm the transaction. Once confirmed, the other party will lock their Monero."}
</DialogContentText> </DialogContentText>
)} )}
<Box style={{ <Box
display: "flex", style={{
flexDirection: "column", display: "flex",
gap: "1rem", flexDirection: "column",
}}> gap: "1rem",
}}
>
{btc_lock_confirmations >= BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && ( {btc_lock_confirmations >= BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
<SwapStatusAlert swap={swapInfo} isRunning={true} /> <SwapStatusAlert swap={swapInfo} isRunning={true} />
)} )}
@ -37,9 +39,9 @@ export default function BitcoinLockTxInMempoolPage({
loading loading
additionalContent={ additionalContent={
<> <>
Most makers require one confirmation before locking their Most makers require one confirmation before locking their Monero.
Monero. After they lock their funds and the Monero transaction After they lock their funds and the Monero transaction receives
receives one confirmation, the swap will proceed to the next step. one confirmation, the swap will proceed to the next step.
<br /> <br />
Confirmations: {btc_lock_confirmations} Confirmations: {btc_lock_confirmations}
</> </>

View file

@ -1,14 +1,24 @@
import { useConservativeBitcoinSyncProgress, usePendingBackgroundProcesses } from "store/hooks"; import {
import CircularProgressWithSubtitle, { LinearProgressWithSubtitle } from "../../CircularProgressWithSubtitle"; useConservativeBitcoinSyncProgress,
usePendingBackgroundProcesses,
} from "store/hooks";
import CircularProgressWithSubtitle, {
LinearProgressWithSubtitle,
} from "../../CircularProgressWithSubtitle";
export default function ReceivedQuotePage() { export default function ReceivedQuotePage() {
const syncProgress = useConservativeBitcoinSyncProgress(); const syncProgress = useConservativeBitcoinSyncProgress();
if (syncProgress?.type === "Known") { if (syncProgress?.type === "Known") {
const percentage = Math.round((syncProgress.content.consumed / syncProgress.content.total) * 100); const percentage = Math.round(
(syncProgress.content.consumed / syncProgress.content.total) * 100,
);
return ( return (
<LinearProgressWithSubtitle description={`Syncing Bitcoin wallet (${percentage}%)`} value={percentage} /> <LinearProgressWithSubtitle
description={`Syncing Bitcoin wallet (${percentage}%)`}
value={percentage}
/>
); );
} }

View file

@ -1,51 +1,20 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { resolveApproval } from 'renderer/rpc'; import { resolveApproval } from "renderer/rpc";
import { PendingLockBitcoinApprovalRequest, TauriSwapProgressEventContent } from 'models/tauriModelExt'; import {
PendingLockBitcoinApprovalRequest,
TauriSwapProgressEventContent,
} from "models/tauriModelExt";
import { import {
SatsAmount, SatsAmount,
PiconeroAmount, PiconeroAmount,
MoneroBitcoinExchangeRateFromAmounts MoneroBitcoinExchangeRateFromAmounts,
} from 'renderer/components/other/Units'; } from "renderer/components/other/Units";
import { import { Box, Typography, Divider } from "@mui/material";
Box, import { useActiveSwapId, usePendingLockBitcoinApproval } from "store/hooks";
Typography, import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
Divider, import InfoBox from "renderer/components/modal/swap/InfoBox";
} from '@material-ui/core'; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; import CheckIcon from "@mui/icons-material/Check";
import { useActiveSwapId, usePendingLockBitcoinApproval } from 'store/hooks';
import PromiseInvokeButton from 'renderer/components/PromiseInvokeButton';
import InfoBox from 'renderer/components/modal/swap/InfoBox';
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
import CheckIcon from '@material-ui/icons/Check';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
detailGrid: {
display: 'grid',
gridTemplateColumns: 'auto 1fr',
rowGap: theme.spacing(1),
columnGap: theme.spacing(2),
alignItems: 'center',
marginBlock: theme.spacing(2),
},
label: {
color: theme.palette.text.secondary,
},
receiveValue: {
fontWeight: 'bold',
color: theme.palette.success.main,
},
actions: {
marginTop: theme.spacing(2),
display: 'flex',
justifyContent: 'flex-end',
gap: theme.spacing(2),
},
cancelButton: {
color: theme.palette.text.secondary,
},
})
);
/// A hook that returns the LockBitcoin confirmation request for the active swap /// A hook that returns the LockBitcoin confirmation request for the active swap
/// Returns null if no confirmation request is found /// Returns null if no confirmation request is found
@ -53,14 +22,16 @@ function useActiveLockBitcoinApprovalRequest(): PendingLockBitcoinApprovalReques
const approvals = usePendingLockBitcoinApproval(); const approvals = usePendingLockBitcoinApproval();
const activeSwapId = useActiveSwapId(); const activeSwapId = useActiveSwapId();
return approvals return (
?.find(r => r.content.details.content.swap_id === activeSwapId) || null; approvals?.find(
(r) => r.content.details.content.swap_id === activeSwapId,
) || null
);
} }
export default function SwapSetupInflightPage({ export default function SwapSetupInflightPage({
btc_lock_amount, btc_lock_amount,
}: TauriSwapProgressEventContent<'SwapSetupInflight'>) { }: TauriSwapProgressEventContent<"SwapSetupInflight">) {
const classes = useStyles();
const request = useActiveLockBitcoinApprovalRequest(); const request = useActiveLockBitcoinApprovalRequest();
const [timeLeft, setTimeLeft] = useState<number>(0); const [timeLeft, setTimeLeft] = useState<number>(0);
@ -81,10 +52,19 @@ export default function SwapSetupInflightPage({
// If we do not have an approval request yet for the Bitcoin lock transaction, we haven't received the offer from Alice yet // If we do not have an approval request yet for the Bitcoin lock transaction, we haven't received the offer from Alice yet
// Display a loading spinner to the user for as long as the swap_setup request is in flight // Display a loading spinner to the user for as long as the swap_setup request is in flight
if (!request) { if (!request) {
return <CircularProgressWithSubtitle description={<>Negotiating offer for <SatsAmount amount={btc_lock_amount} /></>} />; return (
<CircularProgressWithSubtitle
description={
<>
Negotiating offer for <SatsAmount amount={btc_lock_amount} />
</>
}
/>
);
} }
const { btc_network_fee, xmr_receive_amount } = request.content.details.content; const { btc_network_fee, xmr_receive_amount } =
request.content.details.content;
return ( return (
<InfoBox <InfoBox
@ -94,23 +74,53 @@ export default function SwapSetupInflightPage({
mainContent={ mainContent={
<> <>
<Divider /> <Divider />
<Box className={classes.detailGrid}> <Box
<Typography className={classes.label}>You send</Typography> sx={{
display: "grid",
gridTemplateColumns: "auto 1fr",
rowGap: 1,
columnGap: 2,
alignItems: "center",
marginBlock: 2,
}}
>
<Typography
sx={(theme) => ({ color: theme.palette.text.secondary })}
>
You send
</Typography>
<Typography> <Typography>
<SatsAmount amount={btc_lock_amount} /> <SatsAmount amount={btc_lock_amount} />
</Typography> </Typography>
<Typography className={classes.label}>Bitcoin network fees</Typography> <Typography
sx={(theme) => ({ color: theme.palette.text.secondary })}
>
Bitcoin network fees
</Typography>
<Typography> <Typography>
<SatsAmount amount={btc_network_fee} /> <SatsAmount amount={btc_network_fee} />
</Typography> </Typography>
<Typography className={classes.label}>You receive</Typography> <Typography
<Typography className={classes.receiveValue}> sx={(theme) => ({ color: theme.palette.text.secondary })}
>
You receive
</Typography>
<Typography
sx={(theme) => ({
fontWeight: "bold",
color: theme.palette.success.main,
})}
>
<PiconeroAmount amount={xmr_receive_amount} /> <PiconeroAmount amount={xmr_receive_amount} />
</Typography> </Typography>
<Typography className={classes.label}>Exchange rate</Typography> <Typography
sx={(theme) => ({ color: theme.palette.text.secondary })}
>
Exchange rate
</Typography>
<Typography> <Typography>
<MoneroBitcoinExchangeRateFromAmounts <MoneroBitcoinExchangeRateFromAmounts
satsAmount={btc_lock_amount} satsAmount={btc_lock_amount}
@ -122,11 +132,18 @@ export default function SwapSetupInflightPage({
</> </>
} }
additionalContent={ additionalContent={
<Box className={classes.actions}> <Box
sx={{
marginTop: 2,
display: "flex",
justifyContent: "flex-end",
gap: 2,
}}
>
<PromiseInvokeButton <PromiseInvokeButton
variant="text" variant="text"
size="large" size="large"
className={classes.cancelButton} sx={(theme) => ({ color: theme.palette.text.secondary })}
onInvoke={() => resolveApproval(request.content.request_id, false)} onInvoke={() => resolveApproval(request.content.request_id, false)}
displayErrorSnackbar displayErrorSnackbar
requiresContext requiresContext
@ -149,4 +166,4 @@ export default function SwapSetupInflightPage({
} }
/> />
); );
} }

View file

@ -1,4 +1,4 @@
import { Box, DialogContentText } from "@material-ui/core"; import { Box, DialogContentText } from "@mui/material";
import { TauriSwapProgressEventContent } from "models/tauriModelExt"; import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox"; import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";

View file

@ -1,4 +1,4 @@
import { Box, makeStyles, TextField, Typography } from "@material-ui/core"; import { Box, TextField, Typography } from "@mui/material";
import { BidQuote } from "models/tauriModel"; import { BidQuote } from "models/tauriModel";
import { useState } from "react"; import { useState } from "react";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";
@ -7,23 +7,6 @@ import { MoneroAmount } from "../../../../other/Units";
const MONERO_FEE = 0.000016; const MONERO_FEE = 0.000016;
const useStyles = makeStyles((theme) => ({
outer: {
display: "flex",
alignItems: "center",
gap: theme.spacing(1),
},
textField: {
"& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button": {
display: "none",
},
"& input[type=number]": {
MozAppearance: "textfield",
},
maxWidth: theme.spacing(16),
},
}));
function calcBtcAmountWithoutFees(amount: number, fees: number) { function calcBtcAmountWithoutFees(amount: number, fees: number) {
return amount - fees; return amount - fees;
} }
@ -39,7 +22,6 @@ export default function DepositAmountHelper({
min_bitcoin_lock_tx_fee: number; min_bitcoin_lock_tx_fee: number;
quote: BidQuote; quote: BidQuote;
}) { }) {
const classes = useStyles();
const [amount, setAmount] = useState(min_deposit_until_swap_will_start); const [amount, setAmount] = useState(min_deposit_until_swap_will_start);
const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0; const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0;
@ -70,7 +52,13 @@ export default function DepositAmountHelper({
} }
return ( return (
<Box className={classes.outer}> <Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
}}
>
<Typography variant="subtitle2"> <Typography variant="subtitle2">
Depositing {bitcoinBalance > 0 && <>another</>} Depositing {bitcoinBalance > 0 && <>another</>}
</Typography> </Typography>
@ -80,7 +68,16 @@ export default function DepositAmountHelper({
onChange={(e) => setAmount(btcToSats(parseFloat(e.target.value)))} onChange={(e) => setAmount(btcToSats(parseFloat(e.target.value)))}
size="small" size="small"
type="number" type="number"
className={classes.textField} sx={{
"& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button":
{
display: "none",
},
"& input[type=number]": {
MozAppearance: "textfield",
},
maxWidth: 16,
}}
/> />
<Typography variant="subtitle2"> <Typography variant="subtitle2">
BTC will give you approximately{" "} BTC will give you approximately{" "}

View file

@ -1,14 +0,0 @@
import { MoneroWalletRpcUpdateState } from "../../../../../../models/storeModel";
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function DownloadingMoneroWalletRpcPage({
updateState,
}: {
updateState: MoneroWalletRpcUpdateState;
}) {
return (
<CircularProgressWithSubtitle
description={`Updating monero-wallet-rpc (${updateState.progress}) `}
/>
);
}

View file

@ -1,12 +1,5 @@
import { import { Box, Paper, Tab, Tabs, Typography } from "@mui/material";
Box, import PlayArrowIcon from "@mui/icons-material/PlayArrow";
makeStyles,
Paper,
Tab,
Tabs,
Typography,
} from "@material-ui/core";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import { useState } from "react"; import { useState } from "react";
import RemainingFundsWillBeUsedAlert from "renderer/components/alert/RemainingFundsWillBeUsedAlert"; import RemainingFundsWillBeUsedAlert from "renderer/components/alert/RemainingFundsWillBeUsedAlert";
import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField"; import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField";
@ -15,20 +8,7 @@ import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { buyXmr } from "renderer/rpc"; import { buyXmr } from "renderer/rpc";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";
const useStyles = makeStyles((theme) => ({
initButton: {
marginTop: theme.spacing(1),
},
fieldsOuter: {
display: "flex",
flexDirection: "column",
gap: theme.spacing(1.5),
},
}));
export default function InitPage() { export default function InitPage() {
const classes = useStyles();
const [redeemAddress, setRedeemAddress] = useState(""); const [redeemAddress, setRedeemAddress] = useState("");
const [refundAddress, setRefundAddress] = useState(""); const [refundAddress, setRefundAddress] = useState("");
const [useExternalRefundAddress, setUseExternalRefundAddress] = const [useExternalRefundAddress, setUseExternalRefundAddress] =
@ -37,9 +17,7 @@ export default function InitPage() {
const [redeemAddressValid, setRedeemAddressValid] = useState(false); const [redeemAddressValid, setRedeemAddressValid] = useState(false);
const [refundAddressValid, setRefundAddressValid] = useState(false); const [refundAddressValid, setRefundAddressValid] = useState(false);
const selectedMaker = useAppSelector( const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
(state) => state.makers.selectedMaker,
);
async function init() { async function init() {
await buyXmr( await buyXmr(
@ -51,7 +29,13 @@ export default function InitPage() {
return ( return (
<Box> <Box>
<Box className={classes.fieldsOuter}> <Box
sx={{
display: "flex",
flexDirection: "column",
gap: 1.5,
}}
>
<RemainingFundsWillBeUsedAlert /> <RemainingFundsWillBeUsedAlert />
<MoneroAddressTextField <MoneroAddressTextField
label="Monero redeem address" label="Monero redeem address"
@ -104,7 +88,7 @@ export default function InitPage() {
variant="contained" variant="contained"
color="primary" color="primary"
size="large" size="large"
className={classes.initButton} sx={{ marginTop: 1 }}
endIcon={<PlayArrowIcon />} endIcon={<PlayArrowIcon />}
onInvoke={init} onInvoke={init}
displayErrorSnackbar displayErrorSnackbar

Some files were not shown because too many files have changed in this diff Show more