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

* task(gui): update to mui v5

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

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

* task(gui): update to mui v7

* task(gui): update react

* fix(gui): fix import

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

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

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

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

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

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

* refactor: fix a few eslint errors

* dprint fmt

* fix tsc complains

* nitpick: small spacing issue

---------

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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