UNPKG

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