diff --git a/package-lock.json b/package-lock.json index d6a4b93..95abc3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,10 +19,12 @@ "lodash": "^4.17.21", "moment": "^2.30.1", "react": "^18.2.0", + "react-copy-to-clipboard": "^5.1.0", "react-countdown-circle-timer": "^3.2.1", "react-dom": "^18.2.0", "react-ga4": "^2.1.0", "react-loader-spinner": "^6.1.6", + "react-qr-code": "^2.0.15", "react-router-dom": "^6.23.0", "react-toastify": "^10.0.5", "sass": "^1.76.0", @@ -3967,6 +3969,14 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/core-js-compat": { "version": "3.38.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", @@ -6328,6 +6338,11 @@ "node": ">=6" } }, + "node_modules/qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6368,6 +6383,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-copy-to-clipboard": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", + "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", + "dependencies": { + "copy-to-clipboard": "^3.3.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3.0 || 16 || 17 || 18" + } + }, "node_modules/react-countdown-circle-timer": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/react-countdown-circle-timer/-/react-countdown-circle-timer-3.2.1.tgz", @@ -6414,6 +6441,18 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-qr-code": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.15.tgz", + "integrity": "sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==", + "dependencies": { + "prop-types": "^15.8.1", + "qr.js": "0.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -7286,6 +7325,11 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", diff --git a/package.json b/package.json index f23f0d3..77087a4 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,12 @@ "lodash": "^4.17.21", "moment": "^2.30.1", "react": "^18.2.0", + "react-copy-to-clipboard": "^5.1.0", "react-countdown-circle-timer": "^3.2.1", "react-dom": "^18.2.0", "react-ga4": "^2.1.0", "react-loader-spinner": "^6.1.6", + "react-qr-code": "^2.0.15", "react-router-dom": "^6.23.0", "react-toastify": "^10.0.5", "sass": "^1.76.0", diff --git a/src/App.tsx b/src/App.tsx index 13fa6bb..a99a84e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -256,7 +256,7 @@ function App() { }; }, [userInfo?.address]); - const getCoinLabel = (coin?: string)=> { + const getCoinLabel = (coin?: string)=> { switch(coin || selectedCoin){ case "LITECOIN":{ diff --git a/src/assets/SVG/Copy.svg b/src/assets/SVG/Copy.svg new file mode 100644 index 0000000..0348fc9 --- /dev/null +++ b/src/assets/SVG/Copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/header/AddressQRCode.tsx b/src/components/header/AddressQRCode.tsx new file mode 100644 index 0000000..f4858fb --- /dev/null +++ b/src/components/header/AddressQRCode.tsx @@ -0,0 +1,50 @@ +import React, { useState } from "react"; +import QRCode from "react-qr-code"; +import { Box, Typography } from "@mui/material"; + +export const AddressQRCode = ({ targetAddress }) => { + return ( + + + + + + + + + + + + ); +}; diff --git a/src/components/header/Header-styles.tsx b/src/components/header/Header-styles.tsx index 7778779..b0898f7 100644 --- a/src/components/header/Header-styles.tsx +++ b/src/components/header/Header-styles.tsx @@ -216,6 +216,7 @@ export const CoinSelectRow = styled(Box)({ flexDirection: "column", gap: "5px", alignSelf: "flex-start", + marginBottom: '5px' }); export const CoinActionContainer = styled(Box)({ diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx index 3fa5edc..4d8c57f 100644 --- a/src/components/header/Header.tsx +++ b/src/components/header/Header.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useContext, ChangeEvent } from "react"; +import { useState, useEffect, useRef, useContext, ChangeEvent, useMemo } from "react"; import { BubbleCardColored1, CoinActionContainer, @@ -25,6 +25,10 @@ import { UserContext } from "../../contexts/userContext"; import { cropAddress } from "../../utils/cropAddress"; import qtradeLogo from "../../components/common/icons/qtradeLogo.png"; import qortIcon from "../../assets/img/qort.png"; +import ErrorIcon from "@mui/icons-material/Error"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import Copy from "../../assets/SVG/Copy.svg"; +import {AddressQRCode} from './AddressQRCode' import { Alert, AppBar, @@ -39,6 +43,7 @@ import { Snackbar, SnackbarCloseReason, Switch, + Typography, styled, } from "@mui/material"; import { sendRequestToExtension } from "../../App"; @@ -145,7 +150,8 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => { useState(null); const [receiverAddress, setReceiverAddress] = useState(""); const [senderAddress, setSenderAddress] = useState(""); - + const [amount, setAmount] = useState(""); + const [coinAddresses, setCoinAddresses] = useState({}); const { isUsingGateway } = useContext(gameContext); const handleChange = (event: ChangeEvent) => { @@ -160,6 +166,7 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => { useContext(gameContext); const { setNotification } = useContext(NotificationContext); + const LocalNodeSwitch = styled(Switch)(({ theme }) => ({ padding: 8, "& .MuiSwitch-track": { @@ -231,6 +238,40 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => { // } // }, [userInfo]); + const sendCoin = async ()=> { + try { + const coin = getCoinLabel() + if(!coin) return + setOpen(true); + setInfo({ + type: "info", + message: "Sending Coin...", + autoHideDurationOff: true + }); + const response = await qortalRequest({ + action: "SEND_COIN", + coin, + destinationAddress: senderAddress, + amount: +amount + }); + if(response?.error){ + throw new Error(response?.error || "Failed to send coin.") + } + setOpen(true); + setInfo({ + type: "success", + message: "Coin sent", + }); + setAmount('') + } catch (error) { + setOpen(true); + setInfo({ + type: "error", + message: error?.error || error?.message, + }); + } + } + return ( <> { - { > {info?.message} + )} + {openCoinActionModal && ( { setOpenCoinActionModal(null); + setAmount('') + setSenderAddress('') }} backdrop > + {openCoinActionModal.type === "send" ? <> {openCoinActionModal.type === "send" && @@ -523,12 +570,12 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => { style={{ flexGrow: 1 }} name={ openCoinActionModal.type === "send" - ? "Sender Address" + ? "Recipient Address" : "Receive Address" } label={ openCoinActionModal.type === "send" - ? "Sender Address" + ? "Recipient Address" : "Receive Address" } variant="filled" @@ -548,25 +595,171 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => { /> - - setOpenCoinActionModal(null)}> - Cancel - - { - setNotification({ - alertType: "alertInfo", - msg: "Sending...", - }); - }} - > - {openCoinActionModal.type === "send" ? "Send" : "Receive"} - - + {openCoinActionModal.type === "send" && ( + + + { + setAmount(e.target.value) + }} + /> + + + )} + : ( + <> + + + )} + {openCoinActionModal.type === 'send' && ( + + setOpenCoinActionModal(null)}> + Cancel + + { + if(openCoinActionModal.type === 'send'){ + sendCoin() + } + setNotification({ + alertType: "alertInfo", + msg: "Sending...", + }); + }} + > + {openCoinActionModal.type === "send" ? "Send" : "Receive"} + + + )} + + + )} + ); }; + +export const AddressBox = styled(Box)` +display: flex; +border: 1px solid var(--50-white, rgba(255, 255, 255, 0.5)); +justify-content: space-between; +align-items: center; +width: auto; +word-break: break-word; +padding: 5px 15px 5px 15px; +gap: 5px; +border-radius: 100px; +font-family: Inter; +font-size: 12px; +font-weight: 600; +line-height: 14.52px; +text-align: left; +color: var(--50-white, rgba(255, 255, 255, 0.5)); +cursor: pointer; +transition: all 0.2s; +&:hover { + background-color: rgba(41, 41, 43, 1); + color: white; + svg path { + fill: white; // Fill color changes to white on hover + } + } + +` + + +const ReceiveCoin = ({coinAddresses, setCoinAddresses, selectedCoin, setOpen, setInfo})=> { + const [errorMsg, setErrorMsg] = useState('') + const foreignAddress = useMemo(()=> { + return coinAddresses[selectedCoin] || null + }, [coinAddresses, selectedCoin]) + + const getForeignAddress = async (coin)=> { + try { + setOpen(true); + setInfo({ + type: "info", + message: "Retrieving address...", + }); + const response = await qortalRequest({ + action: "GET_USER_WALLET", + coin + }); + if(response?.address){ + setCoinAddresses((prev)=> { + return { + ...prev, + [coin]: response.address + } + }) + } + if(response?.error){ + throw new Error(response?.error || "Failed to send coin.") + } + } catch (error) { + setErrorMsg(error?.message) + } finally { + setOpen(false); + setInfo(null); + } + } + + useEffect(()=> { + if(!selectedCoin) return + if(!coinAddresses[selectedCoin]){ + getForeignAddress(selectedCoin) + } + }, [selectedCoin, coinAddresses]) + + return ( + + {`Send ${selectedCoin} to your address below`} + + {foreignAddress && ( + { + setOpen(true); + setInfo({ + type: "info", + message: "Address copied!", + }); + }}> + + {foreignAddress} + + + )} + {foreignAddress && ( + <> + + + )} + {errorMsg && ( + <> + + {errorMsg} + + )} + + ) +} \ No newline at end of file diff --git a/src/contexts/gameContext.ts b/src/contexts/gameContext.ts index ab2d9dd..010b0cd 100644 --- a/src/contexts/gameContext.ts +++ b/src/contexts/gameContext.ts @@ -34,7 +34,7 @@ export interface IContextProps { isUsingGateway: boolean; selectedCoin: string; setSelectedCoin: (val: any)=> void; - getCoinLabel: ()=> void; + getCoinLabel: ()=> string | null; } const defaultState: IContextProps = { diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 69fdc30..0396f54 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -23,7 +23,7 @@ export const HomePage = () => { selectedCoin, } = useContext(gameContext); const { setNotification } = useContext(NotificationContext); - const [mode, setMode] = useState("history"); + const [mode, setMode] = useState("buy"); const filteredOngoingTrades = useMemo(() => { return onGoingTrades?.filter( (item) => item?.tradeInfo?.foreignBlockchain === selectedCoin