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