UNPKG

@daimo/pay

Version:

Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.

338 lines (329 loc) 14.2 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { isHydrated, getChainName, DepositAddressPaymentOptions, ethereumUSDC, polygonUSDC, baseUSDC, arbitrumUSDC, optimismUSDC, getAddressContraction } from '@daimo/pay-common'; import { useState, useEffect, useMemo } from 'react'; import { keyframes } from 'styled-components'; import { AlertIcon, WarningIcon } from '../../../assets/icons.js'; import { useDaimoPay } from '../../../hooks/useDaimoPay.js'; import useIsMobile from '../../../hooks/useIsMobile.js'; import { usePayContext } from '../../../hooks/usePayContext.js'; import styled from '../../../styles/styled/index.js'; import Button from '../../Common/Button/index.js'; import CircleTimer from '../../Common/CircleTimer.js'; import CopyToClipboardIcon from '../../Common/CopyToClipboard/CopyToClipboardIcon.js'; import CustomQRCode from '../../Common/CustomQRCode/index.js'; import { ModalBody, PageContent, ModalContent, ModalH1 } from '../../Common/Modal/styles.js'; import SelectAnotherMethodButton from '../../Common/SelectAnotherMethodButton/index.js'; import TokenChainLogo from '../../Common/TokenChainLogo/index.js'; // Centered container for icon + text in Tron underpay screen const CenterContainer = styled.div ` display: flex; flex-direction: column; align-items: center; padding: 16px; max-width: 100%; `; function WaitingDepositAddress() { const context = usePayContext(); const { triggerResize, paymentState } = context; const { payWithDepositAddress, selectedDepositAddressOption } = paymentState; const { order } = useDaimoPay(); // Detect Optimism USDT0 under-payment: the order has received some funds // but less than required. const tronUnderpay = order != null && isHydrated(order) && order.sourceTokenAmount != null && order.sourceTokenAmount.token.chainId === 10 && order.sourceTokenAmount.token.symbol.toUpperCase() === "USDT0" && Number(order.sourceTokenAmount.usd) < order.usdValue; const [depAddr, setDepAddr] = useState(); const [failed, setFailed] = useState(false); // If we selected a deposit address option, generate the address... const generateDepositAddress = () => { if (selectedDepositAddressOption == null) { if (order == null || !isHydrated(order)) return; if (order.sourceTokenAmount == null) return; // Pay underpaid order const taPaid = order.sourceTokenAmount; const usdPaid = taPaid.usd; // TODO: get usdPaid directly from the order const usdToPay = Math.max(order.usdValue - usdPaid, 0.01); const dispDecimals = taPaid.token.displayDecimals; const unitsToPay = (usdToPay / taPaid.token.usd).toFixed(dispDecimals); const unitsPaid = (Number(taPaid.amount) / 10 ** taPaid.token.decimals).toFixed(dispDecimals); // (Removed duplicate tronUnderpay calculation now handled at top-level) // Hack to always show a <= 60 minute countdown let expirationS = (order.createdAt ?? 0) + 59.5 * 60; if (order.expirationTs != null && Number(order.expirationTs) < expirationS) { expirationS = Number(order.expirationTs); } setDepAddr({ address: order.intentAddr, amount: unitsToPay, underpayment: { unitsPaid, coin: taPaid.token.symbol }, coins: `${taPaid.token.symbol} on ${getChainName(taPaid.token.chainId)}`, expirationS: expirationS, uri: order.intentAddr, displayToken: taPaid.token, logoURI: "", // Not needed for underpaid orders }); } else { const displayToken = getDisplayToken(selectedDepositAddressOption); const logoURI = selectedDepositAddressOption.logoURI; setDepAddr({ displayToken, logoURI, }); payWithDepositAddress(selectedDepositAddressOption.id).then((details) => { if (details) { setDepAddr({ address: details.address, amount: details.amount, coins: details.suffix, expirationS: details.expirationS, uri: details.uri, displayToken, logoURI, }); } else { setFailed(true); } }); } }; // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(generateDepositAddress, [selectedDepositAddressOption]); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(triggerResize, [depAddr, failed]); return (jsx(PageContent, { children: tronUnderpay ? (jsx(TronUnderpayContent, { orderId: order?.id?.toString() })) : failed ? (selectedDepositAddressOption && (jsx(DepositFailed, { name: selectedDepositAddressOption.id }))) : (depAddr && (jsx(DepositAddressInfo, { depAddr: depAddr, refresh: generateDepositAddress, triggerResize: triggerResize }))) })); } function TronUnderpayContent({ orderId }) { return (jsx(ModalContent, { style: { display: "flex", justifyContent: "center", alignItems: "center", paddingBottom: 0, position: "relative", }, children: jsxs(CenterContainer, { children: [jsx(FailIcon, {}), jsx(ModalH1, { style: { textAlign: "center", marginTop: 16 }, children: "USDT Tron Payment Was Too Low" }), jsx("div", { style: { height: 16 } }), jsxs(ModalBody, { style: { textAlign: "center" }, children: ["Your funds are safe.", jsx("br", {}), "Email support@daimo.com for a refund."] }), jsx(Button, { onClick: () => window.open(`mailto:support@daimo.com?subject=Underpaid%20USDT%20Tron%20payment%20for%20order%20${orderId}`, "_blank"), style: { marginTop: 16, width: 200 }, children: "Contact Support" })] }) })); } function DepositAddressInfo({ depAddr, refresh, triggerResize, }) { const { isMobile } = useIsMobile(); const [remainingS, totalS] = useCountdown(depAddr?.expirationS); const isExpired = depAddr?.expirationS != null && remainingS === 0; // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(triggerResize, [isExpired]); const logoOffset = isMobile ? 4 : 0; const logoElement = depAddr.displayToken ? (jsx(TokenChainLogo, { token: depAddr.displayToken, size: 64, offset: logoOffset })) : (jsx("img", { src: depAddr.logoURI, width: "64px", height: "64px" })); return (jsxs(ModalContent, { children: [isExpired ? (jsx(LogoRow, { children: jsx(Button, { onClick: refresh, style: { width: 128 }, children: "Refresh" }) })) : isMobile ? (jsx(LogoRow, { children: jsx(LogoWrap, { children: logoElement }) })) : (jsx(QRWrap, { children: jsx(CustomQRCode, { value: depAddr?.uri, contentPadding: 24, size: 200, image: logoElement }) })), jsx(CopyableInfo, { depAddr: depAddr, remainingS: remainingS, totalS: totalS })] })); } function getDisplayToken(meta) { switch (meta.id) { case DepositAddressPaymentOptions.OP_MAINNET: return optimismUSDC; case DepositAddressPaymentOptions.ARBITRUM: return arbitrumUSDC; case DepositAddressPaymentOptions.BASE: return baseUSDC; case DepositAddressPaymentOptions.POLYGON: return polygonUSDC; case DepositAddressPaymentOptions.ETH_L1: return ethereumUSDC; default: return null; } } const LogoWrap = styled.div ` position: relative; width: 64px; height: 64px; `; const LogoRow = styled.div ` padding: 32px 0; height: 128px; display: flex; align-items: center; gap: 8px; justify-content: center; `; const QRWrap = styled.div ` margin: 0 auto; width: 280px; `; function CopyableInfo({ depAddr, remainingS, totalS, }) { const underpayment = depAddr?.underpayment; const isExpired = depAddr?.expirationS != null && remainingS === 0; return (jsxs(CopyableInfoWrapper, { children: [underpayment && jsx(UnderpaymentInfo, { underpayment: underpayment }), jsx(CopyRowOrThrobber, { title: "Send Exactly", value: depAddr?.amount, smallText: depAddr?.coins, disabled: isExpired }), jsx(CopyRowOrThrobber, { title: "Receiving Address", value: depAddr?.address, valueText: depAddr?.address && getAddressContraction(depAddr.address), disabled: isExpired }), jsx(CountdownWrap, { children: jsx(CountdownTimer, { remainingS: remainingS, totalS: totalS }) })] })); } function UnderpaymentInfo({ underpayment }) { // Default message return (jsxs(UnderpaymentWrapper, { children: [jsxs(UnderpaymentHeader, { children: [jsx(WarningIcon, {}), jsxs("span", { children: ["Received ", underpayment.unitsPaid, " ", underpayment.coin] })] }), jsx(SmallText, { children: "Finish by sending the extra amount below." })] })); } const UnderpaymentWrapper = styled.div ` background: var(--ck-body-background-tertiary); border-radius: 8px; padding: 16px; margin: 0 4px 16px 4px; margin-bottom: 16px; `; const UnderpaymentHeader = styled.div ` font-weight: 500; display: flex; justify-content: center; align-items: flex-end; gap: 8px; margin-bottom: 8px; `; const CopyableInfoWrapper = styled.div ` display: flex; flex-direction: column; justify-content: stretch; gap: 0; margin-top: 8px; `; const CountdownWrap = styled.div ` margin-top: 24px; height: 16px; `; const FailIcon = styled(AlertIcon) ` color: var(--ck-body-color-alert); width: 32px; height: 32px; margin-top: auto; margin-bottom: 16px; `; function useCountdown(expirationS) { // eslint-disable-next-line react-hooks/exhaustive-deps const initMs = useMemo(() => Date.now(), [expirationS]); const [ms, setMs] = useState(initMs); useEffect(() => { const interval = setInterval(() => setMs(Date.now()), 1000); return () => clearInterval(interval); }, []); if (expirationS == null) return [0, 0]; const remainingS = Math.max(0, (expirationS - ms / 1000) | 0); const totalS = Math.max(0, (expirationS - initMs / 1000) | 0); return [remainingS, totalS]; } function CountdownTimer({ remainingS, totalS, }) { if (totalS == 0 || remainingS > 3600) { return jsx(SmallText, { children: "Send only once" }); } const isExpired = remainingS === 0; return (jsx(ModalBody, { children: jsxs(CountdownRow, { children: [jsx(CircleTimer, { total: totalS, currentTime: remainingS, size: 18, stroke: 3 }), jsx("strong", { children: isExpired ? "Expired" : formatTime(remainingS) })] }) })); } const CountdownRow = styled.div ` display: flex; align-items: center; justify-content: center; gap: 8px; font-variant-numeric: tabular-nums; `; const formatTime = (sec) => { const m = `${Math.floor(sec / 60)}`.padStart(2, "0"); const s = `${sec % 60}`.padStart(2, "0"); return `${m}:${s}`; }; function DepositFailed({ name }) { return (jsxs(ModalContent, { style: { marginLeft: 24, marginRight: 24 }, children: [jsxs(ModalH1, { children: [name, " unavailable"] }), jsxs(ModalBody, { children: ["We're unable to process ", name, " payments at this time. Please select another payment method."] }), jsx(SelectAnotherMethodButton, {})] })); } const CopyRow = styled.button ` display: block; height: 64px; border-radius: 8px; padding: 8px 16px; cursor: pointer; background-color: var(--ck-body-background); background-color: var(--ck-body-background); display: flex; align-items: center; justify-content: space-between; transition: all 100ms ease; &:hover { opacity: 0.8; } &:active { transform: scale(0.98); background-color: var(--ck-body-background-secondary); } &:disabled { cursor: default; opacity: 0.5; transform: scale(0.98); background-color: var(--ck-body-background-secondary); } `; const LabelRow = styled.div ` margin-bottom: 4px; `; const MainRow = styled.div ` display: flex; align-items: center; justify-content: space-between; `; const ValueContainer = styled.div ` display: flex; align-items: center; gap: 8px; `; const SmallText = styled.span ` font-size: 14px; color: var(--ck-primary-button-color); `; const ValueText = styled.span ` font-size: 14px; font-weight: 600; color: var(--ck-primary-button-color); `; const LabelText = styled(ModalBody) ` margin: 0; text-align: left; `; const pulse = keyframes ` 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } `; const Skeleton = styled.div ` width: 80px; height: 16px; border-radius: 8px; background-color: rgba(0, 0, 0, 0.1); animation: ${pulse} 1.5s ease-in-out infinite; `; function CopyRowOrThrobber({ title, value, valueText, smallText, disabled, }) { const [copied, setCopied] = useState(false); const handleCopy = () => { if (disabled) return; if (!value) return; const str = value.trim(); if (navigator.clipboard) { navigator.clipboard.writeText(str); } setCopied(true); setTimeout(() => setCopied(false), 1000); }; if (!value) { return (jsxs(CopyRow, { children: [jsx(LabelRow, { children: jsx(LabelText, { children: title }) }), jsx(MainRow, { children: jsx(Skeleton, {}) })] })); } const displayValue = valueText || value; return (jsxs(CopyRow, { as: "button", onClick: handleCopy, disabled: disabled, children: [jsxs("div", { children: [jsx(LabelRow, { children: jsx(LabelText, { children: title }) }), jsx(MainRow, { children: jsxs(ValueContainer, { children: [jsx(ValueText, { children: displayValue }), smallText && jsx(SmallText, { children: smallText })] }) })] }), jsx(CopyIconWrap, { children: jsx(CopyToClipboardIcon, { copied: copied, dark: true }) })] })); } const CopyIconWrap = styled.div ` --color: var(--ck-copytoclipboard-stroke); --bg: var(--ck-body-background); `; export { WaitingDepositAddress as default }; //# sourceMappingURL=index.js.map