mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-08-24 14:15:55 -04:00
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:
parent
2ba69ba340
commit
430a22fbf6
169 changed files with 12883 additions and 3950 deletions
|
@ -101,7 +101,7 @@ jobs:
|
|||
- name: install dprint globally
|
||||
uses: taiki-e/cache-cargo-install-action@v2
|
||||
with:
|
||||
tool: dprint@0.39.1
|
||||
tool: dprint@0.50.0
|
||||
|
||||
- name: Build Tauri App
|
||||
env:
|
||||
|
|
|
@ -87,7 +87,7 @@ jobs:
|
|||
- name: install dprint globally
|
||||
uses: taiki-e/cache-cargo-install-action@v2
|
||||
with:
|
||||
tool: dprint@0.39.1
|
||||
tool: dprint@0.50.0
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
|
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -40,7 +40,7 @@ jobs:
|
|||
- name: Check formatting
|
||||
uses: dprint/check@v2.2
|
||||
with:
|
||||
dprint-version: 0.39.1
|
||||
dprint-version: 0.50.0
|
||||
|
||||
- name: Run clippy with default features
|
||||
run: cargo clippy --workspace --all-targets -- -D warnings
|
||||
|
|
2
.github/workflows/draft-new-release.yml
vendored
2
.github/workflows/draft-new-release.yml
vendored
|
@ -52,7 +52,7 @@ jobs:
|
|||
- name: Commit changelog and manifest files
|
||||
id: make-commit
|
||||
env:
|
||||
DPRINT_VERSION: "0.39.1"
|
||||
DPRINT_VERSION: "0.50.0"
|
||||
RUST_TOOLCHAIN: "1.82"
|
||||
run: |
|
||||
rustup component add rustfmt --toolchain "$RUST_TOOLCHAIN-x86_64-unknown-linux-gnu"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [ "monero-rpc", "swap", "monero-wallet", "src-tauri" ]
|
||||
members = ["monero-rpc", "monero-wallet", "src-tauri", "swap"]
|
||||
|
||||
[patch.crates-io]
|
||||
# patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51
|
||||
|
|
|
@ -59,9 +59,9 @@ For example:
|
|||
```toml
|
||||
[network]
|
||||
rendezvous_point = [
|
||||
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
|
||||
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw",
|
||||
"/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU"
|
||||
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
|
||||
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw",
|
||||
"/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU",
|
||||
]
|
||||
external_addresses = ["/dns4/example.com/tcp/9939"]
|
||||
```
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
import Image from 'next/image';
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Logo() {
|
||||
return <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Image src="/favicon.svg" alt="UnstoppableSwap" width={32} height={32} style={{ borderRadius: '20%' }}/>
|
||||
<span>UnstoppableSwap</span>
|
||||
</div>;
|
||||
}
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<Image
|
||||
src="/favicon.svg"
|
||||
alt="UnstoppableSwap"
|
||||
width={32}
|
||||
height={32}
|
||||
style={{ borderRadius: "20%" }}
|
||||
/>
|
||||
<span>UnstoppableSwap</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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() {
|
||||
function satsToBtc(sats) {
|
||||
|
@ -40,9 +40,7 @@ export default function SwapMakerTable() {
|
|||
<tbody>
|
||||
{makers.map((maker) => (
|
||||
<Tr key={maker.peerId}>
|
||||
<Td>
|
||||
{maker.testnet ? "Testnet" : "Mainnet"}
|
||||
</Td>
|
||||
<Td>{maker.testnet ? "Testnet" : "Mainnet"}</Td>
|
||||
<Td>{maker.multiAddr}</Td>
|
||||
<Td>{maker.peerId}</Td>
|
||||
<Td>{satsToBtc(maker.minSwapAmount)} BTC</Td>
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
"becoming_a_maker": "Becoming a Maker",
|
||||
"send_feedback": "Send Feedback",
|
||||
"donate": "Donate"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"overview": "Overview"
|
||||
}
|
||||
"overview": "Overview"
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"install_instructions": "Installation"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/))
|
||||
- `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`)
|
||||
|
||||
After that you only need to clone the repository and run the following commands:
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
"first_swap": "Complete your first swap",
|
||||
"market_maker_discovery": "Maker discovery",
|
||||
"refund_punish": "Cancel, Refund and Punish explained"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
|
@ -18,12 +14,6 @@
|
|||
"jsx": "preserve",
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
24
dprint.json
24
dprint.json
|
@ -4,25 +4,31 @@
|
|||
"incremental": true,
|
||||
"markdown": {},
|
||||
"exec": {
|
||||
"associations": "**/*.{rs}",
|
||||
"rustfmt": "rustfmt --edition 2021",
|
||||
"rustfmt.associations": "**/*.rs"
|
||||
"commands": [
|
||||
{
|
||||
"command": "rustfmt --edition 2021",
|
||||
"exts": ["rs"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"includes": [
|
||||
"**/*.{md}",
|
||||
"**/*.{toml}",
|
||||
"**/*.{rs}"
|
||||
"**/*.{rs}",
|
||||
"**/*.{js,jsx,ts,tsx,json,css,scss,html}"
|
||||
],
|
||||
"excludes": [
|
||||
"target/",
|
||||
"src-tauri/Cargo.toml",
|
||||
"monero-sys/monero/",
|
||||
".git/**"
|
||||
".git/**",
|
||||
"**/node_modules/**",
|
||||
"**/dist/**"
|
||||
],
|
||||
"plugins": [
|
||||
"https://plugins.dprint.dev/markdown-0.13.1.wasm",
|
||||
"https://github.com/thomaseizinger/dprint-plugin-cargo-toml/releases/download/0.1.0/cargo-toml-0.1.0.wasm",
|
||||
"https://plugins.dprint.dev/exec-0.3.5.json@d687dda57be0fe9a0088ccdaefa5147649ff24127d8b3ea227536c68ee7abeab",
|
||||
"https://plugins.dprint.dev/prettier-0.26.6.json@0118376786f37496e41bb19dbcfd1e7214e2dc859a55035c5e54d1107b4c9c57"
|
||||
"https://plugins.dprint.dev/markdown-0.18.0.wasm",
|
||||
"https://plugins.dprint.dev/toml-0.7.0.wasm",
|
||||
"https://plugins.dprint.dev/exec-0.5.1.json@492414e39dea4dccc07b4af796d2f4efdb89e84bae2bd4e1e924c0cc050855bf",
|
||||
"https://plugins.dprint.dev/prettier-0.57.0.json@1bc6b449e982d5b91a25a7c59894102d40c5748651a08a095fb3926e64d55a31"
|
||||
]
|
||||
}
|
||||
|
|
13
justfile
13
justfile
|
@ -69,6 +69,19 @@ kill_monero_wallet_rpc:
|
|||
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
|
||||
docker-prune-network:
|
||||
docker network prune -f
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "monero-harness"
|
||||
version = "0.1.0"
|
||||
authors = [ "CoBloX Team <team@coblox.tech>" ]
|
||||
authors = ["CoBloX Team <team@coblox.tech>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
|
@ -11,6 +11,6 @@ futures = "0.3"
|
|||
monero-rpc = { path = "../monero-rpc" }
|
||||
rand = "0.7"
|
||||
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-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"] }
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
[package]
|
||||
name = "monero-rpc"
|
||||
version = "0.1.0"
|
||||
authors = [ "CoBloX Team <team@coblox.tech>" ]
|
||||
authors = ["CoBloX Team <team@coblox.tech>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
curve25519-dalek = "3.1"
|
||||
hex = "0.4"
|
||||
jsonrpc_client = { version = "0.7", features = [ "reqwest" ] }
|
||||
jsonrpc_client = { version = "0.7", features = ["reqwest"] }
|
||||
monero = "0.12"
|
||||
monero-epee-bin-serde = "1"
|
||||
rand = "0.7"
|
||||
reqwest = { version = "0.12", default-features = false, features = [ "json" ] }
|
||||
rust_decimal = { version = "1", features = [ "serde-float" ] }
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json"] }
|
||||
rust_decimal = { version = "1", features = ["serde-float"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.4"
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "monero-wallet"
|
||||
version = "0.1.0"
|
||||
authors = [ "CoBloX Team <team@coblox.tech>" ]
|
||||
authors = ["CoBloX Team <team@coblox.tech>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
@ -15,5 +15,5 @@ curve25519-dalek = "3"
|
|||
monero-harness = { path = "../monero-harness" }
|
||||
rand = "0.7"
|
||||
testcontainers = "0.15"
|
||||
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" ] }
|
||||
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"] }
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
- 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 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.
|
||||
|
||||
## Start development servers
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import js from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginReact from "eslint-plugin-react";
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ["node_modules", "dist"],
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
{ ignores: ["node_modules", "dist"] },
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
{
|
||||
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
|
||||
languageOptions: { globals: globals.browser },
|
||||
languageOptions: {
|
||||
globals: globals.browser,
|
||||
},
|
||||
rules: {
|
||||
"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": [
|
||||
"warn",
|
||||
{
|
||||
|
@ -24,7 +26,6 @@ export default [
|
|||
"Use the open(...) function from @tauri-apps/plugin-shell instead",
|
||||
},
|
||||
],
|
||||
// Disallow the use of the `open` on the `window` object
|
||||
"no-restricted-properties": [
|
||||
"warn",
|
||||
{
|
||||
|
|
|
@ -1,35 +1,33 @@
|
|||
<!doctype html>
|
||||
<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>
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/renderer/index.tsx"></script>
|
||||
<style>
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/renderer/index.tsx"></script>
|
||||
<style>
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
|
8284
src-gui/package-lock.json
generated
Normal file
8284
src-gui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -16,10 +16,12 @@
|
|||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@fontsource/roboto": "^5.1.0",
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@mui/lab": "^7.0.0-beta.13",
|
||||
"@mui/material": "^7.1.1",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-cli": "^2.0.0",
|
||||
|
@ -37,11 +39,11 @@
|
|||
"notistack": "^3.0.1",
|
||||
"pino": "^9.2.0",
|
||||
"pino-pretty": "^11.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-qr-code": "^2.0.15",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.6.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"semver": "^7.6.2",
|
||||
"virtua": "^0.33.2"
|
||||
|
@ -54,9 +56,10 @@
|
|||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/humanize-duration": "^3.27.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/react": "^19.1.6",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@types/react-is": "^19.0.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^9.9.0",
|
||||
|
|
|
@ -6,7 +6,7 @@ export interface ExtendedMakerStatus extends MakerStatus {
|
|||
recommended?: boolean;
|
||||
}
|
||||
|
||||
export interface MakerStatus extends MakerQuote, Maker { }
|
||||
export interface MakerStatus extends MakerQuote, Maker {}
|
||||
|
||||
export interface MakerQuote {
|
||||
price: number;
|
||||
|
@ -29,16 +29,16 @@ export interface Alert {
|
|||
|
||||
// Define the correct 9-element tuple type for PrimitiveDateTime
|
||||
export type PrimitiveDateTimeString = [
|
||||
number, // Year
|
||||
number, // Day of Year
|
||||
number, // Hour
|
||||
number, // Minute
|
||||
number, // Second
|
||||
number, // Nanosecond
|
||||
number, // Offset Hour
|
||||
number, // Offset Minute
|
||||
number // Offset Second
|
||||
];
|
||||
number, // Year
|
||||
number, // Day of Year
|
||||
number, // Hour
|
||||
number, // Minute
|
||||
number, // Second
|
||||
number, // Nanosecond
|
||||
number, // Offset Hour
|
||||
number, // Offset Minute
|
||||
number, // Offset Second
|
||||
];
|
||||
|
||||
export interface Feedback {
|
||||
id: string;
|
||||
|
@ -46,7 +46,7 @@ export interface Feedback {
|
|||
}
|
||||
|
||||
export interface Attachment {
|
||||
id: number;
|
||||
id: number;
|
||||
message_id: number;
|
||||
key: string;
|
||||
content: string;
|
||||
|
|
|
@ -61,4 +61,3 @@ export function parseCliLogString(log: string): CliLog | string {
|
|||
return log;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ export type TauriSwapProgressEventContent<
|
|||
T extends TauriSwapProgressEventType,
|
||||
> = 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
|
||||
// TODO: Replace this with a typeshare definition
|
||||
|
@ -36,19 +37,32 @@ export enum BobStateName {
|
|||
|
||||
export function bobStateNameToHumanReadable(stateName: BobStateName): string {
|
||||
switch (stateName) {
|
||||
case BobStateName.Started: return "Started";
|
||||
case BobStateName.SwapSetupCompleted: return "Setup completed";
|
||||
case BobStateName.BtcLocked: return "Bitcoin locked";
|
||||
case BobStateName.XmrLockProofReceived: return "Monero locked";
|
||||
case BobStateName.XmrLocked: return "Monero locked and fully confirmed";
|
||||
case BobStateName.EncSigSent: return "Encrypted signature sent";
|
||||
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";
|
||||
case BobStateName.Started:
|
||||
return "Started";
|
||||
case BobStateName.SwapSetupCompleted:
|
||||
return "Setup completed";
|
||||
case BobStateName.BtcLocked:
|
||||
return "Bitcoin locked";
|
||||
case BobStateName.XmrLockProofReceived:
|
||||
return "Monero locked";
|
||||
case BobStateName.XmrLocked:
|
||||
return "Monero locked and fully confirmed";
|
||||
case BobStateName.EncSigSent:
|
||||
return "Encrypted signature sent";
|
||||
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:
|
||||
return exhaustiveGuard(stateName);
|
||||
}
|
||||
|
@ -64,7 +78,11 @@ export type TimelockCancel = Extract<ExpiredTimelocks, { type: "Cancel" }>;
|
|||
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
|
||||
export function getAbsoluteBlock(timelock: ExpiredTimelocks, cancelTimelock: number, punishTimelock: number): number {
|
||||
export function getAbsoluteBlock(
|
||||
timelock: ExpiredTimelocks,
|
||||
cancelTimelock: number,
|
||||
punishTimelock: number,
|
||||
): number {
|
||||
if (timelock.type === "None") {
|
||||
return cancelTimelock - timelock.content.blocks_left;
|
||||
}
|
||||
|
@ -208,12 +226,15 @@ export function isGetSwapInfoResponseRunningSwap(
|
|||
* @returns True if the timelock exists, false otherwise
|
||||
*/
|
||||
export function isGetSwapInfoResponseWithTimelock(
|
||||
response: GetSwapInfoResponseExt
|
||||
response: GetSwapInfoResponseExt,
|
||||
): response is GetSwapInfoResponseExtWithTimelock {
|
||||
return response.timelock !== null;
|
||||
}
|
||||
|
||||
export type PendingApprovalRequest = Extract<ApprovalRequest, { state: "Pending" }>;
|
||||
export type PendingApprovalRequest = Extract<
|
||||
ApprovalRequest,
|
||||
{ state: "Pending" }
|
||||
>;
|
||||
|
||||
export type PendingLockBitcoinApprovalRequest = PendingApprovalRequest & {
|
||||
content: {
|
||||
|
@ -239,7 +260,10 @@ export function isPendingBackgroundProcess(
|
|||
return process.progress.type === "Pending";
|
||||
}
|
||||
|
||||
export type TauriBitcoinSyncProgress = Extract<TauriBackgroundProgress, { componentName: "SyncingBitcoinWallet" }>;
|
||||
export type TauriBitcoinSyncProgress = Extract<
|
||||
TauriBackgroundProgress,
|
||||
{ componentName: "SyncingBitcoinWallet" }
|
||||
>;
|
||||
|
||||
export function isBitcoinSyncProgress(
|
||||
progress: TauriBackgroundProgress,
|
||||
|
|
|
@ -5,20 +5,34 @@
|
|||
// - and to submit feedback
|
||||
// - 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 { setBtcPrice, setXmrBtcRate, setXmrPrice } from "store/features/ratesSlice";
|
||||
import {
|
||||
setBtcPrice,
|
||||
setXmrBtcRate,
|
||||
setXmrPrice,
|
||||
} from "store/features/ratesSlice";
|
||||
import { FiatCurrency } from "store/features/settingsSlice";
|
||||
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 { setConversation } from "store/features/conversationsSlice";
|
||||
|
||||
const PUBLIC_REGISTRY_API_BASE_URL = "https://api.unstoppableswap.net";
|
||||
|
||||
async function fetchMakersViaHttp(): Promise<
|
||||
ExtendedMakerStatus[]
|
||||
> {
|
||||
async function fetchMakersViaHttp(): Promise<ExtendedMakerStatus[]> {
|
||||
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/list`);
|
||||
return (await response.json()) as ExtendedMakerStatus[];
|
||||
}
|
||||
|
@ -30,7 +44,7 @@ async function fetchAlertsViaHttp(): Promise<Alert[]> {
|
|||
|
||||
export async function submitFeedbackViaHttp(
|
||||
content: string,
|
||||
attachments?: AttachmentInput[]
|
||||
attachments?: AttachmentInput[],
|
||||
): Promise<string> {
|
||||
type Response = string;
|
||||
|
||||
|
@ -39,64 +53,83 @@ export async function submitFeedbackViaHttp(
|
|||
attachments: attachments || [], // Ensure attachments is always an array
|
||||
};
|
||||
|
||||
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/submit-feedback`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const response = await fetch(
|
||||
`${PUBLIC_REGISTRY_API_BASE_URL}/api/submit-feedback`,
|
||||
{
|
||||
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) {
|
||||
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;
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
export async function fetchFeedbackMessagesViaHttp(feedbackId: string): Promise<Message[]> {
|
||||
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/feedback/${feedbackId}/messages`);
|
||||
export async function fetchFeedbackMessagesViaHttp(
|
||||
feedbackId: string,
|
||||
): Promise<Message[]> {
|
||||
const response = await fetch(
|
||||
`${PUBLIC_REGISTRY_API_BASE_URL}/api/feedback/${feedbackId}/messages`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
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
|
||||
return (await response.json()) as Message[];
|
||||
return (await response.json()) as Message[];
|
||||
}
|
||||
|
||||
export async function appendFeedbackMessageViaHttp(
|
||||
feedbackId: string,
|
||||
feedbackId: string,
|
||||
content: string,
|
||||
attachments?: AttachmentInput[]
|
||||
attachments?: AttachmentInput[],
|
||||
): Promise<number> {
|
||||
type Response = number;
|
||||
type Response = number;
|
||||
|
||||
const body = {
|
||||
feedback_id: feedbackId,
|
||||
feedback_id: feedbackId,
|
||||
content,
|
||||
attachments: attachments || [], // Ensure attachments is always an array
|
||||
};
|
||||
|
||||
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/append-feedback-message`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const response = await fetch(
|
||||
`${PUBLIC_REGISTRY_API_BASE_URL}/api/append-feedback-message`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body), // Send new structure
|
||||
},
|
||||
body: JSON.stringify(body), // Send new structure
|
||||
});
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
throw new Error(`Failed to append message for feedback ${feedbackId}. Status: ${response.status}. Body: ${errorBody}`);
|
||||
const errorBody = await response.text();
|
||||
throw new Error(
|
||||
`Failed to append message for feedback ${feedbackId}. Status: ${response.status}. Body: ${errorBody}`,
|
||||
);
|
||||
}
|
||||
|
||||
const responseBody = (await response.json()) as Response;
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
async function fetchCurrencyPrice(currency: string, fiatCurrency: FiatCurrency): Promise<number> {
|
||||
async function fetchCurrencyPrice(
|
||||
currency: string,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): Promise<number> {
|
||||
const response = await fetch(
|
||||
`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> {
|
||||
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();
|
||||
|
||||
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.
|
||||
*/
|
||||
export async function updateRates(): Promise<void> {
|
||||
const settings = store.getState().settings;
|
||||
if (!settings.fetchFiatPrices)
|
||||
return;
|
||||
if (!settings.fetchFiatPrices) return;
|
||||
|
||||
try {
|
||||
const xmrBtcRate = await fetchXmrBtcRate();
|
||||
|
@ -191,8 +225,12 @@ export async function fetchAllConversations(): Promise<void> {
|
|||
const messages = await fetchFeedbackMessagesViaHttp(feedbackId);
|
||||
console.log("Fetched messages for feedback id", feedbackId, messages);
|
||||
store.dispatch(setConversation({ feedbackId, messages }));
|
||||
} catch (error) {
|
||||
logger.error(error, "Error fetching messages for feedback id", feedbackId);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error,
|
||||
"Error fetching messages for feedback id",
|
||||
feedbackId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,27 @@
|
|||
import { listen } from "@tauri-apps/api/event";
|
||||
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 logger from "utils/logger";
|
||||
import { fetchAllConversations, updateAlerts, updatePublicRegistry, updateRates } from "./api";
|
||||
import { checkContextAvailability, getSwapInfo, initializeContext, updateAllNodeStatuses } from "./rpc";
|
||||
import {
|
||||
fetchAllConversations,
|
||||
updateAlerts,
|
||||
updatePublicRegistry,
|
||||
updateRates,
|
||||
} from "./api";
|
||||
import {
|
||||
checkContextAvailability,
|
||||
getSwapInfo,
|
||||
initializeContext,
|
||||
updateAllNodeStatuses,
|
||||
} from "./rpc";
|
||||
import { store } from "./store/storeRenderer";
|
||||
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;
|
||||
|
||||
function setIntervalImmediate(callback: () => void, interval: number): void {
|
||||
callback();
|
||||
setInterval(callback, interval);
|
||||
callback();
|
||||
setInterval(callback, interval);
|
||||
}
|
||||
|
||||
export async function setupBackgroundTasks(): Promise<void> {
|
||||
// Setup periodic fetch tasks
|
||||
setIntervalImmediate(updatePublicRegistry, PROVIDER_UPDATE_INTERVAL);
|
||||
setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
|
||||
setIntervalImmediate(updateRates, UPDATE_RATE_INTERVAL);
|
||||
setIntervalImmediate(fetchAllConversations, FETCH_CONVERSATIONS_INTERVAL);
|
||||
// Setup periodic fetch tasks
|
||||
setIntervalImmediate(updatePublicRegistry, PROVIDER_UPDATE_INTERVAL);
|
||||
setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
|
||||
setIntervalImmediate(updateRates, UPDATE_RATE_INTERVAL);
|
||||
setIntervalImmediate(fetchAllConversations, FETCH_CONVERSATIONS_INTERVAL);
|
||||
|
||||
// Fetch all alerts
|
||||
updateAlerts();
|
||||
// Fetch all alerts
|
||||
updateAlerts();
|
||||
|
||||
// Setup Tauri event listeners
|
||||
// Check if the context is already available. This is to prevent unnecessary re-initialization
|
||||
if (await checkContextAvailability()) {
|
||||
store.dispatch(contextStatusEventReceived(TauriContextStatusEvent.Available));
|
||||
} else {
|
||||
// Warning: If we reload the page while the Context is being initialized, this function will throw an error
|
||||
// Setup Tauri event listeners
|
||||
// Check if the context is already available. This is to prevent unnecessary re-initialization
|
||||
if (await checkContextAvailability()) {
|
||||
store.dispatch(
|
||||
contextStatusEventReceived(TauriContextStatusEvent.Available),
|
||||
);
|
||||
} 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) => {
|
||||
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) => {
|
||||
logger.error(e, "Failed to initialize context even after retry");
|
||||
});
|
||||
}, 2000);
|
||||
logger.error(e, "Failed to initialize context even after retry");
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}, 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { Box, CssBaseline, makeStyles } from "@material-ui/core";
|
||||
import { ThemeProvider } from "@material-ui/core/styles";
|
||||
import { Box, CssBaseline } from "@mui/material";
|
||||
import {
|
||||
ThemeProvider,
|
||||
Theme,
|
||||
StyledEngineProvider,
|
||||
} from "@mui/material/styles";
|
||||
import "@tauri-apps/plugin-shell";
|
||||
import { Route, MemoryRouter as Router, Routes } from "react-router-dom";
|
||||
import Navigation, { drawerWidth } from "./navigation/Navigation";
|
||||
|
@ -10,21 +14,21 @@ import WalletPage from "./pages/wallet/WalletPage";
|
|||
import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider";
|
||||
import UpdaterDialog from "./modal/updater/UpdaterDialog";
|
||||
import { useSettings } from "store/hooks";
|
||||
import { themes } from "./theme";
|
||||
import { Theme as ThemeEnum, themes } from "./theme";
|
||||
import { useEffect } from "react";
|
||||
import { setupBackgroundTasks } from "renderer/background";
|
||||
import "@fontsource/roboto";
|
||||
import FeedbackPage from "./pages/feedback/FeedbackPage";
|
||||
import IntroductionModal from "./modal/introduction/IntroductionModal";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
innerContent: {
|
||||
padding: theme.spacing(4),
|
||||
marginLeft: drawerWidth,
|
||||
maxHeight: `100vh`,
|
||||
flex: 1,
|
||||
},
|
||||
}));
|
||||
declare module "@mui/material/styles" {
|
||||
interface Theme {
|
||||
// Add your custom theme properties here if needed
|
||||
}
|
||||
interface ThemeOptions {
|
||||
// Add your custom theme options here if needed
|
||||
}
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
useEffect(() => {
|
||||
|
@ -32,27 +36,37 @@ export default function App() {
|
|||
}, []);
|
||||
|
||||
const theme = useSettings((s) => s.theme);
|
||||
const currentTheme = themes[theme] || themes[ThemeEnum.Dark];
|
||||
|
||||
console.log("Current theme:", { theme, currentTheme });
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={themes[theme]}>
|
||||
<GlobalSnackbarProvider>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={currentTheme}>
|
||||
<CssBaseline />
|
||||
<IntroductionModal/>
|
||||
<Router>
|
||||
<Navigation />
|
||||
<InnerContent />
|
||||
<UpdaterDialog />
|
||||
</Router>
|
||||
</GlobalSnackbarProvider>
|
||||
</ThemeProvider>
|
||||
<GlobalSnackbarProvider>
|
||||
<IntroductionModal />
|
||||
<Router>
|
||||
<Navigation />
|
||||
<InnerContent />
|
||||
<UpdaterDialog />
|
||||
</Router>
|
||||
</GlobalSnackbarProvider>
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function InnerContent() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.innerContent}>
|
||||
<Box
|
||||
sx={{
|
||||
padding: 4,
|
||||
marginLeft: drawerWidth,
|
||||
maxHeight: `100vh`,
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/swap" element={<SwapPage />} />
|
||||
<Route path="/history" element={<HistoryPage />} />
|
||||
|
@ -63,4 +77,4 @@ function InnerContent() {
|
|||
</Routes>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import {
|
|||
IconButton,
|
||||
IconButtonProps,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
} from "@mui/material";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { useIsContextAvailable } from "store/hooks";
|
||||
|
@ -84,6 +84,10 @@ export default function PromiseInvokeButton<T>({
|
|||
onClick={handleClick}
|
||||
disabled={isDisabled}
|
||||
{...(rest as IconButtonProps)}
|
||||
size="large"
|
||||
sx={{
|
||||
padding: "0.25rem",
|
||||
}}
|
||||
>
|
||||
{actualEndIcon}
|
||||
</IconButton>
|
||||
|
|
|
@ -1,42 +1,49 @@
|
|||
import { BackgroundRefundState } from "models/tauriModel";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
|
||||
import { AlertTitle } from "@material-ui/lab";
|
||||
import { AlertTitle } from "@mui/material";
|
||||
import TruncatedText from "../other/TruncatedText";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function BackgroundRefundAlert() {
|
||||
const backgroundRefund = useAppSelector(state => state.rpc.state.backgroundRefund);
|
||||
const notistack = useSnackbar();
|
||||
const backgroundRefund = useAppSelector(
|
||||
(state) => state.rpc.state.backgroundRefund,
|
||||
);
|
||||
const notistack = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
// If we failed to refund, show a notification
|
||||
if (backgroundRefund?.state.type === "Failed") {
|
||||
notistack.enqueueSnackbar(
|
||||
<>
|
||||
Our attempt to refund {backgroundRefund.swapId} in the background failed.
|
||||
<br />
|
||||
Error: {backgroundRefund.state.content.error}
|
||||
</>,
|
||||
{ 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>
|
||||
useEffect(() => {
|
||||
// If we failed to refund, show a notification
|
||||
if (backgroundRefund?.state.type === "Failed") {
|
||||
notistack.enqueueSnackbar(
|
||||
<>
|
||||
Our attempt to refund {backgroundRefund.swapId} in the background
|
||||
failed.
|
||||
<br />
|
||||
Error: {backgroundRefund.state.content.error}
|
||||
</>,
|
||||
{ variant: "error", autoHideDuration: 60 * 1000 },
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,34 +1,36 @@
|
|||
import { Box, Button, LinearProgress, makeStyles, Badge } from "@material-ui/core";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import { Box, Button, LinearProgress, Badge } from "@mui/material";
|
||||
import { Alert } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAppSelector, usePendingBackgroundProcesses } from "store/hooks";
|
||||
import { exhaustiveGuard } from "utils/typescriptUtils";
|
||||
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
|
||||
import { bytesToMb } from "utils/conversionUtils";
|
||||
import { TauriBackgroundProgress, TauriContextStatusEvent } from "models/tauriModel";
|
||||
import {
|
||||
TauriBackgroundProgress,
|
||||
TauriContextStatusEvent,
|
||||
} from "models/tauriModel";
|
||||
import { useEffect, useState } from "react";
|
||||
import TruncatedText from "../other/TruncatedText";
|
||||
import BitcoinIcon from "../icons/BitcoinIcon";
|
||||
import MoneroIcon from "../icons/MoneroIcon";
|
||||
import TorIcon from "../icons/TorIcon";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
innerAlert: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
function AlertWithLinearProgress({ title, progress, icon, count }: {
|
||||
title: React.ReactNode,
|
||||
progress: number | null,
|
||||
icon?: React.ReactNode | null,
|
||||
count?: number
|
||||
function AlertWithLinearProgress({
|
||||
title,
|
||||
progress,
|
||||
icon,
|
||||
count,
|
||||
}: {
|
||||
title: React.ReactNode;
|
||||
progress: number | null;
|
||||
icon?: React.ReactNode | null;
|
||||
count?: number;
|
||||
}) {
|
||||
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(() => {
|
||||
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
|
||||
// as it'd be confusing to show a 100% progress bar for longer than a second or so.
|
||||
return <Alert severity="info" icon={displayIcon}>
|
||||
<Box style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||
{title}
|
||||
{(progress === null || progress === 0 || progress >= 100) ? (
|
||||
<LinearProgress variant="indeterminate" />
|
||||
) : (
|
||||
<LinearProgress variant="buffer" value={progress} valueBuffer={Math.min(progress + bufferProgressAddition, 100)} />
|
||||
)}
|
||||
</Box>
|
||||
</Alert>
|
||||
return (
|
||||
<Alert severity="info" icon={displayIcon}>
|
||||
<Box style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||
{title}
|
||||
{progress === null || progress === 0 || progress >= 100 ? (
|
||||
<LinearProgress variant="indeterminate" />
|
||||
) : (
|
||||
<LinearProgress
|
||||
variant="buffer"
|
||||
value={progress}
|
||||
valueBuffer={Math.min(progress + bufferProgressAddition, 100)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
function PartialInitStatus({ status, totalOfType, classes }: {
|
||||
status: TauriBackgroundProgress,
|
||||
totalOfType: number,
|
||||
classes: ReturnType<typeof useStyles>
|
||||
function PartialInitStatus({
|
||||
status,
|
||||
totalOfType,
|
||||
}: {
|
||||
status: TauriBackgroundProgress;
|
||||
totalOfType: number;
|
||||
}) {
|
||||
if (status.progress.type === "Completed") {
|
||||
return null;
|
||||
|
@ -70,90 +80,82 @@ function PartialInitStatus({ status, totalOfType, classes }: {
|
|||
case "EstablishingTorCircuits":
|
||||
return (
|
||||
<AlertWithLinearProgress
|
||||
title={
|
||||
<>
|
||||
Establishing Tor circuits
|
||||
</>
|
||||
}
|
||||
title={<>Establishing Tor circuits</>}
|
||||
progress={status.progress.content.frac * 100}
|
||||
count={totalOfType}
|
||||
icon={<TorIcon />}
|
||||
/>
|
||||
);
|
||||
case "SyncingBitcoinWallet":
|
||||
case "SyncingBitcoinWallet": {
|
||||
const progressValue =
|
||||
status.progress.content?.type === "Known" ?
|
||||
(status.progress.content?.content?.consumed / status.progress.content?.content?.total) * 100 : null;
|
||||
status.progress.content?.type === "Known"
|
||||
? (status.progress.content?.content?.consumed /
|
||||
status.progress.content?.content?.total) *
|
||||
100
|
||||
: null;
|
||||
|
||||
return (
|
||||
<AlertWithLinearProgress
|
||||
title={
|
||||
<>
|
||||
Syncing Bitcoin wallet
|
||||
</>
|
||||
}
|
||||
title={<>Syncing Bitcoin wallet</>}
|
||||
progress={progressValue}
|
||||
icon={<BitcoinIcon />}
|
||||
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 (
|
||||
<AlertWithLinearProgress
|
||||
title={
|
||||
<>
|
||||
Full scan of Bitcoin wallet (one time operation)
|
||||
</>
|
||||
}
|
||||
title={<>Full scan of Bitcoin wallet (one time operation)</>}
|
||||
progress={fullScanProgressValue}
|
||||
icon={<BitcoinIcon />}
|
||||
count={totalOfType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "OpeningBitcoinWallet":
|
||||
return (
|
||||
<LoadingSpinnerAlert severity="info">
|
||||
<>
|
||||
Opening Bitcoin wallet
|
||||
</>
|
||||
<>Opening Bitcoin wallet</>
|
||||
</LoadingSpinnerAlert>
|
||||
);
|
||||
case "DownloadingMoneroWalletRpc":
|
||||
case "DownloadingMoneroWalletRpc": {
|
||||
const moneroRpcTitle = `Downloading and verifying the Monero wallet RPC (${bytesToMb(status.progress.content.size).toFixed(2)} MB)`;
|
||||
return (
|
||||
<AlertWithLinearProgress
|
||||
title={
|
||||
<>
|
||||
{moneroRpcTitle}
|
||||
</>
|
||||
}
|
||||
title={<>{moneroRpcTitle}</>}
|
||||
progress={status.progress.content.progress}
|
||||
icon={<MoneroIcon />}
|
||||
count={totalOfType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "OpeningMoneroWallet":
|
||||
return (
|
||||
<LoadingSpinnerAlert severity="info">
|
||||
<>
|
||||
Opening the Monero wallet
|
||||
</>
|
||||
<>Opening the Monero wallet</>
|
||||
</LoadingSpinnerAlert>
|
||||
);
|
||||
case "OpeningDatabase":
|
||||
return (
|
||||
<LoadingSpinnerAlert severity="info">
|
||||
<>
|
||||
Opening the local database
|
||||
</>
|
||||
<>Opening the local database</>
|
||||
</LoadingSpinnerAlert>
|
||||
);
|
||||
case "BackgroundRefund":
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
|
@ -166,13 +168,24 @@ export default function DaemonStatusAlert() {
|
|||
const contextStatus = useAppSelector((s) => s.rpc.status);
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (contextStatus === null || contextStatus === TauriContextStatusEvent.NotInitialized) {
|
||||
return <LoadingSpinnerAlert severity="warning">Checking for available remote nodes</LoadingSpinnerAlert>;
|
||||
if (
|
||||
contextStatus === null ||
|
||||
contextStatus === TauriContextStatusEvent.NotInitialized
|
||||
) {
|
||||
return (
|
||||
<LoadingSpinnerAlert severity="warning">
|
||||
Checking for available remote nodes
|
||||
</LoadingSpinnerAlert>
|
||||
);
|
||||
}
|
||||
|
||||
switch (contextStatus) {
|
||||
case TauriContextStatusEvent.Initializing:
|
||||
return <LoadingSpinnerAlert severity="warning">Core components are loading</LoadingSpinnerAlert>;
|
||||
return (
|
||||
<LoadingSpinnerAlert severity="warning">
|
||||
Core components are loading
|
||||
</LoadingSpinnerAlert>
|
||||
);
|
||||
case TauriContextStatusEvent.Available:
|
||||
return <Alert severity="success">The daemon is running</Alert>;
|
||||
case TauriContextStatusEvent.Failed:
|
||||
|
@ -199,7 +212,6 @@ export default function DaemonStatusAlert() {
|
|||
|
||||
export function BackgroundProgressAlerts() {
|
||||
const backgroundProgress = usePendingBackgroundProcesses();
|
||||
const classes = useStyles();
|
||||
|
||||
if (backgroundProgress.length === 0) {
|
||||
return null;
|
||||
|
@ -207,7 +219,8 @@ export function BackgroundProgressAlerts() {
|
|||
|
||||
const componentCounts: Record<string, number> = {};
|
||||
backgroundProgress.forEach(([, status]) => {
|
||||
componentCounts[status.componentName] = (componentCounts[status.componentName] || 0) + 1;
|
||||
componentCounts[status.componentName] =
|
||||
(componentCounts[status.componentName] || 0) + 1;
|
||||
});
|
||||
|
||||
const renderedComponentNames = new Set<string>();
|
||||
|
@ -219,12 +232,15 @@ export function BackgroundProgressAlerts() {
|
|||
return false;
|
||||
});
|
||||
|
||||
return uniqueBackgroundProcesses.map(([id, status]) => (
|
||||
<PartialInitStatus
|
||||
key={id}
|
||||
status={status}
|
||||
classes={classes}
|
||||
totalOfType={componentCounts[status.componentName]}
|
||||
/>
|
||||
));
|
||||
}
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
{uniqueBackgroundProcesses.map(([id, status]) => (
|
||||
<PartialInitStatus
|
||||
key={id}
|
||||
status={status}
|
||||
totalOfType={componentCounts[status.componentName]}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Button } from "@material-ui/core";
|
||||
import Alert from "@material-ui/lab/Alert";
|
||||
import { Button } from "@mui/material";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { CircularProgress } from "@material-ui/core";
|
||||
import { AlertProps, Alert } from "@material-ui/lab";
|
||||
import { CircularProgress } from "@mui/material";
|
||||
import { Alert } from "@mui/material";
|
||||
import { AlertProps } from "@mui/material";
|
||||
|
||||
export function LoadingSpinnerAlert({ ...rest }: AlertProps) {
|
||||
return <Alert icon={<CircularProgress size={22} />} {...rest} />;
|
||||
return <Alert icon={<CircularProgress size={22} />} {...rest} />;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -1,33 +1,26 @@
|
|||
import { Box, makeStyles } from "@material-ui/core";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import { Box } from "@mui/material";
|
||||
import { Alert } from "@mui/material";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import { SatsAmount } from "../other/Units";
|
||||
import WalletRefreshButton from "../pages/wallet/WalletRefreshButton";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
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) {
|
||||
return <></>;
|
||||
}
|
||||
if (balance == null || balance <= 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<Alert
|
||||
severity="warning"
|
||||
action={<WalletRefreshButton />}
|
||||
variant="filled"
|
||||
>
|
||||
The remaining funds of <SatsAmount amount={balance} /> in the wallet
|
||||
will be used for the next swap
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box sx={{ paddingBottom: 1 }}>
|
||||
<Alert
|
||||
severity="warning"
|
||||
action={<WalletRefreshButton />}
|
||||
variant="filled"
|
||||
>
|
||||
The remaining funds of <SatsAmount amount={balance} /> in the wallet
|
||||
will be used for the next swap
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Box, makeStyles } from "@material-ui/core";
|
||||
import { Alert, AlertTitle } from "@material-ui/lab/";
|
||||
import { Box, Alert, AlertTitle } from "@mui/material";
|
||||
import {
|
||||
BobStateName,
|
||||
GetSwapInfoResponseExt,
|
||||
|
@ -16,41 +15,32 @@ import TruncatedText from "../../other/TruncatedText";
|
|||
import { SwapMoneroRecoveryButton } from "../../pages/history/table/SwapMoneroRecoveryButton";
|
||||
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.
|
||||
* @param messages - Array of messages to display.
|
||||
* @returns JSX.Element
|
||||
*/
|
||||
function MessageList({ messages }: { messages: ReactNode[]; }) {
|
||||
const classes = useStyles();
|
||||
|
||||
function MessageList({ messages }: { messages: ReactNode[] }) {
|
||||
return (
|
||||
<ul className={classes.list}>
|
||||
{messages.filter(msg => msg != null).map((msg, i) => (
|
||||
<li key={i}>{msg}</li>
|
||||
))}
|
||||
</ul>
|
||||
<Box
|
||||
component="ul"
|
||||
sx={{
|
||||
padding: "0px",
|
||||
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.
|
||||
* @returns JSX.Element
|
||||
*/
|
||||
function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt; }) {
|
||||
const classes = useStyles();
|
||||
function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt }) {
|
||||
return (
|
||||
<Box className={classes.box}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<MessageList
|
||||
messages={[
|
||||
"The Bitcoin has been redeemed by the other party",
|
||||
"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",
|
||||
"If this step fails, you can manually redeem your funds",
|
||||
]} />
|
||||
]}
|
||||
/>
|
||||
<SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" />
|
||||
</Box>
|
||||
);
|
||||
|
@ -82,7 +78,10 @@ function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt; })
|
|||
* @returns JSX.Element
|
||||
*/
|
||||
function BitcoinLockedNoTimelockExpiredStateAlert({
|
||||
timelock, cancelTimelockOffset, punishTimelockOffset, isRunning,
|
||||
timelock,
|
||||
cancelTimelockOffset,
|
||||
punishTimelockOffset,
|
||||
isRunning,
|
||||
}: {
|
||||
timelock: TimelockNone;
|
||||
cancelTimelockOffset: number;
|
||||
|
@ -92,20 +91,25 @@ function BitcoinLockedNoTimelockExpiredStateAlert({
|
|||
return (
|
||||
<MessageList
|
||||
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
|
||||
blocks={timelock.content.blocks_left}
|
||||
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",
|
||||
<>
|
||||
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
|
||||
*/
|
||||
function BitcoinPossiblyCancelledAlert({
|
||||
swap, timelock,
|
||||
swap,
|
||||
timelock,
|
||||
}: {
|
||||
swap: GetSwapInfoResponseExt;
|
||||
timelock: TimelockCancel;
|
||||
|
@ -130,10 +135,13 @@ function BitcoinPossiblyCancelledAlert({
|
|||
<>
|
||||
If we haven't refunded in{" "}
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={timelock.content.blocks_left} />
|
||||
, cooperation from the other party will be required to recover the funds
|
||||
</>
|
||||
]} />
|
||||
blocks={timelock.content.blocks_left}
|
||||
/>
|
||||
, 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 might still be able to redeem the Monero. However, this will require cooperation from the other party",
|
||||
"Resume the swap as soon as possible",
|
||||
]} />
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -157,8 +166,13 @@ function PunishTimelockExpiredAlert() {
|
|||
* @param swap - The swap information.
|
||||
* @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) {
|
||||
// This is the state where the swap is safe because the other party has redeemed the Bitcoin
|
||||
// It cannot be punished anymore
|
||||
|
@ -218,8 +232,6 @@ export default function SwapStatusAlert({
|
|||
swap: GetSwapInfoResponseExt;
|
||||
isRunning: boolean;
|
||||
}): JSX.Element | null {
|
||||
const classes = useStyles();
|
||||
|
||||
// If the swap is completed, we do not need to display anything
|
||||
if (!isGetSwapInfoResponseRunningSwap(swap)) {
|
||||
return null;
|
||||
|
@ -235,12 +247,29 @@ export default function SwapStatusAlert({
|
|||
key={swap.swap_id}
|
||||
severity="warning"
|
||||
variant="filled"
|
||||
classes={{ message: classes.alertMessage }}
|
||||
classes={{ message: "alert-message-flex-grow" }}
|
||||
sx={{
|
||||
"& .alert-message-flex-grow": {
|
||||
flexGrow: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<Box className={classes.box}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<StateAlert swap={swap} isRunning={isRunning} />
|
||||
<TimelockTimeline swap={swap} />
|
||||
</Box>
|
||||
|
|
|
@ -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 { GetSwapInfoResponseExt, getAbsoluteBlock } from "models/tauriModelExt";
|
||||
import HumanizedBitcoinBlockDuration from "renderer/components/other/HumanizedBitcoinBlockDuration";
|
||||
|
||||
interface TimelineSegment {
|
||||
title: string;
|
||||
label: string;
|
||||
bgcolor: string;
|
||||
startBlock: number;
|
||||
title: string;
|
||||
label: string;
|
||||
bgcolor: string;
|
||||
startBlock: number;
|
||||
}
|
||||
|
||||
interface TimelineSegmentProps {
|
||||
segment: TimelineSegment;
|
||||
isActive: boolean;
|
||||
absoluteBlock: number;
|
||||
durationOfSegment: number | null;
|
||||
totalBlocks: number;
|
||||
segment: TimelineSegment;
|
||||
isActive: boolean;
|
||||
absoluteBlock: number;
|
||||
durationOfSegment: number | null;
|
||||
totalBlocks: number;
|
||||
}
|
||||
|
||||
function TimelineSegment({
|
||||
segment,
|
||||
isActive,
|
||||
absoluteBlock,
|
||||
durationOfSegment,
|
||||
totalBlocks
|
||||
segment,
|
||||
isActive,
|
||||
absoluteBlock,
|
||||
durationOfSegment,
|
||||
totalBlocks,
|
||||
}: TimelineSegmentProps) {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Tooltip title={<Typography variant="caption">{segment.title}</Typography>}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bgcolor: segment.bgcolor,
|
||||
width: `${durationOfSegment ? ((durationOfSegment / totalBlocks) * 85) : 15}%`,
|
||||
position: 'relative',
|
||||
}} style={{
|
||||
opacity: isActive ? 1 : 0.3
|
||||
}}>
|
||||
{isActive && (
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: '100%',
|
||||
width: `${Math.max(5, ((absoluteBlock - segment.startBlock) / durationOfSegment) * 100)}%`,
|
||||
zIndex: 1,
|
||||
}}>
|
||||
<LinearProgress
|
||||
variant="indeterminate"
|
||||
color="primary"
|
||||
style={{
|
||||
height: '100%',
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
opacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Typography variant="subtitle2" color="inherit" align="center" style={{ zIndex: 2 }}>
|
||||
{segment.label}
|
||||
</Typography>
|
||||
{durationOfSegment && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="inherit"
|
||||
align="center"
|
||||
style={{
|
||||
zIndex: 2,
|
||||
opacity: 0.8
|
||||
}}
|
||||
>
|
||||
{isActive && (
|
||||
<>
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={durationOfSegment - (absoluteBlock - segment.startBlock)}
|
||||
/>{" "}left
|
||||
</>
|
||||
)}
|
||||
{!isActive && (
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={durationOfSegment}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
return (
|
||||
<Tooltip title={<Typography variant="caption">{segment.title}</Typography>}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
bgcolor: segment.bgcolor,
|
||||
width: `${durationOfSegment ? (durationOfSegment / totalBlocks) * 85 : 15}%`,
|
||||
position: "relative",
|
||||
}}
|
||||
style={{
|
||||
opacity: isActive ? 1 : 0.3,
|
||||
}}
|
||||
>
|
||||
{isActive && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: "100%",
|
||||
width: `${Math.max(5, ((absoluteBlock - segment.startBlock) / durationOfSegment) * 100)}%`,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<LinearProgress
|
||||
variant="indeterminate"
|
||||
color="primary"
|
||||
style={{
|
||||
height: "100%",
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
opacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
color="inherit"
|
||||
align="center"
|
||||
style={{ zIndex: 2 }}
|
||||
>
|
||||
{segment.label}
|
||||
</Typography>
|
||||
{durationOfSegment && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="inherit"
|
||||
align="center"
|
||||
style={{
|
||||
zIndex: 2,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{isActive && (
|
||||
<>
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={
|
||||
durationOfSegment - (absoluteBlock - segment.startBlock)
|
||||
}
|
||||
/>{" "}
|
||||
left
|
||||
</>
|
||||
)}
|
||||
{!isActive && (
|
||||
<HumanizedBitcoinBlockDuration blocks={durationOfSegment} />
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function TimelockTimeline({ swap }: {
|
||||
// This forces the timelock to not be null
|
||||
swap: GetSwapInfoResponseExt & { timelock: ExpiredTimelocks }
|
||||
export function TimelockTimeline({
|
||||
swap,
|
||||
}: {
|
||||
// This forces the timelock to not be null
|
||||
swap: GetSwapInfoResponseExt & { timelock: ExpiredTimelocks };
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
const timelineSegments: TimelineSegment[] = [
|
||||
{
|
||||
title: "Normally a swap is completed during this period",
|
||||
label: "Normal",
|
||||
bgcolor: theme.palette.success.main,
|
||||
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",
|
||||
label: "Refund",
|
||||
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",
|
||||
bgcolor: theme.palette.error.main,
|
||||
startBlock: swap.cancel_timelock + swap.punish_timelock,
|
||||
}
|
||||
];
|
||||
const timelineSegments: TimelineSegment[] = [
|
||||
{
|
||||
title: "Normally a swap is completed during this period",
|
||||
label: "Normal",
|
||||
bgcolor: theme.palette.success.main,
|
||||
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",
|
||||
label: "Refund",
|
||||
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",
|
||||
bgcolor: theme.palette.error.main,
|
||||
startBlock: 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 totalBlocks = swap.cancel_timelock + swap.punish_timelock;
|
||||
const absoluteBlock = getAbsoluteBlock(
|
||||
swap.timelock,
|
||||
swap.cancel_timelock,
|
||||
swap.punish_timelock,
|
||||
);
|
||||
|
||||
// This calculates the duration of a segment
|
||||
// by getting the the difference to the next segment
|
||||
function durationOfSegment(index: number): number | null {
|
||||
const nextSegment = timelineSegments[index + 1];
|
||||
if (nextSegment == 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;
|
||||
// This calculates the duration of a segment
|
||||
// by getting the the difference to the next segment
|
||||
function durationOfSegment(index: number): number | null {
|
||||
const nextSegment = timelineSegments[index + 1];
|
||||
if (nextSegment == 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 (
|
||||
<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>
|
||||
Array.from(
|
||||
timelineSegments
|
||||
.slice()
|
||||
// We use .entries() to keep the indexes despite reversing
|
||||
.entries(),
|
||||
)
|
||||
.reverse()
|
||||
.find(([_, segment]) => absoluteBlock >= segment.startBlock)?.[0] ?? 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,15 @@
|
|||
import { Box, makeStyles } from "@material-ui/core";
|
||||
import { Box } from "@mui/material";
|
||||
import { useSwapInfosSortedByDate } from "store/hooks";
|
||||
import SwapStatusAlert from "./SwapStatusAlert/SwapStatusAlert";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function SwapTxLockAlertsBox() {
|
||||
const classes = useStyles();
|
||||
|
||||
// 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)
|
||||
// the SwapStatusAlert component will not render an Alert
|
||||
const swaps = useSwapInfosSortedByDate();
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
{swaps.map((swap) => (
|
||||
<SwapStatusAlert key={swap.swap_id} swap={swap} isRunning={false} />
|
||||
))}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Button } from "@material-ui/core";
|
||||
import Alert from "@material-ui/lab/Alert";
|
||||
import { Button } from "@mui/material";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useResumeableSwapsCountExcludingPunished } from "store/hooks";
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { SvgIcon } from "@material-ui/core";
|
||||
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
|
||||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
|
||||
export default function BitcoinIcon(props: SvgIconProps) {
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { SvgIcon } from "@material-ui/core";
|
||||
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
|
||||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
|
||||
export default function DiscordIcon(props: SvgIconProps) {
|
||||
return (
|
||||
|
|
|
@ -1,29 +1,30 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import * as jdenticon from 'jdenticon';
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import * as jdenticon from "jdenticon";
|
||||
|
||||
interface IdentIconProps {
|
||||
value: string;
|
||||
size?: number | string;
|
||||
className?: string;
|
||||
value: string;
|
||||
size?: number | string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function IdentIcon({ value, size = 40, className = '' }: IdentIconProps) {
|
||||
const iconRef = useRef<SVGSVGElement>(null);
|
||||
function IdentIcon({ value, size = 40, className = "" }: IdentIconProps) {
|
||||
const iconRef = useRef<SVGSVGElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (iconRef.current) {
|
||||
jdenticon.update(iconRef.current, value);
|
||||
}
|
||||
}, [value]);
|
||||
useEffect(() => {
|
||||
if (iconRef.current) {
|
||||
jdenticon.update(iconRef.current, value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={iconRef}
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
data-jdenticon-value={value} />
|
||||
);
|
||||
return (
|
||||
<svg
|
||||
ref={iconRef}
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
data-jdenticon-value={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default IdentIcon;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IconButton } from "@material-ui/core";
|
||||
import { IconButton } from "@mui/material";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
|
@ -10,7 +10,7 @@ export default function LinkIconButton({
|
|||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<IconButton component="span" onClick={() => open(url)}>
|
||||
<IconButton component="span" onClick={() => open(url)} size="large">
|
||||
{children}
|
||||
</IconButton>
|
||||
);
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { SvgIcon } from "@material-ui/core";
|
||||
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
|
||||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
|
||||
export default function MatrixIcon(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox="0 0 27.9 32" {...props}>
|
||||
<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="M0.936 0.732v30.5h2.19v0.732h-3.04v-32h3.03v0.732z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SvgIcon viewBox="0 0 27.9 32" {...props}>
|
||||
<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="M0.936 0.732v30.5h2.19v0.732h-3.04v-32h3.03v0.732z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { SvgIcon } from "@material-ui/core";
|
||||
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
|
||||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
|
||||
export default function MoneroIcon(props: SvgIconProps) {
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { SvgIcon } from "@material-ui/core";
|
||||
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
|
||||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
|
||||
export default function TorIcon(props: SvgIconProps) {
|
||||
return (
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { TextField } from "@material-ui/core";
|
||||
import { TextFieldProps } from "@material-ui/core/TextField/TextField";
|
||||
import TextField, { TextFieldProps } from "@mui/material/TextField";
|
||||
import { useEffect } from "react";
|
||||
import { isTestnet } from "store/config";
|
||||
import { isBtcAddressValid } from "utils/conversionUtils";
|
||||
|
|
|
@ -1,39 +1,45 @@
|
|||
import { createContext, useContext, useState, ReactNode } from 'react'
|
||||
import { createContext, useContext, useState, ReactNode } from "react";
|
||||
|
||||
interface CardSelectionContextType {
|
||||
selectedValue: string
|
||||
setSelectedValue: (value: string) => void
|
||||
selectedValue: string;
|
||||
setSelectedValue: (value: string) => void;
|
||||
}
|
||||
|
||||
const CardSelectionContext = createContext<CardSelectionContextType | undefined>(undefined)
|
||||
const CardSelectionContext = createContext<
|
||||
CardSelectionContextType | undefined
|
||||
>(undefined);
|
||||
|
||||
export function CardSelectionProvider({
|
||||
children,
|
||||
initialValue,
|
||||
onChange
|
||||
}: {
|
||||
children: ReactNode
|
||||
initialValue: string
|
||||
onChange?: (value: string) => void
|
||||
export function CardSelectionProvider({
|
||||
children,
|
||||
initialValue,
|
||||
onChange,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
initialValue: string;
|
||||
onChange?: (value: string) => void;
|
||||
}) {
|
||||
const [selectedValue, setSelectedValue] = useState(initialValue)
|
||||
const [selectedValue, setSelectedValue] = useState(initialValue);
|
||||
|
||||
const handleValueChange = (value: string) => {
|
||||
setSelectedValue(value)
|
||||
onChange?.(value)
|
||||
}
|
||||
const handleValueChange = (value: string) => {
|
||||
setSelectedValue(value);
|
||||
onChange?.(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<CardSelectionContext.Provider value={{ selectedValue, setSelectedValue: handleValueChange }}>
|
||||
{children}
|
||||
</CardSelectionContext.Provider>
|
||||
)
|
||||
return (
|
||||
<CardSelectionContext.Provider
|
||||
value={{ selectedValue, setSelectedValue: handleValueChange }}
|
||||
>
|
||||
{children}
|
||||
</CardSelectionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useCardSelection() {
|
||||
const context = useContext(CardSelectionContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('useCardSelection must be used within a CardSelectionProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
const context = useContext(CardSelectionContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useCardSelection must be used within a CardSelectionProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
|
|
@ -1,23 +1,30 @@
|
|||
import { Box } from '@material-ui/core'
|
||||
import CheckIcon from '@material-ui/icons/Check'
|
||||
import { CardSelectionProvider } from './CardSelectionContext'
|
||||
import { Box } from "@mui/material";
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import { CardSelectionProvider } from "./CardSelectionContext";
|
||||
|
||||
interface CardSelectionGroupProps {
|
||||
children: React.ReactElement<{ value: string }>[]
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
children: React.ReactElement<{ value: string }>[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function CardSelectionGroup({
|
||||
children,
|
||||
value,
|
||||
onChange,
|
||||
children,
|
||||
value,
|
||||
onChange,
|
||||
}: CardSelectionGroupProps) {
|
||||
return (
|
||||
<CardSelectionProvider initialValue={value} onChange={onChange}>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 12 }}>
|
||||
{children}
|
||||
</Box>
|
||||
</CardSelectionProvider>
|
||||
)
|
||||
return (
|
||||
<CardSelectionProvider initialValue={value} onChange={onChange}>
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 12,
|
||||
marginTop: 12,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</CardSelectionProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,53 +1,65 @@
|
|||
import { Box } from "@material-ui/core";
|
||||
import CheckIcon from '@material-ui/icons/Check'
|
||||
import { useCardSelection } from './CardSelectionContext'
|
||||
import { Box } from "@mui/material";
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import { useCardSelection } from "./CardSelectionContext";
|
||||
|
||||
// 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}) {
|
||||
const { selectedValue, setSelectedValue } = useCardSelection()
|
||||
const selected = value === selectedValue
|
||||
export default function CardSelectionOption({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: string;
|
||||
}) {
|
||||
const { selectedValue, setSelectedValue } = useCardSelection();
|
||||
const selected = value === selectedValue;
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={() => setSelectedValue(value)}
|
||||
return (
|
||||
<Box
|
||||
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={{
|
||||
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',
|
||||
transition: "all 0.2s ease-in-out",
|
||||
transform: "scale(1)",
|
||||
animation: "checkIn 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={{
|
||||
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>
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
pt: 0.5,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
import { Box, Button, Dialog, DialogActions, DialogContent, IconButton, List, ListItem, ListItemText, TextField } from "@material-ui/core";
|
||||
import { TextFieldProps } from "@material-ui/core/TextField/TextField";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
IconButton,
|
||||
List,
|
||||
ListItemText,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { TextFieldProps } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getMoneroAddresses } from "renderer/rpc";
|
||||
import { isTestnet } from "store/config";
|
||||
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 ListItemButton from "@mui/material/ListItemButton";
|
||||
|
||||
type MoneroAddressTextFieldProps = TextFieldProps & {
|
||||
address: string;
|
||||
onAddressChange: (address: string) => void;
|
||||
onAddressValidityChange: (valid: boolean) => void;
|
||||
helperText: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function MoneroAddressTextField({
|
||||
address,
|
||||
|
@ -59,12 +71,14 @@ export default function MoneroAddressTextField({
|
|||
helperText={address.length > 0 ? errorText || helperText : helperText}
|
||||
placeholder={placeholder}
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
endAdornment: addresses?.length > 0 && (
|
||||
<IconButton onClick={() => setShowDialog(true)} size="small">
|
||||
<ImportContactsIcon />
|
||||
</IconButton>
|
||||
)
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: addresses?.length > 0 && (
|
||||
<IconButton onClick={() => setShowDialog(true)} size="small">
|
||||
<ImportContactsIcon />
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -90,26 +104,21 @@ function RecentlyUsedAddressesDialog({
|
|||
open,
|
||||
onClose,
|
||||
addresses,
|
||||
onAddressSelect
|
||||
onAddressSelect,
|
||||
}: RecentlyUsedAddressesDialogProps) {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogContent>
|
||||
<List>
|
||||
{addresses.map((addr) => (
|
||||
<ListItem
|
||||
button
|
||||
key={addr}
|
||||
onClick={() => onAddressSelect(addr)}
|
||||
>
|
||||
<ListItemText
|
||||
<ListItemButton key={addr} onClick={() => onAddressSelect(addr)}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box fontFamily="monospace">
|
||||
<Box
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
<TruncatedText limit={40} truncateMiddle>
|
||||
{addr}
|
||||
</TruncatedText>
|
||||
|
@ -117,16 +126,12 @@ function RecentlyUsedAddressesDialog({
|
|||
}
|
||||
secondary="Recently used as a redeem address"
|
||||
/>
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
<Button onClick={onClose} variant="contained" color="primary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
import { DialogTitle, makeStyles, Typography } from "@material-ui/core";
|
||||
import { DialogTitle, Typography } from "@mui/material";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
});
|
||||
|
||||
type DialogTitleProps = {
|
||||
title: ReactNode;
|
||||
};
|
||||
|
||||
export default function DialogHeader({ title }: DialogTitleProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<DialogTitle disableTypography className={classes.root}>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
</DialogTitle>
|
||||
);
|
||||
|
|
|
@ -1,31 +1,25 @@
|
|||
import { Button, makeStyles, Paper, Typography } from "@material-ui/core";
|
||||
|
||||
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),
|
||||
},
|
||||
}));
|
||||
import { Button, Paper, Typography } from "@mui/material";
|
||||
|
||||
export default function PaperTextBox({ stdOut }: { stdOut: string }) {
|
||||
const classes = useStyles();
|
||||
|
||||
function handleCopyLogs() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
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">
|
||||
{stdOut}
|
||||
</Typography>
|
||||
<Button onClick={handleCopyLogs} className={classes.copyButton}>
|
||||
<Button onClick={handleCopyLogs} sx={{ marginTop: 1 }}>
|
||||
Copy
|
||||
</Button>
|
||||
</Paper>
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
} from "@material-ui/core";
|
||||
} from "@mui/material";
|
||||
import { suspendCurrentSwap } from "renderer/rpc";
|
||||
import PromiseInvokeButton from "../PromiseInvokeButton";
|
||||
|
||||
|
|
|
@ -1,247 +1,241 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
Paper,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@material-ui/core'
|
||||
import { ErrorOutline, Visibility } from '@material-ui/icons'
|
||||
import ExternalLink from 'renderer/components/other/ExternalLink'
|
||||
import SwapSelectDropDown from './SwapSelectDropDown'
|
||||
import LogViewer from './LogViewer'
|
||||
import { useFeedback, MAX_FEEDBACK_LENGTH } from './useFeedback'
|
||||
import { useState } from 'react'
|
||||
import PromiseInvokeButton from 'renderer/components/PromiseInvokeButton'
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
Paper,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { ErrorOutline, Visibility } from "@mui/icons-material";
|
||||
import ExternalLink from "renderer/components/other/ExternalLink";
|
||||
import SwapSelectDropDown from "./SwapSelectDropDown";
|
||||
import LogViewer from "./LogViewer";
|
||||
import { useFeedback, MAX_FEEDBACK_LENGTH } from "./useFeedback";
|
||||
import { useState } from "react";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
|
||||
export default function FeedbackDialog({
|
||||
open,
|
||||
onClose,
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [swapLogsEditorOpen, setSwapLogsEditorOpen] = useState(false)
|
||||
const [daemonLogsEditorOpen, setDaemonLogsEditorOpen] = useState(false)
|
||||
const [swapLogsEditorOpen, setSwapLogsEditorOpen] = useState(false);
|
||||
const [daemonLogsEditorOpen, setDaemonLogsEditorOpen] = useState(false);
|
||||
|
||||
const { input, setInputState, logs, error, clearState, submitFeedback } =
|
||||
useFeedback()
|
||||
const { input, setInputState, logs, error, clearState, submitFeedback } =
|
||||
useFeedback();
|
||||
|
||||
const handleClose = () => {
|
||||
clearState()
|
||||
onClose()
|
||||
}
|
||||
const handleClose = () => {
|
||||
clearState();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const bodyTooLong = input.bodyText.length > MAX_FEEDBACK_LENGTH
|
||||
const bodyTooLong = input.bodyText.length > MAX_FEEDBACK_LENGTH;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle style={{ paddingBottom: '0.5rem' }}>
|
||||
Submit Feedback
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle style={{ paddingBottom: "0.5rem" }}>
|
||||
Submit Feedback
|
||||
</DialogTitle>
|
||||
<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
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1.5rem',
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{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
|
||||
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}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => setSwapLogsEditorOpen(true)}
|
||||
disabled={input.selectedSwap === null}
|
||||
size="large"
|
||||
>
|
||||
<Visibility />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<PromiseInvokeButton
|
||||
requiresContext={false}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onInvoke={submitFeedback}
|
||||
onSuccess={handleClose}
|
||||
</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",
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</PromiseInvokeButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
<IconButton
|
||||
onClick={() => setDaemonLogsEditorOpen(true)}
|
||||
disabled={input.attachDaemonLogs === false}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
Paper,
|
||||
Switch,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
} from "@mui/material";
|
||||
import { CliLog } from "models/cliModel";
|
||||
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
||||
|
||||
|
@ -25,39 +25,58 @@ export default function LogViewer({
|
|||
setOpen,
|
||||
logs,
|
||||
setIsRedacted,
|
||||
isRedacted
|
||||
isRedacted,
|
||||
}: LogViewerProps) {
|
||||
return (
|
||||
<Dialog open={open} onClose={() => setOpen(false)} fullWidth>
|
||||
<DialogContent>
|
||||
<Box>
|
||||
<DialogContentText>
|
||||
<Box style={{ display: "flex", flexDirection: "row", alignItems: "center" }}>
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography>
|
||||
These are the logs that would be attached to your feedback message and provided to us developers.
|
||||
They help us narrow down the problem you encountered.
|
||||
These are the logs that would be attached to your feedback
|
||||
message and provided to us developers. They help us narrow down
|
||||
the problem you encountered.
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogContentText>
|
||||
|
||||
<CliLogsBox
|
||||
label="Logs"
|
||||
logs={logs}
|
||||
<CliLogsBox
|
||||
label="Logs"
|
||||
logs={logs}
|
||||
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
|
||||
<Switch
|
||||
color="primary"
|
||||
<Switch
|
||||
color="primary"
|
||||
checked={isRedacted}
|
||||
onChange={(_, checked: boolean) => setIsRedacted(checked)}
|
||||
onChange={(_, checked: boolean) => setIsRedacted(checked)}
|
||||
/>
|
||||
</Paper>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" color="primary" onClick={() => setOpen(false)}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
|
|
@ -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 { PiconeroAmount } from "../../other/Units";
|
||||
import { parseDateString } from "utils/parseUtils";
|
||||
|
@ -26,20 +26,20 @@ export default function SwapSelectDropDown({
|
|||
<Select
|
||||
value={selectedSwap ?? ""}
|
||||
variant="outlined"
|
||||
onChange={(e) => setSelectedSwap(e.target.value as string || null)}
|
||||
onChange={(e) => setSelectedSwap((e.target.value as string) || null)}
|
||||
style={{ width: "100%" }}
|
||||
displayEmpty
|
||||
>
|
||||
{swaps.map((swap) => (
|
||||
<MenuItem value={swap.swap_id} key={swap.swap_id}>
|
||||
<Box component="span" style={{ whiteSpace: 'pre' }}>
|
||||
Swap <TruncatedText>{swap.swap_id}</TruncatedText> from{' '}
|
||||
{new Date(parseDateString(swap.start_date)).toDateString()} (
|
||||
<PiconeroAmount amount={swap.xmr_amount} />)
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem value={swap.swap_id} key={swap.swap_id}>
|
||||
<Box component="span" style={{ whiteSpace: "pre" }}>
|
||||
Swap <TruncatedText>{swap.swap_id}</TruncatedText> from{" "}
|
||||
{new Date(parseDateString(swap.start_date)).toDateString()} (
|
||||
<PiconeroAmount amount={swap.xmr_amount} />)
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem value="">Do not attach a swap</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,163 +1,163 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { store } from 'renderer/store/storeRenderer'
|
||||
import { useActiveSwapInfo } from 'store/hooks'
|
||||
import { logsToRawString } from 'utils/parseUtils'
|
||||
import { getLogsOfSwap, redactLogs } from 'renderer/rpc'
|
||||
import { CliLog, parseCliLogString } from 'models/cliModel'
|
||||
import logger from 'utils/logger'
|
||||
import { submitFeedbackViaHttp } from 'renderer/api'
|
||||
import { addFeedbackId } from 'store/features/conversationsSlice'
|
||||
import { AttachmentInput } from 'models/apiModel'
|
||||
import { useSnackbar } from 'notistack'
|
||||
import { useState, useEffect } from "react";
|
||||
import { store } from "renderer/store/storeRenderer";
|
||||
import { useActiveSwapInfo } from "store/hooks";
|
||||
import { logsToRawString } from "utils/parseUtils";
|
||||
import { getLogsOfSwap, redactLogs } from "renderer/rpc";
|
||||
import { CliLog, parseCliLogString } from "models/cliModel";
|
||||
import logger from "utils/logger";
|
||||
import { submitFeedbackViaHttp } from "renderer/api";
|
||||
import { addFeedbackId } from "store/features/conversationsSlice";
|
||||
import { AttachmentInput } from "models/apiModel";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
export const MAX_FEEDBACK_LENGTH = 4000
|
||||
export const MAX_FEEDBACK_LENGTH = 4000;
|
||||
|
||||
interface FeedbackInputState {
|
||||
bodyText: string
|
||||
selectedSwap: string | null
|
||||
attachDaemonLogs: boolean
|
||||
isSwapLogsRedacted: boolean
|
||||
isDaemonLogsRedacted: boolean
|
||||
bodyText: string;
|
||||
selectedSwap: string | null;
|
||||
attachDaemonLogs: boolean;
|
||||
isSwapLogsRedacted: boolean;
|
||||
isDaemonLogsRedacted: boolean;
|
||||
}
|
||||
|
||||
interface FeedbackLogsState {
|
||||
swapLogs: (string | CliLog)[] | null
|
||||
daemonLogs: (string | CliLog)[] | null
|
||||
swapLogs: (string | CliLog)[] | null;
|
||||
daemonLogs: (string | CliLog)[] | null;
|
||||
}
|
||||
|
||||
const initialInputState: FeedbackInputState = {
|
||||
bodyText: '',
|
||||
selectedSwap: null,
|
||||
attachDaemonLogs: true,
|
||||
isSwapLogsRedacted: false,
|
||||
isDaemonLogsRedacted: false,
|
||||
}
|
||||
bodyText: "",
|
||||
selectedSwap: null,
|
||||
attachDaemonLogs: true,
|
||||
isSwapLogsRedacted: false,
|
||||
isDaemonLogsRedacted: false,
|
||||
};
|
||||
|
||||
const initialLogsState: FeedbackLogsState = {
|
||||
swapLogs: null,
|
||||
daemonLogs: null,
|
||||
}
|
||||
swapLogs: null,
|
||||
daemonLogs: null,
|
||||
};
|
||||
|
||||
export function useFeedback() {
|
||||
const currentSwapId = useActiveSwapInfo()
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
const currentSwapId = useActiveSwapInfo();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [inputState, setInputState] = useState<FeedbackInputState>({
|
||||
...initialInputState,
|
||||
selectedSwap: currentSwapId?.swap_id || null,
|
||||
})
|
||||
const [logsState, setLogsState] =
|
||||
useState<FeedbackLogsState>(initialLogsState)
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [inputState, setInputState] = useState<FeedbackInputState>({
|
||||
...initialInputState,
|
||||
selectedSwap: currentSwapId?.swap_id || null,
|
||||
});
|
||||
const [logsState, setLogsState] =
|
||||
useState<FeedbackLogsState>(initialLogsState);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const bodyTooLong = inputState.bodyText.length > MAX_FEEDBACK_LENGTH
|
||||
const bodyTooLong = inputState.bodyText.length > MAX_FEEDBACK_LENGTH;
|
||||
|
||||
useEffect(() => {
|
||||
if (inputState.selectedSwap === null) {
|
||||
setLogsState((prev) => ({ ...prev, swapLogs: null }))
|
||||
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)
|
||||
useEffect(() => {
|
||||
if (inputState.selectedSwap === null) {
|
||||
setLogsState((prev) => ({ ...prev, swapLogs: null }));
|
||||
return;
|
||||
}
|
||||
|
||||
const submitFeedback = async () => {
|
||||
if (inputState.bodyText.length === 0) {
|
||||
setError('Please enter a message')
|
||||
throw new Error('User did not enter a message')
|
||||
}
|
||||
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]);
|
||||
|
||||
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))
|
||||
useEffect(() => {
|
||||
if (!inputState.attachDaemonLogs) {
|
||||
setLogsState((prev) => ({ ...prev, daemonLogs: null }));
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
input: inputState,
|
||||
setInputState,
|
||||
logs: logsState,
|
||||
error,
|
||||
clearState,
|
||||
submitFeedback,
|
||||
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 () => {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,114 +1,108 @@
|
|||
import { makeStyles, Modal } from '@material-ui/core'
|
||||
import { useState } from 'react'
|
||||
import Slide01_GettingStarted from './slides/Slide01_GettingStarted'
|
||||
import Slide02_ChooseAMaker from './slides/Slide02_ChooseAMaker'
|
||||
import Slide03_PrepareSwap from './slides/Slide03_PrepareSwap'
|
||||
import Slide04_ExecuteSwap from './slides/Slide04_ExecuteSwap'
|
||||
import Slide05_KeepAnEyeOnYourSwaps from './slides/Slide05_KeepAnEyeOnYourSwaps'
|
||||
import Slide06_FiatPricePreference from './slides/Slide06_FiatPricePreference'
|
||||
import Slide07_ReachOut from './slides/Slide07_ReachOut'
|
||||
import { Modal } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import Slide01_GettingStarted from "./slides/Slide01_GettingStarted";
|
||||
import Slide02_ChooseAMaker from "./slides/Slide02_ChooseAMaker";
|
||||
import Slide03_PrepareSwap from "./slides/Slide03_PrepareSwap";
|
||||
import Slide04_ExecuteSwap from "./slides/Slide04_ExecuteSwap";
|
||||
import Slide05_KeepAnEyeOnYourSwaps from "./slides/Slide05_KeepAnEyeOnYourSwaps";
|
||||
import Slide06_FiatPricePreference from "./slides/Slide06_FiatPricePreference";
|
||||
import Slide07_ReachOut from "./slides/Slide07_ReachOut";
|
||||
import {
|
||||
setFetchFiatPrices,
|
||||
setUserHasSeenIntroduction,
|
||||
} from 'store/features/settingsSlice'
|
||||
import { useAppDispatch, useSettings } from 'store/hooks'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
modal: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
paper: {
|
||||
width: '80%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
})
|
||||
setFetchFiatPrices,
|
||||
setUserHasSeenIntroduction,
|
||||
} from "store/features/settingsSlice";
|
||||
import { useAppDispatch, useSettings } from "store/hooks";
|
||||
|
||||
export default function IntroductionModal() {
|
||||
const userHasSeenIntroduction = useSettings(
|
||||
(s) => s.userHasSeenIntroduction
|
||||
)
|
||||
const userHasSeenIntroduction = useSettings((s) => s.userHasSeenIntroduction);
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Handle Display State
|
||||
const [open, setOpen] = useState<boolean>(!userHasSeenIntroduction)
|
||||
const [showFiat, setShowFiat] = useState<boolean>(true)
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
// Handle Display State
|
||||
const [open, setOpen] = useState<boolean>(!userHasSeenIntroduction);
|
||||
const [showFiat, setShowFiat] = useState<boolean>(true);
|
||||
const handleClose = () => {
|
||||
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
|
||||
const [currentSlideIndex, setCurrentSlideIndex] = useState(0)
|
||||
setCurrentSlideIndex((i) => i + 1);
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
if (currentSlideIndex == slideComponents.length - 1) {
|
||||
handleClose()
|
||||
dispatch(setUserHasSeenIntroduction(true))
|
||||
dispatch(setFetchFiatPrices(showFiat))
|
||||
return
|
||||
}
|
||||
|
||||
setCurrentSlideIndex((i) => i + 1)
|
||||
const handlePrevious = () => {
|
||||
if (currentSlideIndex == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentSlideIndex == 0) {
|
||||
return
|
||||
}
|
||||
setCurrentSlideIndex((i) => i - 1);
|
||||
};
|
||||
|
||||
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 = [
|
||||
<Slide01_GettingStarted
|
||||
handleContinue={handleContinue}
|
||||
handlePrevious={handlePrevious}
|
||||
hidePreviousButton
|
||||
/>,
|
||||
<Slide02_ChooseAMaker
|
||||
handleContinue={handleContinue}
|
||||
handlePrevious={handlePrevious}
|
||||
/>,
|
||||
<Slide03_PrepareSwap
|
||||
handleContinue={handleContinue}
|
||||
handlePrevious={handlePrevious}
|
||||
/>,
|
||||
<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>
|
||||
)
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
disableAutoFocus
|
||||
closeAfterTransition
|
||||
>
|
||||
{slideComponents[currentSlideIndex]}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,23 +1,19 @@
|
|||
import { Typography } from '@material-ui/core'
|
||||
import SlideTemplate from './SlideTemplate'
|
||||
import imagePath from 'assets/walletWithBitcoinAndMonero.png'
|
||||
import { Typography } from "@mui/material";
|
||||
import SlideTemplate from "./SlideTemplate";
|
||||
import imagePath from "assets/walletWithBitcoinAndMonero.png";
|
||||
|
||||
export default function Slide01_GettingStarted(props: slideProps) {
|
||||
return (
|
||||
<SlideTemplate
|
||||
title="Getting Started"
|
||||
{...props}
|
||||
imagePath={imagePath}
|
||||
>
|
||||
<Typography variant="subtitle1">
|
||||
To start swapping, you'll need:
|
||||
</Typography>
|
||||
<Typography>
|
||||
<ul>
|
||||
<li>A Bitcoin wallet with funds to swap</li>
|
||||
<li>A Monero wallet to receive your Monero</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
</SlideTemplate>
|
||||
)
|
||||
return (
|
||||
<SlideTemplate title="Getting Started" {...props} imagePath={imagePath}>
|
||||
<Typography variant="subtitle1">
|
||||
To start swapping, you'll need:
|
||||
</Typography>
|
||||
<Typography>
|
||||
<ul>
|
||||
<li>A Bitcoin wallet with funds to swap</li>
|
||||
<li>A Monero wallet to receive your Monero</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
</SlideTemplate>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { Typography } from '@material-ui/core'
|
||||
import SlideTemplate from './SlideTemplate'
|
||||
import imagePath from 'assets/mockMakerSelection.svg'
|
||||
import { Typography } from "@mui/material";
|
||||
import SlideTemplate from "./SlideTemplate";
|
||||
import imagePath from "assets/mockMakerSelection.svg";
|
||||
|
||||
export default function Slide02_ChooseAMaker(props: slideProps) {
|
||||
return (
|
||||
<SlideTemplate
|
||||
title="Choose a Maker"
|
||||
stepLabel="Step 1"
|
||||
{...props}
|
||||
imagePath={imagePath}
|
||||
>
|
||||
<Typography variant="subtitle1">
|
||||
To start a swap, choose a maker. Each maker offers different exchange rates and limits.
|
||||
</Typography>
|
||||
</SlideTemplate>
|
||||
)
|
||||
return (
|
||||
<SlideTemplate
|
||||
title="Choose a Maker"
|
||||
stepLabel="Step 1"
|
||||
{...props}
|
||||
imagePath={imagePath}
|
||||
>
|
||||
<Typography variant="subtitle1">
|
||||
To start a swap, choose a maker. Each maker offers different exchange
|
||||
rates and limits.
|
||||
</Typography>
|
||||
</SlideTemplate>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import { Typography } from '@material-ui/core'
|
||||
import SlideTemplate from './SlideTemplate'
|
||||
import imagePath from 'assets/mockConfigureSwap.svg'
|
||||
import { Typography } from "@mui/material";
|
||||
import SlideTemplate from "./SlideTemplate";
|
||||
import imagePath from "assets/mockConfigureSwap.svg";
|
||||
|
||||
export default function Slide02_ChooseAMaker(props: slideProps) {
|
||||
return (
|
||||
<SlideTemplate title="Prepare Swap" stepLabel="Step 2" {...props} imagePath={imagePath}>
|
||||
<Typography variant="subtitle1">
|
||||
To initiate a swap, provide a Monero address and optionally a Bitcoin refund address.
|
||||
</Typography>
|
||||
</SlideTemplate>
|
||||
)
|
||||
return (
|
||||
<SlideTemplate
|
||||
title="Prepare Swap"
|
||||
stepLabel="Step 2"
|
||||
{...props}
|
||||
imagePath={imagePath}
|
||||
>
|
||||
<Typography variant="subtitle1">
|
||||
To initiate a swap, provide a Monero address and optionally a Bitcoin
|
||||
refund address.
|
||||
</Typography>
|
||||
</SlideTemplate>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
import { Typography } from '@material-ui/core'
|
||||
import SlideTemplate from './SlideTemplate'
|
||||
import imagePath from 'assets/simpleSwapFlowDiagram.svg'
|
||||
import { Typography } from "@mui/material";
|
||||
import SlideTemplate from "./SlideTemplate";
|
||||
import imagePath from "assets/simpleSwapFlowDiagram.svg";
|
||||
|
||||
export default function Slide02_ChooseAMaker(props: slideProps) {
|
||||
return (
|
||||
<SlideTemplate
|
||||
title="Execute Swap"
|
||||
stepLabel="Step 3"
|
||||
{...props}
|
||||
imagePath={imagePath}
|
||||
>
|
||||
<Typography variant="subtitle1">
|
||||
After confirming:
|
||||
</Typography>
|
||||
<Typography>
|
||||
<ol>
|
||||
<li>Your Bitcoin are locked</li>
|
||||
<li>Maker locks the Monero</li>
|
||||
<li>Maker reedems the Bitcoin</li>
|
||||
<li>Monero is sent to your address</li>
|
||||
</ol>
|
||||
</Typography>
|
||||
</SlideTemplate>
|
||||
)
|
||||
return (
|
||||
<SlideTemplate
|
||||
title="Execute Swap"
|
||||
stepLabel="Step 3"
|
||||
{...props}
|
||||
imagePath={imagePath}
|
||||
>
|
||||
<Typography variant="subtitle1">After confirming:</Typography>
|
||||
<Typography>
|
||||
<ol>
|
||||
<li>Your Bitcoin are locked</li>
|
||||
<li>Maker locks the Monero</li>
|
||||
<li>Maker reedems the Bitcoin</li>
|
||||
<li>Monero is sent to your address</li>
|
||||
</ol>
|
||||
</Typography>
|
||||
</SlideTemplate>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import { Link, Typography } from '@material-ui/core'
|
||||
import SlideTemplate from './SlideTemplate'
|
||||
import imagePath from 'assets/mockHistoryPage.svg'
|
||||
import ExternalLink from 'renderer/components/other/ExternalLink'
|
||||
import { Link, Typography } from "@mui/material";
|
||||
import SlideTemplate from "./SlideTemplate";
|
||||
import imagePath from "assets/mockHistoryPage.svg";
|
||||
import ExternalLink from "renderer/components/other/ExternalLink";
|
||||
|
||||
export default function Slide05_KeepAnEyeOnYourSwaps(props: slideProps) {
|
||||
return (
|
||||
<SlideTemplate
|
||||
title="Monitor Your Swaps"
|
||||
stepLabel="Step 3"
|
||||
{...props}
|
||||
imagePath={imagePath}
|
||||
>
|
||||
<Typography>
|
||||
Monitor active swaps to ensure everything proceeds smoothly.
|
||||
</Typography>
|
||||
<Typography>
|
||||
<ExternalLink href='https://docs.unstoppableswap.net/usage/first_swap'>
|
||||
Learn more about atomic swaps
|
||||
</ExternalLink>
|
||||
</Typography>
|
||||
</SlideTemplate>
|
||||
)
|
||||
return (
|
||||
<SlideTemplate
|
||||
title="Monitor Your Swaps"
|
||||
stepLabel="Step 3"
|
||||
{...props}
|
||||
imagePath={imagePath}
|
||||
>
|
||||
<Typography>
|
||||
Monitor active swaps to ensure everything proceeds smoothly.
|
||||
</Typography>
|
||||
<Typography>
|
||||
<ExternalLink href="https://docs.unstoppableswap.net/usage/first_swap">
|
||||
Learn more about atomic swaps
|
||||
</ExternalLink>
|
||||
</Typography>
|
||||
</SlideTemplate>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,53 +1,54 @@
|
|||
import { Box, Typography, Paper, Button, Slide } from '@material-ui/core'
|
||||
import CardSelectionGroup from 'renderer/components/inputs/CardSelection/CardSelectionGroup'
|
||||
import CardSelectionOption from 'renderer/components/inputs/CardSelection/CardSelectionOption'
|
||||
import SlideTemplate from './SlideTemplate'
|
||||
import imagePath from 'assets/currencyFetching.svg'
|
||||
import { Box, Typography, Paper, Button, Slide } from "@mui/material";
|
||||
import CardSelectionGroup from "renderer/components/inputs/CardSelection/CardSelectionGroup";
|
||||
import CardSelectionOption from "renderer/components/inputs/CardSelection/CardSelectionOption";
|
||||
import SlideTemplate from "./SlideTemplate";
|
||||
import imagePath from "assets/currencyFetching.svg";
|
||||
|
||||
const FiatPricePreferenceSlide = ({
|
||||
handleContinue,
|
||||
handlePrevious,
|
||||
showFiat,
|
||||
onChange,
|
||||
handleContinue,
|
||||
handlePrevious,
|
||||
showFiat,
|
||||
onChange,
|
||||
}: slideProps & {
|
||||
showFiat: boolean
|
||||
onChange: (value: string) => void
|
||||
showFiat: boolean;
|
||||
onChange: (value: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<SlideTemplate handleContinue={handleContinue} handlePrevious={handlePrevious} title="Fiat Prices" imagePath={imagePath}>
|
||||
<Typography variant="subtitle1" color="textSecondary">
|
||||
Do you want to show fiat prices?
|
||||
</Typography>
|
||||
<CardSelectionGroup
|
||||
value={showFiat ? 'show' : 'hide'}
|
||||
onChange={onChange}
|
||||
>
|
||||
<CardSelectionOption value="show">
|
||||
<Typography>Show fiat prices</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
paragraph
|
||||
style={{ marginBottom: 4 }}
|
||||
>
|
||||
We connect to CoinGecko to provide realtime currency
|
||||
prices.
|
||||
</Typography>
|
||||
</CardSelectionOption>
|
||||
<CardSelectionOption value="hide">
|
||||
<Typography>Don't show fiat prices</Typography>
|
||||
</CardSelectionOption>
|
||||
</CardSelectionGroup>
|
||||
<Box style={{ marginTop: "0.5rem" }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
>
|
||||
You can change your preference later in the settings
|
||||
</Typography>
|
||||
</Box>
|
||||
</SlideTemplate>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<SlideTemplate
|
||||
handleContinue={handleContinue}
|
||||
handlePrevious={handlePrevious}
|
||||
title="Fiat Prices"
|
||||
imagePath={imagePath}
|
||||
>
|
||||
<Typography variant="subtitle1" color="textSecondary">
|
||||
Do you want to show fiat prices?
|
||||
</Typography>
|
||||
<CardSelectionGroup
|
||||
value={showFiat ? "show" : "hide"}
|
||||
onChange={onChange}
|
||||
>
|
||||
<CardSelectionOption value="show">
|
||||
<Typography>Show fiat prices</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
paragraph
|
||||
style={{ marginBottom: 4 }}
|
||||
>
|
||||
We connect to CoinGecko to provide realtime currency prices.
|
||||
</Typography>
|
||||
</CardSelectionOption>
|
||||
<CardSelectionOption value="hide">
|
||||
<Typography>Don't show fiat prices</Typography>
|
||||
</CardSelectionOption>
|
||||
</CardSelectionGroup>
|
||||
<Box style={{ marginTop: "0.5rem" }}>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
You can change your preference later in the settings
|
||||
</Typography>
|
||||
</Box>
|
||||
</SlideTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default FiatPricePreferenceSlide
|
||||
export default FiatPricePreferenceSlide;
|
||||
|
|
|
@ -1,25 +1,34 @@
|
|||
import { Box, Typography } from '@material-ui/core'
|
||||
import SlideTemplate from './SlideTemplate'
|
||||
import imagePath from 'assets/groupWithChatbubbles.png'
|
||||
import GitHubIcon from "@material-ui/icons/GitHub"
|
||||
import MatrixIcon from 'renderer/components/icons/MatrixIcon'
|
||||
import LinkIconButton from 'renderer/components/icons/LinkIconButton'
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import SlideTemplate from "./SlideTemplate";
|
||||
import imagePath from "assets/groupWithChatbubbles.png";
|
||||
import GitHubIcon from "@mui/icons-material/GitHub";
|
||||
import MatrixIcon from "renderer/components/icons/MatrixIcon";
|
||||
import LinkIconButton from "renderer/components/icons/LinkIconButton";
|
||||
|
||||
export default function Slide02_ChooseAMaker(props: slideProps) {
|
||||
return (
|
||||
<SlideTemplate title="Reach out" {...props} imagePath={imagePath} customContinueButtonText="Get Started">
|
||||
<Typography variant="subtitle1">
|
||||
We would love to hear about your experience with Unstoppable
|
||||
Swap and invite you to join our community.
|
||||
</Typography>
|
||||
<Box mt={3}>
|
||||
<LinkIconButton url="https://github.com/UnstoppableSwap/core">
|
||||
<GitHubIcon/>
|
||||
</LinkIconButton>
|
||||
<LinkIconButton url="https://matrix.to/#/#unstoppableswap:matrix.org">
|
||||
<MatrixIcon/>
|
||||
</LinkIconButton>
|
||||
</Box>
|
||||
</SlideTemplate>
|
||||
)
|
||||
return (
|
||||
<SlideTemplate
|
||||
title="Reach out"
|
||||
{...props}
|
||||
imagePath={imagePath}
|
||||
customContinueButtonText="Get Started"
|
||||
>
|
||||
<Typography variant="subtitle1">
|
||||
We would love to hear about your experience with Unstoppable Swap and
|
||||
invite you to join our community.
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
mt: 3,
|
||||
}}
|
||||
>
|
||||
<LinkIconButton url="https://github.com/UnstoppableSwap/core">
|
||||
<GitHubIcon />
|
||||
</LinkIconButton>
|
||||
<LinkIconButton url="https://matrix.to/#/#unstoppableswap:matrix.org">
|
||||
<MatrixIcon />
|
||||
</LinkIconButton>
|
||||
</Box>
|
||||
</SlideTemplate>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,94 +1,94 @@
|
|||
import { makeStyles, Paper, Box, Typography, Button } from '@material-ui/core'
|
||||
import { Paper, Box, Typography, Button } from "@mui/material";
|
||||
|
||||
type slideTemplateProps = {
|
||||
handleContinue: () => void
|
||||
handlePrevious: () => void
|
||||
hidePreviousButton?: boolean
|
||||
stepLabel?: String
|
||||
title: String
|
||||
children?: React.ReactNode
|
||||
imagePath?: string
|
||||
imagePadded?: boolean
|
||||
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'
|
||||
}
|
||||
})
|
||||
handleContinue: () => void;
|
||||
handlePrevious: () => void;
|
||||
hidePreviousButton?: boolean;
|
||||
stepLabel?: string;
|
||||
title: string;
|
||||
children?: React.ReactNode;
|
||||
imagePath?: string;
|
||||
imagePadded?: boolean;
|
||||
customContinueButtonText?: string;
|
||||
};
|
||||
|
||||
export default function SlideTemplate({
|
||||
handleContinue,
|
||||
handlePrevious,
|
||||
hidePreviousButton,
|
||||
stepLabel,
|
||||
title,
|
||||
children,
|
||||
imagePath,
|
||||
imagePadded,
|
||||
customContinueButtonText
|
||||
handleContinue,
|
||||
handlePrevious,
|
||||
hidePreviousButton,
|
||||
stepLabel,
|
||||
title,
|
||||
children,
|
||||
imagePath,
|
||||
imagePadded,
|
||||
customContinueButtonText,
|
||||
}: slideTemplateProps) {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Paper className={classes.paper}>
|
||||
<Box m={3} flex alignContent="center" position="relative" width="50%" flexGrow={1}>
|
||||
<Box>
|
||||
{stepLabel && (
|
||||
<Typography
|
||||
variant="overline"
|
||||
className={classes.stepLabel}
|
||||
>
|
||||
{stepLabel}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="h4" style={{ marginBottom: 16 }}>{title}</Typography>
|
||||
{children}
|
||||
</Box>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
width="100%"
|
||||
display="flex"
|
||||
justifyContent={
|
||||
hidePreviousButton ? 'flex-end' : 'space-between'
|
||||
}
|
||||
>
|
||||
{!hidePreviousButton && (
|
||||
<Button onClick={handlePrevious}>Back</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
>
|
||||
{customContinueButtonText ? customContinueButtonText : 'Next' }
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
{imagePath && (
|
||||
<Box
|
||||
bgcolor="#212121"
|
||||
width="50%"
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
p={imagePadded ? "1.5em" : 0}
|
||||
>
|
||||
<img src={imagePath} className={classes.splitImage} />
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
)
|
||||
return (
|
||||
<Paper
|
||||
sx={{
|
||||
height: "80%",
|
||||
width: "80%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
m: 3,
|
||||
alignContent: "center",
|
||||
position: "relative",
|
||||
width: "50%",
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
{stepLabel && (
|
||||
<Typography variant="overline" sx={{ textTransform: "uppercase" }}>
|
||||
{stepLabel}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="h4" style={{ marginBottom: 16 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{children}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: hidePreviousButton ? "flex-end" : "space-between",
|
||||
}}
|
||||
>
|
||||
{!hidePreviousButton && (
|
||||
<Button onClick={handlePrevious}>Back</Button>
|
||||
)}
|
||||
<Button onClick={handleContinue} variant="contained" color="primary">
|
||||
{customContinueButtonText ? customContinueButtonText : "Next"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
{imagePath && (
|
||||
<Box
|
||||
sx={{
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
type slideProps = {
|
||||
handleContinue: () => void
|
||||
handlePrevious: () => void
|
||||
hidePreviousButton?: boolean
|
||||
}
|
||||
handleContinue: () => void;
|
||||
handlePrevious: () => void;
|
||||
hidePreviousButton?: boolean;
|
||||
};
|
||||
|
|
|
@ -7,28 +7,21 @@ import {
|
|||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
makeStyles,
|
||||
TextField,
|
||||
Theme,
|
||||
} from "@material-ui/core";
|
||||
} from "@mui/material";
|
||||
import { ListSellersResponse } from "models/tauriModel";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||
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 { useAppDispatch } from "store/hooks";
|
||||
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
chipOuter: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
type ListSellersDialogProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
|
@ -38,7 +31,6 @@ export default function ListSellersDialog({
|
|||
open,
|
||||
onClose,
|
||||
}: ListSellersDialogProps) {
|
||||
const classes = useStyles();
|
||||
const [rendezvousAddress, setRendezvousAddress] = useState("");
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const dispatch = useAppDispatch();
|
||||
|
@ -101,7 +93,7 @@ export default function ListSellersDialog({
|
|||
placeholder="/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE"
|
||||
error={!!getMultiAddressError()}
|
||||
/>
|
||||
<Box className={classes.chipOuter}>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
|
||||
{PRESET_RENDEZVOUS_POINTS.map((rAddress) => (
|
||||
<Chip
|
||||
key={rAddress}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Box, Chip, makeStyles, Paper, Tooltip, Typography } from "@material-ui/core";
|
||||
import { VerifiedUser } from "@material-ui/icons";
|
||||
import { Box, Chip, Paper, Tooltip, Typography } from "@mui/material";
|
||||
import { VerifiedUser } from "@mui/icons-material";
|
||||
import { ExtendedMakerStatus } from "models/apiModel";
|
||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||
import {
|
||||
|
@ -7,44 +7,17 @@ import {
|
|||
SatsAmount,
|
||||
} from "renderer/components/other/Units";
|
||||
import { getMarkup, satsToBtc, secondsToDays } from "utils/conversionUtils";
|
||||
import { isMakerOutdated, isMakerVersionOutdated } from 'utils/multiAddrUtils';
|
||||
import WarningIcon from '@material-ui/icons/Warning';
|
||||
import { useAppSelector, useMakerVersion } from "store/hooks";
|
||||
import { isMakerOutdated, isMakerVersionOutdated } from "utils/multiAddrUtils";
|
||||
import WarningIcon from "@mui/icons-material/Warning";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
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.
|
||||
*/
|
||||
function MakerMarkupChip({ maker }: { maker: ExtendedMakerStatus }) {
|
||||
const marketExchangeRate = useAppSelector(s => s.rates?.xmrBtcRate);
|
||||
if (marketExchangeRate == null)
|
||||
return null;
|
||||
const marketExchangeRate = useAppSelector((s) => s.rates?.xmrBtcRate);
|
||||
if (marketExchangeRate == null) return null;
|
||||
|
||||
const makerExchangeRate = satsToBtc(maker.price);
|
||||
/** 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({
|
||||
maker,
|
||||
}: {
|
||||
maker: ExtendedMakerStatus;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
export default function MakerInfo({ maker }: { maker: ExtendedMakerStatus }) {
|
||||
const isOutdated = isMakerOutdated(maker);
|
||||
|
||||
return (
|
||||
<Box className={classes.content}>
|
||||
<Box className={classes.peerIdContainer}>
|
||||
<Tooltip title={"This avatar is deterministically derived from the public key of the maker"} arrow>
|
||||
<Box className={classes.peerIdContainer}>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
"& *": {
|
||||
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"} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Box>
|
||||
<Typography variant="subtitle1">
|
||||
<TruncatedText limit={16} truncateMiddle>{maker.peerId}</TruncatedText>
|
||||
<TruncatedText limit={16} truncateMiddle>
|
||||
{maker.peerId}
|
||||
</TruncatedText>
|
||||
</Typography>
|
||||
<Typography color="textSecondary" variant="body2">
|
||||
{maker.multiAddr}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className={classes.quoteOuter}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<Typography variant="caption">
|
||||
Exchange rate:{" "}
|
||||
<MoneroBitcoinExchangeRate rate={satsToBtc(maker.price)} />
|
||||
|
@ -94,7 +79,7 @@ export default function MakerInfo({
|
|||
Maximum amount: <SatsAmount amount={maker.maxSwapAmount} />
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box className={classes.chipsOuter}>
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
|
||||
{maker.testnet && <Chip label="Testnet" />}
|
||||
{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.">
|
||||
|
@ -103,8 +88,9 @@ export default function MakerInfo({
|
|||
)}
|
||||
{maker.age ? (
|
||||
<Chip
|
||||
label={`Went online ${Math.round(secondsToDays(maker.age))} ${maker.age === 1 ? "day" : "days"
|
||||
} ago`}
|
||||
label={`Went online ${Math.round(secondsToDays(maker.age))} ${
|
||||
maker.age === 1 ? "day" : "days"
|
||||
} ago`}
|
||||
/>
|
||||
) : (
|
||||
<Chip label="Discovered via rendezvous point" />
|
||||
|
@ -121,7 +107,6 @@ export default function MakerInfo({
|
|||
)}
|
||||
<MakerMarkupChip maker={maker} />
|
||||
</Box>
|
||||
</Box >
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,13 +6,11 @@ import {
|
|||
DialogContent,
|
||||
DialogTitle,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
makeStyles,
|
||||
} from "@material-ui/core";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
} from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import { ExtendedMakerStatus } from "models/apiModel";
|
||||
import { useState } from "react";
|
||||
import { setSelectedMaker } from "store/features/makersSlice";
|
||||
|
@ -21,11 +19,7 @@ import ListSellersDialog from "../listSellers/ListSellersDialog";
|
|||
import MakerInfo from "./MakerInfo";
|
||||
import MakerSubmitDialog from "./MakerSubmitDialog";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
dialogContent: {
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
|
||||
type MakerSelectDialogProps = {
|
||||
open: boolean;
|
||||
|
@ -36,9 +30,8 @@ export function MakerSubmitDialogOpenButton() {
|
|||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
<ListItemButton
|
||||
autoFocus
|
||||
button
|
||||
onClick={() => {
|
||||
// Prevents background from being clicked and reopening dialog
|
||||
if (!open) {
|
||||
|
@ -53,7 +46,7 @@ export function MakerSubmitDialogOpenButton() {
|
|||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Add a new maker to public registry" />
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -61,9 +54,8 @@ export function ListSellersDialogOpenButton() {
|
|||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
<ListItemButton
|
||||
autoFocus
|
||||
button
|
||||
onClick={() => {
|
||||
// Prevents background from being clicked and reopening dialog
|
||||
if (!open) {
|
||||
|
@ -78,7 +70,7 @@ export function ListSellersDialogOpenButton() {
|
|||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Discover makers by connecting to a rendezvous point" />
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -86,7 +78,6 @@ export default function MakerListDialog({
|
|||
open,
|
||||
onClose,
|
||||
}: MakerSelectDialogProps) {
|
||||
const classes = useStyles();
|
||||
const makers = useAllMakers();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -98,23 +89,20 @@ export default function MakerListDialog({
|
|||
return (
|
||||
<Dialog onClose={onClose} open={open}>
|
||||
<DialogTitle>Select a maker</DialogTitle>
|
||||
|
||||
<DialogContent className={classes.dialogContent} dividers>
|
||||
<DialogContent sx={{ padding: 0 }} dividers>
|
||||
<List>
|
||||
{makers.map((maker) => (
|
||||
<ListItem
|
||||
button
|
||||
<ListItemButton
|
||||
onClick={() => handleMakerChange(maker)}
|
||||
key={maker.peerId}
|
||||
>
|
||||
<MakerInfo maker={maker} key={maker.peerId} />
|
||||
</ListItem>
|
||||
</ListItemButton>
|
||||
))}
|
||||
<ListSellersDialogOpenButton />
|
||||
<MakerSubmitDialogOpenButton />
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
</DialogActions>
|
||||
|
|
|
@ -1,37 +1,13 @@
|
|||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
} from "@material-ui/core";
|
||||
import ArrowForwardIosIcon from "@material-ui/icons/ArrowForwardIos";
|
||||
import { Paper, Card, CardContent, IconButton } from "@mui/material";
|
||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
||||
import { useState } from "react";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import MakerInfo from "./MakerInfo";
|
||||
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() {
|
||||
const classes = useStyles();
|
||||
const [selectDialogOpen, setSelectDialogOpen] = useState(false);
|
||||
const selectedMaker = useAppSelector(
|
||||
(state) => state.makers.selectedMaker,
|
||||
);
|
||||
const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
|
||||
|
||||
if (!selectedMaker) return <>No maker selected</>;
|
||||
|
||||
|
@ -44,19 +20,19 @@ export default function MakerSelect() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Paper variant="outlined" elevation={4}>
|
||||
<MakerListDialog
|
||||
open={selectDialogOpen}
|
||||
onClose={handleSelectDialogClose}
|
||||
/>
|
||||
<Card variant="outlined" className={classes.makerCard}>
|
||||
<CardContent className={classes.makerCardContent}>
|
||||
<Card sx={{ width: "100%" }}>
|
||||
<CardContent sx={{ display: "flex", alignItems: "center" }}>
|
||||
<MakerInfo maker={selectedMaker} />
|
||||
<IconButton onClick={handleSelectDialogOpen} size="small">
|
||||
<ArrowForwardIosIcon />
|
||||
</IconButton>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
DialogContentText,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
} from "@material-ui/core";
|
||||
} from "@mui/material";
|
||||
import { Multiaddr } from "multiaddr";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
|
||||
|
@ -68,8 +68,8 @@ export default function MakerSubmitDialog({
|
|||
<DialogTitle>Submit a maker to the public registry</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<DialogContentText>
|
||||
If the maker is valid and reachable, it will be displayed to all
|
||||
other users to trade with.
|
||||
If the maker is valid and reachable, it will be displayed to all other
|
||||
users to trade with.
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Box } from "@material-ui/core";
|
||||
import { Box } from "@mui/material";
|
||||
import QRCode from "react-qr-code";
|
||||
|
||||
export default function BitcoinQrCode({ address }: { address: string }) {
|
||||
|
|
|
@ -2,33 +2,26 @@ import {
|
|||
Box,
|
||||
CircularProgress,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
} from "@mui/material";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
subtitle: {
|
||||
paddingTop: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function CircularProgressWithSubtitle({
|
||||
description,
|
||||
}: {
|
||||
description: string | ReactNode;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
flexDirection="column"
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={50} />
|
||||
<Typography variant="subtitle2" className={classes.subtitle}>
|
||||
<Typography variant="subtitle2" sx={{ paddingTop: 1 }}>
|
||||
{description}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
@ -42,16 +35,26 @@ export function LinearProgressWithSubtitle({
|
|||
description: string | ReactNode;
|
||||
value: number;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" alignItems="center" justifyContent="center" style={{ gap: "0.5rem" }}>
|
||||
<Typography variant="subtitle2" className={classes.subtitle}>
|
||||
<Box
|
||||
style={{ gap: "0.5rem" }}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ paddingTop: 1 }}>
|
||||
{description}
|
||||
</Typography>
|
||||
<Box width="10rem">
|
||||
<Box
|
||||
sx={{
|
||||
width: "10rem",
|
||||
}}
|
||||
>
|
||||
<LinearProgress variant="determinate" value={value} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Button } from "@material-ui/core";
|
||||
import { ButtonProps } from "@material-ui/core/Button/Button";
|
||||
import Button, { ButtonProps } from "@mui/material/Button";
|
||||
|
||||
export default function ClipboardIconButton({
|
||||
text,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Box } from "@material-ui/core";
|
||||
import { Box } from "@mui/material";
|
||||
import { ReactNode } from "react";
|
||||
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
||||
import InfoBox from "./InfoBox";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import { Alert } from "@mui/material";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
|
@ -20,7 +20,13 @@ export default function DepositAddressInfoBox({
|
|||
return (
|
||||
<InfoBox
|
||||
title={title}
|
||||
mainContent={<ActionableMonospaceTextBox content={address} displayCopyIcon={true} enableQrCode={true} />}
|
||||
mainContent={
|
||||
<ActionableMonospaceTextBox
|
||||
content={address}
|
||||
displayCopyIcon={true}
|
||||
enableQrCode={true}
|
||||
/>
|
||||
}
|
||||
additionalContent={
|
||||
<Box
|
||||
style={{
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
Box,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { Box, LinearProgress, Paper, Typography } from "@mui/material";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
|
@ -16,21 +10,6 @@ type Props = {
|
|||
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({
|
||||
id = null,
|
||||
title,
|
||||
|
@ -39,12 +18,20 @@ export default function InfoBox({
|
|||
icon,
|
||||
loading,
|
||||
}: Props) {
|
||||
const classes = useStyles();
|
||||
|
||||
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>
|
||||
<Box className={classes.upperContent}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
{icon}
|
||||
{mainContent}
|
||||
</Box>
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
makeStyles,
|
||||
} from "@material-ui/core";
|
||||
import { Button, Dialog, DialogActions, DialogContent } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { swapReset } from "store/features/swapSlice";
|
||||
import { useAppDispatch, useAppSelector, useIsSwapRunning } from "store/hooks";
|
||||
|
@ -14,15 +8,6 @@ import SwapStatePage from "./pages/SwapStatePage";
|
|||
import SwapDialogTitle from "./SwapDialogTitle";
|
||||
import SwapStateStepper from "./SwapStateStepper";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
content: {
|
||||
minHeight: "25rem",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
});
|
||||
|
||||
export default function SwapDialog({
|
||||
open,
|
||||
onClose,
|
||||
|
@ -30,8 +15,6 @@ export default function SwapDialog({
|
|||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
|
||||
const swap = useAppSelector((state) => state.swap);
|
||||
const isSwapRunning = useIsSwapRunning();
|
||||
const [debug, setDebug] = useState(false);
|
||||
|
@ -59,7 +42,15 @@ export default function SwapDialog({
|
|||
title="Swap Bitcoin for Monero"
|
||||
/>
|
||||
|
||||
<DialogContent dividers className={classes.content}>
|
||||
<DialogContent
|
||||
dividers
|
||||
sx={{
|
||||
minHeight: "25rem",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{debug ? (
|
||||
<DebugPage />
|
||||
) : (
|
||||
|
|
|
@ -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 FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge";
|
||||
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({
|
||||
title,
|
||||
debug,
|
||||
|
@ -25,12 +12,16 @@ export default function SwapDialogTitle({
|
|||
debug: boolean;
|
||||
setDebug: (d: boolean) => void;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<DialogTitle disableTypography className={classes.root}>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
<Box className={classes.rightSide}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gridGap: 1 }}>
|
||||
<FeedbackSubmitBadge />
|
||||
<DebugPageSwitchBadge enabled={debug} setEnabled={setDebug} />
|
||||
<TorStatusBadge />
|
||||
|
|
|
@ -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 { useAppSelector } from "store/hooks";
|
||||
import logger from "utils/logger";
|
||||
|
@ -119,7 +119,7 @@ function getActiveStep(state: SwapState | null): PathStep | null {
|
|||
default:
|
||||
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.
|
||||
// return exhaustiveGuard(latestState.type);
|
||||
// return exhaustiveGuard(latestState.type);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Link, Typography } from "@material-ui/core";
|
||||
import { Link, Typography } from "@mui/material";
|
||||
import { ReactNode } from "react";
|
||||
import InfoBox from "./InfoBox";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, DialogContentText } from "@material-ui/core";
|
||||
import { Box, DialogContentText } from "@mui/material";
|
||||
import {
|
||||
useActiveSwapInfo,
|
||||
useActiveSwapLogs,
|
||||
|
@ -35,7 +35,10 @@ export default function DebugPage() {
|
|||
data={cliState}
|
||||
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>
|
||||
</DialogContentText>
|
||||
</Box>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Tooltip } from "@material-ui/core";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import DeveloperBoardIcon from "@material-ui/icons/DeveloperBoard";
|
||||
import { Tooltip } from "@mui/material";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import DeveloperBoardIcon from "@mui/icons-material/DeveloperBoard";
|
||||
|
||||
export default function DebugPageSwitchBadge({
|
||||
enabled,
|
||||
|
@ -18,6 +18,7 @@ export default function DebugPageSwitchBadge({
|
|||
<IconButton
|
||||
onClick={handleToggle}
|
||||
color={enabled ? "primary" : "default"}
|
||||
size="large"
|
||||
>
|
||||
<DeveloperBoardIcon />
|
||||
</IconButton>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { IconButton } from "@material-ui/core";
|
||||
import FeedbackIcon from "@material-ui/icons/Feedback";
|
||||
import { IconButton } from "@mui/material";
|
||||
import FeedbackIcon from "@mui/icons-material/Feedback";
|
||||
import { useState } from "react";
|
||||
import FeedbackDialog from "../../feedback/FeedbackDialog";
|
||||
|
||||
|
@ -14,7 +14,7 @@ export default function FeedbackSubmitBadge() {
|
|||
onClose={() => setShowFeedbackDialog(false)}
|
||||
/>
|
||||
)}
|
||||
<IconButton onClick={() => setShowFeedbackDialog(true)}>
|
||||
<IconButton onClick={() => setShowFeedbackDialog(true)} size="large">
|
||||
<FeedbackIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Box } from "@material-ui/core";
|
||||
import { Box } from "@mui/material";
|
||||
import { SwapState } from "models/storeModel";
|
||||
import CircularProgressWithSubtitle from "../CircularProgressWithSubtitle";
|
||||
import BitcoinPunishedPage from "./done/BitcoinPunishedPage";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IconButton, Tooltip } from "@material-ui/core";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import TorIcon from "../../../icons/TorIcon";
|
||||
|
||||
|
@ -8,7 +8,7 @@ export default function TorStatusBadge() {
|
|||
if (tor.processRunning) {
|
||||
return (
|
||||
<Tooltip title="Tor is running in the background">
|
||||
<IconButton>
|
||||
<IconButton size="large">
|
||||
<TorIcon htmlColor="#7D4698" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { Box, DialogContentText } from '@material-ui/core';
|
||||
import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox';
|
||||
import { TauriSwapProgressEventExt } from 'models/tauriModelExt';
|
||||
import { Box, DialogContentText } from "@mui/material";
|
||||
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
||||
import { TauriSwapProgressEventExt } from "models/tauriModelExt";
|
||||
|
||||
export default function BitcoinPunishedPage({
|
||||
state,
|
||||
}: {
|
||||
state: TauriSwapProgressEventExt<"BtcPunished"> | TauriSwapProgressEventExt<"CooperativeRedeemRejected">
|
||||
state:
|
||||
| TauriSwapProgressEventExt<"BtcPunished">
|
||||
| TauriSwapProgressEventExt<"CooperativeRedeemRejected">;
|
||||
}) {
|
||||
return (
|
||||
<Box>
|
||||
|
@ -13,13 +15,13 @@ export default function BitcoinPunishedPage({
|
|||
Unfortunately, the swap was unsuccessful. Since you did not refund in
|
||||
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
|
||||
is not guaranteed.{' '}
|
||||
is not guaranteed.{" "}
|
||||
{state.type === "CooperativeRedeemRejected" && (
|
||||
<>
|
||||
<br />
|
||||
We tried to redeem the Monero with the other party's help, but it
|
||||
was unsuccessful (reason: {state.content.reason}). Attempting again at a
|
||||
later time might yield success. <br />
|
||||
was unsuccessful (reason: {state.content.reason}). Attempting again
|
||||
at a later time might yield success. <br />
|
||||
</>
|
||||
)}
|
||||
</DialogContentText>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, DialogContentText } from "@material-ui/core";
|
||||
import { Box, DialogContentText } from "@mui/material";
|
||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||
import { useActiveSwapInfo } from "store/hooks";
|
||||
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, DialogContentText } from "@material-ui/core";
|
||||
import { Box, DialogContentText } from "@mui/material";
|
||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
||||
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, DialogContentText } from "@material-ui/core";
|
||||
import { Box, DialogContentText } from "@mui/material";
|
||||
import { TauriSwapProgressEvent } from "models/tauriModel";
|
||||
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
||||
import { useActiveSwapInfo, useActiveSwapLogs } from "store/hooks";
|
||||
|
|
|
@ -2,7 +2,7 @@ import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
|||
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
|
||||
import SwapStatusAlert from "renderer/components/alert/SwapStatusAlert/SwapStatusAlert";
|
||||
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
|
||||
const BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD = 2;
|
||||
|
@ -17,17 +17,19 @@ export default function BitcoinLockTxInMempoolPage({
|
|||
<Box>
|
||||
{btc_lock_confirmations < BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
|
||||
<DialogContentText>
|
||||
Your Bitcoin has been locked. {btc_lock_confirmations > 0 ?
|
||||
"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."
|
||||
}
|
||||
Your Bitcoin has been locked.{" "}
|
||||
{btc_lock_confirmations > 0
|
||||
? "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>
|
||||
)}
|
||||
<Box style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1rem",
|
||||
}}>
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
{btc_lock_confirmations >= BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
|
||||
<SwapStatusAlert swap={swapInfo} isRunning={true} />
|
||||
)}
|
||||
|
@ -37,9 +39,9 @@ export default function BitcoinLockTxInMempoolPage({
|
|||
loading
|
||||
additionalContent={
|
||||
<>
|
||||
Most makers require one confirmation before locking their
|
||||
Monero. After they lock their funds and the Monero transaction
|
||||
receives one confirmation, the swap will proceed to the next step.
|
||||
Most makers require one confirmation before locking their Monero.
|
||||
After they lock their funds and the Monero transaction receives
|
||||
one confirmation, the swap will proceed to the next step.
|
||||
<br />
|
||||
Confirmations: {btc_lock_confirmations}
|
||||
</>
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
import { useConservativeBitcoinSyncProgress, usePendingBackgroundProcesses } from "store/hooks";
|
||||
import CircularProgressWithSubtitle, { LinearProgressWithSubtitle } from "../../CircularProgressWithSubtitle";
|
||||
import {
|
||||
useConservativeBitcoinSyncProgress,
|
||||
usePendingBackgroundProcesses,
|
||||
} from "store/hooks";
|
||||
import CircularProgressWithSubtitle, {
|
||||
LinearProgressWithSubtitle,
|
||||
} from "../../CircularProgressWithSubtitle";
|
||||
|
||||
export default function ReceivedQuotePage() {
|
||||
const syncProgress = useConservativeBitcoinSyncProgress();
|
||||
|
||||
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 (
|
||||
<LinearProgressWithSubtitle description={`Syncing Bitcoin wallet (${percentage}%)`} value={percentage} />
|
||||
<LinearProgressWithSubtitle
|
||||
description={`Syncing Bitcoin wallet (${percentage}%)`}
|
||||
value={percentage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,51 +1,20 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { resolveApproval } from 'renderer/rpc';
|
||||
import { PendingLockBitcoinApprovalRequest, TauriSwapProgressEventContent } from 'models/tauriModelExt';
|
||||
import { useState, useEffect } from "react";
|
||||
import { resolveApproval } from "renderer/rpc";
|
||||
import {
|
||||
PendingLockBitcoinApprovalRequest,
|
||||
TauriSwapProgressEventContent,
|
||||
} from "models/tauriModelExt";
|
||||
import {
|
||||
SatsAmount,
|
||||
PiconeroAmount,
|
||||
MoneroBitcoinExchangeRateFromAmounts
|
||||
} from 'renderer/components/other/Units';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Divider,
|
||||
} from '@material-ui/core';
|
||||
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
|
||||
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,
|
||||
},
|
||||
})
|
||||
);
|
||||
MoneroBitcoinExchangeRateFromAmounts,
|
||||
} from "renderer/components/other/Units";
|
||||
import { Box, Typography, Divider } from "@mui/material";
|
||||
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 "@mui/icons-material/Check";
|
||||
|
||||
/// A hook that returns the LockBitcoin confirmation request for the active swap
|
||||
/// Returns null if no confirmation request is found
|
||||
|
@ -53,14 +22,16 @@ function useActiveLockBitcoinApprovalRequest(): PendingLockBitcoinApprovalReques
|
|||
const approvals = usePendingLockBitcoinApproval();
|
||||
const activeSwapId = useActiveSwapId();
|
||||
|
||||
return approvals
|
||||
?.find(r => r.content.details.content.swap_id === activeSwapId) || null;
|
||||
return (
|
||||
approvals?.find(
|
||||
(r) => r.content.details.content.swap_id === activeSwapId,
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
export default function SwapSetupInflightPage({
|
||||
btc_lock_amount,
|
||||
}: TauriSwapProgressEventContent<'SwapSetupInflight'>) {
|
||||
const classes = useStyles();
|
||||
}: TauriSwapProgressEventContent<"SwapSetupInflight">) {
|
||||
const request = useActiveLockBitcoinApprovalRequest();
|
||||
|
||||
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
|
||||
// Display a loading spinner to the user for as long as the swap_setup request is in flight
|
||||
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 (
|
||||
<InfoBox
|
||||
|
@ -94,23 +74,53 @@ export default function SwapSetupInflightPage({
|
|||
mainContent={
|
||||
<>
|
||||
<Divider />
|
||||
<Box className={classes.detailGrid}>
|
||||
<Typography className={classes.label}>You send</Typography>
|
||||
<Box
|
||||
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>
|
||||
<SatsAmount amount={btc_lock_amount} />
|
||||
</Typography>
|
||||
|
||||
<Typography className={classes.label}>Bitcoin network fees</Typography>
|
||||
<Typography
|
||||
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||
>
|
||||
Bitcoin network fees
|
||||
</Typography>
|
||||
<Typography>
|
||||
<SatsAmount amount={btc_network_fee} />
|
||||
</Typography>
|
||||
|
||||
<Typography className={classes.label}>You receive</Typography>
|
||||
<Typography className={classes.receiveValue}>
|
||||
<Typography
|
||||
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} />
|
||||
</Typography>
|
||||
|
||||
<Typography className={classes.label}>Exchange rate</Typography>
|
||||
<Typography
|
||||
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||
>
|
||||
Exchange rate
|
||||
</Typography>
|
||||
<Typography>
|
||||
<MoneroBitcoinExchangeRateFromAmounts
|
||||
satsAmount={btc_lock_amount}
|
||||
|
@ -122,11 +132,18 @@ export default function SwapSetupInflightPage({
|
|||
</>
|
||||
}
|
||||
additionalContent={
|
||||
<Box className={classes.actions}>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 2,
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<PromiseInvokeButton
|
||||
variant="text"
|
||||
size="large"
|
||||
className={classes.cancelButton}
|
||||
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||
onInvoke={() => resolveApproval(request.content.request_id, false)}
|
||||
displayErrorSnackbar
|
||||
requiresContext
|
||||
|
@ -149,4 +166,4 @@ export default function SwapSetupInflightPage({
|
|||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, DialogContentText } from "@material-ui/core";
|
||||
import { Box, DialogContentText } from "@mui/material";
|
||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
|
||||
|
||||
|
|
|
@ -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 { useState } from "react";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
|
@ -7,23 +7,6 @@ import { MoneroAmount } from "../../../../other/Units";
|
|||
|
||||
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) {
|
||||
return amount - fees;
|
||||
}
|
||||
|
@ -39,7 +22,6 @@ export default function DepositAmountHelper({
|
|||
min_bitcoin_lock_tx_fee: number;
|
||||
quote: BidQuote;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
const [amount, setAmount] = useState(min_deposit_until_swap_will_start);
|
||||
const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0;
|
||||
|
||||
|
@ -70,7 +52,13 @@ export default function DepositAmountHelper({
|
|||
}
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2">
|
||||
Depositing {bitcoinBalance > 0 && <>another</>}
|
||||
</Typography>
|
||||
|
@ -80,7 +68,16 @@ export default function DepositAmountHelper({
|
|||
onChange={(e) => setAmount(btcToSats(parseFloat(e.target.value)))}
|
||||
size="small"
|
||||
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">
|
||||
BTC will give you approximately{" "}
|
||||
|
|
|
@ -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}) `}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,12 +1,5 @@
|
|||
import {
|
||||
Box,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Tab,
|
||||
Tabs,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
||||
import { Box, Paper, Tab, Tabs, Typography } from "@mui/material";
|
||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||
import { useState } from "react";
|
||||
import RemainingFundsWillBeUsedAlert from "renderer/components/alert/RemainingFundsWillBeUsedAlert";
|
||||
import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField";
|
||||
|
@ -15,20 +8,7 @@ import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
|||
import { buyXmr } from "renderer/rpc";
|
||||
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() {
|
||||
const classes = useStyles();
|
||||
|
||||
const [redeemAddress, setRedeemAddress] = useState("");
|
||||
const [refundAddress, setRefundAddress] = useState("");
|
||||
const [useExternalRefundAddress, setUseExternalRefundAddress] =
|
||||
|
@ -37,9 +17,7 @@ export default function InitPage() {
|
|||
const [redeemAddressValid, setRedeemAddressValid] = useState(false);
|
||||
const [refundAddressValid, setRefundAddressValid] = useState(false);
|
||||
|
||||
const selectedMaker = useAppSelector(
|
||||
(state) => state.makers.selectedMaker,
|
||||
);
|
||||
const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
|
||||
|
||||
async function init() {
|
||||
await buyXmr(
|
||||
|
@ -51,7 +29,13 @@ export default function InitPage() {
|
|||
|
||||
return (
|
||||
<Box>
|
||||
<Box className={classes.fieldsOuter}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<RemainingFundsWillBeUsedAlert />
|
||||
<MoneroAddressTextField
|
||||
label="Monero redeem address"
|
||||
|
@ -104,7 +88,7 @@ export default function InitPage() {
|
|||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
className={classes.initButton}
|
||||
sx={{ marginTop: 1 }}
|
||||
endIcon={<PlayArrowIcon />}
|
||||
onInvoke={init}
|
||||
displayErrorSnackbar
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue