mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-21 00:23:01 -05: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
|
- name: install dprint globally
|
||||||
uses: taiki-e/cache-cargo-install-action@v2
|
uses: taiki-e/cache-cargo-install-action@v2
|
||||||
with:
|
with:
|
||||||
tool: dprint@0.39.1
|
tool: dprint@0.50.0
|
||||||
|
|
||||||
- name: Build Tauri App
|
- name: Build Tauri App
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ jobs:
|
||||||
- name: install dprint globally
|
- name: install dprint globally
|
||||||
uses: taiki-e/cache-cargo-install-action@v2
|
uses: taiki-e/cache-cargo-install-action@v2
|
||||||
with:
|
with:
|
||||||
tool: dprint@0.39.1
|
tool: dprint@0.50.0
|
||||||
|
|
||||||
- uses: tauri-apps/tauri-action@v0
|
- uses: tauri-apps/tauri-action@v0
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
uses: dprint/check@v2.2
|
uses: dprint/check@v2.2
|
||||||
with:
|
with:
|
||||||
dprint-version: 0.39.1
|
dprint-version: 0.50.0
|
||||||
|
|
||||||
- name: Run clippy with default features
|
- name: Run clippy with default features
|
||||||
run: cargo clippy --workspace --all-targets -- -D warnings
|
run: cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
|
|
||||||
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
|
- name: Commit changelog and manifest files
|
||||||
id: make-commit
|
id: make-commit
|
||||||
env:
|
env:
|
||||||
DPRINT_VERSION: "0.39.1"
|
DPRINT_VERSION: "0.50.0"
|
||||||
RUST_TOOLCHAIN: "1.82"
|
RUST_TOOLCHAIN: "1.82"
|
||||||
run: |
|
run: |
|
||||||
rustup component add rustfmt --toolchain "$RUST_TOOLCHAIN-x86_64-unknown-linux-gnu"
|
rustup component add rustfmt --toolchain "$RUST_TOOLCHAIN-x86_64-unknown-linux-gnu"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [ "monero-rpc", "swap", "monero-wallet", "src-tauri" ]
|
members = ["monero-rpc", "monero-wallet", "src-tauri", "swap"]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51
|
# patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,9 @@ For example:
|
||||||
```toml
|
```toml
|
||||||
[network]
|
[network]
|
||||||
rendezvous_point = [
|
rendezvous_point = [
|
||||||
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
|
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
|
||||||
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw",
|
"/dns4/discover2.unstoppableswap.net/tcp/8888/p2p/12D3KooWGRvf7qVQDrNR5nfYD6rKrbgeTi9x8RrbdxbmsPvxL4mw",
|
||||||
"/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU"
|
"/dns4/darkness.su/tcp/8888/p2p/12D3KooWFQAgVVS9t9UgL6v1sLprJVM7am5hFK7vy9iBCCoCBYmU",
|
||||||
]
|
]
|
||||||
external_addresses = ["/dns4/example.com/tcp/9939"]
|
external_addresses = ["/dns4/example.com/tcp/9939"]
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
import Image from 'next/image';
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function Logo() {
|
export default function Logo() {
|
||||||
return <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
return (
|
||||||
<Image src="/favicon.svg" alt="UnstoppableSwap" width={32} height={32} style={{ borderRadius: '20%' }}/>
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
<span>UnstoppableSwap</span>
|
<Image
|
||||||
</div>;
|
src="/favicon.svg"
|
||||||
}
|
alt="UnstoppableSwap"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
style={{ borderRadius: "20%" }}
|
||||||
|
/>
|
||||||
|
<span>UnstoppableSwap</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Table, Td, Th, Tr } from 'nextra/components'
|
import { Table, Td, Th, Tr } from "nextra/components";
|
||||||
|
|
||||||
export default function SwapMakerTable() {
|
export default function SwapMakerTable() {
|
||||||
function satsToBtc(sats) {
|
function satsToBtc(sats) {
|
||||||
|
|
@ -40,9 +40,7 @@ export default function SwapMakerTable() {
|
||||||
<tbody>
|
<tbody>
|
||||||
{makers.map((maker) => (
|
{makers.map((maker) => (
|
||||||
<Tr key={maker.peerId}>
|
<Tr key={maker.peerId}>
|
||||||
<Td>
|
<Td>{maker.testnet ? "Testnet" : "Mainnet"}</Td>
|
||||||
{maker.testnet ? "Testnet" : "Mainnet"}
|
|
||||||
</Td>
|
|
||||||
<Td>{maker.multiAddr}</Td>
|
<Td>{maker.multiAddr}</Td>
|
||||||
<Td>{maker.peerId}</Td>
|
<Td>{maker.peerId}</Td>
|
||||||
<Td>{satsToBtc(maker.minSwapAmount)} BTC</Td>
|
<Td>{satsToBtc(maker.minSwapAmount)} BTC</Td>
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,4 @@
|
||||||
"becoming_a_maker": "Becoming a Maker",
|
"becoming_a_maker": "Becoming a Maker",
|
||||||
"send_feedback": "Send Feedback",
|
"send_feedback": "Send Feedback",
|
||||||
"donate": "Donate"
|
"donate": "Donate"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"overview": "Overview"
|
"overview": "Overview"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"install_instructions": "Installation"
|
"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/))
|
- `cargo` ([installation](https://www.rust-lang.org/tools/install)) and `cargo tauri` ([installation](https://v2.tauri.app/reference/cli/) and [prerequisites](https://v2.tauri.app/start/prerequisites/))
|
||||||
- `node` ([installation](https://nodejs.org/en/download/)) and `yarn` (version 1.22, not 4.x)
|
- `node` ([installation](https://nodejs.org/en/download/)) and `yarn` (version 1.22, not 4.x)
|
||||||
- `dprint` (`cargo install dprint@0.39.1`)
|
- `dprint` (`cargo install dprint@0.50.0`)
|
||||||
- `typeshare` (`cargo install typeshare-cli`)
|
- `typeshare` (`cargo install typeshare-cli`)
|
||||||
|
|
||||||
After that you only need to clone the repository and run the following commands:
|
After that you only need to clone the repository and run the following commands:
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@
|
||||||
"first_swap": "Complete your first swap",
|
"first_swap": "Complete your first swap",
|
||||||
"market_maker_discovery": "Maker discovery",
|
"market_maker_discovery": "Maker discovery",
|
||||||
"refund_punish": "Cancel, Refund and Punish explained"
|
"refund_punish": "Cancel, Refund and Punish explained"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
|
|
@ -18,12 +14,6 @@
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"target": "ES2017"
|
"target": "ES2017"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"next-env.d.ts",
|
"exclude": ["node_modules"]
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
dprint.json
24
dprint.json
|
|
@ -4,25 +4,31 @@
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"markdown": {},
|
"markdown": {},
|
||||||
"exec": {
|
"exec": {
|
||||||
"associations": "**/*.{rs}",
|
"commands": [
|
||||||
"rustfmt": "rustfmt --edition 2021",
|
{
|
||||||
"rustfmt.associations": "**/*.rs"
|
"command": "rustfmt --edition 2021",
|
||||||
|
"exts": ["rs"]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"includes": [
|
"includes": [
|
||||||
"**/*.{md}",
|
"**/*.{md}",
|
||||||
"**/*.{toml}",
|
"**/*.{toml}",
|
||||||
"**/*.{rs}"
|
"**/*.{rs}",
|
||||||
|
"**/*.{js,jsx,ts,tsx,json,css,scss,html}"
|
||||||
],
|
],
|
||||||
"excludes": [
|
"excludes": [
|
||||||
"target/",
|
"target/",
|
||||||
"src-tauri/Cargo.toml",
|
"src-tauri/Cargo.toml",
|
||||||
"monero-sys/monero/",
|
"monero-sys/monero/",
|
||||||
".git/**"
|
".git/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/dist/**"
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"https://plugins.dprint.dev/markdown-0.13.1.wasm",
|
"https://plugins.dprint.dev/markdown-0.18.0.wasm",
|
||||||
"https://github.com/thomaseizinger/dprint-plugin-cargo-toml/releases/download/0.1.0/cargo-toml-0.1.0.wasm",
|
"https://plugins.dprint.dev/toml-0.7.0.wasm",
|
||||||
"https://plugins.dprint.dev/exec-0.3.5.json@d687dda57be0fe9a0088ccdaefa5147649ff24127d8b3ea227536c68ee7abeab",
|
"https://plugins.dprint.dev/exec-0.5.1.json@492414e39dea4dccc07b4af796d2f4efdb89e84bae2bd4e1e924c0cc050855bf",
|
||||||
"https://plugins.dprint.dev/prettier-0.26.6.json@0118376786f37496e41bb19dbcfd1e7214e2dc859a55035c5e54d1107b4c9c57"
|
"https://plugins.dprint.dev/prettier-0.57.0.json@1bc6b449e982d5b91a25a7c59894102d40c5748651a08a095fb3926e64d55a31"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
justfile
13
justfile
|
|
@ -69,6 +69,19 @@ kill_monero_wallet_rpc:
|
||||||
fmt:
|
fmt:
|
||||||
dprint fmt
|
dprint fmt
|
||||||
|
|
||||||
|
# Run eslint for the GUI frontend
|
||||||
|
check_gui_eslint:
|
||||||
|
cd src-gui && yarn run eslint
|
||||||
|
|
||||||
|
# Run the typescript type checker for the GUI frontend
|
||||||
|
check_gui_tsc:
|
||||||
|
cd src-gui && yarn run tsc --noEmit
|
||||||
|
|
||||||
|
# Run the checks for the GUI frontend
|
||||||
|
check_gui:
|
||||||
|
just check_gui_eslint || true
|
||||||
|
just check_gui_tsc
|
||||||
|
|
||||||
# Sometimes you have to prune the docker network to get the integration tests to work
|
# Sometimes you have to prune the docker network to get the integration tests to work
|
||||||
docker-prune-network:
|
docker-prune-network:
|
||||||
docker network prune -f
|
docker network prune -f
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monero-harness"
|
name = "monero-harness"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = [ "CoBloX Team <team@coblox.tech>" ]
|
authors = ["CoBloX Team <team@coblox.tech>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
|
@ -11,6 +11,6 @@ futures = "0.3"
|
||||||
monero-rpc = { path = "../monero-rpc" }
|
monero-rpc = { path = "../monero-rpc" }
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
testcontainers = "0.15"
|
testcontainers = "0.15"
|
||||||
tokio = { version = "1", default-features = false, features = [ "rt-multi-thread", "time", "macros" ] }
|
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "time", "macros"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", "ansi", "env-filter", "tracing-log" ] }
|
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "ansi", "env-filter", "tracing-log"] }
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monero-rpc"
|
name = "monero-rpc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = [ "CoBloX Team <team@coblox.tech>" ]
|
authors = ["CoBloX Team <team@coblox.tech>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
curve25519-dalek = "3.1"
|
curve25519-dalek = "3.1"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
jsonrpc_client = { version = "0.7", features = [ "reqwest" ] }
|
jsonrpc_client = { version = "0.7", features = ["reqwest"] }
|
||||||
monero = "0.12"
|
monero = "0.12"
|
||||||
monero-epee-bin-serde = "1"
|
monero-epee-bin-serde = "1"
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
reqwest = { version = "0.12", default-features = false, features = [ "json" ] }
|
reqwest = { version = "0.12", default-features = false, features = ["json"] }
|
||||||
rust_decimal = { version = "1", features = [ "serde-float" ] }
|
rust_decimal = { version = "1", features = ["serde-float"] }
|
||||||
serde = { version = "1.0", features = [ "derive" ] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hex-literal = "0.4"
|
hex-literal = "0.4"
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monero-wallet"
|
name = "monero-wallet"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = [ "CoBloX Team <team@coblox.tech>" ]
|
authors = ["CoBloX Team <team@coblox.tech>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
@ -15,5 +15,5 @@ curve25519-dalek = "3"
|
||||||
monero-harness = { path = "../monero-harness" }
|
monero-harness = { path = "../monero-harness" }
|
||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
testcontainers = "0.15"
|
testcontainers = "0.15"
|
||||||
tokio = { version = "1", features = [ "rt-multi-thread", "time", "macros", "sync", "process", "fs" ] }
|
tokio = { version = "1", features = ["rt-multi-thread", "time", "macros", "sync", "process", "fs"] }
|
||||||
tracing-subscriber = { version = "0.3", default-features = false, features = [ "fmt", "ansi", "env-filter", "chrono", "tracing-log" ] }
|
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "ansi", "env-filter", "chrono", "tracing-log"] }
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
- For compiling the Rust code: `cargo` and `cargo tauri` ([installation](https://v2.tauri.app/reference/cli/))
|
- For compiling the Rust code: `cargo` and `cargo tauri` ([installation](https://v2.tauri.app/reference/cli/))
|
||||||
- For running the Typescript code: `node` and `yarn`
|
- For running the Typescript code: `node` and `yarn`
|
||||||
- For formatting and bindings: `dprint` (`cargo install dprint@0.39.1`) and `typeshare` (`cargo install typeshare-cli`)
|
- For formatting and bindings: `dprint` (`cargo install dprint@0.50.0`) and `typeshare` (`cargo install typeshare-cli`)
|
||||||
- If you are on Windows and you want to use the `check-bindings` command you'll need to manually install the GNU DiffUtils ([installation](https://gnuwin32.sourceforge.net/packages/diffutils.htm)) and GNU CoreUtils ([installtion](https://gnuwin32.sourceforge.net/packages/coreutils.htm)). Remember to add the installation path (probably `C:\Program Files (x86)\GnuWin32\bin`) to the `PATH` in your enviroment variables.
|
- If you are on Windows and you want to use the `check-bindings` command you'll need to manually install the GNU DiffUtils ([installation](https://gnuwin32.sourceforge.net/packages/diffutils.htm)) and GNU CoreUtils ([installtion](https://gnuwin32.sourceforge.net/packages/coreutils.htm)). Remember to add the installation path (probably `C:\Program Files (x86)\GnuWin32\bin`) to the `PATH` in your enviroment variables.
|
||||||
|
|
||||||
## Start development servers
|
## Start development servers
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,23 @@
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import pluginJs from "@eslint/js";
|
import js from "@eslint/js";
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
import pluginReact from "eslint-plugin-react";
|
import pluginReact from "eslint-plugin-react";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{ ignores: ["node_modules", "dist"] },
|
||||||
ignores: ["node_modules", "dist"],
|
js.configs.recommended,
|
||||||
},
|
|
||||||
pluginJs.configs.recommended,
|
|
||||||
...tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
pluginReact.configs.flat.recommended,
|
pluginReact.configs.flat.recommended,
|
||||||
{
|
{
|
||||||
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
|
languageOptions: {
|
||||||
languageOptions: { globals: globals.browser },
|
globals: globals.browser,
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
// Disallow the use of the `open` on the gloal object
|
"react/no-unescaped-entities": "off",
|
||||||
|
"react/no-children-prop": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-empty-object-type": "off",
|
||||||
"no-restricted-globals": [
|
"no-restricted-globals": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
|
|
@ -24,7 +26,6 @@ export default [
|
||||||
"Use the open(...) function from @tauri-apps/plugin-shell instead",
|
"Use the open(...) function from @tauri-apps/plugin-shell instead",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// Disallow the use of the `open` on the `window` object
|
|
||||||
"no-restricted-properties": [
|
"no-restricted-properties": [
|
||||||
"warn",
|
"warn",
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,33 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
</head>
|
||||||
|
|
||||||
<head>
|
<body>
|
||||||
<meta charset="UTF-8" />
|
<div id="root"></div>
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<script type="module" src="/src/renderer/index.tsx"></script>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<style>
|
||||||
</head>
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
<body>
|
*,
|
||||||
<div id="root"></div>
|
*::after,
|
||||||
<script type="module" src="/src/renderer/index.tsx"></script>
|
*::before {
|
||||||
<style>
|
-webkit-user-select: none;
|
||||||
::-webkit-scrollbar {
|
-webkit-user-drag: none;
|
||||||
display: none;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
html,
|
||||||
*::after,
|
body {
|
||||||
*::before {
|
height: 100%;
|
||||||
-webkit-user-select: none;
|
margin: 0;
|
||||||
-webkit-user-drag: none;
|
overflow: auto;
|
||||||
-webkit-app-region: no-drag;
|
}
|
||||||
}
|
</style>
|
||||||
|
</body>
|
||||||
html,
|
</html>
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
|
||||||
8284
src-gui/package-lock.json
generated
Normal file
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"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.0",
|
||||||
"@fontsource/roboto": "^5.1.0",
|
"@fontsource/roboto": "^5.1.0",
|
||||||
"@material-ui/core": "^4.12.4",
|
"@mui/icons-material": "^7.1.1",
|
||||||
"@material-ui/icons": "^4.11.3",
|
"@mui/lab": "^7.0.0-beta.13",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
"@mui/material": "^7.1.1",
|
||||||
"@reduxjs/toolkit": "^2.3.0",
|
"@reduxjs/toolkit": "^2.3.0",
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
"@tauri-apps/plugin-cli": "^2.0.0",
|
"@tauri-apps/plugin-cli": "^2.0.0",
|
||||||
|
|
@ -37,11 +39,11 @@
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
"pino": "^9.2.0",
|
"pino": "^9.2.0",
|
||||||
"pino-pretty": "^11.2.1",
|
"pino-pretty": "^11.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-qr-code": "^2.0.15",
|
"react-qr-code": "^2.0.15",
|
||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^7.6.1",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"semver": "^7.6.2",
|
"semver": "^7.6.2",
|
||||||
"virtua": "^0.33.2"
|
"virtua": "^0.33.2"
|
||||||
|
|
@ -54,9 +56,10 @@
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/humanize-duration": "^3.27.4",
|
"@types/humanize-duration": "^3.27.4",
|
||||||
"@types/lodash": "^4.17.6",
|
"@types/lodash": "^4.17.6",
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^22.15.29",
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^19.1.6",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^19.1.5",
|
||||||
|
"@types/react-is": "^19.0.0",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"eslint": "^9.9.0",
|
"eslint": "^9.9.0",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export interface ExtendedMakerStatus extends MakerStatus {
|
||||||
recommended?: boolean;
|
recommended?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MakerStatus extends MakerQuote, Maker { }
|
export interface MakerStatus extends MakerQuote, Maker {}
|
||||||
|
|
||||||
export interface MakerQuote {
|
export interface MakerQuote {
|
||||||
price: number;
|
price: number;
|
||||||
|
|
@ -29,16 +29,16 @@ export interface Alert {
|
||||||
|
|
||||||
// Define the correct 9-element tuple type for PrimitiveDateTime
|
// Define the correct 9-element tuple type for PrimitiveDateTime
|
||||||
export type PrimitiveDateTimeString = [
|
export type PrimitiveDateTimeString = [
|
||||||
number, // Year
|
number, // Year
|
||||||
number, // Day of Year
|
number, // Day of Year
|
||||||
number, // Hour
|
number, // Hour
|
||||||
number, // Minute
|
number, // Minute
|
||||||
number, // Second
|
number, // Second
|
||||||
number, // Nanosecond
|
number, // Nanosecond
|
||||||
number, // Offset Hour
|
number, // Offset Hour
|
||||||
number, // Offset Minute
|
number, // Offset Minute
|
||||||
number // Offset Second
|
number, // Offset Second
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface Feedback {
|
export interface Feedback {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -46,7 +46,7 @@ export interface Feedback {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Attachment {
|
export interface Attachment {
|
||||||
id: number;
|
id: number;
|
||||||
message_id: number;
|
message_id: number;
|
||||||
key: string;
|
key: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
|
|
||||||
|
|
@ -61,4 +61,3 @@ export function parseCliLogString(log: string): CliLog | string {
|
||||||
return log;
|
return log;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ export type TauriSwapProgressEventContent<
|
||||||
T extends TauriSwapProgressEventType,
|
T extends TauriSwapProgressEventType,
|
||||||
> = Extract<TauriSwapProgressEvent, { type: T }>["content"];
|
> = Extract<TauriSwapProgressEvent, { type: T }>["content"];
|
||||||
|
|
||||||
export type TauriSwapProgressEventExt<T extends TauriSwapProgressEventType> = Extract<TauriSwapProgressEvent, { type: T }>;
|
export type TauriSwapProgressEventExt<T extends TauriSwapProgressEventType> =
|
||||||
|
Extract<TauriSwapProgressEvent, { type: T }>;
|
||||||
|
|
||||||
// See /swap/src/protocol/bob/state.rs#L57
|
// See /swap/src/protocol/bob/state.rs#L57
|
||||||
// TODO: Replace this with a typeshare definition
|
// TODO: Replace this with a typeshare definition
|
||||||
|
|
@ -36,19 +37,32 @@ export enum BobStateName {
|
||||||
|
|
||||||
export function bobStateNameToHumanReadable(stateName: BobStateName): string {
|
export function bobStateNameToHumanReadable(stateName: BobStateName): string {
|
||||||
switch (stateName) {
|
switch (stateName) {
|
||||||
case BobStateName.Started: return "Started";
|
case BobStateName.Started:
|
||||||
case BobStateName.SwapSetupCompleted: return "Setup completed";
|
return "Started";
|
||||||
case BobStateName.BtcLocked: return "Bitcoin locked";
|
case BobStateName.SwapSetupCompleted:
|
||||||
case BobStateName.XmrLockProofReceived: return "Monero locked";
|
return "Setup completed";
|
||||||
case BobStateName.XmrLocked: return "Monero locked and fully confirmed";
|
case BobStateName.BtcLocked:
|
||||||
case BobStateName.EncSigSent: return "Encrypted signature sent";
|
return "Bitcoin locked";
|
||||||
case BobStateName.BtcRedeemed: return "Bitcoin redeemed";
|
case BobStateName.XmrLockProofReceived:
|
||||||
case BobStateName.CancelTimelockExpired: return "Cancel timelock expired";
|
return "Monero locked";
|
||||||
case BobStateName.BtcCancelled: return "Bitcoin cancelled";
|
case BobStateName.XmrLocked:
|
||||||
case BobStateName.BtcRefunded: return "Bitcoin refunded";
|
return "Monero locked and fully confirmed";
|
||||||
case BobStateName.XmrRedeemed: return "Monero redeemed";
|
case BobStateName.EncSigSent:
|
||||||
case BobStateName.BtcPunished: return "Bitcoin punished";
|
return "Encrypted signature sent";
|
||||||
case BobStateName.SafelyAborted: return "Safely aborted";
|
case BobStateName.BtcRedeemed:
|
||||||
|
return "Bitcoin redeemed";
|
||||||
|
case BobStateName.CancelTimelockExpired:
|
||||||
|
return "Cancel timelock expired";
|
||||||
|
case BobStateName.BtcCancelled:
|
||||||
|
return "Bitcoin cancelled";
|
||||||
|
case BobStateName.BtcRefunded:
|
||||||
|
return "Bitcoin refunded";
|
||||||
|
case BobStateName.XmrRedeemed:
|
||||||
|
return "Monero redeemed";
|
||||||
|
case BobStateName.BtcPunished:
|
||||||
|
return "Bitcoin punished";
|
||||||
|
case BobStateName.SafelyAborted:
|
||||||
|
return "Safely aborted";
|
||||||
default:
|
default:
|
||||||
return exhaustiveGuard(stateName);
|
return exhaustiveGuard(stateName);
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +78,11 @@ export type TimelockCancel = Extract<ExpiredTimelocks, { type: "Cancel" }>;
|
||||||
export type TimelockPunish = Extract<ExpiredTimelocks, { type: "Punish" }>;
|
export type TimelockPunish = Extract<ExpiredTimelocks, { type: "Punish" }>;
|
||||||
|
|
||||||
// This function returns the absolute block number of the timelock relative to the block the tx_lock was included in
|
// This function returns the absolute block number of the timelock relative to the block the tx_lock was included in
|
||||||
export function getAbsoluteBlock(timelock: ExpiredTimelocks, cancelTimelock: number, punishTimelock: number): number {
|
export function getAbsoluteBlock(
|
||||||
|
timelock: ExpiredTimelocks,
|
||||||
|
cancelTimelock: number,
|
||||||
|
punishTimelock: number,
|
||||||
|
): number {
|
||||||
if (timelock.type === "None") {
|
if (timelock.type === "None") {
|
||||||
return cancelTimelock - timelock.content.blocks_left;
|
return cancelTimelock - timelock.content.blocks_left;
|
||||||
}
|
}
|
||||||
|
|
@ -208,12 +226,15 @@ export function isGetSwapInfoResponseRunningSwap(
|
||||||
* @returns True if the timelock exists, false otherwise
|
* @returns True if the timelock exists, false otherwise
|
||||||
*/
|
*/
|
||||||
export function isGetSwapInfoResponseWithTimelock(
|
export function isGetSwapInfoResponseWithTimelock(
|
||||||
response: GetSwapInfoResponseExt
|
response: GetSwapInfoResponseExt,
|
||||||
): response is GetSwapInfoResponseExtWithTimelock {
|
): response is GetSwapInfoResponseExtWithTimelock {
|
||||||
return response.timelock !== null;
|
return response.timelock !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PendingApprovalRequest = Extract<ApprovalRequest, { state: "Pending" }>;
|
export type PendingApprovalRequest = Extract<
|
||||||
|
ApprovalRequest,
|
||||||
|
{ state: "Pending" }
|
||||||
|
>;
|
||||||
|
|
||||||
export type PendingLockBitcoinApprovalRequest = PendingApprovalRequest & {
|
export type PendingLockBitcoinApprovalRequest = PendingApprovalRequest & {
|
||||||
content: {
|
content: {
|
||||||
|
|
@ -239,7 +260,10 @@ export function isPendingBackgroundProcess(
|
||||||
return process.progress.type === "Pending";
|
return process.progress.type === "Pending";
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TauriBitcoinSyncProgress = Extract<TauriBackgroundProgress, { componentName: "SyncingBitcoinWallet" }>;
|
export type TauriBitcoinSyncProgress = Extract<
|
||||||
|
TauriBackgroundProgress,
|
||||||
|
{ componentName: "SyncingBitcoinWallet" }
|
||||||
|
>;
|
||||||
|
|
||||||
export function isBitcoinSyncProgress(
|
export function isBitcoinSyncProgress(
|
||||||
progress: TauriBackgroundProgress,
|
progress: TauriBackgroundProgress,
|
||||||
|
|
|
||||||
|
|
@ -5,20 +5,34 @@
|
||||||
// - and to submit feedback
|
// - and to submit feedback
|
||||||
// - fetch currency rates from CoinGecko
|
// - fetch currency rates from CoinGecko
|
||||||
|
|
||||||
import { Alert, Attachment, AttachmentInput, ExtendedMakerStatus, Feedback, Message, MessageWithAttachments, PrimitiveDateTimeString } from "models/apiModel";
|
import {
|
||||||
|
Alert,
|
||||||
|
Attachment,
|
||||||
|
AttachmentInput,
|
||||||
|
ExtendedMakerStatus,
|
||||||
|
Feedback,
|
||||||
|
Message,
|
||||||
|
MessageWithAttachments,
|
||||||
|
PrimitiveDateTimeString,
|
||||||
|
} from "models/apiModel";
|
||||||
import { store } from "./store/storeRenderer";
|
import { store } from "./store/storeRenderer";
|
||||||
import { setBtcPrice, setXmrBtcRate, setXmrPrice } from "store/features/ratesSlice";
|
import {
|
||||||
|
setBtcPrice,
|
||||||
|
setXmrBtcRate,
|
||||||
|
setXmrPrice,
|
||||||
|
} from "store/features/ratesSlice";
|
||||||
import { FiatCurrency } from "store/features/settingsSlice";
|
import { FiatCurrency } from "store/features/settingsSlice";
|
||||||
import { setAlerts } from "store/features/alertsSlice";
|
import { setAlerts } from "store/features/alertsSlice";
|
||||||
import { registryConnectionFailed, setRegistryMakers } from "store/features/makersSlice";
|
import {
|
||||||
|
registryConnectionFailed,
|
||||||
|
setRegistryMakers,
|
||||||
|
} from "store/features/makersSlice";
|
||||||
import logger from "utils/logger";
|
import logger from "utils/logger";
|
||||||
import { setConversation } from "store/features/conversationsSlice";
|
import { setConversation } from "store/features/conversationsSlice";
|
||||||
|
|
||||||
const PUBLIC_REGISTRY_API_BASE_URL = "https://api.unstoppableswap.net";
|
const PUBLIC_REGISTRY_API_BASE_URL = "https://api.unstoppableswap.net";
|
||||||
|
|
||||||
async function fetchMakersViaHttp(): Promise<
|
async function fetchMakersViaHttp(): Promise<ExtendedMakerStatus[]> {
|
||||||
ExtendedMakerStatus[]
|
|
||||||
> {
|
|
||||||
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/list`);
|
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/list`);
|
||||||
return (await response.json()) as ExtendedMakerStatus[];
|
return (await response.json()) as ExtendedMakerStatus[];
|
||||||
}
|
}
|
||||||
|
|
@ -30,7 +44,7 @@ async function fetchAlertsViaHttp(): Promise<Alert[]> {
|
||||||
|
|
||||||
export async function submitFeedbackViaHttp(
|
export async function submitFeedbackViaHttp(
|
||||||
content: string,
|
content: string,
|
||||||
attachments?: AttachmentInput[]
|
attachments?: AttachmentInput[],
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
type Response = string;
|
type Response = string;
|
||||||
|
|
||||||
|
|
@ -39,64 +53,83 @@ export async function submitFeedbackViaHttp(
|
||||||
attachments: attachments || [], // Ensure attachments is always an array
|
attachments: attachments || [], // Ensure attachments is always an array
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/submit-feedback`, {
|
const response = await fetch(
|
||||||
method: "POST",
|
`${PUBLIC_REGISTRY_API_BASE_URL}/api/submit-feedback`,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestPayload), // Send the corrected structure
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestPayload), // Send the corrected structure
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorBody = await response.text();
|
const errorBody = await response.text();
|
||||||
throw new Error(`Failed to submit feedback. Status: ${response.status}. Body: ${errorBody}`);
|
throw new Error(
|
||||||
|
`Failed to submit feedback. Status: ${response.status}. Body: ${errorBody}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseBody = (await response.json()) as Response;
|
const responseBody = (await response.json()) as Response;
|
||||||
return responseBody;
|
return responseBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchFeedbackMessagesViaHttp(feedbackId: string): Promise<Message[]> {
|
export async function fetchFeedbackMessagesViaHttp(
|
||||||
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/feedback/${feedbackId}/messages`);
|
feedbackId: string,
|
||||||
|
): Promise<Message[]> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${PUBLIC_REGISTRY_API_BASE_URL}/api/feedback/${feedbackId}/messages`,
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorBody = await response.text();
|
const errorBody = await response.text();
|
||||||
throw new Error(`Failed to fetch messages for feedback ${feedbackId}. Status: ${response.status}. Body: ${errorBody}`);
|
throw new Error(
|
||||||
|
`Failed to fetch messages for feedback ${feedbackId}. Status: ${response.status}. Body: ${errorBody}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Assuming the response is directly the Message[] array including attachments
|
// Assuming the response is directly the Message[] array including attachments
|
||||||
return (await response.json()) as Message[];
|
return (await response.json()) as Message[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function appendFeedbackMessageViaHttp(
|
export async function appendFeedbackMessageViaHttp(
|
||||||
feedbackId: string,
|
feedbackId: string,
|
||||||
content: string,
|
content: string,
|
||||||
attachments?: AttachmentInput[]
|
attachments?: AttachmentInput[],
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
type Response = number;
|
type Response = number;
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
feedback_id: feedbackId,
|
feedback_id: feedbackId,
|
||||||
content,
|
content,
|
||||||
attachments: attachments || [], // Ensure attachments is always an array
|
attachments: attachments || [], // Ensure attachments is always an array
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/append-feedback-message`, {
|
const response = await fetch(
|
||||||
method: "POST",
|
`${PUBLIC_REGISTRY_API_BASE_URL}/api/append-feedback-message`,
|
||||||
headers: {
|
{
|
||||||
"Content-Type": "application/json",
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body), // Send new structure
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body), // Send new structure
|
);
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorBody = await response.text();
|
const errorBody = await response.text();
|
||||||
throw new Error(`Failed to append message for feedback ${feedbackId}. Status: ${response.status}. Body: ${errorBody}`);
|
throw new Error(
|
||||||
|
`Failed to append message for feedback ${feedbackId}. Status: ${response.status}. Body: ${errorBody}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseBody = (await response.json()) as Response;
|
const responseBody = (await response.json()) as Response;
|
||||||
return responseBody;
|
return responseBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchCurrencyPrice(currency: string, fiatCurrency: FiatCurrency): Promise<number> {
|
async function fetchCurrencyPrice(
|
||||||
|
currency: string,
|
||||||
|
fiatCurrency: FiatCurrency,
|
||||||
|
): Promise<number> {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=${fiatCurrency.toLowerCase()}`,
|
`https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=${fiatCurrency.toLowerCase()}`,
|
||||||
);
|
);
|
||||||
|
|
@ -105,7 +138,9 @@ async function fetchCurrencyPrice(currency: string, fiatCurrency: FiatCurrency):
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchXmrBtcRate(): Promise<number> {
|
async function fetchXmrBtcRate(): Promise<number> {
|
||||||
const response = await fetch('https://api.kraken.com/0/public/Ticker?pair=XMRXBT');
|
const response = await fetch(
|
||||||
|
"https://api.kraken.com/0/public/Ticker?pair=XMRXBT",
|
||||||
|
);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.error && data.error.length > 0) {
|
if (data.error && data.error.length > 0) {
|
||||||
|
|
@ -127,13 +162,12 @@ async function fetchXmrPrice(fiatCurrency: FiatCurrency): Promise<number> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If enabled by the user, fetch the XMR, BTC and XMR/BTC rates
|
* If enabled by the user, fetch the XMR, BTC and XMR/BTC rates
|
||||||
* and store them in the Redux store.
|
* and store them in the Redux store.
|
||||||
*/
|
*/
|
||||||
export async function updateRates(): Promise<void> {
|
export async function updateRates(): Promise<void> {
|
||||||
const settings = store.getState().settings;
|
const settings = store.getState().settings;
|
||||||
if (!settings.fetchFiatPrices)
|
if (!settings.fetchFiatPrices) return;
|
||||||
return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const xmrBtcRate = await fetchXmrBtcRate();
|
const xmrBtcRate = await fetchXmrBtcRate();
|
||||||
|
|
@ -191,8 +225,12 @@ export async function fetchAllConversations(): Promise<void> {
|
||||||
const messages = await fetchFeedbackMessagesViaHttp(feedbackId);
|
const messages = await fetchFeedbackMessagesViaHttp(feedbackId);
|
||||||
console.log("Fetched messages for feedback id", feedbackId, messages);
|
console.log("Fetched messages for feedback id", feedbackId, messages);
|
||||||
store.dispatch(setConversation({ feedbackId, messages }));
|
store.dispatch(setConversation({ feedbackId, messages }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error, "Error fetching messages for feedback id", feedbackId);
|
logger.error(
|
||||||
|
error,
|
||||||
|
"Error fetching messages for feedback id",
|
||||||
|
feedbackId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,27 @@
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { TauriContextStatusEvent, TauriEvent } from "models/tauriModel";
|
import { TauriContextStatusEvent, TauriEvent } from "models/tauriModel";
|
||||||
import { contextStatusEventReceived, receivedCliLog, rpcSetBalance, timelockChangeEventReceived, approvalEventReceived, backgroundProgressEventReceived } from "store/features/rpcSlice";
|
import {
|
||||||
|
contextStatusEventReceived,
|
||||||
|
receivedCliLog,
|
||||||
|
rpcSetBalance,
|
||||||
|
timelockChangeEventReceived,
|
||||||
|
approvalEventReceived,
|
||||||
|
backgroundProgressEventReceived,
|
||||||
|
} from "store/features/rpcSlice";
|
||||||
import { swapProgressEventReceived } from "store/features/swapSlice";
|
import { swapProgressEventReceived } from "store/features/swapSlice";
|
||||||
import logger from "utils/logger";
|
import logger from "utils/logger";
|
||||||
import { fetchAllConversations, updateAlerts, updatePublicRegistry, updateRates } from "./api";
|
import {
|
||||||
import { checkContextAvailability, getSwapInfo, initializeContext, updateAllNodeStatuses } from "./rpc";
|
fetchAllConversations,
|
||||||
|
updateAlerts,
|
||||||
|
updatePublicRegistry,
|
||||||
|
updateRates,
|
||||||
|
} from "./api";
|
||||||
|
import {
|
||||||
|
checkContextAvailability,
|
||||||
|
getSwapInfo,
|
||||||
|
initializeContext,
|
||||||
|
updateAllNodeStatuses,
|
||||||
|
} from "./rpc";
|
||||||
import { store } from "./store/storeRenderer";
|
import { store } from "./store/storeRenderer";
|
||||||
import { exhaustiveGuard } from "utils/typescriptUtils";
|
import { exhaustiveGuard } from "utils/typescriptUtils";
|
||||||
|
|
||||||
|
|
@ -23,81 +40,86 @@ const UPDATE_RATE_INTERVAL = 5 * 60 * 1_000;
|
||||||
const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000;
|
const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000;
|
||||||
|
|
||||||
function setIntervalImmediate(callback: () => void, interval: number): void {
|
function setIntervalImmediate(callback: () => void, interval: number): void {
|
||||||
callback();
|
callback();
|
||||||
setInterval(callback, interval);
|
setInterval(callback, interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupBackgroundTasks(): Promise<void> {
|
export async function setupBackgroundTasks(): Promise<void> {
|
||||||
// Setup periodic fetch tasks
|
// Setup periodic fetch tasks
|
||||||
setIntervalImmediate(updatePublicRegistry, PROVIDER_UPDATE_INTERVAL);
|
setIntervalImmediate(updatePublicRegistry, PROVIDER_UPDATE_INTERVAL);
|
||||||
setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
|
setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
|
||||||
setIntervalImmediate(updateRates, UPDATE_RATE_INTERVAL);
|
setIntervalImmediate(updateRates, UPDATE_RATE_INTERVAL);
|
||||||
setIntervalImmediate(fetchAllConversations, FETCH_CONVERSATIONS_INTERVAL);
|
setIntervalImmediate(fetchAllConversations, FETCH_CONVERSATIONS_INTERVAL);
|
||||||
|
|
||||||
// Fetch all alerts
|
// Fetch all alerts
|
||||||
updateAlerts();
|
updateAlerts();
|
||||||
|
|
||||||
// Setup Tauri event listeners
|
// Setup Tauri event listeners
|
||||||
// Check if the context is already available. This is to prevent unnecessary re-initialization
|
// Check if the context is already available. This is to prevent unnecessary re-initialization
|
||||||
if (await checkContextAvailability()) {
|
if (await checkContextAvailability()) {
|
||||||
store.dispatch(contextStatusEventReceived(TauriContextStatusEvent.Available));
|
store.dispatch(
|
||||||
} else {
|
contextStatusEventReceived(TauriContextStatusEvent.Available),
|
||||||
// Warning: If we reload the page while the Context is being initialized, this function will throw an error
|
);
|
||||||
|
} else {
|
||||||
|
// Warning: If we reload the page while the Context is being initialized, this function will throw an error
|
||||||
|
initializeContext().catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
e,
|
||||||
|
"Failed to initialize context on page load. This might be because we reloaded the page while the context was being initialized",
|
||||||
|
);
|
||||||
|
// Wait a short time before retrying
|
||||||
|
setTimeout(() => {
|
||||||
initializeContext().catch((e) => {
|
initializeContext().catch((e) => {
|
||||||
logger.error(e, "Failed to initialize context on page load. This might be because we reloaded the page while the context was being initialized");
|
logger.error(e, "Failed to initialize context even after retry");
|
||||||
// Wait a short time before retrying
|
|
||||||
setTimeout(() => {
|
|
||||||
initializeContext().catch((e) => {
|
|
||||||
logger.error(e, "Failed to initialize context even after retry");
|
|
||||||
});
|
|
||||||
}, 2000);
|
|
||||||
});
|
});
|
||||||
}
|
}, 2000);
|
||||||
|
|
||||||
// Listen for the unified event
|
|
||||||
listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
|
|
||||||
const { channelName, event: eventData } = event.payload;
|
|
||||||
|
|
||||||
switch (channelName) {
|
|
||||||
case "SwapProgress":
|
|
||||||
store.dispatch(swapProgressEventReceived(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "ContextInitProgress":
|
|
||||||
store.dispatch(contextStatusEventReceived(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "CliLog":
|
|
||||||
store.dispatch(receivedCliLog(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "BalanceChange":
|
|
||||||
store.dispatch(rpcSetBalance((eventData).balance));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "SwapDatabaseStateUpdate":
|
|
||||||
getSwapInfo(eventData.swap_id);
|
|
||||||
|
|
||||||
// This is ugly but it's the best we can do for now
|
|
||||||
// Sometimes we are too quick to fetch the swap info and the new state is not yet reflected
|
|
||||||
// in the database. So we wait a bit before fetching the new state
|
|
||||||
setTimeout(() => getSwapInfo(eventData.swap_id), 3000);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "TimelockChange":
|
|
||||||
store.dispatch(timelockChangeEventReceived(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Approval":
|
|
||||||
store.dispatch(approvalEventReceived(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "BackgroundProgress":
|
|
||||||
store.dispatch(backgroundProgressEventReceived(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
exhaustiveGuard(channelName);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for the unified event
|
||||||
|
listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
|
||||||
|
const { channelName, event: eventData } = event.payload;
|
||||||
|
|
||||||
|
switch (channelName) {
|
||||||
|
case "SwapProgress":
|
||||||
|
store.dispatch(swapProgressEventReceived(eventData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ContextInitProgress":
|
||||||
|
store.dispatch(contextStatusEventReceived(eventData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "CliLog":
|
||||||
|
store.dispatch(receivedCliLog(eventData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "BalanceChange":
|
||||||
|
store.dispatch(rpcSetBalance(eventData.balance));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "SwapDatabaseStateUpdate":
|
||||||
|
getSwapInfo(eventData.swap_id);
|
||||||
|
|
||||||
|
// This is ugly but it's the best we can do for now
|
||||||
|
// Sometimes we are too quick to fetch the swap info and the new state is not yet reflected
|
||||||
|
// in the database. So we wait a bit before fetching the new state
|
||||||
|
setTimeout(() => getSwapInfo(eventData.swap_id), 3000);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "TimelockChange":
|
||||||
|
store.dispatch(timelockChangeEventReceived(eventData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Approval":
|
||||||
|
store.dispatch(approvalEventReceived(eventData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "BackgroundProgress":
|
||||||
|
store.dispatch(backgroundProgressEventReceived(eventData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
exhaustiveGuard(channelName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import { Box, CssBaseline, makeStyles } from "@material-ui/core";
|
import { Box, CssBaseline } from "@mui/material";
|
||||||
import { ThemeProvider } from "@material-ui/core/styles";
|
import {
|
||||||
|
ThemeProvider,
|
||||||
|
Theme,
|
||||||
|
StyledEngineProvider,
|
||||||
|
} from "@mui/material/styles";
|
||||||
import "@tauri-apps/plugin-shell";
|
import "@tauri-apps/plugin-shell";
|
||||||
import { Route, MemoryRouter as Router, Routes } from "react-router-dom";
|
import { Route, MemoryRouter as Router, Routes } from "react-router-dom";
|
||||||
import Navigation, { drawerWidth } from "./navigation/Navigation";
|
import Navigation, { drawerWidth } from "./navigation/Navigation";
|
||||||
|
|
@ -10,21 +14,21 @@ import WalletPage from "./pages/wallet/WalletPage";
|
||||||
import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider";
|
import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider";
|
||||||
import UpdaterDialog from "./modal/updater/UpdaterDialog";
|
import UpdaterDialog from "./modal/updater/UpdaterDialog";
|
||||||
import { useSettings } from "store/hooks";
|
import { useSettings } from "store/hooks";
|
||||||
import { themes } from "./theme";
|
import { Theme as ThemeEnum, themes } from "./theme";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { setupBackgroundTasks } from "renderer/background";
|
import { setupBackgroundTasks } from "renderer/background";
|
||||||
import "@fontsource/roboto";
|
import "@fontsource/roboto";
|
||||||
import FeedbackPage from "./pages/feedback/FeedbackPage";
|
import FeedbackPage from "./pages/feedback/FeedbackPage";
|
||||||
import IntroductionModal from "./modal/introduction/IntroductionModal";
|
import IntroductionModal from "./modal/introduction/IntroductionModal";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
declare module "@mui/material/styles" {
|
||||||
innerContent: {
|
interface Theme {
|
||||||
padding: theme.spacing(4),
|
// Add your custom theme properties here if needed
|
||||||
marginLeft: drawerWidth,
|
}
|
||||||
maxHeight: `100vh`,
|
interface ThemeOptions {
|
||||||
flex: 1,
|
// Add your custom theme options here if needed
|
||||||
},
|
}
|
||||||
}));
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -32,27 +36,37 @@ export default function App() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const theme = useSettings((s) => s.theme);
|
const theme = useSettings((s) => s.theme);
|
||||||
|
const currentTheme = themes[theme] || themes[ThemeEnum.Dark];
|
||||||
|
|
||||||
|
console.log("Current theme:", { theme, currentTheme });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={themes[theme]}>
|
<StyledEngineProvider injectFirst>
|
||||||
<GlobalSnackbarProvider>
|
<ThemeProvider theme={currentTheme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<IntroductionModal/>
|
<GlobalSnackbarProvider>
|
||||||
<Router>
|
<IntroductionModal />
|
||||||
<Navigation />
|
<Router>
|
||||||
<InnerContent />
|
<Navigation />
|
||||||
<UpdaterDialog />
|
<InnerContent />
|
||||||
</Router>
|
<UpdaterDialog />
|
||||||
</GlobalSnackbarProvider>
|
</Router>
|
||||||
</ThemeProvider>
|
</GlobalSnackbarProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</StyledEngineProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InnerContent() {
|
function InnerContent() {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.innerContent}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: 4,
|
||||||
|
marginLeft: drawerWidth,
|
||||||
|
maxHeight: `100vh`,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/swap" element={<SwapPage />} />
|
<Route path="/swap" element={<SwapPage />} />
|
||||||
<Route path="/history" element={<HistoryPage />} />
|
<Route path="/history" element={<HistoryPage />} />
|
||||||
|
|
@ -63,4 +77,4 @@ function InnerContent() {
|
||||||
</Routes>
|
</Routes>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import {
|
||||||
IconButton,
|
IconButton,
|
||||||
IconButtonProps,
|
IconButtonProps,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@material-ui/core";
|
} from "@mui/material";
|
||||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
import { ReactNode, useState } from "react";
|
import { ReactNode, useState } from "react";
|
||||||
import { useIsContextAvailable } from "store/hooks";
|
import { useIsContextAvailable } from "store/hooks";
|
||||||
|
|
@ -84,6 +84,10 @@ export default function PromiseInvokeButton<T>({
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
{...(rest as IconButtonProps)}
|
{...(rest as IconButtonProps)}
|
||||||
|
size="large"
|
||||||
|
sx={{
|
||||||
|
padding: "0.25rem",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{actualEndIcon}
|
{actualEndIcon}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,49 @@
|
||||||
import { BackgroundRefundState } from "models/tauriModel";
|
import { BackgroundRefundState } from "models/tauriModel";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
|
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
|
||||||
import { AlertTitle } from "@material-ui/lab";
|
import { AlertTitle } from "@mui/material";
|
||||||
import TruncatedText from "../other/TruncatedText";
|
import TruncatedText from "../other/TruncatedText";
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export default function BackgroundRefundAlert() {
|
export default function BackgroundRefundAlert() {
|
||||||
const backgroundRefund = useAppSelector(state => state.rpc.state.backgroundRefund);
|
const backgroundRefund = useAppSelector(
|
||||||
const notistack = useSnackbar();
|
(state) => state.rpc.state.backgroundRefund,
|
||||||
|
);
|
||||||
|
const notistack = useSnackbar();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If we failed to refund, show a notification
|
// If we failed to refund, show a notification
|
||||||
if (backgroundRefund?.state.type === "Failed") {
|
if (backgroundRefund?.state.type === "Failed") {
|
||||||
notistack.enqueueSnackbar(
|
notistack.enqueueSnackbar(
|
||||||
<>
|
<>
|
||||||
Our attempt to refund {backgroundRefund.swapId} in the background failed.
|
Our attempt to refund {backgroundRefund.swapId} in the background
|
||||||
<br />
|
failed.
|
||||||
Error: {backgroundRefund.state.content.error}
|
<br />
|
||||||
</>,
|
Error: {backgroundRefund.state.content.error}
|
||||||
{ variant: "error", autoHideDuration: 60 * 1000 }
|
</>,
|
||||||
);
|
{ variant: "error", autoHideDuration: 60 * 1000 },
|
||||||
}
|
);
|
||||||
|
|
||||||
// If we successfully refunded, show a notification as well
|
|
||||||
if (backgroundRefund?.state.type === "Completed") {
|
|
||||||
notistack.enqueueSnackbar(`The swap ${backgroundRefund.swapId} has been refunded in the background.`, { variant: "success", persist: true });
|
|
||||||
}
|
|
||||||
}, [backgroundRefund]);
|
|
||||||
|
|
||||||
if (backgroundRefund?.state.type === "Started") {
|
|
||||||
return <LoadingSpinnerAlert>
|
|
||||||
<AlertTitle>
|
|
||||||
Refund in progress
|
|
||||||
</AlertTitle>
|
|
||||||
The swap <TruncatedText>{backgroundRefund.swapId}</TruncatedText> is being refunded in the background.
|
|
||||||
</LoadingSpinnerAlert>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
// If we successfully refunded, show a notification as well
|
||||||
}
|
if (backgroundRefund?.state.type === "Completed") {
|
||||||
|
notistack.enqueueSnackbar(
|
||||||
|
`The swap ${backgroundRefund.swapId} has been refunded in the background.`,
|
||||||
|
{ variant: "success", persist: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [backgroundRefund]);
|
||||||
|
|
||||||
|
if (backgroundRefund?.state.type === "Started") {
|
||||||
|
return (
|
||||||
|
<LoadingSpinnerAlert>
|
||||||
|
<AlertTitle>Refund in progress</AlertTitle>
|
||||||
|
The swap <TruncatedText>{backgroundRefund.swapId}</TruncatedText> is
|
||||||
|
being refunded in the background.
|
||||||
|
</LoadingSpinnerAlert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,36 @@
|
||||||
import { Box, Button, LinearProgress, makeStyles, Badge } from "@material-ui/core";
|
import { Box, Button, LinearProgress, Badge } from "@mui/material";
|
||||||
import { Alert } from "@material-ui/lab";
|
import { Alert } from "@mui/material";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAppSelector, usePendingBackgroundProcesses } from "store/hooks";
|
import { useAppSelector, usePendingBackgroundProcesses } from "store/hooks";
|
||||||
import { exhaustiveGuard } from "utils/typescriptUtils";
|
import { exhaustiveGuard } from "utils/typescriptUtils";
|
||||||
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
|
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
|
||||||
import { bytesToMb } from "utils/conversionUtils";
|
import { bytesToMb } from "utils/conversionUtils";
|
||||||
import { TauriBackgroundProgress, TauriContextStatusEvent } from "models/tauriModel";
|
import {
|
||||||
|
TauriBackgroundProgress,
|
||||||
|
TauriContextStatusEvent,
|
||||||
|
} from "models/tauriModel";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import TruncatedText from "../other/TruncatedText";
|
import TruncatedText from "../other/TruncatedText";
|
||||||
import BitcoinIcon from "../icons/BitcoinIcon";
|
import BitcoinIcon from "../icons/BitcoinIcon";
|
||||||
import MoneroIcon from "../icons/MoneroIcon";
|
import MoneroIcon from "../icons/MoneroIcon";
|
||||||
import TorIcon from "../icons/TorIcon";
|
import TorIcon from "../icons/TorIcon";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
function AlertWithLinearProgress({
|
||||||
innerAlert: {
|
title,
|
||||||
display: "flex",
|
progress,
|
||||||
flexDirection: "column",
|
icon,
|
||||||
gap: theme.spacing(2),
|
count,
|
||||||
},
|
}: {
|
||||||
}));
|
title: React.ReactNode;
|
||||||
|
progress: number | null;
|
||||||
function AlertWithLinearProgress({ title, progress, icon, count }: {
|
icon?: React.ReactNode | null;
|
||||||
title: React.ReactNode,
|
count?: number;
|
||||||
progress: number | null,
|
|
||||||
icon?: React.ReactNode | null,
|
|
||||||
count?: number
|
|
||||||
}) {
|
}) {
|
||||||
const BUFFER_PROGRESS_ADDITION_MAX = 20;
|
const BUFFER_PROGRESS_ADDITION_MAX = 20;
|
||||||
|
|
||||||
const [bufferProgressAddition, setBufferProgressAddition] = useState(Math.random() * BUFFER_PROGRESS_ADDITION_MAX);
|
const [bufferProgressAddition, setBufferProgressAddition] = useState(
|
||||||
|
Math.random() * BUFFER_PROGRESS_ADDITION_MAX,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBufferProgressAddition(Math.random() * BUFFER_PROGRESS_ADDITION_MAX);
|
setBufferProgressAddition(Math.random() * BUFFER_PROGRESS_ADDITION_MAX);
|
||||||
|
|
@ -45,22 +47,30 @@ function AlertWithLinearProgress({ title, progress, icon, count }: {
|
||||||
|
|
||||||
// If the progress is already at 100%, but not finished yet we show an indeterminate progress bar
|
// If the progress is already at 100%, but not finished yet we show an indeterminate progress bar
|
||||||
// as it'd be confusing to show a 100% progress bar for longer than a second or so.
|
// as it'd be confusing to show a 100% progress bar for longer than a second or so.
|
||||||
return <Alert severity="info" icon={displayIcon}>
|
return (
|
||||||
<Box style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
<Alert severity="info" icon={displayIcon}>
|
||||||
{title}
|
<Box style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||||
{(progress === null || progress === 0 || progress >= 100) ? (
|
{title}
|
||||||
<LinearProgress variant="indeterminate" />
|
{progress === null || progress === 0 || progress >= 100 ? (
|
||||||
) : (
|
<LinearProgress variant="indeterminate" />
|
||||||
<LinearProgress variant="buffer" value={progress} valueBuffer={Math.min(progress + bufferProgressAddition, 100)} />
|
) : (
|
||||||
)}
|
<LinearProgress
|
||||||
</Box>
|
variant="buffer"
|
||||||
</Alert>
|
value={progress}
|
||||||
|
valueBuffer={Math.min(progress + bufferProgressAddition, 100)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PartialInitStatus({ status, totalOfType, classes }: {
|
function PartialInitStatus({
|
||||||
status: TauriBackgroundProgress,
|
status,
|
||||||
totalOfType: number,
|
totalOfType,
|
||||||
classes: ReturnType<typeof useStyles>
|
}: {
|
||||||
|
status: TauriBackgroundProgress;
|
||||||
|
totalOfType: number;
|
||||||
}) {
|
}) {
|
||||||
if (status.progress.type === "Completed") {
|
if (status.progress.type === "Completed") {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -70,90 +80,82 @@ function PartialInitStatus({ status, totalOfType, classes }: {
|
||||||
case "EstablishingTorCircuits":
|
case "EstablishingTorCircuits":
|
||||||
return (
|
return (
|
||||||
<AlertWithLinearProgress
|
<AlertWithLinearProgress
|
||||||
title={
|
title={<>Establishing Tor circuits</>}
|
||||||
<>
|
|
||||||
Establishing Tor circuits
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
progress={status.progress.content.frac * 100}
|
progress={status.progress.content.frac * 100}
|
||||||
count={totalOfType}
|
count={totalOfType}
|
||||||
icon={<TorIcon />}
|
icon={<TorIcon />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "SyncingBitcoinWallet":
|
case "SyncingBitcoinWallet": {
|
||||||
const progressValue =
|
const progressValue =
|
||||||
status.progress.content?.type === "Known" ?
|
status.progress.content?.type === "Known"
|
||||||
(status.progress.content?.content?.consumed / status.progress.content?.content?.total) * 100 : null;
|
? (status.progress.content?.content?.consumed /
|
||||||
|
status.progress.content?.content?.total) *
|
||||||
|
100
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertWithLinearProgress
|
<AlertWithLinearProgress
|
||||||
title={
|
title={<>Syncing Bitcoin wallet</>}
|
||||||
<>
|
|
||||||
Syncing Bitcoin wallet
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
progress={progressValue}
|
progress={progressValue}
|
||||||
icon={<BitcoinIcon />}
|
icon={<BitcoinIcon />}
|
||||||
count={totalOfType}
|
count={totalOfType}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "FullScanningBitcoinWallet":
|
}
|
||||||
const fullScanProgressValue = status.progress.content?.type === "Known" ? (status.progress.content?.content?.current_index / status.progress.content?.content?.assumed_total) * 100 : null;
|
case "FullScanningBitcoinWallet": {
|
||||||
|
const fullScanProgressValue =
|
||||||
|
status.progress.content?.type === "Known"
|
||||||
|
? (status.progress.content?.content?.current_index /
|
||||||
|
status.progress.content?.content?.assumed_total) *
|
||||||
|
100
|
||||||
|
: null;
|
||||||
return (
|
return (
|
||||||
<AlertWithLinearProgress
|
<AlertWithLinearProgress
|
||||||
title={
|
title={<>Full scan of Bitcoin wallet (one time operation)</>}
|
||||||
<>
|
|
||||||
Full scan of Bitcoin wallet (one time operation)
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
progress={fullScanProgressValue}
|
progress={fullScanProgressValue}
|
||||||
icon={<BitcoinIcon />}
|
icon={<BitcoinIcon />}
|
||||||
count={totalOfType}
|
count={totalOfType}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
case "OpeningBitcoinWallet":
|
case "OpeningBitcoinWallet":
|
||||||
return (
|
return (
|
||||||
<LoadingSpinnerAlert severity="info">
|
<LoadingSpinnerAlert severity="info">
|
||||||
<>
|
<>Opening Bitcoin wallet</>
|
||||||
Opening Bitcoin wallet
|
|
||||||
</>
|
|
||||||
</LoadingSpinnerAlert>
|
</LoadingSpinnerAlert>
|
||||||
);
|
);
|
||||||
case "DownloadingMoneroWalletRpc":
|
case "DownloadingMoneroWalletRpc": {
|
||||||
const moneroRpcTitle = `Downloading and verifying the Monero wallet RPC (${bytesToMb(status.progress.content.size).toFixed(2)} MB)`;
|
const moneroRpcTitle = `Downloading and verifying the Monero wallet RPC (${bytesToMb(status.progress.content.size).toFixed(2)} MB)`;
|
||||||
return (
|
return (
|
||||||
<AlertWithLinearProgress
|
<AlertWithLinearProgress
|
||||||
title={
|
title={<>{moneroRpcTitle}</>}
|
||||||
<>
|
|
||||||
{moneroRpcTitle}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
progress={status.progress.content.progress}
|
progress={status.progress.content.progress}
|
||||||
icon={<MoneroIcon />}
|
icon={<MoneroIcon />}
|
||||||
count={totalOfType}
|
count={totalOfType}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
case "OpeningMoneroWallet":
|
case "OpeningMoneroWallet":
|
||||||
return (
|
return (
|
||||||
<LoadingSpinnerAlert severity="info">
|
<LoadingSpinnerAlert severity="info">
|
||||||
<>
|
<>Opening the Monero wallet</>
|
||||||
Opening the Monero wallet
|
|
||||||
</>
|
|
||||||
</LoadingSpinnerAlert>
|
</LoadingSpinnerAlert>
|
||||||
);
|
);
|
||||||
case "OpeningDatabase":
|
case "OpeningDatabase":
|
||||||
return (
|
return (
|
||||||
<LoadingSpinnerAlert severity="info">
|
<LoadingSpinnerAlert severity="info">
|
||||||
<>
|
<>Opening the local database</>
|
||||||
Opening the local database
|
|
||||||
</>
|
|
||||||
</LoadingSpinnerAlert>
|
</LoadingSpinnerAlert>
|
||||||
);
|
);
|
||||||
case "BackgroundRefund":
|
case "BackgroundRefund":
|
||||||
return (
|
return (
|
||||||
<LoadingSpinnerAlert severity="info">
|
<LoadingSpinnerAlert severity="info">
|
||||||
<>
|
<>
|
||||||
Refunding swap <TruncatedText limit={10}>{status.progress.content.swap_id}</TruncatedText>
|
Refunding swap{" "}
|
||||||
|
<TruncatedText limit={10}>
|
||||||
|
{status.progress.content.swap_id}
|
||||||
|
</TruncatedText>
|
||||||
</>
|
</>
|
||||||
</LoadingSpinnerAlert>
|
</LoadingSpinnerAlert>
|
||||||
);
|
);
|
||||||
|
|
@ -166,13 +168,24 @@ export default function DaemonStatusAlert() {
|
||||||
const contextStatus = useAppSelector((s) => s.rpc.status);
|
const contextStatus = useAppSelector((s) => s.rpc.status);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (contextStatus === null || contextStatus === TauriContextStatusEvent.NotInitialized) {
|
if (
|
||||||
return <LoadingSpinnerAlert severity="warning">Checking for available remote nodes</LoadingSpinnerAlert>;
|
contextStatus === null ||
|
||||||
|
contextStatus === TauriContextStatusEvent.NotInitialized
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<LoadingSpinnerAlert severity="warning">
|
||||||
|
Checking for available remote nodes
|
||||||
|
</LoadingSpinnerAlert>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (contextStatus) {
|
switch (contextStatus) {
|
||||||
case TauriContextStatusEvent.Initializing:
|
case TauriContextStatusEvent.Initializing:
|
||||||
return <LoadingSpinnerAlert severity="warning">Core components are loading</LoadingSpinnerAlert>;
|
return (
|
||||||
|
<LoadingSpinnerAlert severity="warning">
|
||||||
|
Core components are loading
|
||||||
|
</LoadingSpinnerAlert>
|
||||||
|
);
|
||||||
case TauriContextStatusEvent.Available:
|
case TauriContextStatusEvent.Available:
|
||||||
return <Alert severity="success">The daemon is running</Alert>;
|
return <Alert severity="success">The daemon is running</Alert>;
|
||||||
case TauriContextStatusEvent.Failed:
|
case TauriContextStatusEvent.Failed:
|
||||||
|
|
@ -199,7 +212,6 @@ export default function DaemonStatusAlert() {
|
||||||
|
|
||||||
export function BackgroundProgressAlerts() {
|
export function BackgroundProgressAlerts() {
|
||||||
const backgroundProgress = usePendingBackgroundProcesses();
|
const backgroundProgress = usePendingBackgroundProcesses();
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
if (backgroundProgress.length === 0) {
|
if (backgroundProgress.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -207,7 +219,8 @@ export function BackgroundProgressAlerts() {
|
||||||
|
|
||||||
const componentCounts: Record<string, number> = {};
|
const componentCounts: Record<string, number> = {};
|
||||||
backgroundProgress.forEach(([, status]) => {
|
backgroundProgress.forEach(([, status]) => {
|
||||||
componentCounts[status.componentName] = (componentCounts[status.componentName] || 0) + 1;
|
componentCounts[status.componentName] =
|
||||||
|
(componentCounts[status.componentName] || 0) + 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderedComponentNames = new Set<string>();
|
const renderedComponentNames = new Set<string>();
|
||||||
|
|
@ -219,12 +232,15 @@ export function BackgroundProgressAlerts() {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
return uniqueBackgroundProcesses.map(([id, status]) => (
|
return (
|
||||||
<PartialInitStatus
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
key={id}
|
{uniqueBackgroundProcesses.map(([id, status]) => (
|
||||||
status={status}
|
<PartialInitStatus
|
||||||
classes={classes}
|
key={id}
|
||||||
totalOfType={componentCounts[status.componentName]}
|
status={status}
|
||||||
/>
|
totalOfType={componentCounts[status.componentName]}
|
||||||
));
|
/>
|
||||||
}
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Button } from "@material-ui/core";
|
import { Button } from "@mui/material";
|
||||||
import Alert from "@material-ui/lab/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { CircularProgress } from "@material-ui/core";
|
import { CircularProgress } from "@mui/material";
|
||||||
import { AlertProps, Alert } from "@material-ui/lab";
|
import { Alert } from "@mui/material";
|
||||||
|
import { AlertProps } from "@mui/material";
|
||||||
|
|
||||||
export function LoadingSpinnerAlert({ ...rest }: AlertProps) {
|
export function LoadingSpinnerAlert({ ...rest }: AlertProps) {
|
||||||
return <Alert icon={<CircularProgress size={22} />} {...rest} />;
|
return <Alert icon={<CircularProgress size={22} />} {...rest} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { Box } from "@mui/material";
|
||||||
import { Alert } from "@material-ui/lab";
|
import { Alert } from "@mui/material";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
import { SatsAmount } from "../other/Units";
|
import { SatsAmount } from "../other/Units";
|
||||||
import WalletRefreshButton from "../pages/wallet/WalletRefreshButton";
|
import WalletRefreshButton from "../pages/wallet/WalletRefreshButton";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
outer: {
|
|
||||||
paddingBottom: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function RemainingFundsWillBeUsedAlert() {
|
export default function RemainingFundsWillBeUsedAlert() {
|
||||||
const classes = useStyles();
|
const balance = useAppSelector((s) => s.rpc.state.balance);
|
||||||
const balance = useAppSelector((s) => s.rpc.state.balance);
|
|
||||||
|
|
||||||
if (balance == null || balance <= 0) {
|
if (balance == null || balance <= 0) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.outer}>
|
<Box sx={{ paddingBottom: 1 }}>
|
||||||
<Alert
|
<Alert
|
||||||
severity="warning"
|
severity="warning"
|
||||||
action={<WalletRefreshButton />}
|
action={<WalletRefreshButton />}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
>
|
>
|
||||||
The remaining funds of <SatsAmount amount={balance} /> in the wallet
|
The remaining funds of <SatsAmount amount={balance} /> in the wallet
|
||||||
will be used for the next swap
|
will be used for the next swap
|
||||||
</Alert>
|
</Alert>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { Box, makeStyles } from "@material-ui/core";
|
import { Box, Alert, AlertTitle } from "@mui/material";
|
||||||
import { Alert, AlertTitle } from "@material-ui/lab/";
|
|
||||||
import {
|
import {
|
||||||
BobStateName,
|
BobStateName,
|
||||||
GetSwapInfoResponseExt,
|
GetSwapInfoResponseExt,
|
||||||
|
|
@ -16,41 +15,32 @@ import TruncatedText from "../../other/TruncatedText";
|
||||||
import { SwapMoneroRecoveryButton } from "../../pages/history/table/SwapMoneroRecoveryButton";
|
import { SwapMoneroRecoveryButton } from "../../pages/history/table/SwapMoneroRecoveryButton";
|
||||||
import { TimelockTimeline } from "./TimelockTimeline";
|
import { TimelockTimeline } from "./TimelockTimeline";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
box: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
padding: "0px",
|
|
||||||
margin: "0px",
|
|
||||||
"& li": {
|
|
||||||
marginBottom: theme.spacing(0.5),
|
|
||||||
"&:last-child": {
|
|
||||||
marginBottom: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
alertMessage: {
|
|
||||||
flexGrow: 1,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for displaying a list of messages.
|
* Component for displaying a list of messages.
|
||||||
* @param messages - Array of messages to display.
|
* @param messages - Array of messages to display.
|
||||||
* @returns JSX.Element
|
* @returns JSX.Element
|
||||||
*/
|
*/
|
||||||
function MessageList({ messages }: { messages: ReactNode[]; }) {
|
function MessageList({ messages }: { messages: ReactNode[] }) {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={classes.list}>
|
<Box
|
||||||
{messages.filter(msg => msg != null).map((msg, i) => (
|
component="ul"
|
||||||
<li key={i}>{msg}</li>
|
sx={{
|
||||||
))}
|
padding: "0px",
|
||||||
</ul>
|
margin: "0px",
|
||||||
|
"& li": {
|
||||||
|
marginBottom: 0.5,
|
||||||
|
"&:last-child": {
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{messages
|
||||||
|
.filter((msg) => msg != null)
|
||||||
|
.map((msg, i) => (
|
||||||
|
<li key={i}>{msg}</li>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,17 +49,23 @@ function MessageList({ messages }: { messages: ReactNode[]; }) {
|
||||||
* @param swap - The swap information.
|
* @param swap - The swap information.
|
||||||
* @returns JSX.Element
|
* @returns JSX.Element
|
||||||
*/
|
*/
|
||||||
function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt; }) {
|
function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt }) {
|
||||||
const classes = useStyles();
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.box}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<MessageList
|
<MessageList
|
||||||
messages={[
|
messages={[
|
||||||
"The Bitcoin has been redeemed by the other party",
|
"The Bitcoin has been redeemed by the other party",
|
||||||
"There is no risk of losing funds. Take as much time as you need",
|
"There is no risk of losing funds. Take as much time as you need",
|
||||||
"The Monero will automatically be redeemed to your provided address once you resume the swap",
|
"The Monero will automatically be redeemed to your provided address once you resume the swap",
|
||||||
"If this step fails, you can manually redeem your funds",
|
"If this step fails, you can manually redeem your funds",
|
||||||
]} />
|
]}
|
||||||
|
/>
|
||||||
<SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" />
|
<SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
@ -82,7 +78,10 @@ function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt; })
|
||||||
* @returns JSX.Element
|
* @returns JSX.Element
|
||||||
*/
|
*/
|
||||||
function BitcoinLockedNoTimelockExpiredStateAlert({
|
function BitcoinLockedNoTimelockExpiredStateAlert({
|
||||||
timelock, cancelTimelockOffset, punishTimelockOffset, isRunning,
|
timelock,
|
||||||
|
cancelTimelockOffset,
|
||||||
|
punishTimelockOffset,
|
||||||
|
isRunning,
|
||||||
}: {
|
}: {
|
||||||
timelock: TimelockNone;
|
timelock: TimelockNone;
|
||||||
cancelTimelockOffset: number;
|
cancelTimelockOffset: number;
|
||||||
|
|
@ -92,20 +91,25 @@ function BitcoinLockedNoTimelockExpiredStateAlert({
|
||||||
return (
|
return (
|
||||||
<MessageList
|
<MessageList
|
||||||
messages={[
|
messages={[
|
||||||
isRunning ? "We are waiting for the other party to lock their Monero" : null,
|
isRunning
|
||||||
|
? "We are waiting for the other party to lock their Monero"
|
||||||
|
: null,
|
||||||
<>
|
<>
|
||||||
If the swap isn't completed in {" "}
|
If the swap isn't completed in{" "}
|
||||||
<HumanizedBitcoinBlockDuration
|
<HumanizedBitcoinBlockDuration
|
||||||
blocks={timelock.content.blocks_left}
|
blocks={timelock.content.blocks_left}
|
||||||
displayBlocks={false}
|
displayBlocks={false}
|
||||||
/>, it needs to be refunded
|
/>
|
||||||
|
, it needs to be refunded
|
||||||
</>,
|
</>,
|
||||||
"For that, you need to have the app open sometime within the refund period",
|
"For that, you need to have the app open sometime within the refund period",
|
||||||
<>
|
<>
|
||||||
After that, cooperation from the other party would be required to recover the funds
|
After that, cooperation from the other party would be required to
|
||||||
|
recover the funds
|
||||||
</>,
|
</>,
|
||||||
isRunning ? null : "Please resume the swap to continue"
|
isRunning ? null : "Please resume the swap to continue",
|
||||||
]} />
|
]}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,7 +121,8 @@ function BitcoinLockedNoTimelockExpiredStateAlert({
|
||||||
* @returns JSX.Element
|
* @returns JSX.Element
|
||||||
*/
|
*/
|
||||||
function BitcoinPossiblyCancelledAlert({
|
function BitcoinPossiblyCancelledAlert({
|
||||||
swap, timelock,
|
swap,
|
||||||
|
timelock,
|
||||||
}: {
|
}: {
|
||||||
swap: GetSwapInfoResponseExt;
|
swap: GetSwapInfoResponseExt;
|
||||||
timelock: TimelockCancel;
|
timelock: TimelockCancel;
|
||||||
|
|
@ -130,10 +135,13 @@ function BitcoinPossiblyCancelledAlert({
|
||||||
<>
|
<>
|
||||||
If we haven't refunded in{" "}
|
If we haven't refunded in{" "}
|
||||||
<HumanizedBitcoinBlockDuration
|
<HumanizedBitcoinBlockDuration
|
||||||
blocks={timelock.content.blocks_left} />
|
blocks={timelock.content.blocks_left}
|
||||||
, cooperation from the other party will be required to recover the funds
|
/>
|
||||||
</>
|
, cooperation from the other party will be required to recover the
|
||||||
]} />
|
funds
|
||||||
|
</>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,7 +156,8 @@ function PunishTimelockExpiredAlert() {
|
||||||
"We couldn't refund within the refund period",
|
"We couldn't refund within the refund period",
|
||||||
"We might still be able to redeem the Monero. However, this will require cooperation from the other party",
|
"We might still be able to redeem the Monero. However, this will require cooperation from the other party",
|
||||||
"Resume the swap as soon as possible",
|
"Resume the swap as soon as possible",
|
||||||
]} />
|
]}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,8 +166,13 @@ function PunishTimelockExpiredAlert() {
|
||||||
* @param swap - The swap information.
|
* @param swap - The swap information.
|
||||||
* @returns JSX.Element | null
|
* @returns JSX.Element | null
|
||||||
*/
|
*/
|
||||||
export function StateAlert({ swap, isRunning }: { swap: GetSwapInfoResponseExtRunningSwap; isRunning: boolean; }) {
|
export function StateAlert({
|
||||||
|
swap,
|
||||||
|
isRunning,
|
||||||
|
}: {
|
||||||
|
swap: GetSwapInfoResponseExtRunningSwap;
|
||||||
|
isRunning: boolean;
|
||||||
|
}) {
|
||||||
switch (swap.state_name) {
|
switch (swap.state_name) {
|
||||||
// This is the state where the swap is safe because the other party has redeemed the Bitcoin
|
// This is the state where the swap is safe because the other party has redeemed the Bitcoin
|
||||||
// It cannot be punished anymore
|
// It cannot be punished anymore
|
||||||
|
|
@ -218,8 +232,6 @@ export default function SwapStatusAlert({
|
||||||
swap: GetSwapInfoResponseExt;
|
swap: GetSwapInfoResponseExt;
|
||||||
isRunning: boolean;
|
isRunning: boolean;
|
||||||
}): JSX.Element | null {
|
}): JSX.Element | null {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
// If the swap is completed, we do not need to display anything
|
// If the swap is completed, we do not need to display anything
|
||||||
if (!isGetSwapInfoResponseRunningSwap(swap)) {
|
if (!isGetSwapInfoResponseRunningSwap(swap)) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -235,12 +247,29 @@ export default function SwapStatusAlert({
|
||||||
key={swap.swap_id}
|
key={swap.swap_id}
|
||||||
severity="warning"
|
severity="warning"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
classes={{ message: classes.alertMessage }}
|
classes={{ message: "alert-message-flex-grow" }}
|
||||||
|
sx={{
|
||||||
|
"& .alert-message-flex-grow": {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
{isRunning ? "Swap has been running for a while" : <>Swap <TruncatedText>{swap.swap_id}</TruncatedText> is not running</>}
|
{isRunning ? (
|
||||||
|
"Swap has been running for a while"
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is not running
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<Box className={classes.box}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<StateAlert swap={swap} isRunning={isRunning} />
|
<StateAlert swap={swap} isRunning={isRunning} />
|
||||||
<TimelockTimeline swap={swap} />
|
<TimelockTimeline swap={swap} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,176 +1,214 @@
|
||||||
import { useTheme, Tooltip, Typography, Box, LinearProgress, Paper } from "@material-ui/core";
|
import {
|
||||||
|
useTheme,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
LinearProgress,
|
||||||
|
Paper,
|
||||||
|
} from "@mui/material";
|
||||||
import { ExpiredTimelocks } from "models/tauriModel";
|
import { ExpiredTimelocks } from "models/tauriModel";
|
||||||
import { GetSwapInfoResponseExt, getAbsoluteBlock } from "models/tauriModelExt";
|
import { GetSwapInfoResponseExt, getAbsoluteBlock } from "models/tauriModelExt";
|
||||||
import HumanizedBitcoinBlockDuration from "renderer/components/other/HumanizedBitcoinBlockDuration";
|
import HumanizedBitcoinBlockDuration from "renderer/components/other/HumanizedBitcoinBlockDuration";
|
||||||
|
|
||||||
interface TimelineSegment {
|
interface TimelineSegment {
|
||||||
title: string;
|
title: string;
|
||||||
label: string;
|
label: string;
|
||||||
bgcolor: string;
|
bgcolor: string;
|
||||||
startBlock: number;
|
startBlock: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TimelineSegmentProps {
|
interface TimelineSegmentProps {
|
||||||
segment: TimelineSegment;
|
segment: TimelineSegment;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
absoluteBlock: number;
|
absoluteBlock: number;
|
||||||
durationOfSegment: number | null;
|
durationOfSegment: number | null;
|
||||||
totalBlocks: number;
|
totalBlocks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimelineSegment({
|
function TimelineSegment({
|
||||||
segment,
|
segment,
|
||||||
isActive,
|
isActive,
|
||||||
absoluteBlock,
|
absoluteBlock,
|
||||||
durationOfSegment,
|
durationOfSegment,
|
||||||
totalBlocks
|
totalBlocks,
|
||||||
}: TimelineSegmentProps) {
|
}: TimelineSegmentProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={<Typography variant="caption">{segment.title}</Typography>}>
|
<Tooltip title={<Typography variant="caption">{segment.title}</Typography>}>
|
||||||
<Box sx={{
|
<Box
|
||||||
display: 'flex',
|
sx={{
|
||||||
flexDirection: 'column',
|
display: "flex",
|
||||||
alignItems: 'center',
|
flexDirection: "column",
|
||||||
justifyContent: 'center',
|
alignItems: "center",
|
||||||
bgcolor: segment.bgcolor,
|
justifyContent: "center",
|
||||||
width: `${durationOfSegment ? ((durationOfSegment / totalBlocks) * 85) : 15}%`,
|
bgcolor: segment.bgcolor,
|
||||||
position: 'relative',
|
width: `${durationOfSegment ? (durationOfSegment / totalBlocks) * 85 : 15}%`,
|
||||||
}} style={{
|
position: "relative",
|
||||||
opacity: isActive ? 1 : 0.3
|
}}
|
||||||
}}>
|
style={{
|
||||||
{isActive && (
|
opacity: isActive ? 1 : 0.3,
|
||||||
<Box sx={{
|
}}
|
||||||
position: 'absolute',
|
>
|
||||||
top: 0,
|
{isActive && (
|
||||||
left: 0,
|
<Box
|
||||||
height: '100%',
|
sx={{
|
||||||
width: `${Math.max(5, ((absoluteBlock - segment.startBlock) / durationOfSegment) * 100)}%`,
|
position: "absolute",
|
||||||
zIndex: 1,
|
top: 0,
|
||||||
}}>
|
left: 0,
|
||||||
<LinearProgress
|
height: "100%",
|
||||||
variant="indeterminate"
|
width: `${Math.max(5, ((absoluteBlock - segment.startBlock) / durationOfSegment) * 100)}%`,
|
||||||
color="primary"
|
zIndex: 1,
|
||||||
style={{
|
}}
|
||||||
height: '100%',
|
>
|
||||||
backgroundColor: theme.palette.primary.dark,
|
<LinearProgress
|
||||||
opacity: 0.3,
|
variant="indeterminate"
|
||||||
}}
|
color="primary"
|
||||||
/>
|
style={{
|
||||||
</Box>
|
height: "100%",
|
||||||
)}
|
backgroundColor: theme.palette.primary.dark,
|
||||||
<Typography variant="subtitle2" color="inherit" align="center" style={{ zIndex: 2 }}>
|
opacity: 0.3,
|
||||||
{segment.label}
|
}}
|
||||||
</Typography>
|
/>
|
||||||
{durationOfSegment && (
|
</Box>
|
||||||
<Typography
|
)}
|
||||||
variant="caption"
|
<Typography
|
||||||
color="inherit"
|
variant="subtitle2"
|
||||||
align="center"
|
color="inherit"
|
||||||
style={{
|
align="center"
|
||||||
zIndex: 2,
|
style={{ zIndex: 2 }}
|
||||||
opacity: 0.8
|
>
|
||||||
}}
|
{segment.label}
|
||||||
>
|
</Typography>
|
||||||
{isActive && (
|
{durationOfSegment && (
|
||||||
<>
|
<Typography
|
||||||
<HumanizedBitcoinBlockDuration
|
variant="caption"
|
||||||
blocks={durationOfSegment - (absoluteBlock - segment.startBlock)}
|
color="inherit"
|
||||||
/>{" "}left
|
align="center"
|
||||||
</>
|
style={{
|
||||||
)}
|
zIndex: 2,
|
||||||
{!isActive && (
|
opacity: 0.8,
|
||||||
<HumanizedBitcoinBlockDuration
|
}}
|
||||||
blocks={durationOfSegment}
|
>
|
||||||
/>
|
{isActive && (
|
||||||
)}
|
<>
|
||||||
</Typography>
|
<HumanizedBitcoinBlockDuration
|
||||||
)}
|
blocks={
|
||||||
</Box>
|
durationOfSegment - (absoluteBlock - segment.startBlock)
|
||||||
</Tooltip>
|
}
|
||||||
);
|
/>{" "}
|
||||||
|
left
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isActive && (
|
||||||
|
<HumanizedBitcoinBlockDuration blocks={durationOfSegment} />
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TimelockTimeline({ swap }: {
|
export function TimelockTimeline({
|
||||||
// This forces the timelock to not be null
|
swap,
|
||||||
swap: GetSwapInfoResponseExt & { timelock: ExpiredTimelocks }
|
}: {
|
||||||
|
// This forces the timelock to not be null
|
||||||
|
swap: GetSwapInfoResponseExt & { timelock: ExpiredTimelocks };
|
||||||
}) {
|
}) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const timelineSegments: TimelineSegment[] = [
|
const timelineSegments: TimelineSegment[] = [
|
||||||
{
|
{
|
||||||
title: "Normally a swap is completed during this period",
|
title: "Normally a swap is completed during this period",
|
||||||
label: "Normal",
|
label: "Normal",
|
||||||
bgcolor: theme.palette.success.main,
|
bgcolor: theme.palette.success.main,
|
||||||
startBlock: 0,
|
startBlock: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "If the swap hasn't been completed before we reach this period, the Bitcoin needs to be refunded. For that, you need to have the app open sometime within the refund period",
|
title:
|
||||||
label: "Refund",
|
"If the swap hasn't been completed before we reach this period, the Bitcoin needs to be refunded. For that, you need to have the app open sometime within the refund period",
|
||||||
bgcolor: theme.palette.warning.main,
|
label: "Refund",
|
||||||
startBlock: swap.cancel_timelock,
|
bgcolor: theme.palette.warning.main,
|
||||||
},
|
startBlock: swap.cancel_timelock,
|
||||||
{
|
},
|
||||||
title: "If you didn't refund within the refund window, you will enter this period. At this point, the Bitcoin can no longer be refunded. It may still be possible to redeem the Monero with cooperation from the other party but this cannot be guaranteed.",
|
{
|
||||||
label: "Danger",
|
title:
|
||||||
bgcolor: theme.palette.error.main,
|
"If you didn't refund within the refund window, you will enter this period. At this point, the Bitcoin can no longer be refunded. It may still be possible to redeem the Monero with cooperation from the other party but this cannot be guaranteed.",
|
||||||
startBlock: swap.cancel_timelock + swap.punish_timelock,
|
label: "Danger",
|
||||||
}
|
bgcolor: theme.palette.error.main,
|
||||||
];
|
startBlock: swap.cancel_timelock + swap.punish_timelock,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const totalBlocks = swap.cancel_timelock + swap.punish_timelock;
|
const totalBlocks = swap.cancel_timelock + swap.punish_timelock;
|
||||||
const absoluteBlock = getAbsoluteBlock(swap.timelock, swap.cancel_timelock, swap.punish_timelock);
|
const absoluteBlock = getAbsoluteBlock(
|
||||||
|
swap.timelock,
|
||||||
|
swap.cancel_timelock,
|
||||||
|
swap.punish_timelock,
|
||||||
|
);
|
||||||
|
|
||||||
// This calculates the duration of a segment
|
// This calculates the duration of a segment
|
||||||
// by getting the the difference to the next segment
|
// by getting the the difference to the next segment
|
||||||
function durationOfSegment(index: number): number | null {
|
function durationOfSegment(index: number): number | null {
|
||||||
const nextSegment = timelineSegments[index + 1];
|
const nextSegment = timelineSegments[index + 1];
|
||||||
if (nextSegment == null) {
|
if (nextSegment == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
return nextSegment.startBlock - timelineSegments[index].startBlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function returns the index of the active segment based on the current block
|
|
||||||
// We iterate in reverse to find the first segment that has a start block less than the current block
|
|
||||||
function getActiveSegmentIndex() {
|
|
||||||
return Array.from(timelineSegments
|
|
||||||
.slice()
|
|
||||||
// We use .entries() to keep the indexes despite reversing
|
|
||||||
.entries())
|
|
||||||
.reverse()
|
|
||||||
.find(([_, segment]) => absoluteBlock >= segment.startBlock)?.[0] ?? 0;
|
|
||||||
}
|
}
|
||||||
|
return nextSegment.startBlock - timelineSegments[index].startBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function returns the index of the active segment based on the current block
|
||||||
|
// We iterate in reverse to find the first segment that has a start block less than the current block
|
||||||
|
function getActiveSegmentIndex() {
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
Array.from(
|
||||||
width: '100%',
|
timelineSegments
|
||||||
minWidth: '100%',
|
.slice()
|
||||||
flexGrow: 1
|
// We use .entries() to keep the indexes despite reversing
|
||||||
}}>
|
.entries(),
|
||||||
<Paper style={{
|
)
|
||||||
position: 'relative',
|
.reverse()
|
||||||
height: '5rem',
|
.find(([_, segment]) => absoluteBlock >= segment.startBlock)?.[0] ?? 0
|
||||||
overflow: 'hidden',
|
|
||||||
}} elevation={3} variant="outlined">
|
|
||||||
<Box sx={{
|
|
||||||
position: 'relative',
|
|
||||||
height: '100%',
|
|
||||||
display: 'flex'
|
|
||||||
}}>
|
|
||||||
{timelineSegments.map((segment, index) => (
|
|
||||||
<TimelineSegment
|
|
||||||
key={index}
|
|
||||||
segment={segment}
|
|
||||||
isActive={getActiveSegmentIndex() === index}
|
|
||||||
absoluteBlock={absoluteBlock}
|
|
||||||
durationOfSegment={durationOfSegment(index)}
|
|
||||||
totalBlocks={totalBlocks}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
minWidth: "100%",
|
||||||
|
flexGrow: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
height: "5rem",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
elevation={3}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{timelineSegments.map((segment, index) => (
|
||||||
|
<TimelineSegment
|
||||||
|
key={index}
|
||||||
|
segment={segment}
|
||||||
|
isActive={getActiveSegmentIndex() === index}
|
||||||
|
absoluteBlock={absoluteBlock}
|
||||||
|
durationOfSegment={durationOfSegment(index)}
|
||||||
|
totalBlocks={totalBlocks}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,15 @@
|
||||||
import { Box, makeStyles } from "@material-ui/core";
|
import { Box } from "@mui/material";
|
||||||
import { useSwapInfosSortedByDate } from "store/hooks";
|
import { useSwapInfosSortedByDate } from "store/hooks";
|
||||||
import SwapStatusAlert from "./SwapStatusAlert/SwapStatusAlert";
|
import SwapStatusAlert from "./SwapStatusAlert/SwapStatusAlert";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
outer: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function SwapTxLockAlertsBox() {
|
export default function SwapTxLockAlertsBox() {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
// We specifically choose ALL swaps here
|
// We specifically choose ALL swaps here
|
||||||
// If a swap is in a state where an Alert is not needed (becaue no Bitcoin have been locked or because the swap has been completed)
|
// If a swap is in a state where an Alert is not needed (becaue no Bitcoin have been locked or because the swap has been completed)
|
||||||
// the SwapStatusAlert component will not render an Alert
|
// the SwapStatusAlert component will not render an Alert
|
||||||
const swaps = useSwapInfosSortedByDate();
|
const swaps = useSwapInfosSortedByDate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.outer}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
{swaps.map((swap) => (
|
{swaps.map((swap) => (
|
||||||
<SwapStatusAlert key={swap.swap_id} swap={swap} isRunning={false} />
|
<SwapStatusAlert key={swap.swap_id} swap={swap} isRunning={false} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Button } from "@material-ui/core";
|
import { Button } from "@mui/material";
|
||||||
import Alert from "@material-ui/lab/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useResumeableSwapsCountExcludingPunished } from "store/hooks";
|
import { useResumeableSwapsCountExcludingPunished } from "store/hooks";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { SvgIcon } from "@material-ui/core";
|
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||||
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
|
|
||||||
|
|
||||||
export default function BitcoinIcon(props: SvgIconProps) {
|
export default function BitcoinIcon(props: SvgIconProps) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { SvgIcon } from "@material-ui/core";
|
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||||
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
|
|
||||||
|
|
||||||
export default function DiscordIcon(props: SvgIconProps) {
|
export default function DiscordIcon(props: SvgIconProps) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,30 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from "react";
|
||||||
import * as jdenticon from 'jdenticon';
|
import * as jdenticon from "jdenticon";
|
||||||
|
|
||||||
interface IdentIconProps {
|
interface IdentIconProps {
|
||||||
value: string;
|
value: string;
|
||||||
size?: number | string;
|
size?: number | string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function IdentIcon({ value, size = 40, className = '' }: IdentIconProps) {
|
function IdentIcon({ value, size = 40, className = "" }: IdentIconProps) {
|
||||||
const iconRef = useRef<SVGSVGElement>(null);
|
const iconRef = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (iconRef.current) {
|
if (iconRef.current) {
|
||||||
jdenticon.update(iconRef.current, value);
|
jdenticon.update(iconRef.current, value);
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
ref={iconRef}
|
ref={iconRef}
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
className={className}
|
className={className}
|
||||||
data-jdenticon-value={value} />
|
data-jdenticon-value={value}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IdentIcon;
|
export default IdentIcon;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { IconButton } from "@material-ui/core";
|
import { IconButton } from "@mui/material";
|
||||||
import { open } from "@tauri-apps/plugin-shell";
|
import { open } from "@tauri-apps/plugin-shell";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ export default function LinkIconButton({
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<IconButton component="span" onClick={() => open(url)}>
|
<IconButton component="span" onClick={() => open(url)} size="large">
|
||||||
{children}
|
{children}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { SvgIcon } from "@material-ui/core";
|
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||||
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
|
|
||||||
|
|
||||||
export default function MatrixIcon(props: SvgIconProps) {
|
export default function MatrixIcon(props: SvgIconProps) {
|
||||||
return (
|
return (
|
||||||
<SvgIcon viewBox="0 0 27.9 32" {...props}>
|
<SvgIcon viewBox="0 0 27.9 32" {...props}>
|
||||||
<path d="M27.1 31.2V0.7h-2.19V0h3.04v32h-3.04v-0.732z" />
|
<path d="M27.1 31.2V0.7h-2.19V0h3.04v32h-3.04v-0.732z" />
|
||||||
<path d="M8.23 10.4v1.54h0.044c0.385-0.564 0.893-1.03 1.49-1.37 0.58-0.323 1.25-0.485 1.99-0.485 0.72 0 1.38 0.14 1.97 0.42 0.595 0.279 1.05 0.771 1.36 1.48 0.338-0.5 0.796-0.941 1.38-1.32 0.58-0.383 1.27-0.574 2.06-0.574 0.602 0 1.16 0.074 1.67 0.22 0.514 0.148 0.954 0.383 1.32 0.707 0.366 0.323 0.653 0.746 0.859 1.27 0.205 0.522 0.308 1.15 0.308 1.89v7.63h-3.13v-6.46c0-0.383-0.015-0.743-0.044-1.08-0.0209-0.307-0.103-0.607-0.242-0.882-0.133-0.251-0.336-0.458-0.584-0.596-0.257-0.146-0.606-0.22-1.05-0.22-0.44 0-0.796 0.085-1.07 0.253-0.272 0.17-0.485 0.39-0.639 0.662-0.159 0.287-0.264 0.602-0.308 0.927-0.052 0.347-0.078 0.697-0.078 1.05v6.35h-3.13v-6.4c0-0.338-7e-3-0.673-0.021-1-0.0114-0.314-0.0749-0.623-0.188-0.916-0.108-0.277-0.3-0.512-0.55-0.673-0.258-0.168-0.636-0.253-1.14-0.253-0.198 0.0083-0.394 0.042-0.584 0.1-0.258 0.0745-0.498 0.202-0.705 0.374-0.228 0.184-0.422 0.449-0.584 0.794-0.161 0.346-0.242 0.798-0.242 1.36v6.62h-3.13v-11.4z" />
|
<path d="M8.23 10.4v1.54h0.044c0.385-0.564 0.893-1.03 1.49-1.37 0.58-0.323 1.25-0.485 1.99-0.485 0.72 0 1.38 0.14 1.97 0.42 0.595 0.279 1.05 0.771 1.36 1.48 0.338-0.5 0.796-0.941 1.38-1.32 0.58-0.383 1.27-0.574 2.06-0.574 0.602 0 1.16 0.074 1.67 0.22 0.514 0.148 0.954 0.383 1.32 0.707 0.366 0.323 0.653 0.746 0.859 1.27 0.205 0.522 0.308 1.15 0.308 1.89v7.63h-3.13v-6.46c0-0.383-0.015-0.743-0.044-1.08-0.0209-0.307-0.103-0.607-0.242-0.882-0.133-0.251-0.336-0.458-0.584-0.596-0.257-0.146-0.606-0.22-1.05-0.22-0.44 0-0.796 0.085-1.07 0.253-0.272 0.17-0.485 0.39-0.639 0.662-0.159 0.287-0.264 0.602-0.308 0.927-0.052 0.347-0.078 0.697-0.078 1.05v6.35h-3.13v-6.4c0-0.338-7e-3-0.673-0.021-1-0.0114-0.314-0.0749-0.623-0.188-0.916-0.108-0.277-0.3-0.512-0.55-0.673-0.258-0.168-0.636-0.253-1.14-0.253-0.198 0.0083-0.394 0.042-0.584 0.1-0.258 0.0745-0.498 0.202-0.705 0.374-0.228 0.184-0.422 0.449-0.584 0.794-0.161 0.346-0.242 0.798-0.242 1.36v6.62h-3.13v-11.4z" />
|
||||||
<path d="M0.936 0.732v30.5h2.19v0.732h-3.04v-32h3.03v0.732z" />
|
<path d="M0.936 0.732v30.5h2.19v0.732h-3.04v-32h3.03v0.732z" />
|
||||||
</SvgIcon>
|
</SvgIcon>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { SvgIcon } from "@material-ui/core";
|
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||||
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
|
|
||||||
|
|
||||||
export default function MoneroIcon(props: SvgIconProps) {
|
export default function MoneroIcon(props: SvgIconProps) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { SvgIcon } from "@material-ui/core";
|
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||||
import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
|
|
||||||
|
|
||||||
export default function TorIcon(props: SvgIconProps) {
|
export default function TorIcon(props: SvgIconProps) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { TextField } from "@material-ui/core";
|
import TextField, { TextFieldProps } from "@mui/material/TextField";
|
||||||
import { TextFieldProps } from "@material-ui/core/TextField/TextField";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { isTestnet } from "store/config";
|
import { isTestnet } from "store/config";
|
||||||
import { isBtcAddressValid } from "utils/conversionUtils";
|
import { isBtcAddressValid } from "utils/conversionUtils";
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,45 @@
|
||||||
import { createContext, useContext, useState, ReactNode } from 'react'
|
import { createContext, useContext, useState, ReactNode } from "react";
|
||||||
|
|
||||||
interface CardSelectionContextType {
|
interface CardSelectionContextType {
|
||||||
selectedValue: string
|
selectedValue: string;
|
||||||
setSelectedValue: (value: string) => void
|
setSelectedValue: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardSelectionContext = createContext<CardSelectionContextType | undefined>(undefined)
|
const CardSelectionContext = createContext<
|
||||||
|
CardSelectionContextType | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
export function CardSelectionProvider({
|
export function CardSelectionProvider({
|
||||||
children,
|
children,
|
||||||
initialValue,
|
initialValue,
|
||||||
onChange
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
initialValue: string
|
initialValue: string;
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [selectedValue, setSelectedValue] = useState(initialValue)
|
const [selectedValue, setSelectedValue] = useState(initialValue);
|
||||||
|
|
||||||
const handleValueChange = (value: string) => {
|
const handleValueChange = (value: string) => {
|
||||||
setSelectedValue(value)
|
setSelectedValue(value);
|
||||||
onChange?.(value)
|
onChange?.(value);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardSelectionContext.Provider value={{ selectedValue, setSelectedValue: handleValueChange }}>
|
<CardSelectionContext.Provider
|
||||||
{children}
|
value={{ selectedValue, setSelectedValue: handleValueChange }}
|
||||||
</CardSelectionContext.Provider>
|
>
|
||||||
)
|
{children}
|
||||||
|
</CardSelectionContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCardSelection() {
|
export function useCardSelection() {
|
||||||
const context = useContext(CardSelectionContext)
|
const context = useContext(CardSelectionContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error('useCardSelection must be used within a CardSelectionProvider')
|
throw new Error(
|
||||||
}
|
"useCardSelection must be used within a CardSelectionProvider",
|
||||||
return context
|
);
|
||||||
}
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,30 @@
|
||||||
import { Box } from '@material-ui/core'
|
import { Box } from "@mui/material";
|
||||||
import CheckIcon from '@material-ui/icons/Check'
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
import { CardSelectionProvider } from './CardSelectionContext'
|
import { CardSelectionProvider } from "./CardSelectionContext";
|
||||||
|
|
||||||
interface CardSelectionGroupProps {
|
interface CardSelectionGroupProps {
|
||||||
children: React.ReactElement<{ value: string }>[]
|
children: React.ReactElement<{ value: string }>[];
|
||||||
value: string
|
value: string;
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CardSelectionGroup({
|
export default function CardSelectionGroup({
|
||||||
children,
|
children,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: CardSelectionGroupProps) {
|
}: CardSelectionGroupProps) {
|
||||||
return (
|
return (
|
||||||
<CardSelectionProvider initialValue={value} onChange={onChange}>
|
<CardSelectionProvider initialValue={value} onChange={onChange}>
|
||||||
<Box style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 12 }}>
|
<Box
|
||||||
{children}
|
style={{
|
||||||
</Box>
|
display: "flex",
|
||||||
</CardSelectionProvider>
|
flexDirection: "column",
|
||||||
)
|
gap: 12,
|
||||||
|
marginTop: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</CardSelectionProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,65 @@
|
||||||
import { Box } from "@material-ui/core";
|
import { Box } from "@mui/material";
|
||||||
import CheckIcon from '@material-ui/icons/Check'
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
import { useCardSelection } from './CardSelectionContext'
|
import { useCardSelection } from "./CardSelectionContext";
|
||||||
|
|
||||||
// The value prop is used by the parent CardSelectionGroup to determine which option is selected
|
// The value prop is used by the parent CardSelectionGroup to determine which option is selected
|
||||||
export default function CardSelectionOption({children, value}: {children: React.ReactNode, value: string}) {
|
export default function CardSelectionOption({
|
||||||
const { selectedValue, setSelectedValue } = useCardSelection()
|
children,
|
||||||
const selected = value === selectedValue
|
value,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
value: string;
|
||||||
|
}) {
|
||||||
|
const { selectedValue, setSelectedValue } = useCardSelection();
|
||||||
|
const selected = value === selectedValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
onClick={() => setSelectedValue(value)}
|
onClick={() => setSelectedValue(value)}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: 16,
|
||||||
|
border: selected ? "2px solid #FF5C1B" : "2px solid #555",
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: "1em",
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 0.2s ease-in-out",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
border: selected ? "2px solid #FF5C1B" : "2px solid #555",
|
||||||
|
borderRadius: 99999,
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
background: selected ? "#FF5C1B" : "transparent",
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
transition: "all 0.2s ease-in-out",
|
||||||
|
transform: selected ? "scale(1.1)" : "scale(1)",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selected ? (
|
||||||
|
<CheckIcon
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
transition: "all 0.2s ease-in-out",
|
||||||
alignItems: 'flex-start',
|
transform: "scale(1)",
|
||||||
gap: 16,
|
animation: "checkIn 0.2s ease-in-out",
|
||||||
border: selected ? '2px solid #FF5C1B' : '2px solid #555',
|
|
||||||
borderRadius: 16,
|
|
||||||
padding: '1em',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s ease-in-out',
|
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<Box
|
) : null}
|
||||||
style={{
|
</Box>
|
||||||
border: selected ? '2px solid #FF5C1B' : '2px solid #555',
|
<Box
|
||||||
borderRadius: 99999,
|
sx={{
|
||||||
width: 28,
|
pt: 0.5,
|
||||||
height: 28,
|
}}
|
||||||
background: selected ? '#FF5C1B' : 'transparent',
|
>
|
||||||
overflow: 'hidden',
|
{children}
|
||||||
display: 'flex',
|
</Box>
|
||||||
alignItems: 'center',
|
</Box>
|
||||||
justifyContent: 'center',
|
);
|
||||||
transition: 'all 0.2s ease-in-out',
|
}
|
||||||
transform: selected ? 'scale(1.1)' : 'scale(1)',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selected ? (
|
|
||||||
<CheckIcon
|
|
||||||
style={{
|
|
||||||
transition: 'all 0.2s ease-in-out',
|
|
||||||
transform: 'scale(1)',
|
|
||||||
animation: 'checkIn 0.2s ease-in-out'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Box>
|
|
||||||
<Box pt={0.5}>{children}</Box>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,30 @@
|
||||||
import { Box, Button, Dialog, DialogActions, DialogContent, IconButton, List, ListItem, ListItemText, TextField } from "@material-ui/core";
|
import {
|
||||||
import { TextFieldProps } from "@material-ui/core/TextField/TextField";
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItemText,
|
||||||
|
TextField,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { TextFieldProps } from "@mui/material";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { getMoneroAddresses } from "renderer/rpc";
|
import { getMoneroAddresses } from "renderer/rpc";
|
||||||
import { isTestnet } from "store/config";
|
import { isTestnet } from "store/config";
|
||||||
import { isXmrAddressValid } from "utils/conversionUtils";
|
import { isXmrAddressValid } from "utils/conversionUtils";
|
||||||
import ImportContactsIcon from '@material-ui/icons/ImportContacts';
|
import ImportContactsIcon from "@mui/icons-material/ImportContacts";
|
||||||
import TruncatedText from "../other/TruncatedText";
|
import TruncatedText from "../other/TruncatedText";
|
||||||
|
|
||||||
|
import ListItemButton from "@mui/material/ListItemButton";
|
||||||
|
|
||||||
type MoneroAddressTextFieldProps = TextFieldProps & {
|
type MoneroAddressTextFieldProps = TextFieldProps & {
|
||||||
address: string;
|
address: string;
|
||||||
onAddressChange: (address: string) => void;
|
onAddressChange: (address: string) => void;
|
||||||
onAddressValidityChange: (valid: boolean) => void;
|
onAddressValidityChange: (valid: boolean) => void;
|
||||||
helperText: string;
|
helperText: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function MoneroAddressTextField({
|
export default function MoneroAddressTextField({
|
||||||
address,
|
address,
|
||||||
|
|
@ -59,12 +71,14 @@ export default function MoneroAddressTextField({
|
||||||
helperText={address.length > 0 ? errorText || helperText : helperText}
|
helperText={address.length > 0 ? errorText || helperText : helperText}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
InputProps={{
|
slotProps={{
|
||||||
endAdornment: addresses?.length > 0 && (
|
input: {
|
||||||
<IconButton onClick={() => setShowDialog(true)} size="small">
|
endAdornment: addresses?.length > 0 && (
|
||||||
<ImportContactsIcon />
|
<IconButton onClick={() => setShowDialog(true)} size="small">
|
||||||
</IconButton>
|
<ImportContactsIcon />
|
||||||
)
|
</IconButton>
|
||||||
|
),
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
@ -90,26 +104,21 @@ function RecentlyUsedAddressesDialog({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
addresses,
|
addresses,
|
||||||
onAddressSelect
|
onAddressSelect,
|
||||||
}: RecentlyUsedAddressesDialogProps) {
|
}: RecentlyUsedAddressesDialogProps) {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||||
open={open}
|
|
||||||
onClose={onClose}
|
|
||||||
maxWidth="sm"
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<List>
|
<List>
|
||||||
{addresses.map((addr) => (
|
{addresses.map((addr) => (
|
||||||
<ListItem
|
<ListItemButton key={addr} onClick={() => onAddressSelect(addr)}>
|
||||||
button
|
<ListItemText
|
||||||
key={addr}
|
|
||||||
onClick={() => onAddressSelect(addr)}
|
|
||||||
>
|
|
||||||
<ListItemText
|
|
||||||
primary={
|
primary={
|
||||||
<Box fontFamily="monospace">
|
<Box
|
||||||
|
sx={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TruncatedText limit={40} truncateMiddle>
|
<TruncatedText limit={40} truncateMiddle>
|
||||||
{addr}
|
{addr}
|
||||||
</TruncatedText>
|
</TruncatedText>
|
||||||
|
|
@ -117,16 +126,12 @@ function RecentlyUsedAddressesDialog({
|
||||||
}
|
}
|
||||||
secondary="Recently used as a redeem address"
|
secondary="Recently used as a redeem address"
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button
|
<Button onClick={onClose} variant="contained" color="primary">
|
||||||
onClick={onClose}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,18 @@
|
||||||
import { DialogTitle, makeStyles, Typography } from "@material-ui/core";
|
import { DialogTitle, Typography } from "@mui/material";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
root: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
type DialogTitleProps = {
|
type DialogTitleProps = {
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DialogHeader({ title }: DialogTitleProps) {
|
export default function DialogHeader({ title }: DialogTitleProps) {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogTitle disableTypography className={classes.root}>
|
<DialogTitle
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography variant="h6">{title}</Typography>
|
<Typography variant="h6">{title}</Typography>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,25 @@
|
||||||
import { Button, makeStyles, Paper, Typography } from "@material-ui/core";
|
import { Button, Paper, Typography } from "@mui/material";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
logsOuter: {
|
|
||||||
overflow: "auto",
|
|
||||||
padding: theme.spacing(1),
|
|
||||||
marginTop: theme.spacing(1),
|
|
||||||
marginBottom: theme.spacing(1),
|
|
||||||
maxHeight: "10rem",
|
|
||||||
},
|
|
||||||
copyButton: {
|
|
||||||
marginTop: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function PaperTextBox({ stdOut }: { stdOut: string }) {
|
export default function PaperTextBox({ stdOut }: { stdOut: string }) {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
function handleCopyLogs() {
|
function handleCopyLogs() {
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className={classes.logsOuter} variant="outlined">
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
overflow: "auto",
|
||||||
|
padding: 1,
|
||||||
|
marginTop: 1,
|
||||||
|
marginBottom: 1,
|
||||||
|
maxHeight: "10rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography component="pre" variant="body2">
|
<Typography component="pre" variant="body2">
|
||||||
{stdOut}
|
{stdOut}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button onClick={handleCopyLogs} className={classes.copyButton}>
|
<Button onClick={handleCopyLogs} sx={{ marginTop: 1 }}>
|
||||||
Copy
|
Copy
|
||||||
</Button>
|
</Button>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@material-ui/core";
|
} from "@mui/material";
|
||||||
import { suspendCurrentSwap } from "renderer/rpc";
|
import { suspendCurrentSwap } from "renderer/rpc";
|
||||||
import PromiseInvokeButton from "../PromiseInvokeButton";
|
import PromiseInvokeButton from "../PromiseInvokeButton";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,247 +1,241 @@
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
IconButton,
|
IconButton,
|
||||||
Paper,
|
Paper,
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@material-ui/core'
|
} from "@mui/material";
|
||||||
import { ErrorOutline, Visibility } from '@material-ui/icons'
|
import { ErrorOutline, Visibility } from "@mui/icons-material";
|
||||||
import ExternalLink from 'renderer/components/other/ExternalLink'
|
import ExternalLink from "renderer/components/other/ExternalLink";
|
||||||
import SwapSelectDropDown from './SwapSelectDropDown'
|
import SwapSelectDropDown from "./SwapSelectDropDown";
|
||||||
import LogViewer from './LogViewer'
|
import LogViewer from "./LogViewer";
|
||||||
import { useFeedback, MAX_FEEDBACK_LENGTH } from './useFeedback'
|
import { useFeedback, MAX_FEEDBACK_LENGTH } from "./useFeedback";
|
||||||
import { useState } from 'react'
|
import { useState } from "react";
|
||||||
import PromiseInvokeButton from 'renderer/components/PromiseInvokeButton'
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
|
|
||||||
export default function FeedbackDialog({
|
export default function FeedbackDialog({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
open: boolean
|
open: boolean;
|
||||||
onClose: () => void
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [swapLogsEditorOpen, setSwapLogsEditorOpen] = useState(false)
|
const [swapLogsEditorOpen, setSwapLogsEditorOpen] = useState(false);
|
||||||
const [daemonLogsEditorOpen, setDaemonLogsEditorOpen] = useState(false)
|
const [daemonLogsEditorOpen, setDaemonLogsEditorOpen] = useState(false);
|
||||||
|
|
||||||
const { input, setInputState, logs, error, clearState, submitFeedback } =
|
const { input, setInputState, logs, error, clearState, submitFeedback } =
|
||||||
useFeedback()
|
useFeedback();
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
clearState()
|
clearState();
|
||||||
onClose()
|
onClose();
|
||||||
}
|
};
|
||||||
|
|
||||||
const bodyTooLong = input.bodyText.length > MAX_FEEDBACK_LENGTH
|
const bodyTooLong = input.bodyText.length > MAX_FEEDBACK_LENGTH;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={handleClose}>
|
<Dialog open={open} onClose={handleClose}>
|
||||||
<DialogTitle style={{ paddingBottom: '0.5rem' }}>
|
<DialogTitle style={{ paddingBottom: "0.5rem" }}>
|
||||||
Submit Feedback
|
Submit Feedback
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error && (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "start",
|
||||||
|
gap: "0.5rem",
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: "hsla(0, 45%, 17%, 1)",
|
||||||
|
padding: "0.5rem",
|
||||||
|
borderRadius: "0.5rem",
|
||||||
|
border: "1px solid hsla(0, 61%, 32%, 1)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ErrorOutline style={{ color: "hsla(0, 77%, 75%, 1)" }} />
|
||||||
|
<Typography style={{ color: "hsla(0, 83%, 91%, 1)" }} noWrap>
|
||||||
|
{error}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box>
|
||||||
|
<Typography style={{ marginBottom: "0.5rem" }}>
|
||||||
|
Have a question or need assistance? Message us below or{" "}
|
||||||
|
<ExternalLink href="https://docs.unstoppableswap.net/send_feedback#email-support">
|
||||||
|
email us
|
||||||
|
</ExternalLink>
|
||||||
|
!
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
variant="outlined"
|
||||||
|
value={input.bodyText}
|
||||||
|
onChange={(e) =>
|
||||||
|
setInputState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
bodyText: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
bodyTooLong
|
||||||
|
? `Text is too long (${input.bodyText.length}/${MAX_FEEDBACK_LENGTH})`
|
||||||
|
: "Message"
|
||||||
|
}
|
||||||
|
multiline
|
||||||
|
minRows={4}
|
||||||
|
maxRows={4}
|
||||||
|
fullWidth
|
||||||
|
error={bodyTooLong}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography style={{ marginBottom: "0.5rem" }}>
|
||||||
|
Attach logs with your feedback for better support.
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "1rem",
|
||||||
|
paddingBottom: "0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SwapSelectDropDown
|
||||||
|
selectedSwap={input.selectedSwap}
|
||||||
|
setSelectedSwap={(swapId) =>
|
||||||
|
setInputState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
selectedSwap: swapId,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Tooltip title="View the logs">
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
flexDirection: 'column',
|
alignItems: "center",
|
||||||
gap: '1.5rem',
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{error && (
|
<IconButton
|
||||||
<Box
|
onClick={() => setSwapLogsEditorOpen(true)}
|
||||||
style={{
|
disabled={input.selectedSwap === null}
|
||||||
display: 'flex',
|
size="large"
|
||||||
alignItems: 'center',
|
>
|
||||||
justifyContent: 'start',
|
<Visibility />
|
||||||
gap: '0.5rem',
|
</IconButton>
|
||||||
width: '100%',
|
|
||||||
backgroundColor: 'hsla(0, 45%, 17%, 1)',
|
|
||||||
padding: '0.5rem',
|
|
||||||
borderRadius: '0.5rem',
|
|
||||||
border: '1px solid hsla(0, 61%, 32%, 1)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ErrorOutline style={{ color: 'hsla(0, 77%, 75%, 1)' }} />
|
|
||||||
<Typography style={{ color: 'hsla(0, 83%, 91%, 1)' }} noWrap>
|
|
||||||
{error}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<Box>
|
|
||||||
<Typography style={{ marginBottom: '0.5rem' }}>
|
|
||||||
Have a question or need assistance? Message us below
|
|
||||||
or{' '}
|
|
||||||
<ExternalLink href="https://docs.unstoppableswap.net/send_feedback#email-support">
|
|
||||||
email us
|
|
||||||
</ExternalLink>
|
|
||||||
!
|
|
||||||
</Typography>
|
|
||||||
<TextField
|
|
||||||
variant="outlined"
|
|
||||||
value={input.bodyText}
|
|
||||||
onChange={(e) =>
|
|
||||||
setInputState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
bodyText: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
label={
|
|
||||||
bodyTooLong
|
|
||||||
? `Text is too long (${input.bodyText.length}/${MAX_FEEDBACK_LENGTH})`
|
|
||||||
: 'Message'
|
|
||||||
}
|
|
||||||
multiline
|
|
||||||
minRows={4}
|
|
||||||
maxRows={4}
|
|
||||||
fullWidth
|
|
||||||
error={bodyTooLong}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography style={{ marginBottom: '0.5rem' }}>
|
|
||||||
Attach logs with your feedback for better support.
|
|
||||||
</Typography>
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: '1rem',
|
|
||||||
paddingBottom: '0.5rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SwapSelectDropDown
|
|
||||||
selectedSwap={input.selectedSwap}
|
|
||||||
setSelectedSwap={(swapId) =>
|
|
||||||
setInputState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
selectedSwap: swapId,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Tooltip title="View the logs">
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
onClick={() =>
|
|
||||||
setSwapLogsEditorOpen(true)
|
|
||||||
}
|
|
||||||
disabled={input.selectedSwap === null}
|
|
||||||
>
|
|
||||||
<Visibility />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
<LogViewer
|
|
||||||
open={swapLogsEditorOpen}
|
|
||||||
setOpen={setSwapLogsEditorOpen}
|
|
||||||
logs={logs.swapLogs}
|
|
||||||
setIsRedacted={(redact) =>
|
|
||||||
setInputState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
isSwapLogsRedacted: redact,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
isRedacted={input.isSwapLogsRedacted}
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: '1rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Paper
|
|
||||||
variant="outlined"
|
|
||||||
style={{ padding: '0.5rem', width: '100%' }}
|
|
||||||
>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
color="primary"
|
|
||||||
checked={input.attachDaemonLogs}
|
|
||||||
onChange={(e) =>
|
|
||||||
setInputState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
attachDaemonLogs:
|
|
||||||
e.target.checked,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Attach logs from the current session"
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
<Tooltip title="View the logs">
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
onClick={() =>
|
|
||||||
setDaemonLogsEditorOpen(true)
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
input.attachDaemonLogs === false
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Visibility />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
color="textSecondary"
|
|
||||||
style={{ marginBottom: '0.5rem' }}
|
|
||||||
>
|
|
||||||
Your feedback will be answered in the app and can be
|
|
||||||
found in the Feedback tab
|
|
||||||
</Typography>
|
|
||||||
<LogViewer
|
|
||||||
open={daemonLogsEditorOpen}
|
|
||||||
setOpen={setDaemonLogsEditorOpen}
|
|
||||||
logs={logs.daemonLogs}
|
|
||||||
setIsRedacted={(redact) =>
|
|
||||||
setInputState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
isDaemonLogsRedacted: redact,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
isRedacted={input.isDaemonLogsRedacted}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContent>
|
</Tooltip>
|
||||||
<DialogActions>
|
</Box>
|
||||||
<Button onClick={handleClose}>Cancel</Button>
|
<LogViewer
|
||||||
<PromiseInvokeButton
|
open={swapLogsEditorOpen}
|
||||||
requiresContext={false}
|
setOpen={setSwapLogsEditorOpen}
|
||||||
color="primary"
|
logs={logs.swapLogs}
|
||||||
variant="contained"
|
setIsRedacted={(redact) =>
|
||||||
onInvoke={submitFeedback}
|
setInputState((prev) => ({
|
||||||
onSuccess={handleClose}
|
...prev,
|
||||||
|
isSwapLogsRedacted: redact,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
isRedacted={input.isSwapLogsRedacted}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
style={{ padding: "0.5rem", width: "100%" }}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
color="primary"
|
||||||
|
checked={input.attachDaemonLogs}
|
||||||
|
onChange={(e) =>
|
||||||
|
setInputState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
attachDaemonLogs: e.target.checked,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Attach logs from the current session"
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
<Tooltip title="View the logs">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Submit
|
<IconButton
|
||||||
</PromiseInvokeButton>
|
onClick={() => setDaemonLogsEditorOpen(true)}
|
||||||
</DialogActions>
|
disabled={input.attachDaemonLogs === false}
|
||||||
</Dialog>
|
size="large"
|
||||||
)
|
>
|
||||||
|
<Visibility />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="textSecondary"
|
||||||
|
style={{ marginBottom: "0.5rem" }}
|
||||||
|
>
|
||||||
|
Your feedback will be answered in the app and can be found in the
|
||||||
|
Feedback tab
|
||||||
|
</Typography>
|
||||||
|
<LogViewer
|
||||||
|
open={daemonLogsEditorOpen}
|
||||||
|
setOpen={setDaemonLogsEditorOpen}
|
||||||
|
logs={logs.daemonLogs}
|
||||||
|
setIsRedacted={(redact) =>
|
||||||
|
setInputState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
isDaemonLogsRedacted: redact,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
isRedacted={input.isDaemonLogsRedacted}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose}>Cancel</Button>
|
||||||
|
<PromiseInvokeButton
|
||||||
|
requiresContext={false}
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
onInvoke={submitFeedback}
|
||||||
|
onSuccess={handleClose}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</PromiseInvokeButton>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
Paper,
|
Paper,
|
||||||
Switch,
|
Switch,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@material-ui/core";
|
} from "@mui/material";
|
||||||
import { CliLog } from "models/cliModel";
|
import { CliLog } from "models/cliModel";
|
||||||
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
||||||
|
|
||||||
|
|
@ -25,39 +25,58 @@ export default function LogViewer({
|
||||||
setOpen,
|
setOpen,
|
||||||
logs,
|
logs,
|
||||||
setIsRedacted,
|
setIsRedacted,
|
||||||
isRedacted
|
isRedacted,
|
||||||
}: LogViewerProps) {
|
}: LogViewerProps) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={() => setOpen(false)} fullWidth>
|
<Dialog open={open} onClose={() => setOpen(false)} fullWidth>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Box>
|
<Box>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
<Box style={{ display: "flex", flexDirection: "row", alignItems: "center" }}>
|
<Box
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography>
|
<Typography>
|
||||||
These are the logs that would be attached to your feedback message and provided to us developers.
|
These are the logs that would be attached to your feedback
|
||||||
They help us narrow down the problem you encountered.
|
message and provided to us developers. They help us narrow down
|
||||||
|
the problem you encountered.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
|
|
||||||
<CliLogsBox
|
<CliLogsBox
|
||||||
label="Logs"
|
label="Logs"
|
||||||
logs={logs}
|
logs={logs}
|
||||||
topRightButton={
|
topRightButton={
|
||||||
<Paper style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', paddingLeft: "0.5rem" }} variant="outlined">
|
<Paper
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingLeft: "0.5rem",
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
Redact
|
Redact
|
||||||
<Switch
|
<Switch
|
||||||
color="primary"
|
color="primary"
|
||||||
checked={isRedacted}
|
checked={isRedacted}
|
||||||
onChange={(_, checked: boolean) => setIsRedacted(checked)}
|
onChange={(_, checked: boolean) => setIsRedacted(checked)}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="contained" color="primary" onClick={() => setOpen(false)}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { MenuItem, Select, Box } from "@material-ui/core";
|
import { MenuItem, Select, Box } from "@mui/material";
|
||||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||||
import { PiconeroAmount } from "../../other/Units";
|
import { PiconeroAmount } from "../../other/Units";
|
||||||
import { parseDateString } from "utils/parseUtils";
|
import { parseDateString } from "utils/parseUtils";
|
||||||
|
|
@ -26,20 +26,20 @@ export default function SwapSelectDropDown({
|
||||||
<Select
|
<Select
|
||||||
value={selectedSwap ?? ""}
|
value={selectedSwap ?? ""}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={(e) => setSelectedSwap(e.target.value as string || null)}
|
onChange={(e) => setSelectedSwap((e.target.value as string) || null)}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
displayEmpty
|
displayEmpty
|
||||||
>
|
>
|
||||||
{swaps.map((swap) => (
|
{swaps.map((swap) => (
|
||||||
<MenuItem value={swap.swap_id} key={swap.swap_id}>
|
<MenuItem value={swap.swap_id} key={swap.swap_id}>
|
||||||
<Box component="span" style={{ whiteSpace: 'pre' }}>
|
<Box component="span" style={{ whiteSpace: "pre" }}>
|
||||||
Swap <TruncatedText>{swap.swap_id}</TruncatedText> from{' '}
|
Swap <TruncatedText>{swap.swap_id}</TruncatedText> from{" "}
|
||||||
{new Date(parseDateString(swap.start_date)).toDateString()} (
|
{new Date(parseDateString(swap.start_date)).toDateString()} (
|
||||||
<PiconeroAmount amount={swap.xmr_amount} />)
|
<PiconeroAmount amount={swap.xmr_amount} />)
|
||||||
</Box>
|
</Box>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
<MenuItem value="">Do not attach a swap</MenuItem>
|
<MenuItem value="">Do not attach a swap</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,163 +1,163 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from "react";
|
||||||
import { store } from 'renderer/store/storeRenderer'
|
import { store } from "renderer/store/storeRenderer";
|
||||||
import { useActiveSwapInfo } from 'store/hooks'
|
import { useActiveSwapInfo } from "store/hooks";
|
||||||
import { logsToRawString } from 'utils/parseUtils'
|
import { logsToRawString } from "utils/parseUtils";
|
||||||
import { getLogsOfSwap, redactLogs } from 'renderer/rpc'
|
import { getLogsOfSwap, redactLogs } from "renderer/rpc";
|
||||||
import { CliLog, parseCliLogString } from 'models/cliModel'
|
import { CliLog, parseCliLogString } from "models/cliModel";
|
||||||
import logger from 'utils/logger'
|
import logger from "utils/logger";
|
||||||
import { submitFeedbackViaHttp } from 'renderer/api'
|
import { submitFeedbackViaHttp } from "renderer/api";
|
||||||
import { addFeedbackId } from 'store/features/conversationsSlice'
|
import { addFeedbackId } from "store/features/conversationsSlice";
|
||||||
import { AttachmentInput } from 'models/apiModel'
|
import { AttachmentInput } from "models/apiModel";
|
||||||
import { useSnackbar } from 'notistack'
|
import { useSnackbar } from "notistack";
|
||||||
|
|
||||||
export const MAX_FEEDBACK_LENGTH = 4000
|
export const MAX_FEEDBACK_LENGTH = 4000;
|
||||||
|
|
||||||
interface FeedbackInputState {
|
interface FeedbackInputState {
|
||||||
bodyText: string
|
bodyText: string;
|
||||||
selectedSwap: string | null
|
selectedSwap: string | null;
|
||||||
attachDaemonLogs: boolean
|
attachDaemonLogs: boolean;
|
||||||
isSwapLogsRedacted: boolean
|
isSwapLogsRedacted: boolean;
|
||||||
isDaemonLogsRedacted: boolean
|
isDaemonLogsRedacted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FeedbackLogsState {
|
interface FeedbackLogsState {
|
||||||
swapLogs: (string | CliLog)[] | null
|
swapLogs: (string | CliLog)[] | null;
|
||||||
daemonLogs: (string | CliLog)[] | null
|
daemonLogs: (string | CliLog)[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialInputState: FeedbackInputState = {
|
const initialInputState: FeedbackInputState = {
|
||||||
bodyText: '',
|
bodyText: "",
|
||||||
selectedSwap: null,
|
selectedSwap: null,
|
||||||
attachDaemonLogs: true,
|
attachDaemonLogs: true,
|
||||||
isSwapLogsRedacted: false,
|
isSwapLogsRedacted: false,
|
||||||
isDaemonLogsRedacted: false,
|
isDaemonLogsRedacted: false,
|
||||||
}
|
};
|
||||||
|
|
||||||
const initialLogsState: FeedbackLogsState = {
|
const initialLogsState: FeedbackLogsState = {
|
||||||
swapLogs: null,
|
swapLogs: null,
|
||||||
daemonLogs: null,
|
daemonLogs: null,
|
||||||
}
|
};
|
||||||
|
|
||||||
export function useFeedback() {
|
export function useFeedback() {
|
||||||
const currentSwapId = useActiveSwapInfo()
|
const currentSwapId = useActiveSwapInfo();
|
||||||
const { enqueueSnackbar } = useSnackbar()
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
const [inputState, setInputState] = useState<FeedbackInputState>({
|
const [inputState, setInputState] = useState<FeedbackInputState>({
|
||||||
...initialInputState,
|
...initialInputState,
|
||||||
selectedSwap: currentSwapId?.swap_id || null,
|
selectedSwap: currentSwapId?.swap_id || null,
|
||||||
})
|
});
|
||||||
const [logsState, setLogsState] =
|
const [logsState, setLogsState] =
|
||||||
useState<FeedbackLogsState>(initialLogsState)
|
useState<FeedbackLogsState>(initialLogsState);
|
||||||
const [isPending, setIsPending] = useState(false)
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const bodyTooLong = inputState.bodyText.length > MAX_FEEDBACK_LENGTH
|
const bodyTooLong = inputState.bodyText.length > MAX_FEEDBACK_LENGTH;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputState.selectedSwap === null) {
|
if (inputState.selectedSwap === null) {
|
||||||
setLogsState((prev) => ({ ...prev, swapLogs: null }))
|
setLogsState((prev) => ({ ...prev, swapLogs: null }));
|
||||||
return
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
getLogsOfSwap(inputState.selectedSwap, inputState.isSwapLogsRedacted)
|
|
||||||
.then((response) => {
|
|
||||||
setLogsState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
swapLogs: response.logs.map(parseCliLogString),
|
|
||||||
}))
|
|
||||||
setError(null)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
logger.error(`Failed to fetch swap logs: ${e}`)
|
|
||||||
setLogsState((prev) => ({ ...prev, swapLogs: null }))
|
|
||||||
setError(`Failed to fetch swap logs: ${e}`)
|
|
||||||
})
|
|
||||||
}, [inputState.selectedSwap, inputState.isSwapLogsRedacted])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!inputState.attachDaemonLogs) {
|
|
||||||
setLogsState((prev) => ({ ...prev, daemonLogs: null }))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (inputState.isDaemonLogsRedacted) {
|
|
||||||
redactLogs(store.getState().rpc?.logs)
|
|
||||||
.then((redactedLogs) => {
|
|
||||||
setLogsState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
daemonLogs: redactedLogs,
|
|
||||||
}))
|
|
||||||
setError(null)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
logger.error(`Failed to redact daemon logs: ${e}`)
|
|
||||||
setLogsState((prev) => ({ ...prev, daemonLogs: null }))
|
|
||||||
setError(`Failed to redact daemon logs: ${e}`)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setLogsState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
daemonLogs: store.getState().rpc?.logs,
|
|
||||||
}))
|
|
||||||
setError(null)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`Failed to fetch daemon logs: ${e}`)
|
|
||||||
setLogsState((prev) => ({ ...prev, daemonLogs: null }))
|
|
||||||
setError(`Failed to fetch daemon logs: ${e}`)
|
|
||||||
}
|
|
||||||
}, [inputState.attachDaemonLogs, inputState.isDaemonLogsRedacted])
|
|
||||||
|
|
||||||
const clearState = () => {
|
|
||||||
setInputState(initialInputState)
|
|
||||||
setLogsState(initialLogsState)
|
|
||||||
setError(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitFeedback = async () => {
|
getLogsOfSwap(inputState.selectedSwap, inputState.isSwapLogsRedacted)
|
||||||
if (inputState.bodyText.length === 0) {
|
.then((response) => {
|
||||||
setError('Please enter a message')
|
setLogsState((prev) => ({
|
||||||
throw new Error('User did not enter a message')
|
...prev,
|
||||||
}
|
swapLogs: response.logs.map(parseCliLogString),
|
||||||
|
}));
|
||||||
|
setError(null);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error(`Failed to fetch swap logs: ${e}`);
|
||||||
|
setLogsState((prev) => ({ ...prev, swapLogs: null }));
|
||||||
|
setError(`Failed to fetch swap logs: ${e}`);
|
||||||
|
});
|
||||||
|
}, [inputState.selectedSwap, inputState.isSwapLogsRedacted]);
|
||||||
|
|
||||||
const attachments: AttachmentInput[] = []
|
useEffect(() => {
|
||||||
// Add swap logs as an attachment
|
if (!inputState.attachDaemonLogs) {
|
||||||
if (logsState.swapLogs) {
|
setLogsState((prev) => ({ ...prev, daemonLogs: null }));
|
||||||
attachments.push({
|
return;
|
||||||
key: `swap_logs_${inputState.selectedSwap}.txt`,
|
|
||||||
content: logsToRawString(logsState.swapLogs),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle daemon logs
|
|
||||||
if (logsState.daemonLogs) {
|
|
||||||
attachments.push({
|
|
||||||
key: 'daemon_logs.txt',
|
|
||||||
content: logsToRawString(logsState.daemonLogs),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the updated API function
|
|
||||||
const feedbackId = await submitFeedbackViaHttp(
|
|
||||||
inputState.bodyText,
|
|
||||||
attachments
|
|
||||||
)
|
|
||||||
|
|
||||||
enqueueSnackbar('Feedback submitted successfully', {
|
|
||||||
variant: 'success',
|
|
||||||
})
|
|
||||||
|
|
||||||
// Dispatch only the ID
|
|
||||||
store.dispatch(addFeedbackId(feedbackId))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
try {
|
||||||
input: inputState,
|
if (inputState.isDaemonLogsRedacted) {
|
||||||
setInputState,
|
redactLogs(store.getState().rpc?.logs)
|
||||||
logs: logsState,
|
.then((redactedLogs) => {
|
||||||
error,
|
setLogsState((prev) => ({
|
||||||
clearState,
|
...prev,
|
||||||
submitFeedback,
|
daemonLogs: redactedLogs,
|
||||||
|
}));
|
||||||
|
setError(null);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error(`Failed to redact daemon logs: ${e}`);
|
||||||
|
setLogsState((prev) => ({ ...prev, daemonLogs: null }));
|
||||||
|
setError(`Failed to redact daemon logs: ${e}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setLogsState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
daemonLogs: store.getState().rpc?.logs,
|
||||||
|
}));
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Failed to fetch daemon logs: ${e}`);
|
||||||
|
setLogsState((prev) => ({ ...prev, daemonLogs: null }));
|
||||||
|
setError(`Failed to fetch daemon logs: ${e}`);
|
||||||
}
|
}
|
||||||
|
}, [inputState.attachDaemonLogs, inputState.isDaemonLogsRedacted]);
|
||||||
|
|
||||||
|
const clearState = () => {
|
||||||
|
setInputState(initialInputState);
|
||||||
|
setLogsState(initialLogsState);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitFeedback = async () => {
|
||||||
|
if (inputState.bodyText.length === 0) {
|
||||||
|
setError("Please enter a message");
|
||||||
|
throw new Error("User did not enter a message");
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachments: AttachmentInput[] = [];
|
||||||
|
// Add swap logs as an attachment
|
||||||
|
if (logsState.swapLogs) {
|
||||||
|
attachments.push({
|
||||||
|
key: `swap_logs_${inputState.selectedSwap}.txt`,
|
||||||
|
content: logsToRawString(logsState.swapLogs),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle daemon logs
|
||||||
|
if (logsState.daemonLogs) {
|
||||||
|
attachments.push({
|
||||||
|
key: "daemon_logs.txt",
|
||||||
|
content: logsToRawString(logsState.daemonLogs),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the updated API function
|
||||||
|
const feedbackId = await submitFeedbackViaHttp(
|
||||||
|
inputState.bodyText,
|
||||||
|
attachments,
|
||||||
|
);
|
||||||
|
|
||||||
|
enqueueSnackbar("Feedback submitted successfully", {
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dispatch only the ID
|
||||||
|
store.dispatch(addFeedbackId(feedbackId));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
input: inputState,
|
||||||
|
setInputState,
|
||||||
|
logs: logsState,
|
||||||
|
error,
|
||||||
|
clearState,
|
||||||
|
submitFeedback,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,114 +1,108 @@
|
||||||
import { makeStyles, Modal } from '@material-ui/core'
|
import { Modal } from "@mui/material";
|
||||||
import { useState } from 'react'
|
import { useState } from "react";
|
||||||
import Slide01_GettingStarted from './slides/Slide01_GettingStarted'
|
import Slide01_GettingStarted from "./slides/Slide01_GettingStarted";
|
||||||
import Slide02_ChooseAMaker from './slides/Slide02_ChooseAMaker'
|
import Slide02_ChooseAMaker from "./slides/Slide02_ChooseAMaker";
|
||||||
import Slide03_PrepareSwap from './slides/Slide03_PrepareSwap'
|
import Slide03_PrepareSwap from "./slides/Slide03_PrepareSwap";
|
||||||
import Slide04_ExecuteSwap from './slides/Slide04_ExecuteSwap'
|
import Slide04_ExecuteSwap from "./slides/Slide04_ExecuteSwap";
|
||||||
import Slide05_KeepAnEyeOnYourSwaps from './slides/Slide05_KeepAnEyeOnYourSwaps'
|
import Slide05_KeepAnEyeOnYourSwaps from "./slides/Slide05_KeepAnEyeOnYourSwaps";
|
||||||
import Slide06_FiatPricePreference from './slides/Slide06_FiatPricePreference'
|
import Slide06_FiatPricePreference from "./slides/Slide06_FiatPricePreference";
|
||||||
import Slide07_ReachOut from './slides/Slide07_ReachOut'
|
import Slide07_ReachOut from "./slides/Slide07_ReachOut";
|
||||||
import {
|
import {
|
||||||
setFetchFiatPrices,
|
setFetchFiatPrices,
|
||||||
setUserHasSeenIntroduction,
|
setUserHasSeenIntroduction,
|
||||||
} from 'store/features/settingsSlice'
|
} from "store/features/settingsSlice";
|
||||||
import { useAppDispatch, useSettings } from 'store/hooks'
|
import { useAppDispatch, useSettings } from "store/hooks";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
modal: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
paper: {
|
|
||||||
width: '80%',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function IntroductionModal() {
|
export default function IntroductionModal() {
|
||||||
const userHasSeenIntroduction = useSettings(
|
const userHasSeenIntroduction = useSettings((s) => s.userHasSeenIntroduction);
|
||||||
(s) => s.userHasSeenIntroduction
|
|
||||||
)
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
// Handle Display State
|
// Handle Display State
|
||||||
const [open, setOpen] = useState<boolean>(!userHasSeenIntroduction)
|
const [open, setOpen] = useState<boolean>(!userHasSeenIntroduction);
|
||||||
const [showFiat, setShowFiat] = useState<boolean>(true)
|
const [showFiat, setShowFiat] = useState<boolean>(true);
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setOpen(false)
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle Slide Index
|
||||||
|
const [currentSlideIndex, setCurrentSlideIndex] = useState(0);
|
||||||
|
|
||||||
|
const handleContinue = () => {
|
||||||
|
if (currentSlideIndex == slideComponents.length - 1) {
|
||||||
|
handleClose();
|
||||||
|
dispatch(setUserHasSeenIntroduction(true));
|
||||||
|
dispatch(setFetchFiatPrices(showFiat));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Slide Index
|
setCurrentSlideIndex((i) => i + 1);
|
||||||
const [currentSlideIndex, setCurrentSlideIndex] = useState(0)
|
};
|
||||||
|
|
||||||
const handleContinue = () => {
|
const handlePrevious = () => {
|
||||||
if (currentSlideIndex == slideComponents.length - 1) {
|
if (currentSlideIndex == 0) {
|
||||||
handleClose()
|
return;
|
||||||
dispatch(setUserHasSeenIntroduction(true))
|
|
||||||
dispatch(setFetchFiatPrices(showFiat))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentSlideIndex((i) => i + 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePrevious = () => {
|
setCurrentSlideIndex((i) => i - 1);
|
||||||
if (currentSlideIndex == 0) {
|
};
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentSlideIndex((i) => i - 1)
|
const slideComponents = [
|
||||||
}
|
<Slide01_GettingStarted
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
hidePreviousButton
|
||||||
|
key="slide-01"
|
||||||
|
/>,
|
||||||
|
<Slide02_ChooseAMaker
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
key="slide-02"
|
||||||
|
/>,
|
||||||
|
<Slide03_PrepareSwap
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
key="slide-03"
|
||||||
|
/>,
|
||||||
|
<Slide04_ExecuteSwap
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
key="slide-04"
|
||||||
|
/>,
|
||||||
|
<Slide05_KeepAnEyeOnYourSwaps
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
key="slide-05"
|
||||||
|
/>,
|
||||||
|
<Slide06_FiatPricePreference
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
showFiat={showFiat}
|
||||||
|
onChange={(showFiatSetting: string) =>
|
||||||
|
setShowFiat(showFiatSetting === "show")
|
||||||
|
}
|
||||||
|
key="slide-06"
|
||||||
|
/>,
|
||||||
|
<Slide07_ReachOut
|
||||||
|
handleContinue={handleContinue}
|
||||||
|
handlePrevious={handlePrevious}
|
||||||
|
key="slide-07"
|
||||||
|
/>,
|
||||||
|
];
|
||||||
|
|
||||||
const slideComponents = [
|
return (
|
||||||
<Slide01_GettingStarted
|
<Modal
|
||||||
handleContinue={handleContinue}
|
open={open}
|
||||||
handlePrevious={handlePrevious}
|
onClose={handleClose}
|
||||||
hidePreviousButton
|
sx={{
|
||||||
/>,
|
display: "flex",
|
||||||
<Slide02_ChooseAMaker
|
alignItems: "center",
|
||||||
handleContinue={handleContinue}
|
justifyContent: "center",
|
||||||
handlePrevious={handlePrevious}
|
}}
|
||||||
/>,
|
disableAutoFocus
|
||||||
<Slide03_PrepareSwap
|
closeAfterTransition
|
||||||
handleContinue={handleContinue}
|
>
|
||||||
handlePrevious={handlePrevious}
|
{slideComponents[currentSlideIndex]}
|
||||||
/>,
|
</Modal>
|
||||||
<Slide04_ExecuteSwap
|
);
|
||||||
handleContinue={handleContinue}
|
|
||||||
handlePrevious={handlePrevious}
|
|
||||||
/>,
|
|
||||||
<Slide05_KeepAnEyeOnYourSwaps
|
|
||||||
handleContinue={handleContinue}
|
|
||||||
handlePrevious={handlePrevious}
|
|
||||||
/>,
|
|
||||||
<Slide06_FiatPricePreference
|
|
||||||
handleContinue={handleContinue}
|
|
||||||
handlePrevious={handlePrevious}
|
|
||||||
showFiat={showFiat}
|
|
||||||
onChange={(showFiatSetting: string) =>
|
|
||||||
setShowFiat(showFiatSetting === 'show')
|
|
||||||
}
|
|
||||||
/>,
|
|
||||||
<Slide07_ReachOut
|
|
||||||
handleContinue={handleContinue}
|
|
||||||
handlePrevious={handlePrevious}
|
|
||||||
/>,
|
|
||||||
]
|
|
||||||
|
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
open={open}
|
|
||||||
onClose={handleClose}
|
|
||||||
className={classes.modal}
|
|
||||||
disableAutoFocus
|
|
||||||
closeAfterTransition
|
|
||||||
>
|
|
||||||
{slideComponents[currentSlideIndex]}
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,19 @@
|
||||||
import { Typography } from '@material-ui/core'
|
import { Typography } from "@mui/material";
|
||||||
import SlideTemplate from './SlideTemplate'
|
import SlideTemplate from "./SlideTemplate";
|
||||||
import imagePath from 'assets/walletWithBitcoinAndMonero.png'
|
import imagePath from "assets/walletWithBitcoinAndMonero.png";
|
||||||
|
|
||||||
export default function Slide01_GettingStarted(props: slideProps) {
|
export default function Slide01_GettingStarted(props: slideProps) {
|
||||||
return (
|
return (
|
||||||
<SlideTemplate
|
<SlideTemplate title="Getting Started" {...props} imagePath={imagePath}>
|
||||||
title="Getting Started"
|
<Typography variant="subtitle1">
|
||||||
{...props}
|
To start swapping, you'll need:
|
||||||
imagePath={imagePath}
|
</Typography>
|
||||||
>
|
<Typography>
|
||||||
<Typography variant="subtitle1">
|
<ul>
|
||||||
To start swapping, you'll need:
|
<li>A Bitcoin wallet with funds to swap</li>
|
||||||
</Typography>
|
<li>A Monero wallet to receive your Monero</li>
|
||||||
<Typography>
|
</ul>
|
||||||
<ul>
|
</Typography>
|
||||||
<li>A Bitcoin wallet with funds to swap</li>
|
</SlideTemplate>
|
||||||
<li>A Monero wallet to receive your Monero</li>
|
);
|
||||||
</ul>
|
|
||||||
</Typography>
|
|
||||||
</SlideTemplate>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import { Typography } from '@material-ui/core'
|
import { Typography } from "@mui/material";
|
||||||
import SlideTemplate from './SlideTemplate'
|
import SlideTemplate from "./SlideTemplate";
|
||||||
import imagePath from 'assets/mockMakerSelection.svg'
|
import imagePath from "assets/mockMakerSelection.svg";
|
||||||
|
|
||||||
export default function Slide02_ChooseAMaker(props: slideProps) {
|
export default function Slide02_ChooseAMaker(props: slideProps) {
|
||||||
return (
|
return (
|
||||||
<SlideTemplate
|
<SlideTemplate
|
||||||
title="Choose a Maker"
|
title="Choose a Maker"
|
||||||
stepLabel="Step 1"
|
stepLabel="Step 1"
|
||||||
{...props}
|
{...props}
|
||||||
imagePath={imagePath}
|
imagePath={imagePath}
|
||||||
>
|
>
|
||||||
<Typography variant="subtitle1">
|
<Typography variant="subtitle1">
|
||||||
To start a swap, choose a maker. Each maker offers different exchange rates and limits.
|
To start a swap, choose a maker. Each maker offers different exchange
|
||||||
</Typography>
|
rates and limits.
|
||||||
</SlideTemplate>
|
</Typography>
|
||||||
)
|
</SlideTemplate>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
import { Typography } from '@material-ui/core'
|
import { Typography } from "@mui/material";
|
||||||
import SlideTemplate from './SlideTemplate'
|
import SlideTemplate from "./SlideTemplate";
|
||||||
import imagePath from 'assets/mockConfigureSwap.svg'
|
import imagePath from "assets/mockConfigureSwap.svg";
|
||||||
|
|
||||||
export default function Slide02_ChooseAMaker(props: slideProps) {
|
export default function Slide02_ChooseAMaker(props: slideProps) {
|
||||||
return (
|
return (
|
||||||
<SlideTemplate title="Prepare Swap" stepLabel="Step 2" {...props} imagePath={imagePath}>
|
<SlideTemplate
|
||||||
<Typography variant="subtitle1">
|
title="Prepare Swap"
|
||||||
To initiate a swap, provide a Monero address and optionally a Bitcoin refund address.
|
stepLabel="Step 2"
|
||||||
</Typography>
|
{...props}
|
||||||
</SlideTemplate>
|
imagePath={imagePath}
|
||||||
)
|
>
|
||||||
|
<Typography variant="subtitle1">
|
||||||
|
To initiate a swap, provide a Monero address and optionally a Bitcoin
|
||||||
|
refund address.
|
||||||
|
</Typography>
|
||||||
|
</SlideTemplate>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,24 @@
|
||||||
import { Typography } from '@material-ui/core'
|
import { Typography } from "@mui/material";
|
||||||
import SlideTemplate from './SlideTemplate'
|
import SlideTemplate from "./SlideTemplate";
|
||||||
import imagePath from 'assets/simpleSwapFlowDiagram.svg'
|
import imagePath from "assets/simpleSwapFlowDiagram.svg";
|
||||||
|
|
||||||
export default function Slide02_ChooseAMaker(props: slideProps) {
|
export default function Slide02_ChooseAMaker(props: slideProps) {
|
||||||
return (
|
return (
|
||||||
<SlideTemplate
|
<SlideTemplate
|
||||||
title="Execute Swap"
|
title="Execute Swap"
|
||||||
stepLabel="Step 3"
|
stepLabel="Step 3"
|
||||||
{...props}
|
{...props}
|
||||||
imagePath={imagePath}
|
imagePath={imagePath}
|
||||||
>
|
>
|
||||||
<Typography variant="subtitle1">
|
<Typography variant="subtitle1">After confirming:</Typography>
|
||||||
After confirming:
|
<Typography>
|
||||||
</Typography>
|
<ol>
|
||||||
<Typography>
|
<li>Your Bitcoin are locked</li>
|
||||||
<ol>
|
<li>Maker locks the Monero</li>
|
||||||
<li>Your Bitcoin are locked</li>
|
<li>Maker reedems the Bitcoin</li>
|
||||||
<li>Maker locks the Monero</li>
|
<li>Monero is sent to your address</li>
|
||||||
<li>Maker reedems the Bitcoin</li>
|
</ol>
|
||||||
<li>Monero is sent to your address</li>
|
</Typography>
|
||||||
</ol>
|
</SlideTemplate>
|
||||||
</Typography>
|
);
|
||||||
</SlideTemplate>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
import { Link, Typography } from '@material-ui/core'
|
import { Link, Typography } from "@mui/material";
|
||||||
import SlideTemplate from './SlideTemplate'
|
import SlideTemplate from "./SlideTemplate";
|
||||||
import imagePath from 'assets/mockHistoryPage.svg'
|
import imagePath from "assets/mockHistoryPage.svg";
|
||||||
import ExternalLink from 'renderer/components/other/ExternalLink'
|
import ExternalLink from "renderer/components/other/ExternalLink";
|
||||||
|
|
||||||
export default function Slide05_KeepAnEyeOnYourSwaps(props: slideProps) {
|
export default function Slide05_KeepAnEyeOnYourSwaps(props: slideProps) {
|
||||||
return (
|
return (
|
||||||
<SlideTemplate
|
<SlideTemplate
|
||||||
title="Monitor Your Swaps"
|
title="Monitor Your Swaps"
|
||||||
stepLabel="Step 3"
|
stepLabel="Step 3"
|
||||||
{...props}
|
{...props}
|
||||||
imagePath={imagePath}
|
imagePath={imagePath}
|
||||||
>
|
>
|
||||||
<Typography>
|
<Typography>
|
||||||
Monitor active swaps to ensure everything proceeds smoothly.
|
Monitor active swaps to ensure everything proceeds smoothly.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography>
|
<Typography>
|
||||||
<ExternalLink href='https://docs.unstoppableswap.net/usage/first_swap'>
|
<ExternalLink href="https://docs.unstoppableswap.net/usage/first_swap">
|
||||||
Learn more about atomic swaps
|
Learn more about atomic swaps
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</Typography>
|
</Typography>
|
||||||
</SlideTemplate>
|
</SlideTemplate>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,54 @@
|
||||||
import { Box, Typography, Paper, Button, Slide } from '@material-ui/core'
|
import { Box, Typography, Paper, Button, Slide } from "@mui/material";
|
||||||
import CardSelectionGroup from 'renderer/components/inputs/CardSelection/CardSelectionGroup'
|
import CardSelectionGroup from "renderer/components/inputs/CardSelection/CardSelectionGroup";
|
||||||
import CardSelectionOption from 'renderer/components/inputs/CardSelection/CardSelectionOption'
|
import CardSelectionOption from "renderer/components/inputs/CardSelection/CardSelectionOption";
|
||||||
import SlideTemplate from './SlideTemplate'
|
import SlideTemplate from "./SlideTemplate";
|
||||||
import imagePath from 'assets/currencyFetching.svg'
|
import imagePath from "assets/currencyFetching.svg";
|
||||||
|
|
||||||
const FiatPricePreferenceSlide = ({
|
const FiatPricePreferenceSlide = ({
|
||||||
handleContinue,
|
handleContinue,
|
||||||
handlePrevious,
|
handlePrevious,
|
||||||
showFiat,
|
showFiat,
|
||||||
onChange,
|
onChange,
|
||||||
}: slideProps & {
|
}: slideProps & {
|
||||||
showFiat: boolean
|
showFiat: boolean;
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<SlideTemplate handleContinue={handleContinue} handlePrevious={handlePrevious} title="Fiat Prices" imagePath={imagePath}>
|
<SlideTemplate
|
||||||
<Typography variant="subtitle1" color="textSecondary">
|
handleContinue={handleContinue}
|
||||||
Do you want to show fiat prices?
|
handlePrevious={handlePrevious}
|
||||||
</Typography>
|
title="Fiat Prices"
|
||||||
<CardSelectionGroup
|
imagePath={imagePath}
|
||||||
value={showFiat ? 'show' : 'hide'}
|
>
|
||||||
onChange={onChange}
|
<Typography variant="subtitle1" color="textSecondary">
|
||||||
>
|
Do you want to show fiat prices?
|
||||||
<CardSelectionOption value="show">
|
</Typography>
|
||||||
<Typography>Show fiat prices</Typography>
|
<CardSelectionGroup
|
||||||
<Typography
|
value={showFiat ? "show" : "hide"}
|
||||||
variant="caption"
|
onChange={onChange}
|
||||||
color="textSecondary"
|
>
|
||||||
paragraph
|
<CardSelectionOption value="show">
|
||||||
style={{ marginBottom: 4 }}
|
<Typography>Show fiat prices</Typography>
|
||||||
>
|
<Typography
|
||||||
We connect to CoinGecko to provide realtime currency
|
variant="caption"
|
||||||
prices.
|
color="textSecondary"
|
||||||
</Typography>
|
paragraph
|
||||||
</CardSelectionOption>
|
style={{ marginBottom: 4 }}
|
||||||
<CardSelectionOption value="hide">
|
>
|
||||||
<Typography>Don't show fiat prices</Typography>
|
We connect to CoinGecko to provide realtime currency prices.
|
||||||
</CardSelectionOption>
|
</Typography>
|
||||||
</CardSelectionGroup>
|
</CardSelectionOption>
|
||||||
<Box style={{ marginTop: "0.5rem" }}>
|
<CardSelectionOption value="hide">
|
||||||
<Typography
|
<Typography>Don't show fiat prices</Typography>
|
||||||
variant="caption"
|
</CardSelectionOption>
|
||||||
color="textSecondary"
|
</CardSelectionGroup>
|
||||||
>
|
<Box style={{ marginTop: "0.5rem" }}>
|
||||||
You can change your preference later in the settings
|
<Typography variant="caption" color="textSecondary">
|
||||||
</Typography>
|
You can change your preference later in the settings
|
||||||
</Box>
|
</Typography>
|
||||||
</SlideTemplate>
|
</Box>
|
||||||
)
|
</SlideTemplate>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default FiatPricePreferenceSlide
|
export default FiatPricePreferenceSlide;
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,34 @@
|
||||||
import { Box, Typography } from '@material-ui/core'
|
import { Box, Typography } from "@mui/material";
|
||||||
import SlideTemplate from './SlideTemplate'
|
import SlideTemplate from "./SlideTemplate";
|
||||||
import imagePath from 'assets/groupWithChatbubbles.png'
|
import imagePath from "assets/groupWithChatbubbles.png";
|
||||||
import GitHubIcon from "@material-ui/icons/GitHub"
|
import GitHubIcon from "@mui/icons-material/GitHub";
|
||||||
import MatrixIcon from 'renderer/components/icons/MatrixIcon'
|
import MatrixIcon from "renderer/components/icons/MatrixIcon";
|
||||||
import LinkIconButton from 'renderer/components/icons/LinkIconButton'
|
import LinkIconButton from "renderer/components/icons/LinkIconButton";
|
||||||
|
|
||||||
export default function Slide02_ChooseAMaker(props: slideProps) {
|
export default function Slide02_ChooseAMaker(props: slideProps) {
|
||||||
return (
|
return (
|
||||||
<SlideTemplate title="Reach out" {...props} imagePath={imagePath} customContinueButtonText="Get Started">
|
<SlideTemplate
|
||||||
<Typography variant="subtitle1">
|
title="Reach out"
|
||||||
We would love to hear about your experience with Unstoppable
|
{...props}
|
||||||
Swap and invite you to join our community.
|
imagePath={imagePath}
|
||||||
</Typography>
|
customContinueButtonText="Get Started"
|
||||||
<Box mt={3}>
|
>
|
||||||
<LinkIconButton url="https://github.com/UnstoppableSwap/core">
|
<Typography variant="subtitle1">
|
||||||
<GitHubIcon/>
|
We would love to hear about your experience with Unstoppable Swap and
|
||||||
</LinkIconButton>
|
invite you to join our community.
|
||||||
<LinkIconButton url="https://matrix.to/#/#unstoppableswap:matrix.org">
|
</Typography>
|
||||||
<MatrixIcon/>
|
<Box
|
||||||
</LinkIconButton>
|
sx={{
|
||||||
</Box>
|
mt: 3,
|
||||||
</SlideTemplate>
|
}}
|
||||||
)
|
>
|
||||||
|
<LinkIconButton url="https://github.com/UnstoppableSwap/core">
|
||||||
|
<GitHubIcon />
|
||||||
|
</LinkIconButton>
|
||||||
|
<LinkIconButton url="https://matrix.to/#/#unstoppableswap:matrix.org">
|
||||||
|
<MatrixIcon />
|
||||||
|
</LinkIconButton>
|
||||||
|
</Box>
|
||||||
|
</SlideTemplate>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,94 @@
|
||||||
import { makeStyles, Paper, Box, Typography, Button } from '@material-ui/core'
|
import { Paper, Box, Typography, Button } from "@mui/material";
|
||||||
|
|
||||||
type slideTemplateProps = {
|
type slideTemplateProps = {
|
||||||
handleContinue: () => void
|
handleContinue: () => void;
|
||||||
handlePrevious: () => void
|
handlePrevious: () => void;
|
||||||
hidePreviousButton?: boolean
|
hidePreviousButton?: boolean;
|
||||||
stepLabel?: String
|
stepLabel?: string;
|
||||||
title: String
|
title: string;
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
imagePath?: string
|
imagePath?: string;
|
||||||
imagePadded?: boolean
|
imagePadded?: boolean;
|
||||||
customContinueButtonText?: String
|
customContinueButtonText?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
paper: {
|
|
||||||
height: "80%",
|
|
||||||
width: "80%",
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
|
||||||
stepLabel: {
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
},
|
|
||||||
splitImage: {
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
objectFit: 'contain'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function SlideTemplate({
|
export default function SlideTemplate({
|
||||||
handleContinue,
|
handleContinue,
|
||||||
handlePrevious,
|
handlePrevious,
|
||||||
hidePreviousButton,
|
hidePreviousButton,
|
||||||
stepLabel,
|
stepLabel,
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
imagePath,
|
imagePath,
|
||||||
imagePadded,
|
imagePadded,
|
||||||
customContinueButtonText
|
customContinueButtonText,
|
||||||
}: slideTemplateProps) {
|
}: slideTemplateProps) {
|
||||||
const classes = useStyles()
|
return (
|
||||||
|
<Paper
|
||||||
return (
|
sx={{
|
||||||
<Paper className={classes.paper}>
|
height: "80%",
|
||||||
<Box m={3} flex alignContent="center" position="relative" width="50%" flexGrow={1}>
|
width: "80%",
|
||||||
<Box>
|
display: "flex",
|
||||||
{stepLabel && (
|
justifyContent: "space-between",
|
||||||
<Typography
|
}}
|
||||||
variant="overline"
|
>
|
||||||
className={classes.stepLabel}
|
<Box
|
||||||
>
|
sx={{
|
||||||
{stepLabel}
|
m: 3,
|
||||||
</Typography>
|
alignContent: "center",
|
||||||
)}
|
position: "relative",
|
||||||
<Typography variant="h4" style={{ marginBottom: 16 }}>{title}</Typography>
|
width: "50%",
|
||||||
{children}
|
flexGrow: 1,
|
||||||
</Box>
|
}}
|
||||||
<Box
|
>
|
||||||
position="absolute"
|
<Box>
|
||||||
bottom={0}
|
{stepLabel && (
|
||||||
width="100%"
|
<Typography variant="overline" sx={{ textTransform: "uppercase" }}>
|
||||||
display="flex"
|
{stepLabel}
|
||||||
justifyContent={
|
</Typography>
|
||||||
hidePreviousButton ? 'flex-end' : 'space-between'
|
)}
|
||||||
}
|
<Typography variant="h4" style={{ marginBottom: 16 }}>
|
||||||
>
|
{title}
|
||||||
{!hidePreviousButton && (
|
</Typography>
|
||||||
<Button onClick={handlePrevious}>Back</Button>
|
{children}
|
||||||
)}
|
</Box>
|
||||||
<Button
|
<Box
|
||||||
onClick={handleContinue}
|
sx={{
|
||||||
variant="contained"
|
position: "absolute",
|
||||||
color="primary"
|
bottom: 0,
|
||||||
>
|
width: "100%",
|
||||||
{customContinueButtonText ? customContinueButtonText : 'Next' }
|
display: "flex",
|
||||||
</Button>
|
justifyContent: hidePreviousButton ? "flex-end" : "space-between",
|
||||||
</Box>
|
}}
|
||||||
</Box>
|
>
|
||||||
{imagePath && (
|
{!hidePreviousButton && (
|
||||||
<Box
|
<Button onClick={handlePrevious}>Back</Button>
|
||||||
bgcolor="#212121"
|
)}
|
||||||
width="50%"
|
<Button onClick={handleContinue} variant="contained" color="primary">
|
||||||
display="flex"
|
{customContinueButtonText ? customContinueButtonText : "Next"}
|
||||||
justifyContent="center"
|
</Button>
|
||||||
p={imagePadded ? "1.5em" : 0}
|
</Box>
|
||||||
>
|
</Box>
|
||||||
<img src={imagePath} className={classes.splitImage} />
|
{imagePath && (
|
||||||
</Box>
|
<Box
|
||||||
)}
|
sx={{
|
||||||
</Paper>
|
bgcolor: "#212121",
|
||||||
)
|
width: "50%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
p: imagePadded ? "1.5em" : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={imagePath}
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
type slideProps = {
|
type slideProps = {
|
||||||
handleContinue: () => void
|
handleContinue: () => void;
|
||||||
handlePrevious: () => void
|
handlePrevious: () => void;
|
||||||
hidePreviousButton?: boolean
|
hidePreviousButton?: boolean;
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,28 +7,21 @@ import {
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
makeStyles,
|
|
||||||
TextField,
|
TextField,
|
||||||
Theme,
|
} from "@mui/material";
|
||||||
} from "@material-ui/core";
|
|
||||||
import { ListSellersResponse } from "models/tauriModel";
|
import { ListSellersResponse } from "models/tauriModel";
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
import { ChangeEvent, useState } from "react";
|
import { ChangeEvent, useState } from "react";
|
||||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { listSellersAtRendezvousPoint, PRESET_RENDEZVOUS_POINTS } from "renderer/rpc";
|
import {
|
||||||
|
listSellersAtRendezvousPoint,
|
||||||
|
PRESET_RENDEZVOUS_POINTS,
|
||||||
|
} from "renderer/rpc";
|
||||||
import { discoveredMakersByRendezvous } from "store/features/makersSlice";
|
import { discoveredMakersByRendezvous } from "store/features/makersSlice";
|
||||||
import { useAppDispatch } from "store/hooks";
|
import { useAppDispatch } from "store/hooks";
|
||||||
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
|
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
|
||||||
chipOuter: {
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
type ListSellersDialogProps = {
|
type ListSellersDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -38,7 +31,6 @@ export default function ListSellersDialog({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
}: ListSellersDialogProps) {
|
}: ListSellersDialogProps) {
|
||||||
const classes = useStyles();
|
|
||||||
const [rendezvousAddress, setRendezvousAddress] = useState("");
|
const [rendezvousAddress, setRendezvousAddress] = useState("");
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
@ -101,7 +93,7 @@ export default function ListSellersDialog({
|
||||||
placeholder="/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE"
|
placeholder="/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE"
|
||||||
error={!!getMultiAddressError()}
|
error={!!getMultiAddressError()}
|
||||||
/>
|
/>
|
||||||
<Box className={classes.chipOuter}>
|
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
|
||||||
{PRESET_RENDEZVOUS_POINTS.map((rAddress) => (
|
{PRESET_RENDEZVOUS_POINTS.map((rAddress) => (
|
||||||
<Chip
|
<Chip
|
||||||
key={rAddress}
|
key={rAddress}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Box, Chip, makeStyles, Paper, Tooltip, Typography } from "@material-ui/core";
|
import { Box, Chip, Paper, Tooltip, Typography } from "@mui/material";
|
||||||
import { VerifiedUser } from "@material-ui/icons";
|
import { VerifiedUser } from "@mui/icons-material";
|
||||||
import { ExtendedMakerStatus } from "models/apiModel";
|
import { ExtendedMakerStatus } from "models/apiModel";
|
||||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||||
import {
|
import {
|
||||||
|
|
@ -7,44 +7,17 @@ import {
|
||||||
SatsAmount,
|
SatsAmount,
|
||||||
} from "renderer/components/other/Units";
|
} from "renderer/components/other/Units";
|
||||||
import { getMarkup, satsToBtc, secondsToDays } from "utils/conversionUtils";
|
import { getMarkup, satsToBtc, secondsToDays } from "utils/conversionUtils";
|
||||||
import { isMakerOutdated, isMakerVersionOutdated } from 'utils/multiAddrUtils';
|
import { isMakerOutdated, isMakerVersionOutdated } from "utils/multiAddrUtils";
|
||||||
import WarningIcon from '@material-ui/icons/Warning';
|
import WarningIcon from "@mui/icons-material/Warning";
|
||||||
import { useAppSelector, useMakerVersion } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
import IdentIcon from "renderer/components/icons/IdentIcon";
|
import IdentIcon from "renderer/components/icons/IdentIcon";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
content: {
|
|
||||||
flex: 1,
|
|
||||||
"& *": {
|
|
||||||
lineBreak: "anywhere",
|
|
||||||
},
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
chipsOuter: {
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: theme.spacing(0.5),
|
|
||||||
},
|
|
||||||
quoteOuter: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
},
|
|
||||||
peerIdContainer: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A chip that displays the markup of the maker's exchange rate compared to the market rate.
|
* A chip that displays the markup of the maker's exchange rate compared to the market rate.
|
||||||
*/
|
*/
|
||||||
function MakerMarkupChip({ maker }: { maker: ExtendedMakerStatus }) {
|
function MakerMarkupChip({ maker }: { maker: ExtendedMakerStatus }) {
|
||||||
const marketExchangeRate = useAppSelector(s => s.rates?.xmrBtcRate);
|
const marketExchangeRate = useAppSelector((s) => s.rates?.xmrBtcRate);
|
||||||
if (marketExchangeRate == null)
|
if (marketExchangeRate == null) return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
const makerExchangeRate = satsToBtc(maker.price);
|
const makerExchangeRate = satsToBtc(maker.price);
|
||||||
/** The markup of the exchange rate compared to the market rate in percent */
|
/** The markup of the exchange rate compared to the market rate in percent */
|
||||||
|
|
@ -57,32 +30,44 @@ function MakerMarkupChip({ maker }: { maker: ExtendedMakerStatus }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MakerInfo({
|
export default function MakerInfo({ maker }: { maker: ExtendedMakerStatus }) {
|
||||||
maker,
|
|
||||||
}: {
|
|
||||||
maker: ExtendedMakerStatus;
|
|
||||||
}) {
|
|
||||||
const classes = useStyles();
|
|
||||||
const isOutdated = isMakerOutdated(maker);
|
const isOutdated = isMakerOutdated(maker);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.content}>
|
<Box
|
||||||
<Box className={classes.peerIdContainer}>
|
sx={{
|
||||||
<Tooltip title={"This avatar is deterministically derived from the public key of the maker"} arrow>
|
flex: 1,
|
||||||
<Box className={classes.peerIdContainer}>
|
"& *": {
|
||||||
|
lineBreak: "anywhere",
|
||||||
|
},
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
"This avatar is deterministically derived from the public key of the maker"
|
||||||
|
}
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
<IdentIcon value={maker.peerId} size={"3rem"} />
|
<IdentIcon value={maker.peerId} size={"3rem"} />
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="subtitle1">
|
<Typography variant="subtitle1">
|
||||||
<TruncatedText limit={16} truncateMiddle>{maker.peerId}</TruncatedText>
|
<TruncatedText limit={16} truncateMiddle>
|
||||||
|
{maker.peerId}
|
||||||
|
</TruncatedText>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color="textSecondary" variant="body2">
|
<Typography color="textSecondary" variant="body2">
|
||||||
{maker.multiAddr}
|
{maker.multiAddr}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box className={classes.quoteOuter}>
|
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||||
<Typography variant="caption">
|
<Typography variant="caption">
|
||||||
Exchange rate:{" "}
|
Exchange rate:{" "}
|
||||||
<MoneroBitcoinExchangeRate rate={satsToBtc(maker.price)} />
|
<MoneroBitcoinExchangeRate rate={satsToBtc(maker.price)} />
|
||||||
|
|
@ -94,7 +79,7 @@ export default function MakerInfo({
|
||||||
Maximum amount: <SatsAmount amount={maker.maxSwapAmount} />
|
Maximum amount: <SatsAmount amount={maker.maxSwapAmount} />
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box className={classes.chipsOuter}>
|
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
|
||||||
{maker.testnet && <Chip label="Testnet" />}
|
{maker.testnet && <Chip label="Testnet" />}
|
||||||
{maker.uptime && (
|
{maker.uptime && (
|
||||||
<Tooltip title="A high uptime (>90%) indicates reliability. Makers with very low uptime may be unreliable and cause swaps to take longer to complete or fail entirely.">
|
<Tooltip title="A high uptime (>90%) indicates reliability. Makers with very low uptime may be unreliable and cause swaps to take longer to complete or fail entirely.">
|
||||||
|
|
@ -103,8 +88,9 @@ export default function MakerInfo({
|
||||||
)}
|
)}
|
||||||
{maker.age ? (
|
{maker.age ? (
|
||||||
<Chip
|
<Chip
|
||||||
label={`Went online ${Math.round(secondsToDays(maker.age))} ${maker.age === 1 ? "day" : "days"
|
label={`Went online ${Math.round(secondsToDays(maker.age))} ${
|
||||||
} ago`}
|
maker.age === 1 ? "day" : "days"
|
||||||
|
} ago`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Chip label="Discovered via rendezvous point" />
|
<Chip label="Discovered via rendezvous point" />
|
||||||
|
|
@ -121,7 +107,6 @@ export default function MakerInfo({
|
||||||
)}
|
)}
|
||||||
<MakerMarkupChip maker={maker} />
|
<MakerMarkupChip maker={maker} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box >
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,11 @@ import {
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
makeStyles,
|
} from "@mui/material";
|
||||||
} from "@material-ui/core";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import AddIcon from "@material-ui/icons/Add";
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
import SearchIcon from "@material-ui/icons/Search";
|
|
||||||
import { ExtendedMakerStatus } from "models/apiModel";
|
import { ExtendedMakerStatus } from "models/apiModel";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { setSelectedMaker } from "store/features/makersSlice";
|
import { setSelectedMaker } from "store/features/makersSlice";
|
||||||
|
|
@ -21,11 +19,7 @@ import ListSellersDialog from "../listSellers/ListSellersDialog";
|
||||||
import MakerInfo from "./MakerInfo";
|
import MakerInfo from "./MakerInfo";
|
||||||
import MakerSubmitDialog from "./MakerSubmitDialog";
|
import MakerSubmitDialog from "./MakerSubmitDialog";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
import ListItemButton from "@mui/material/ListItemButton";
|
||||||
dialogContent: {
|
|
||||||
padding: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
type MakerSelectDialogProps = {
|
type MakerSelectDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -36,9 +30,8 @@ export function MakerSubmitDialogOpenButton() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItemButton
|
||||||
autoFocus
|
autoFocus
|
||||||
button
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Prevents background from being clicked and reopening dialog
|
// Prevents background from being clicked and reopening dialog
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
|
@ -53,7 +46,7 @@ export function MakerSubmitDialogOpenButton() {
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Add a new maker to public registry" />
|
<ListItemText primary="Add a new maker to public registry" />
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,9 +54,8 @@ export function ListSellersDialogOpenButton() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItemButton
|
||||||
autoFocus
|
autoFocus
|
||||||
button
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Prevents background from being clicked and reopening dialog
|
// Prevents background from being clicked and reopening dialog
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
|
@ -78,7 +70,7 @@ export function ListSellersDialogOpenButton() {
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Discover makers by connecting to a rendezvous point" />
|
<ListItemText primary="Discover makers by connecting to a rendezvous point" />
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,7 +78,6 @@ export default function MakerListDialog({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
}: MakerSelectDialogProps) {
|
}: MakerSelectDialogProps) {
|
||||||
const classes = useStyles();
|
|
||||||
const makers = useAllMakers();
|
const makers = useAllMakers();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
|
@ -98,23 +89,20 @@ export default function MakerListDialog({
|
||||||
return (
|
return (
|
||||||
<Dialog onClose={onClose} open={open}>
|
<Dialog onClose={onClose} open={open}>
|
||||||
<DialogTitle>Select a maker</DialogTitle>
|
<DialogTitle>Select a maker</DialogTitle>
|
||||||
|
<DialogContent sx={{ padding: 0 }} dividers>
|
||||||
<DialogContent className={classes.dialogContent} dividers>
|
|
||||||
<List>
|
<List>
|
||||||
{makers.map((maker) => (
|
{makers.map((maker) => (
|
||||||
<ListItem
|
<ListItemButton
|
||||||
button
|
|
||||||
onClick={() => handleMakerChange(maker)}
|
onClick={() => handleMakerChange(maker)}
|
||||||
key={maker.peerId}
|
key={maker.peerId}
|
||||||
>
|
>
|
||||||
<MakerInfo maker={maker} key={maker.peerId} />
|
<MakerInfo maker={maker} key={maker.peerId} />
|
||||||
</ListItem>
|
</ListItemButton>
|
||||||
))}
|
))}
|
||||||
<ListSellersDialogOpenButton />
|
<ListSellersDialogOpenButton />
|
||||||
<MakerSubmitDialogOpenButton />
|
<MakerSubmitDialogOpenButton />
|
||||||
</List>
|
</List>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose}>Cancel</Button>
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,13 @@
|
||||||
import {
|
import { Paper, Card, CardContent, IconButton } from "@mui/material";
|
||||||
Box,
|
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
IconButton,
|
|
||||||
makeStyles,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import ArrowForwardIosIcon from "@material-ui/icons/ArrowForwardIos";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
import MakerInfo from "./MakerInfo";
|
import MakerInfo from "./MakerInfo";
|
||||||
import MakerListDialog from "./MakerListDialog";
|
import MakerListDialog from "./MakerListDialog";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
inner: {
|
|
||||||
textAlign: "left",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
},
|
|
||||||
makerCard: {
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
makerCardContent: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function MakerSelect() {
|
export default function MakerSelect() {
|
||||||
const classes = useStyles();
|
|
||||||
const [selectDialogOpen, setSelectDialogOpen] = useState(false);
|
const [selectDialogOpen, setSelectDialogOpen] = useState(false);
|
||||||
const selectedMaker = useAppSelector(
|
const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
|
||||||
(state) => state.makers.selectedMaker,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!selectedMaker) return <>No maker selected</>;
|
if (!selectedMaker) return <>No maker selected</>;
|
||||||
|
|
||||||
|
|
@ -44,19 +20,19 @@ export default function MakerSelect() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Paper variant="outlined" elevation={4}>
|
||||||
<MakerListDialog
|
<MakerListDialog
|
||||||
open={selectDialogOpen}
|
open={selectDialogOpen}
|
||||||
onClose={handleSelectDialogClose}
|
onClose={handleSelectDialogClose}
|
||||||
/>
|
/>
|
||||||
<Card variant="outlined" className={classes.makerCard}>
|
<Card sx={{ width: "100%" }}>
|
||||||
<CardContent className={classes.makerCardContent}>
|
<CardContent sx={{ display: "flex", alignItems: "center" }}>
|
||||||
<MakerInfo maker={selectedMaker} />
|
<MakerInfo maker={selectedMaker} />
|
||||||
<IconButton onClick={handleSelectDialogOpen} size="small">
|
<IconButton onClick={handleSelectDialogOpen} size="small">
|
||||||
<ArrowForwardIosIcon />
|
<ArrowForwardIosIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@material-ui/core";
|
} from "@mui/material";
|
||||||
import { Multiaddr } from "multiaddr";
|
import { Multiaddr } from "multiaddr";
|
||||||
import { ChangeEvent, useState } from "react";
|
import { ChangeEvent, useState } from "react";
|
||||||
|
|
||||||
|
|
@ -68,8 +68,8 @@ export default function MakerSubmitDialog({
|
||||||
<DialogTitle>Submit a maker to the public registry</DialogTitle>
|
<DialogTitle>Submit a maker to the public registry</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
If the maker is valid and reachable, it will be displayed to all
|
If the maker is valid and reachable, it will be displayed to all other
|
||||||
other users to trade with.
|
users to trade with.
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Box } from "@material-ui/core";
|
import { Box } from "@mui/material";
|
||||||
import QRCode from "react-qr-code";
|
import QRCode from "react-qr-code";
|
||||||
|
|
||||||
export default function BitcoinQrCode({ address }: { address: string }) {
|
export default function BitcoinQrCode({ address }: { address: string }) {
|
||||||
|
|
|
||||||
|
|
@ -2,33 +2,26 @@ import {
|
||||||
Box,
|
Box,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
makeStyles,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from "@material-ui/core";
|
} from "@mui/material";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
subtitle: {
|
|
||||||
paddingTop: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function CircularProgressWithSubtitle({
|
export default function CircularProgressWithSubtitle({
|
||||||
description,
|
description,
|
||||||
}: {
|
}: {
|
||||||
description: string | ReactNode;
|
description: string | ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
sx={{
|
||||||
justifyContent="center"
|
display: "flex",
|
||||||
alignItems="center"
|
justifyContent: "center",
|
||||||
flexDirection="column"
|
alignItems: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress size={50} />
|
<CircularProgress size={50} />
|
||||||
<Typography variant="subtitle2" className={classes.subtitle}>
|
<Typography variant="subtitle2" sx={{ paddingTop: 1 }}>
|
||||||
{description}
|
{description}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -42,16 +35,26 @@ export function LinearProgressWithSubtitle({
|
||||||
description: string | ReactNode;
|
description: string | ReactNode;
|
||||||
value: number;
|
value: number;
|
||||||
}) {
|
}) {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flexDirection="column" alignItems="center" justifyContent="center" style={{ gap: "0.5rem" }}>
|
<Box
|
||||||
<Typography variant="subtitle2" className={classes.subtitle}>
|
style={{ gap: "0.5rem" }}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle2" sx={{ paddingTop: 1 }}>
|
||||||
{description}
|
{description}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box width="10rem">
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "10rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<LinearProgress variant="determinate" value={value} />
|
<LinearProgress variant="determinate" value={value} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { Button } from "@material-ui/core";
|
import Button, { ButtonProps } from "@mui/material/Button";
|
||||||
import { ButtonProps } from "@material-ui/core/Button/Button";
|
|
||||||
|
|
||||||
export default function ClipboardIconButton({
|
export default function ClipboardIconButton({
|
||||||
text,
|
text,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Box } from "@material-ui/core";
|
import { Box } from "@mui/material";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
||||||
import InfoBox from "./InfoBox";
|
import InfoBox from "./InfoBox";
|
||||||
import { Alert } from "@material-ui/lab";
|
import { Alert } from "@mui/material";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -20,7 +20,13 @@ export default function DepositAddressInfoBox({
|
||||||
return (
|
return (
|
||||||
<InfoBox
|
<InfoBox
|
||||||
title={title}
|
title={title}
|
||||||
mainContent={<ActionableMonospaceTextBox content={address} displayCopyIcon={true} enableQrCode={true} />}
|
mainContent={
|
||||||
|
<ActionableMonospaceTextBox
|
||||||
|
content={address}
|
||||||
|
displayCopyIcon={true}
|
||||||
|
enableQrCode={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
additionalContent={
|
additionalContent={
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { Box, LinearProgress, Paper, Typography } from "@mui/material";
|
||||||
Box,
|
|
||||||
LinearProgress,
|
|
||||||
makeStyles,
|
|
||||||
Paper,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -16,21 +10,6 @@ type Props = {
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
outer: {
|
|
||||||
padding: theme.spacing(1.5),
|
|
||||||
overflow: "hidden",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
upperContent: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function InfoBox({
|
export default function InfoBox({
|
||||||
id = null,
|
id = null,
|
||||||
title,
|
title,
|
||||||
|
|
@ -39,12 +18,20 @@ export default function InfoBox({
|
||||||
icon,
|
icon,
|
||||||
loading,
|
loading,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper variant="outlined" className={classes.outer} id={id}>
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
id={id}
|
||||||
|
sx={{
|
||||||
|
padding: 1.5,
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography variant="subtitle1">{title}</Typography>
|
<Typography variant="subtitle1">{title}</Typography>
|
||||||
<Box className={classes.upperContent}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
{icon}
|
{icon}
|
||||||
{mainContent}
|
{mainContent}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { Button, Dialog, DialogActions, DialogContent } from "@mui/material";
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
makeStyles,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { swapReset } from "store/features/swapSlice";
|
import { swapReset } from "store/features/swapSlice";
|
||||||
import { useAppDispatch, useAppSelector, useIsSwapRunning } from "store/hooks";
|
import { useAppDispatch, useAppSelector, useIsSwapRunning } from "store/hooks";
|
||||||
|
|
@ -14,15 +8,6 @@ import SwapStatePage from "./pages/SwapStatePage";
|
||||||
import SwapDialogTitle from "./SwapDialogTitle";
|
import SwapDialogTitle from "./SwapDialogTitle";
|
||||||
import SwapStateStepper from "./SwapStateStepper";
|
import SwapStateStepper from "./SwapStateStepper";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
content: {
|
|
||||||
minHeight: "25rem",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function SwapDialog({
|
export default function SwapDialog({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
|
|
@ -30,8 +15,6 @@ export default function SwapDialog({
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const swap = useAppSelector((state) => state.swap);
|
const swap = useAppSelector((state) => state.swap);
|
||||||
const isSwapRunning = useIsSwapRunning();
|
const isSwapRunning = useIsSwapRunning();
|
||||||
const [debug, setDebug] = useState(false);
|
const [debug, setDebug] = useState(false);
|
||||||
|
|
@ -59,7 +42,15 @@ export default function SwapDialog({
|
||||||
title="Swap Bitcoin for Monero"
|
title="Swap Bitcoin for Monero"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogContent dividers className={classes.content}>
|
<DialogContent
|
||||||
|
dividers
|
||||||
|
sx={{
|
||||||
|
minHeight: "25rem",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{debug ? (
|
{debug ? (
|
||||||
<DebugPage />
|
<DebugPage />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,8 @@
|
||||||
import { Box, DialogTitle, makeStyles, Typography } from "@material-ui/core";
|
import { Box, DialogTitle, Typography } from "@mui/material";
|
||||||
import DebugPageSwitchBadge from "./pages/DebugPageSwitchBadge";
|
import DebugPageSwitchBadge from "./pages/DebugPageSwitchBadge";
|
||||||
import FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge";
|
import FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge";
|
||||||
import TorStatusBadge from "./pages/TorStatusBadge";
|
import TorStatusBadge from "./pages/TorStatusBadge";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
root: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
rightSide: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gridGap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function SwapDialogTitle({
|
export default function SwapDialogTitle({
|
||||||
title,
|
title,
|
||||||
debug,
|
debug,
|
||||||
|
|
@ -25,12 +12,16 @@ export default function SwapDialogTitle({
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
setDebug: (d: boolean) => void;
|
setDebug: (d: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogTitle disableTypography className={classes.root}>
|
<DialogTitle
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography variant="h6">{title}</Typography>
|
<Typography variant="h6">{title}</Typography>
|
||||||
<Box className={classes.rightSide}>
|
<Box sx={{ display: "flex", alignItems: "center", gridGap: 1 }}>
|
||||||
<FeedbackSubmitBadge />
|
<FeedbackSubmitBadge />
|
||||||
<DebugPageSwitchBadge enabled={debug} setEnabled={setDebug} />
|
<DebugPageSwitchBadge enabled={debug} setEnabled={setDebug} />
|
||||||
<TorStatusBadge />
|
<TorStatusBadge />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Step, StepLabel, Stepper, Typography } from "@material-ui/core";
|
import { Step, StepLabel, Stepper, Typography } from "@mui/material";
|
||||||
import { SwapState } from "models/storeModel";
|
import { SwapState } from "models/storeModel";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
import logger from "utils/logger";
|
import logger from "utils/logger";
|
||||||
|
|
@ -119,7 +119,7 @@ function getActiveStep(state: SwapState | null): PathStep | null {
|
||||||
default:
|
default:
|
||||||
return fallbackStep("No step is assigned to the current state");
|
return fallbackStep("No step is assigned to the current state");
|
||||||
// TODO: Make this guard work. It should force the compiler to check if we have covered all possible cases.
|
// TODO: Make this guard work. It should force the compiler to check if we have covered all possible cases.
|
||||||
// return exhaustiveGuard(latestState.type);
|
// return exhaustiveGuard(latestState.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Link, Typography } from "@material-ui/core";
|
import { Link, Typography } from "@mui/material";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import InfoBox from "./InfoBox";
|
import InfoBox from "./InfoBox";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Box, DialogContentText } from "@material-ui/core";
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
import {
|
import {
|
||||||
useActiveSwapInfo,
|
useActiveSwapInfo,
|
||||||
useActiveSwapLogs,
|
useActiveSwapLogs,
|
||||||
|
|
@ -35,7 +35,10 @@ export default function DebugPage() {
|
||||||
data={cliState}
|
data={cliState}
|
||||||
label="Swap Daemon State (exposed via API)"
|
label="Swap Daemon State (exposed via API)"
|
||||||
/>
|
/>
|
||||||
<CliLogsBox label="Tor Daemon Logs" logs={(torStdOut || "").split("\n")} />
|
<CliLogsBox
|
||||||
|
label="Tor Daemon Logs"
|
||||||
|
logs={(torStdOut || "").split("\n")}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Tooltip } from "@material-ui/core";
|
import { Tooltip } from "@mui/material";
|
||||||
import IconButton from "@material-ui/core/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import DeveloperBoardIcon from "@material-ui/icons/DeveloperBoard";
|
import DeveloperBoardIcon from "@mui/icons-material/DeveloperBoard";
|
||||||
|
|
||||||
export default function DebugPageSwitchBadge({
|
export default function DebugPageSwitchBadge({
|
||||||
enabled,
|
enabled,
|
||||||
|
|
@ -18,6 +18,7 @@ export default function DebugPageSwitchBadge({
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
color={enabled ? "primary" : "default"}
|
color={enabled ? "primary" : "default"}
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
<DeveloperBoardIcon />
|
<DeveloperBoardIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { IconButton } from "@material-ui/core";
|
import { IconButton } from "@mui/material";
|
||||||
import FeedbackIcon from "@material-ui/icons/Feedback";
|
import FeedbackIcon from "@mui/icons-material/Feedback";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import FeedbackDialog from "../../feedback/FeedbackDialog";
|
import FeedbackDialog from "../../feedback/FeedbackDialog";
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ export default function FeedbackSubmitBadge() {
|
||||||
onClose={() => setShowFeedbackDialog(false)}
|
onClose={() => setShowFeedbackDialog(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<IconButton onClick={() => setShowFeedbackDialog(true)}>
|
<IconButton onClick={() => setShowFeedbackDialog(true)} size="large">
|
||||||
<FeedbackIcon />
|
<FeedbackIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Box } from "@material-ui/core";
|
import { Box } from "@mui/material";
|
||||||
import { SwapState } from "models/storeModel";
|
import { SwapState } from "models/storeModel";
|
||||||
import CircularProgressWithSubtitle from "../CircularProgressWithSubtitle";
|
import CircularProgressWithSubtitle from "../CircularProgressWithSubtitle";
|
||||||
import BitcoinPunishedPage from "./done/BitcoinPunishedPage";
|
import BitcoinPunishedPage from "./done/BitcoinPunishedPage";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { IconButton, Tooltip } from "@material-ui/core";
|
import { IconButton, Tooltip } from "@mui/material";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
import TorIcon from "../../../icons/TorIcon";
|
import TorIcon from "../../../icons/TorIcon";
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ export default function TorStatusBadge() {
|
||||||
if (tor.processRunning) {
|
if (tor.processRunning) {
|
||||||
return (
|
return (
|
||||||
<Tooltip title="Tor is running in the background">
|
<Tooltip title="Tor is running in the background">
|
||||||
<IconButton>
|
<IconButton size="large">
|
||||||
<TorIcon htmlColor="#7D4698" />
|
<TorIcon htmlColor="#7D4698" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import { Box, DialogContentText } from '@material-ui/core';
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox';
|
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
||||||
import { TauriSwapProgressEventExt } from 'models/tauriModelExt';
|
import { TauriSwapProgressEventExt } from "models/tauriModelExt";
|
||||||
|
|
||||||
export default function BitcoinPunishedPage({
|
export default function BitcoinPunishedPage({
|
||||||
state,
|
state,
|
||||||
}: {
|
}: {
|
||||||
state: TauriSwapProgressEventExt<"BtcPunished"> | TauriSwapProgressEventExt<"CooperativeRedeemRejected">
|
state:
|
||||||
|
| TauriSwapProgressEventExt<"BtcPunished">
|
||||||
|
| TauriSwapProgressEventExt<"CooperativeRedeemRejected">;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
|
@ -13,13 +15,13 @@ export default function BitcoinPunishedPage({
|
||||||
Unfortunately, the swap was unsuccessful. Since you did not refund in
|
Unfortunately, the swap was unsuccessful. Since you did not refund in
|
||||||
time, the Bitcoin has been lost. However, with the cooperation of the
|
time, the Bitcoin has been lost. However, with the cooperation of the
|
||||||
other party, you might still be able to redeem the Monero, although this
|
other party, you might still be able to redeem the Monero, although this
|
||||||
is not guaranteed.{' '}
|
is not guaranteed.{" "}
|
||||||
{state.type === "CooperativeRedeemRejected" && (
|
{state.type === "CooperativeRedeemRejected" && (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
We tried to redeem the Monero with the other party's help, but it
|
We tried to redeem the Monero with the other party's help, but it
|
||||||
was unsuccessful (reason: {state.content.reason}). Attempting again at a
|
was unsuccessful (reason: {state.content.reason}). Attempting again
|
||||||
later time might yield success. <br />
|
at a later time might yield success. <br />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Box, DialogContentText } from "@material-ui/core";
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||||
import { useActiveSwapInfo } from "store/hooks";
|
import { useActiveSwapInfo } from "store/hooks";
|
||||||
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Box, DialogContentText } from "@material-ui/core";
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||||
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
|
||||||
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
|
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Box, DialogContentText } from "@material-ui/core";
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
import { TauriSwapProgressEvent } from "models/tauriModel";
|
import { TauriSwapProgressEvent } from "models/tauriModel";
|
||||||
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
||||||
import { useActiveSwapInfo, useActiveSwapLogs } from "store/hooks";
|
import { useActiveSwapInfo, useActiveSwapLogs } from "store/hooks";
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||||
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
|
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
|
||||||
import SwapStatusAlert from "renderer/components/alert/SwapStatusAlert/SwapStatusAlert";
|
import SwapStatusAlert from "renderer/components/alert/SwapStatusAlert/SwapStatusAlert";
|
||||||
import { useActiveSwapInfo } from "store/hooks";
|
import { useActiveSwapInfo } from "store/hooks";
|
||||||
import { Box, DialogContentText } from "@material-ui/core";
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
|
|
||||||
// This is the number of blocks after which we consider the swap to be at risk of being unsuccessful
|
// This is the number of blocks after which we consider the swap to be at risk of being unsuccessful
|
||||||
const BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD = 2;
|
const BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD = 2;
|
||||||
|
|
@ -17,17 +17,19 @@ export default function BitcoinLockTxInMempoolPage({
|
||||||
<Box>
|
<Box>
|
||||||
{btc_lock_confirmations < BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
|
{btc_lock_confirmations < BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
Your Bitcoin has been locked. {btc_lock_confirmations > 0 ?
|
Your Bitcoin has been locked.{" "}
|
||||||
"We are waiting for the other party to lock their Monero." :
|
{btc_lock_confirmations > 0
|
||||||
"We are waiting for the blockchain to confirm the transaction. Once confirmed, the other party will lock their Monero."
|
? "We are waiting for the other party to lock their Monero."
|
||||||
}
|
: "We are waiting for the blockchain to confirm the transaction. Once confirmed, the other party will lock their Monero."}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
)}
|
)}
|
||||||
<Box style={{
|
<Box
|
||||||
display: "flex",
|
style={{
|
||||||
flexDirection: "column",
|
display: "flex",
|
||||||
gap: "1rem",
|
flexDirection: "column",
|
||||||
}}>
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{btc_lock_confirmations >= BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
|
{btc_lock_confirmations >= BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
|
||||||
<SwapStatusAlert swap={swapInfo} isRunning={true} />
|
<SwapStatusAlert swap={swapInfo} isRunning={true} />
|
||||||
)}
|
)}
|
||||||
|
|
@ -37,9 +39,9 @@ export default function BitcoinLockTxInMempoolPage({
|
||||||
loading
|
loading
|
||||||
additionalContent={
|
additionalContent={
|
||||||
<>
|
<>
|
||||||
Most makers require one confirmation before locking their
|
Most makers require one confirmation before locking their Monero.
|
||||||
Monero. After they lock their funds and the Monero transaction
|
After they lock their funds and the Monero transaction receives
|
||||||
receives one confirmation, the swap will proceed to the next step.
|
one confirmation, the swap will proceed to the next step.
|
||||||
<br />
|
<br />
|
||||||
Confirmations: {btc_lock_confirmations}
|
Confirmations: {btc_lock_confirmations}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,24 @@
|
||||||
import { useConservativeBitcoinSyncProgress, usePendingBackgroundProcesses } from "store/hooks";
|
import {
|
||||||
import CircularProgressWithSubtitle, { LinearProgressWithSubtitle } from "../../CircularProgressWithSubtitle";
|
useConservativeBitcoinSyncProgress,
|
||||||
|
usePendingBackgroundProcesses,
|
||||||
|
} from "store/hooks";
|
||||||
|
import CircularProgressWithSubtitle, {
|
||||||
|
LinearProgressWithSubtitle,
|
||||||
|
} from "../../CircularProgressWithSubtitle";
|
||||||
|
|
||||||
export default function ReceivedQuotePage() {
|
export default function ReceivedQuotePage() {
|
||||||
const syncProgress = useConservativeBitcoinSyncProgress();
|
const syncProgress = useConservativeBitcoinSyncProgress();
|
||||||
|
|
||||||
if (syncProgress?.type === "Known") {
|
if (syncProgress?.type === "Known") {
|
||||||
const percentage = Math.round((syncProgress.content.consumed / syncProgress.content.total) * 100);
|
const percentage = Math.round(
|
||||||
|
(syncProgress.content.consumed / syncProgress.content.total) * 100,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinearProgressWithSubtitle description={`Syncing Bitcoin wallet (${percentage}%)`} value={percentage} />
|
<LinearProgressWithSubtitle
|
||||||
|
description={`Syncing Bitcoin wallet (${percentage}%)`}
|
||||||
|
value={percentage}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,20 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { resolveApproval } from 'renderer/rpc';
|
import { resolveApproval } from "renderer/rpc";
|
||||||
import { PendingLockBitcoinApprovalRequest, TauriSwapProgressEventContent } from 'models/tauriModelExt';
|
import {
|
||||||
|
PendingLockBitcoinApprovalRequest,
|
||||||
|
TauriSwapProgressEventContent,
|
||||||
|
} from "models/tauriModelExt";
|
||||||
import {
|
import {
|
||||||
SatsAmount,
|
SatsAmount,
|
||||||
PiconeroAmount,
|
PiconeroAmount,
|
||||||
MoneroBitcoinExchangeRateFromAmounts
|
MoneroBitcoinExchangeRateFromAmounts,
|
||||||
} from 'renderer/components/other/Units';
|
} from "renderer/components/other/Units";
|
||||||
import {
|
import { Box, Typography, Divider } from "@mui/material";
|
||||||
Box,
|
import { useActiveSwapId, usePendingLockBitcoinApproval } from "store/hooks";
|
||||||
Typography,
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
Divider,
|
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
||||||
} from '@material-ui/core';
|
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
||||||
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
import { useActiveSwapId, usePendingLockBitcoinApproval } from 'store/hooks';
|
|
||||||
import PromiseInvokeButton from 'renderer/components/PromiseInvokeButton';
|
|
||||||
import InfoBox from 'renderer/components/modal/swap/InfoBox';
|
|
||||||
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
|
|
||||||
import CheckIcon from '@material-ui/icons/Check';
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
|
||||||
createStyles({
|
|
||||||
detailGrid: {
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'auto 1fr',
|
|
||||||
rowGap: theme.spacing(1),
|
|
||||||
columnGap: theme.spacing(2),
|
|
||||||
alignItems: 'center',
|
|
||||||
marginBlock: theme.spacing(2),
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
},
|
|
||||||
receiveValue: {
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: theme.palette.success.main,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
},
|
|
||||||
cancelButton: {
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
/// A hook that returns the LockBitcoin confirmation request for the active swap
|
/// A hook that returns the LockBitcoin confirmation request for the active swap
|
||||||
/// Returns null if no confirmation request is found
|
/// Returns null if no confirmation request is found
|
||||||
|
|
@ -53,14 +22,16 @@ function useActiveLockBitcoinApprovalRequest(): PendingLockBitcoinApprovalReques
|
||||||
const approvals = usePendingLockBitcoinApproval();
|
const approvals = usePendingLockBitcoinApproval();
|
||||||
const activeSwapId = useActiveSwapId();
|
const activeSwapId = useActiveSwapId();
|
||||||
|
|
||||||
return approvals
|
return (
|
||||||
?.find(r => r.content.details.content.swap_id === activeSwapId) || null;
|
approvals?.find(
|
||||||
|
(r) => r.content.details.content.swap_id === activeSwapId,
|
||||||
|
) || null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SwapSetupInflightPage({
|
export default function SwapSetupInflightPage({
|
||||||
btc_lock_amount,
|
btc_lock_amount,
|
||||||
}: TauriSwapProgressEventContent<'SwapSetupInflight'>) {
|
}: TauriSwapProgressEventContent<"SwapSetupInflight">) {
|
||||||
const classes = useStyles();
|
|
||||||
const request = useActiveLockBitcoinApprovalRequest();
|
const request = useActiveLockBitcoinApprovalRequest();
|
||||||
|
|
||||||
const [timeLeft, setTimeLeft] = useState<number>(0);
|
const [timeLeft, setTimeLeft] = useState<number>(0);
|
||||||
|
|
@ -81,10 +52,19 @@ export default function SwapSetupInflightPage({
|
||||||
// If we do not have an approval request yet for the Bitcoin lock transaction, we haven't received the offer from Alice yet
|
// If we do not have an approval request yet for the Bitcoin lock transaction, we haven't received the offer from Alice yet
|
||||||
// Display a loading spinner to the user for as long as the swap_setup request is in flight
|
// Display a loading spinner to the user for as long as the swap_setup request is in flight
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return <CircularProgressWithSubtitle description={<>Negotiating offer for <SatsAmount amount={btc_lock_amount} /></>} />;
|
return (
|
||||||
|
<CircularProgressWithSubtitle
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
Negotiating offer for <SatsAmount amount={btc_lock_amount} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { btc_network_fee, xmr_receive_amount } = request.content.details.content;
|
const { btc_network_fee, xmr_receive_amount } =
|
||||||
|
request.content.details.content;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfoBox
|
<InfoBox
|
||||||
|
|
@ -94,23 +74,53 @@ export default function SwapSetupInflightPage({
|
||||||
mainContent={
|
mainContent={
|
||||||
<>
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Box className={classes.detailGrid}>
|
<Box
|
||||||
<Typography className={classes.label}>You send</Typography>
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "auto 1fr",
|
||||||
|
rowGap: 1,
|
||||||
|
columnGap: 2,
|
||||||
|
alignItems: "center",
|
||||||
|
marginBlock: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||||
|
>
|
||||||
|
You send
|
||||||
|
</Typography>
|
||||||
<Typography>
|
<Typography>
|
||||||
<SatsAmount amount={btc_lock_amount} />
|
<SatsAmount amount={btc_lock_amount} />
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography className={classes.label}>Bitcoin network fees</Typography>
|
<Typography
|
||||||
|
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||||
|
>
|
||||||
|
Bitcoin network fees
|
||||||
|
</Typography>
|
||||||
<Typography>
|
<Typography>
|
||||||
<SatsAmount amount={btc_network_fee} />
|
<SatsAmount amount={btc_network_fee} />
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography className={classes.label}>You receive</Typography>
|
<Typography
|
||||||
<Typography className={classes.receiveValue}>
|
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||||
|
>
|
||||||
|
You receive
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: theme.palette.success.main,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<PiconeroAmount amount={xmr_receive_amount} />
|
<PiconeroAmount amount={xmr_receive_amount} />
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography className={classes.label}>Exchange rate</Typography>
|
<Typography
|
||||||
|
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||||
|
>
|
||||||
|
Exchange rate
|
||||||
|
</Typography>
|
||||||
<Typography>
|
<Typography>
|
||||||
<MoneroBitcoinExchangeRateFromAmounts
|
<MoneroBitcoinExchangeRateFromAmounts
|
||||||
satsAmount={btc_lock_amount}
|
satsAmount={btc_lock_amount}
|
||||||
|
|
@ -122,11 +132,18 @@ export default function SwapSetupInflightPage({
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
additionalContent={
|
additionalContent={
|
||||||
<Box className={classes.actions}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: 2,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PromiseInvokeButton
|
<PromiseInvokeButton
|
||||||
variant="text"
|
variant="text"
|
||||||
size="large"
|
size="large"
|
||||||
className={classes.cancelButton}
|
sx={(theme) => ({ color: theme.palette.text.secondary })}
|
||||||
onInvoke={() => resolveApproval(request.content.request_id, false)}
|
onInvoke={() => resolveApproval(request.content.request_id, false)}
|
||||||
displayErrorSnackbar
|
displayErrorSnackbar
|
||||||
requiresContext
|
requiresContext
|
||||||
|
|
@ -149,4 +166,4 @@ export default function SwapSetupInflightPage({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Box, DialogContentText } from "@material-ui/core";
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||||
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
|
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Box, makeStyles, TextField, Typography } from "@material-ui/core";
|
import { Box, TextField, Typography } from "@mui/material";
|
||||||
import { BidQuote } from "models/tauriModel";
|
import { BidQuote } from "models/tauriModel";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
|
|
@ -7,23 +7,6 @@ import { MoneroAmount } from "../../../../other/Units";
|
||||||
|
|
||||||
const MONERO_FEE = 0.000016;
|
const MONERO_FEE = 0.000016;
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
outer: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
textField: {
|
|
||||||
"& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button": {
|
|
||||||
display: "none",
|
|
||||||
},
|
|
||||||
"& input[type=number]": {
|
|
||||||
MozAppearance: "textfield",
|
|
||||||
},
|
|
||||||
maxWidth: theme.spacing(16),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
function calcBtcAmountWithoutFees(amount: number, fees: number) {
|
function calcBtcAmountWithoutFees(amount: number, fees: number) {
|
||||||
return amount - fees;
|
return amount - fees;
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +22,6 @@ export default function DepositAmountHelper({
|
||||||
min_bitcoin_lock_tx_fee: number;
|
min_bitcoin_lock_tx_fee: number;
|
||||||
quote: BidQuote;
|
quote: BidQuote;
|
||||||
}) {
|
}) {
|
||||||
const classes = useStyles();
|
|
||||||
const [amount, setAmount] = useState(min_deposit_until_swap_will_start);
|
const [amount, setAmount] = useState(min_deposit_until_swap_will_start);
|
||||||
const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0;
|
const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0;
|
||||||
|
|
||||||
|
|
@ -70,7 +52,13 @@ export default function DepositAmountHelper({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.outer}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography variant="subtitle2">
|
<Typography variant="subtitle2">
|
||||||
Depositing {bitcoinBalance > 0 && <>another</>}
|
Depositing {bitcoinBalance > 0 && <>another</>}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
@ -80,7 +68,16 @@ export default function DepositAmountHelper({
|
||||||
onChange={(e) => setAmount(btcToSats(parseFloat(e.target.value)))}
|
onChange={(e) => setAmount(btcToSats(parseFloat(e.target.value)))}
|
||||||
size="small"
|
size="small"
|
||||||
type="number"
|
type="number"
|
||||||
className={classes.textField}
|
sx={{
|
||||||
|
"& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button":
|
||||||
|
{
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
"& input[type=number]": {
|
||||||
|
MozAppearance: "textfield",
|
||||||
|
},
|
||||||
|
maxWidth: 16,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography variant="subtitle2">
|
<Typography variant="subtitle2">
|
||||||
BTC will give you approximately{" "}
|
BTC will give you approximately{" "}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
import { Box, Paper, Tab, Tabs, Typography } from "@mui/material";
|
||||||
Box,
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
makeStyles,
|
|
||||||
Paper,
|
|
||||||
Tab,
|
|
||||||
Tabs,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import RemainingFundsWillBeUsedAlert from "renderer/components/alert/RemainingFundsWillBeUsedAlert";
|
import RemainingFundsWillBeUsedAlert from "renderer/components/alert/RemainingFundsWillBeUsedAlert";
|
||||||
import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField";
|
import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField";
|
||||||
|
|
@ -15,20 +8,7 @@ import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { buyXmr } from "renderer/rpc";
|
import { buyXmr } from "renderer/rpc";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
initButton: {
|
|
||||||
marginTop: theme.spacing(1),
|
|
||||||
},
|
|
||||||
fieldsOuter: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: theme.spacing(1.5),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function InitPage() {
|
export default function InitPage() {
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const [redeemAddress, setRedeemAddress] = useState("");
|
const [redeemAddress, setRedeemAddress] = useState("");
|
||||||
const [refundAddress, setRefundAddress] = useState("");
|
const [refundAddress, setRefundAddress] = useState("");
|
||||||
const [useExternalRefundAddress, setUseExternalRefundAddress] =
|
const [useExternalRefundAddress, setUseExternalRefundAddress] =
|
||||||
|
|
@ -37,9 +17,7 @@ export default function InitPage() {
|
||||||
const [redeemAddressValid, setRedeemAddressValid] = useState(false);
|
const [redeemAddressValid, setRedeemAddressValid] = useState(false);
|
||||||
const [refundAddressValid, setRefundAddressValid] = useState(false);
|
const [refundAddressValid, setRefundAddressValid] = useState(false);
|
||||||
|
|
||||||
const selectedMaker = useAppSelector(
|
const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
|
||||||
(state) => state.makers.selectedMaker,
|
|
||||||
);
|
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await buyXmr(
|
await buyXmr(
|
||||||
|
|
@ -51,7 +29,13 @@ export default function InitPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Box className={classes.fieldsOuter}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<RemainingFundsWillBeUsedAlert />
|
<RemainingFundsWillBeUsedAlert />
|
||||||
<MoneroAddressTextField
|
<MoneroAddressTextField
|
||||||
label="Monero redeem address"
|
label="Monero redeem address"
|
||||||
|
|
@ -104,7 +88,7 @@ export default function InitPage() {
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large"
|
size="large"
|
||||||
className={classes.initButton}
|
sx={{ marginTop: 1 }}
|
||||||
endIcon={<PlayArrowIcon />}
|
endIcon={<PlayArrowIcon />}
|
||||||
onInvoke={init}
|
onInvoke={init}
|
||||||
displayErrorSnackbar
|
displayErrorSnackbar
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue