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')}>
-
-
-
- To
-
-
- {altpaymentShift.depositAddress}
-
- copyToClipboard('to_address')}>
-
+ Send
+
+
-
-
- Network
- {selectedCoinNetwork}
- SideShift ID
-
-
- {altpaymentShift.id}
-
- copyToClipboard('sideshift_id')}>
-
{getCoinName(altpaymentShift.depositCoin)}
+
+ for
+
+
-
-
+ {getCoinName(altpaymentShift.settleCoin)}
+
+
+
+
+ Step 1: Send
+
+
+
+
+ {altpaymentShift.depositAmount}{' '}{altpaymentShift.depositCoin}
+
+
+ { void copyToClipboard(altpaymentShift.depositAmount, 2) }}>
+
+
+
+
+ Step 2: To
+ Network: {formatNetworkName(selectedCoinNetwork)}
+
+
+
+
+ {altpaymentShift.depositAddress}
+
+
+ { void copyToClipboard(altpaymentShift.depositAddress, 3) }}>
+
+
+
+ { void copyToClipboard(shiftQrValue, 3) }}>
+
+
+ Scan to Pay
+
+
+
+ Step 3: SideShift ID
+
+
+ {altpaymentShift.id}
+
+ { void copyToClipboard(altpaymentShift.id, 3) }}>
+
+
+
+
+
)
) : loadingShift ? (
@@ -619,7 +875,7 @@ export const AltpaymentWidget: React.FunctionComponent = props