send and receiving foreign coins

This commit is contained in:
PhilReact 2024-12-26 05:47:01 +02:00
parent 3de91a32d3
commit 1350ef44c6
9 changed files with 317 additions and 24 deletions

44
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -256,7 +256,7 @@ function App() {
};
}, [userInfo?.address]);
const getCoinLabel = (coin?: string)=> {
const getCoinLabel = (coin?: string)=> {
switch(coin || selectedCoin){
case "LITECOIN":{

3
src/assets/SVG/Copy.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="10" height="11" viewBox="0 0 10 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.92857 0.5H8.57143C9.36071 0.5 10 1.13929 10 1.92857V6.57143C10 7.36071 9.36071 8 8.57143 8H8.21429V4.42857C8.21429 3.24643 7.25357 2.28571 6.07143 2.28571H2.5V1.92857C2.5 1.13929 3.13929 0.5 3.92857 0.5ZM1.42857 3H6.07143C6.86041 3 7.5 3.63959 7.5 4.42857V9.07143C7.5 9.86041 6.86041 10.5 6.07143 10.5H1.42857C0.639593 10.5 0 9.86041 0 9.07143V4.42857C0 3.63959 0.639593 3 1.42857 3Z" fill="white" fill-opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 574 B

View File

@ -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 (
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
flexDirection: "column",
marginTop: '10px'
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
justifyContent: "center",
width: "100%",
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
width: "100%",
alignItems: "center",
flexDirection: "column",
marginTop: "20px",
}}
>
<QRCode
value={targetAddress} // Your address here
size={150} // Adjust size as needed
level="M" // Error correction level (L, M, Q, H)
bgColor="#FFFFFF" // Background color (white)
fgColor="#000000" // Foreground color (black)
/>
</Box>
</Box>
</Box>
);
};

View File

@ -216,6 +216,7 @@ export const CoinSelectRow = styled(Box)({
flexDirection: "column",
gap: "5px",
alignSelf: "flex-start",
marginBottom: '5px'
});
export const CoinActionContainer = styled(Box)({

View File

@ -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<CoinModalProps | null>(null);
const [receiverAddress, setReceiverAddress] = useState<string>("");
const [senderAddress, setSenderAddress] = useState<string>("");
const [amount, setAmount] = useState<string>("");
const [coinAddresses, setCoinAddresses] = useState({});
const { isUsingGateway } = useContext(gameContext);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
@ -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 (
<>
<AppBar
@ -444,10 +485,11 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
<Snackbar
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
open={open}
autoHideDuration={6000}
autoHideDuration={info?.autoHideDurationOff ? null : 6000}
onClose={handleClose}
>
<Alert
{info?.type && (
<Alert
onClose={handleClose}
severity={info?.type}
variant="filled"
@ -455,15 +497,20 @@ export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
>
{info?.message}
</Alert>
)}
</Snackbar>
{openCoinActionModal && (
<ReusableModal
onClickClose={() => {
setOpenCoinActionModal(null);
setAmount('')
setSenderAddress('')
}}
backdrop
>
<CoinActionContainer>
{openCoinActionModal.type === "send" ? <>
<CoinActionRow>
<HeaderRow>
{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) => {
/>
</FormControl>
</CoinActionRow>
<CoinActionRow style={{gap: "10px"}}>
<CoinCancelBtn onClick={() => setOpenCoinActionModal(null)}>
Cancel
</CoinCancelBtn>
<CoinConfirmSendBtn
onClick={() => {
setNotification({
alertType: "alertInfo",
msg: "Sending...",
});
}}
>
{openCoinActionModal.type === "send" ? "Send" : "Receive"}
</CoinConfirmSendBtn>
</CoinActionRow>
{openCoinActionModal.type === "send" && (
<CoinActionRow>
<FormControl fullWidth>
<CustomInputField
style={{ flexGrow: 1 }}
name="Amount"
label="Amount"
variant="filled"
type="number"
value={
amount
}
required
onChange={(e) => {
setAmount(e.target.value)
}}
/>
</FormControl>
</CoinActionRow>
)}
</> : (
<>
<ReceiveCoin setOpen={setOpen} setInfo={setInfo} coinAddresses={coinAddresses} setCoinAddresses={setCoinAddresses} selectedCoin={getCoinLabel()} />
</>
)}
{openCoinActionModal.type === 'send' && (
<CoinActionRow style={{gap: "10px"}}>
<CoinCancelBtn onClick={() => setOpenCoinActionModal(null)}>
Cancel
</CoinCancelBtn>
<CoinConfirmSendBtn
onClick={() => {
if(openCoinActionModal.type === 'send'){
sendCoin()
}
setNotification({
alertType: "alertInfo",
msg: "Sending...",
});
}}
>
{openCoinActionModal.type === "send" ? "Send" : "Receive"}
</CoinConfirmSendBtn>
</CoinActionRow>
)}
</CoinActionContainer>
</ReusableModal>
)}
</HeaderNav>
</>
);
};
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 (
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}>
<Typography sx={{
color: 'white'
}}>{`Send ${selectedCoin} to your address below`}</Typography>
<Spacer height="20px" />
{foreignAddress && (
<CopyToClipboard text={foreignAddress} onCopy={()=> {
setOpen(true);
setInfo({
type: "info",
message: "Address copied!",
});
}}>
<AddressBox>
{foreignAddress} <img src={Copy} />
</AddressBox>
</CopyToClipboard>
)}
{foreignAddress && (
<>
<AddressQRCode targetAddress={foreignAddress} />
</>
)}
{errorMsg && (
<>
<Spacer height="20px" />
<Typography sx={{
color: 'white'
}}>{errorMsg}</Typography>
</>
)}
</Box>
)
}

View File

@ -34,7 +34,7 @@ export interface IContextProps {
isUsingGateway: boolean;
selectedCoin: string;
setSelectedCoin: (val: any)=> void;
getCoinLabel: ()=> void;
getCoinLabel: ()=> string | null;
}
const defaultState: IContextProps = {

View File

@ -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