diff --git a/react/lib/components/Widget/AltpaymentWidget.tsx b/react/lib/components/Widget/AltpaymentWidget.tsx index 8f76f9e0..58fbd1df 100644 --- a/react/lib/components/Widget/AltpaymentWidget.tsx +++ b/react/lib/components/Widget/AltpaymentWidget.tsx @@ -1,6 +1,7 @@ import React, { Fragment, useEffect, useRef, useState } from 'react' import { TextField, Select, MenuItem, InputLabel, FormControl, Box, CircularProgress } from '@mui/material' import { styled } from '@mui/material/styles' +import { QRCodeSVG } from 'qrcode.react' import { resolveNumber, CryptoCurrency, DECIMALS } from '../../util' import { Button, animation } from '../Button/Button' @@ -9,6 +10,10 @@ import { AltpaymentCoin, AltpaymentError, AltpaymentPair, AltpaymentShift } from import { SIDESHIFT_BASE_URL } from '../../altpayment/sideshift' import { sideShiftLogo, copyIcon } from './SideShiftLogo' +const XEC_ICON_DATA_URI = `data:image/svg+xml,${encodeURIComponent( + ``, +)}` + interface AltpaymentProps { altpaymentSocket?: Socket; setUseAltpayment: Function; @@ -68,6 +73,7 @@ export const AltpaymentWidget: React.FunctionComponent = props const [selectedCoinNetwork, setSelectedCoinNetwork] = useState(undefined); const [pairAmountFixedDecimals, setPairAmountFixedDecimals] = useState(undefined); const [pairAmount, setPairAmount] = useState(undefined); + const [activeShiftStep, setActiveShiftStep] = useState<1 | 2 | 3>(1) const autoRateRequestedRef = useRef(false); const autoQuoteRequestedRef = useRef(false); const prevAltpaymentSocketRef = useRef(undefined); @@ -312,36 +318,57 @@ export const AltpaymentWidget: React.FunctionComponent = props setShiftCompleted(false) } - const copyToClipboard = (elementId: string) => { - const contentElement = document.getElementById(elementId); - const copiedMessage = document.createElement("div"); - copiedMessage.textContent = "Copied!"; - copiedMessage.style.position = "absolute"; - copiedMessage.style.width = "calc(100% - 10px)"; - copiedMessage.style.height = "calc(100% - 20px)"; - copiedMessage.style.alignItems = "center"; - copiedMessage.style.top = "0"; - copiedMessage.style.left = "0"; - copiedMessage.style.backgroundColor = "#fff"; - copiedMessage.style.borderRadius = "5px"; - copiedMessage.style.padding = "10px 0 10px 10px"; - copiedMessage.style.zIndex = "10"; - copiedMessage.style.display = "none"; - - if (contentElement) { - const content = contentElement.textContent || ""; - navigator.clipboard.writeText(content); - contentElement.appendChild(copiedMessage); - copiedMessage.style.display = "flex"; - - setTimeout(() => { - copiedMessage.style.display = "none"; - if (copiedMessage.parentElement === contentElement) { - contentElement.removeChild(copiedMessage); - } - }, 2000); + const showCopyToast = (message: string): void => { + const existingToast = document.getElementById('paybutton-copy-toast') + if (existingToast) { + existingToast.remove() + } + + const toast = document.createElement('div') + toast.id = 'paybutton-copy-toast' + toast.textContent = message + toast.style.position = 'fixed' + toast.style.left = '50%' + toast.style.bottom = '16px' + toast.style.transform = 'translateX(-50%)' + toast.style.background = 'rgba(35, 31, 32, 0.9)' + toast.style.color = '#fff' + toast.style.padding = '8px 12px' + toast.style.borderRadius = '6px' + toast.style.fontSize = '12px' + toast.style.lineHeight = '1' + toast.style.zIndex = '2147483647' + toast.style.pointerEvents = 'none' + document.body.appendChild(toast) + + setTimeout(() => { + if (toast.parentElement) { + toast.remove() } - }; + }, 1500) + } + + const copyToClipboard = async (value: string, nextStep?: 1 | 2 | 3): Promise => { + if (!value) { + return + } + + try { + await navigator.clipboard.writeText(value) + showCopyToast('Copied') + if (nextStep !== undefined) { + setActiveShiftStep(nextStep) + } + } catch { + showCopyToast('Copy failed') + } + } + + useEffect(() => { + if (altpaymentShift?.id) { + setActiveShiftStep(1) + } + }, [altpaymentShift?.id]) const SideshiftCtn = styled('div')({ alignItems: 'center', @@ -357,7 +384,11 @@ export const AltpaymentWidget: React.FunctionComponent = props bottom: 0, background: '#f5f5f7', overflowY: 'auto', - padding: '24px', + overflowX: 'hidden', + padding: '16px', + '@media (min-width: 760px)': { + padding: '24px', + }, }) const LoadingCenter = styled('div')({ @@ -388,11 +419,56 @@ export const AltpaymentWidget: React.FunctionComponent = props const ShiftReady = styled('div')({ width: '100%', display: 'flex', flexDirection: 'column', - '& h4': { margin: '0', fontSize: '20px', borderBottom: '1px solid #000', paddingBottom: '10px', textAlign: 'center' }, + minWidth: 0, + }) + + const ShiftReadyTitle = styled('h4')({ + margin: 0, + fontSize: '22px', + borderBottom: '1px solid #000', + paddingBottom: '12px', + textAlign: 'center', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + flexWrap: 'wrap', + gap: '8px', + lineHeight: 1.3, + }) + + const ShiftReadyBody = styled('div')({ + display: 'flex', + flexDirection: 'column', + gap: '20px', + marginTop: '14px', + }) + + const ShiftReadyMain = styled('div')({ + display: 'flex', + flexDirection: 'column', + gap: '10px', + flex: 1, + minWidth: 0, + }) + + const ShiftLabelRow = styled('div')({ + display: 'flex', + alignItems: 'baseline', + justifyContent: 'space-between', + gap: '8px', + marginTop: '8px', + marginBottom: '4px', + minWidth: 0, }) const CopyCtn = styled('div')({ - display: 'flex', alignItems: 'center', '& > div': { position: 'relative' } + display: 'flex', + alignItems: 'center', + width: '100%', + minWidth: 0, + overflow: 'hidden', + gap: '6px', + '& > div': { position: 'relative' }, }) const AmountError = styled('p')({ @@ -406,17 +482,98 @@ export const AltpaymentWidget: React.FunctionComponent = props }) const ShiftLabel = styled('span')({ - fontSize: '14px', marginLeft: '5px', marginTop: '20px', marginBottom: '2px', fontWeight: 600 + fontSize: '14px', marginLeft: '5px', fontWeight: 600 + }) + + const ShiftStepLabel = styled('span', { + shouldForwardProp: (prop) => prop !== 'active', + })<{ active?: boolean }>(({ active }) => ({ + fontSize: '14px', + marginLeft: '5px', + fontWeight: 700, + padding: '4px 8px', + borderRadius: '999px', + display: 'inline-flex', + alignItems: 'center', + lineHeight: 1.2, + transition: 'all 140ms ease', + border: active ? '1px solid #0074c2' : '1px solid #d6d6d6', + background: active ? '#e9f3fb' : 'transparent', + color: active ? '#005e9d' : '#2f2f2f', + })) + + const ShiftSubLabel = styled('span')({ + fontSize: '11px', + color: '#8e8e8e', + marginRight: '5px', + lineHeight: 1.2, + whiteSpace: 'nowrap', }) const ShiftInput = styled('div')({ background: '#ffffff', padding: '10px', borderRadius: '5px', fontSize: '14px', - border: '1px solid #b3b3b3', wordBreak: 'break-all', flexGrow: 1, position: 'relative', + border: '1px solid #b3b3b3', wordBreak: 'break-word', overflowWrap: 'anywhere', flex: '1 1 auto', position: 'relative', minWidth: 0, + }) + + const ShiftValueRow = styled('div')({ + display: 'flex', + alignItems: 'center', + gap: '8px', + minWidth: 0, + }) + + const ShiftCurrencyIcon = styled('img')({ + width: '20px', + height: '20px', + flexShrink: 0, + }) + + const ShiftAddress = styled('div')({ + fontSize: '14px', + lineHeight: 1.25, + overflowWrap: 'anywhere', + }) + + const QrCard = styled('div')({ + width: '100%', + maxWidth: '236px', + boxSizing: 'border-box', + background: '#fff', + border: '1px solid #d7d7d7', + borderRadius: '8px', + padding: '12px', + margin: '8px auto 4px', + alignSelf: 'center', + textAlign: 'center', + cursor: 'pointer', + transition: 'box-shadow 160ms ease, transform 160ms ease', + '&:hover': { + boxShadow: '0 4px 14px rgba(0, 0, 0, 0.12)', + transform: 'translateY(-1px)', + }, + }) + + const QrTitle = styled('div')({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gap: '6px', + fontSize: '13px', + fontWeight: 600, + marginBottom: '8px', + }) + + const InlineCoin = styled('span')({ + display: 'inline-flex', + alignItems: 'center', + gap: '6px', + fontWeight: 700, }) const CopyBtn = styled('div')({ background: '#ffffff', padding: '10px', borderRadius: '5px', border: '1px solid #b3b3b3', - marginLeft: '5px', display: 'flex', alignItems: 'center', cursor: 'pointer', + display: 'flex', alignItems: 'center', cursor: 'pointer', + flex: '0 0 auto', alignSelf: 'stretch', transition: 'all ease-in-out 200ms', '&:hover': { background: '#f1f1f1' }, '& img': { width: '15px' }, }) @@ -464,6 +621,60 @@ export const AltpaymentWidget: React.FunctionComponent = props return coinString; } + const formatNetworkName = (network?: string): string => { + if (!network) { + return '' + } + return network.charAt(0).toUpperCase() + network.slice(1) + } + + const getCoinName = (coinCode?: string): string => { + if (!coinCode) { + return '' + } + + const match = coins.find(c => c.coin.toLowerCase() === coinCode.toLowerCase()) + if (match) { + return match.name + } + + const fallbackNames: Record = { + BTC: 'Bitcoin', + BCH: 'Bitcoin Cash', + XEC: 'eCash', + ETH: 'Ethereum', + LTC: 'Litecoin', + XMR: 'Monero', + } + + return fallbackNames[coinCode.toUpperCase()] ?? coinCode + } + + const getCoinIconSrc = (coinCode?: string): string => { + const normalizedCode = coinCode?.toUpperCase() + if (normalizedCode === 'XEC' || coinCode?.toLowerCase() === 'ecash') { + return XEC_ICON_DATA_URI + } + return `${SIDESHIFT_BASE_URL}coins/icon/${checkCoin(coinCode ?? '')}` + } + + const getQrUriScheme = (network?: string, coin?: string): string => { + if (network && network.length > 0) { + return network.toLowerCase() + } + if (coin && coin.length > 0) { + return coin.toLowerCase() + } + return 'bitcoin' + } + + const getShiftQrValue = (shift: AltpaymentShift): string => { + const scheme = getQrUriScheme(selectedCoinNetwork, shift.depositCoin) + return `${scheme}:${shift.depositAddress}?amount=${shift.depositAmount}` + } + + const shiftQrValue = altpaymentShift ? getShiftQrValue(altpaymentShift) : '' + const isAutoStart = Boolean(preselectedCoin) const isAutoStartLoading = isAutoStart && !altpaymentShift && !altpaymentError @@ -501,45 +712,90 @@ export const AltpaymentWidget: React.FunctionComponent = props Shift Completed! ) : ( -

Shift Ready!

- Send - - - {altpaymentShift.depositAmount}{' '}{altpaymentShift.depositCoin} - - copyToClipboard('shift_amount')}> - Copy - - - To - - - {altpaymentShift.depositAddress} - - copyToClipboard('to_address')}> - Copy + Send + + - - - Network - {selectedCoinNetwork} - SideShift ID - - - {altpaymentShift.id} - - copyToClipboard('sideshift_id')}> - Copy{getCoinName(altpaymentShift.depositCoin)} + + for + + - - + {getCoinName(altpaymentShift.settleCoin)} + + + + + Step 1: Send + + + + + {altpaymentShift.depositAmount}{' '}{altpaymentShift.depositCoin} + + + { void copyToClipboard(altpaymentShift.depositAmount, 2) }}> + Copy + + + + Step 2: To + Network: {formatNetworkName(selectedCoinNetwork)} + + + + + {altpaymentShift.depositAddress} + + + { void copyToClipboard(altpaymentShift.depositAddress, 3) }}> + Copy + + + { void copyToClipboard(shiftQrValue, 3) }}> + + + Scan to Pay + + + + Step 3: SideShift ID + + + {altpaymentShift.id} + + { void copyToClipboard(altpaymentShift.id, 3) }}> + Copy + + + +
) ) : loadingShift ? ( @@ -619,7 +875,7 @@ export const AltpaymentWidget: React.FunctionComponent = props diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 4264d1da..3256abd8 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -1146,9 +1146,9 @@ export const Widget: React.FunctionComponent = props => { sx={{ ...classes.root, ...(thisUseAltpayment ? { - minWidth: 340, - width: 340, - minHeight: 520, + minWidth: 320, + width: thisAltpaymentShift ? 'min(94vw, 600px)' : 'min(92vw, 420px)', + minHeight: thisAltpaymentShift ? 640 : 520, } : {}), }} pt={0}