@daimo/pay
Version:
Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.
504 lines (495 loc) • 16.6 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 useLocales from '../../../hooks/useLocales.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';
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();
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);
const generateDepositAddress = () => {
if (selectedDepositAddressOption == null) {
if (order == null || !isHydrated(order)) return;
if (order.sourceTokenAmount == null) return;
const taPaid = order.sourceTokenAmount;
const usdPaid = taPaid.usd;
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);
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,
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);
}
});
}
};
useEffect(generateDepositAddress, [selectedDepositAddressOption]);
useEffect(triggerResize, [depAddr, failed]);
return /* @__PURE__ */ jsx(PageContent, { children: tronUnderpay ? /* @__PURE__ */ jsx(TronUnderpayContent, { orderId: order?.id?.toString() }) : failed ? selectedDepositAddressOption && /* @__PURE__ */ jsx(DepositFailed, { name: selectedDepositAddressOption.id }) : depAddr && /* @__PURE__ */ jsx(
DepositAddressInfo,
{
depAddr,
refresh: generateDepositAddress,
triggerResize
}
) });
}
function TronUnderpayContent({ orderId }) {
const locales = useLocales();
return /* @__PURE__ */ jsx(
ModalContent,
{
style: {
display: "flex",
justifyContent: "center",
alignItems: "center",
paddingBottom: 0,
position: "relative"
},
children: /* @__PURE__ */ jsxs(CenterContainer, { children: [
/* @__PURE__ */ jsx(FailIcon, {}),
/* @__PURE__ */ jsx(ModalH1, { style: { textAlign: "center", marginTop: 16 }, children: "USDT Tron Payment Was Too Low" }),
/* @__PURE__ */ jsx("div", { style: { height: 16 } }),
/* @__PURE__ */ jsxs(ModalBody, { style: { textAlign: "center" }, children: [
"Your funds are safe.",
/* @__PURE__ */ jsx("br", {}),
"Email support@daimo.com for a refund."
] }),
/* @__PURE__ */ 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: locales.contactSupport
}
)
] })
}
);
}
function DepositAddressInfo({
depAddr,
refresh,
triggerResize
}) {
const { isMobile } = useIsMobile();
const locales = useLocales();
const [remainingS, totalS] = useCountdown(depAddr?.expirationS);
const isExpired = depAddr?.expirationS != null && remainingS === 0;
const [showQR, setShowQR] = useState(!isMobile);
useEffect(triggerResize, [isExpired, showQR]);
const logoOffset = isMobile ? 4 : 0;
const logoElement = depAddr.displayToken ? /* @__PURE__ */ jsx(
TokenChainLogo,
{
token: depAddr.displayToken,
size: 64,
offset: logoOffset
}
) : /* @__PURE__ */ jsx("img", { src: depAddr.logoURI, width: "64px", height: "64px" });
return /* @__PURE__ */ jsxs(ModalContent, { children: [
isExpired ? /* @__PURE__ */ jsx(LogoRow, { children: /* @__PURE__ */ jsx(Button, { onClick: refresh, style: { width: 128 }, children: locales.refresh }) }) : showQR ? /* @__PURE__ */ jsx(QRWrap, { children: /* @__PURE__ */ jsx(
CustomQRCode,
{
value: depAddr?.uri,
contentPadding: 24,
size: 200,
image: logoElement
}
) }) : /* @__PURE__ */ jsx(LogoRow, { children: /* @__PURE__ */ jsx(LogoWrap, { children: logoElement }) }),
/* @__PURE__ */ jsx("div", { style: { height: 8 } }),
isMobile && /* @__PURE__ */ jsx(ShowHideQRRow, { showQR, setShowQR }),
/* @__PURE__ */ jsx(CopyableInfo, { depAddr, remainingS, 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;
}
}
function ShowHideQRRow({
showQR,
setShowQR
}) {
const toggleQR = () => setShowQR(!showQR);
const locales = useLocales();
return /* @__PURE__ */ jsx(ShowQRWrap, { children: /* @__PURE__ */ jsxs(CopyRow, { onClick: toggleQR, children: [
/* @__PURE__ */ jsx(SmallText, { children: showQR ? locales.hideQR : locales.showQR }),
/* @__PURE__ */ jsx("div", { style: { width: 8 } }),
/* @__PURE__ */ jsx(ShowQRIcon, { children: /* @__PURE__ */ jsxs(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
fill: "none",
viewBox: "0 0 24 24",
strokeWidth: 1.5,
stroke: "#666",
className: "size-6",
width: 20,
height: 20,
children: [
/* @__PURE__ */ jsx(
"path",
{
strokeLinecap: "round",
strokeLinejoin: "round",
d: "M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0 1 3.75 9.375v-4.5ZM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 0 1-1.125-1.125v-4.5ZM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0 1 13.5 9.375v-4.5Z"
}
),
/* @__PURE__ */ jsx(
"path",
{
strokeLinecap: "round",
strokeLinejoin: "round",
d: "M6.75 6.75h.75v.75h-.75v-.75ZM6.75 16.5h.75v.75h-.75v-.75ZM16.5 6.75h.75v.75h-.75v-.75ZM13.5 13.5h.75v.75h-.75v-.75ZM13.5 19.5h.75v.75h-.75v-.75ZM19.5 13.5h.75v.75h-.75v-.75ZM19.5 19.5h.75v.75h-.75v-.75ZM16.5 16.5h.75v.75h-.75v-.75Z"
}
)
]
}
) })
] }) });
}
const ShowQRWrap = styled.div`
display: flex;
justify-content: center;
color: var(--ck-primary-button-color);
`;
const ShowQRIcon = styled.div`
width: 20px;
`;
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;
const locales = useLocales();
return /* @__PURE__ */ jsxs(CopyableInfoWrapper, { children: [
underpayment && /* @__PURE__ */ jsx(UnderpaymentInfo, { underpayment }),
/* @__PURE__ */ jsx(
CopyRowOrThrobber,
{
title: locales.sendExactly,
value: depAddr?.amount,
smallText: depAddr?.coins,
disabled: isExpired
}
),
/* @__PURE__ */ jsx(
CopyRowOrThrobber,
{
title: locales.receivingAddress,
value: depAddr?.address,
valueText: depAddr?.address && getAddressContraction(depAddr.address),
disabled: isExpired
}
),
/* @__PURE__ */ jsx(CountdownWrap, { children: /* @__PURE__ */ jsx(CountdownTimer, { remainingS, totalS }) })
] });
}
function UnderpaymentInfo({ underpayment }) {
return /* @__PURE__ */ jsxs(UnderpaymentWrapper, { children: [
/* @__PURE__ */ jsxs(UnderpaymentHeader, { children: [
/* @__PURE__ */ jsx(WarningIcon, {}),
/* @__PURE__ */ jsxs("span", { children: [
"Received ",
underpayment.unitsPaid,
" ",
underpayment.coin
] })
] }),
/* @__PURE__ */ 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;
`;
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) {
const initMs = useMemo(() => Date.now(), [expirationS]);
const [ms, setMs] = useState(initMs);
useEffect(() => {
const interval = setInterval(() => setMs(Date.now()), 1e3);
return () => clearInterval(interval);
}, []);
if (expirationS == null) return [0, 0];
const remainingS = Math.max(0, expirationS - ms / 1e3 | 0);
const totalS = Math.max(0, expirationS - initMs / 1e3 | 0);
return [remainingS, totalS];
}
function CountdownTimer({
remainingS,
totalS
}) {
const locales = useLocales();
if (totalS == 0 || remainingS > 3600) {
return /* @__PURE__ */ jsx(SmallText, { children: locales.sendOnlyOnce });
}
const isExpired = remainingS === 0;
return /* @__PURE__ */ jsx(ModalBody, { children: /* @__PURE__ */ jsxs(CountdownRow, { children: [
/* @__PURE__ */ jsx(
CircleTimer,
{
total: totalS,
currentTime: remainingS,
size: 18,
stroke: 3
}
),
/* @__PURE__ */ 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 /* @__PURE__ */ jsxs(ModalContent, { style: { marginLeft: 24, marginRight: 24 }, children: [
/* @__PURE__ */ jsxs(ModalH1, { children: [
name,
" unavailable"
] }),
/* @__PURE__ */ jsxs(ModalBody, { children: [
"We're unable to process ",
name,
" payments at this time. Please select another payment method."
] }),
/* @__PURE__ */ 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), 1e3);
};
if (!value) {
return /* @__PURE__ */ jsxs(CopyRow, { children: [
/* @__PURE__ */ jsx(LabelRow, { children: /* @__PURE__ */ jsx(LabelText, { children: title }) }),
/* @__PURE__ */ jsx(MainRow, { children: /* @__PURE__ */ jsx(Skeleton, {}) })
] });
}
const displayValue = valueText || value;
return /* @__PURE__ */ jsxs(CopyRow, { as: "button", onClick: handleCopy, disabled, children: [
/* @__PURE__ */ jsxs("div", { children: [
/* @__PURE__ */ jsx(LabelRow, { children: /* @__PURE__ */ jsx(LabelText, { children: title }) }),
/* @__PURE__ */ jsx(MainRow, { children: /* @__PURE__ */ jsxs(ValueContainer, { children: [
/* @__PURE__ */ jsx(ValueText, { children: displayValue }),
smallText && /* @__PURE__ */ jsx(SmallText, { children: smallText })
] }) })
] }),
/* @__PURE__ */ jsx(CopyIconWrap, { children: /* @__PURE__ */ jsx(CopyToClipboardIcon, { 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