From 9339d75dc62577d5bceb9f4ac227598248548a5d Mon Sep 17 00:00:00 2001 From: David Klakurka Date: Sun, 21 Jun 2026 22:48:31 -0700 Subject: [PATCH 1/3] Updated disable-altpayments to altpayments (expanded functionality) --- docs/README.md | 14 +- docs/_sidebar.md | 2 +- docs/zh-cn/README.md | 14 +- docs/zh-cn/_sidebar.md | 2 +- docs/zh-tw/README.md | 14 +- docs/zh-tw/_sidebar.md | 2 +- paybutton/dev/demo/index.html | 4 +- paybutton/dev/demo/paybutton-generator.html | 16 +- paybutton/src/index.tsx | 2 +- react/lib/components/PayButton/PayButton.tsx | 135 ++++++--- .../PaymentDialog/PaymentDialog.tsx | 6 +- .../components/Widget/AltpaymentWidget.tsx | 278 +++++++++++++++--- react/lib/components/Widget/Widget.tsx | 41 ++- .../lib/components/Widget/WidgetContainer.tsx | 6 +- react/lib/tests/util/altpayment.test.ts | 43 +++ react/lib/util/altpayment.ts | 32 ++ react/lib/util/index.ts | 1 + 17 files changed, 471 insertions(+), 141 deletions(-) create mode 100644 react/lib/tests/util/altpayment.test.ts create mode 100644 react/lib/util/altpayment.ts diff --git a/docs/README.md b/docs/README.md index 79fefac2..ef874629 100644 --- a/docs/README.md +++ b/docs/README.md @@ -101,7 +101,7 @@ Example using JavaScript to generate a PayButton: disabled: false, wsBaseUrl: 'http://localhost:5000', apiBaseUrl: 'http://localhost:3000' - disableAltpayment: true, + altpayment: 'XEC', contributionOffset: 10, autoClose: true }; @@ -988,11 +988,11 @@ apiBaseUrl = "https://paybutton.org" -## disable-altpayment +## altpayment -> **The ‘disableAltpayment’ parameter disables altpayment logic.** +> **The `altpayment` parameter controls SideShift altpayment behavior.** -?> This parameter is optional. Default value is false. Possible values are true or false. +?> This parameter is optional. When omitted or set to `true`, users see an opt-in "Don't have any XEC/BCH?" link to swap other coins via SideShift. Set to `XEC` or `BCH` to hide that link. Set to `BTC` to automatically open the SideShift flow with BTC pre-selected. Unrecognized coin tickers behave the same as `true`. **Example:** @@ -1000,19 +1000,19 @@ apiBaseUrl = "https://paybutton.org" #### ** HTML ** ```html -disable-altpayment="true" +altpayment="BTC" ``` #### ** JavaScript ** ```javascript -disableAltpayment: 'true' +altpayment: 'BTC' ``` #### ** React ** ```react -disableAltpayment = "true" +altpayment = "BTC" ``` diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 940e9fdb..52592e5c 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -28,7 +28,7 @@ - [disabled](/?id=disabled) - [ws-base-url](/?id=ws-base-url) - [api-base-url](/?id=api-base-url) - - [disable-altpayment](/?id=disable-altpayment) + - [altpayment](/?id=altpayment) - [contribution-offset](/?id=contribution-offset) - [auto-close](/?id=auto-close) - [disable-sound](/?id=disable-sound) diff --git a/docs/zh-cn/README.md b/docs/zh-cn/README.md index dc86beaf..a350adc7 100644 --- a/docs/zh-cn/README.md +++ b/docs/zh-cn/README.md @@ -101,7 +101,7 @@ disabled: false, wsBaseUrl: 'http://localhost:5000', apiBaseUrl: 'http://localhost:3000' - disableAltpayment: true, + altpayment: 'XEC', contributionOffset: 10 }; @@ -982,11 +982,11 @@ apiBaseUrl: 'https://paybutton.org' apiBaseUrl = "https://paybutton.org" ``` -## disable-altpayment +## altpayment -> **“disableAltpayment” 参数用于禁用备用支付逻辑。** +> **“altpayment” 参数用于控制 SideShift 备用支付行为。** -?> 该参数为可选项,默认值为 false。可选值为 true 或 false。 +?> 该参数为可选项。省略或设为 `true` 时,用户会看到“没有 XEC/BCH?”的 SideShift 兑换入口。设为 `XEC` 或 `BCH` 可隐藏该入口。设为 `BTC` 将自动打开 SideShift 流程并预选 BTC。无法识别的币种代码与 `true` 行为相同。 **Example:** @@ -994,19 +994,19 @@ apiBaseUrl = "https://paybutton.org" #### ** HTML ** ```html -disable-altpayment="true" +altpayment="BTC" ``` #### ** JavaScript ** ```javascript -disableAltpayment: 'true' +altpayment: 'BTC' ``` #### ** React ** ```react -disableAltpayment = "true" +altpayment = "BTC" ``` diff --git a/docs/zh-cn/_sidebar.md b/docs/zh-cn/_sidebar.md index 6acc45ce..b0378904 100644 --- a/docs/zh-cn/_sidebar.md +++ b/docs/zh-cn/_sidebar.md @@ -27,7 +27,7 @@ - [disabled](/zh-cn/?id=disabled) - [ws-base-url](/zh-cn/?id=ws-base-url) - [api-base-url](/zh-cn/?id=api-base-url) - - [disable-altpayment](/zh-cn/?id=disable-altpayment) + - [altpayment](/zh-cn/?id=altpayment) - [contribution-offset](/zh-cn/?id=contribution-offset) - [auto-close](/zh-cn/?id=auto-close) - [disable-sound](/zh-cn/?id=disable-sound) diff --git a/docs/zh-tw/README.md b/docs/zh-tw/README.md index 4d004665..25670449 100644 --- a/docs/zh-tw/README.md +++ b/docs/zh-tw/README.md @@ -101,7 +101,7 @@ disabled: false, wsBaseUrl: 'http://localhost:5000', apiBaseUrl: 'http://localhost:3000' - disableAltpayment: true, + altpayment: 'XEC', contributionOffset: 10 }; @@ -982,11 +982,11 @@ apiBaseUrl: 'https://paybutton.org' apiBaseUrl = "https://paybutton.org" ``` -## disable-altpayment +## altpayment -> **“disableAltpayment” 參數用於禁用備用支付邏輯。** +> **「altpayment」參數用於控制 SideShift 備用支付行為。** -?> 該參數為可選項,預設值為 false。可選值為 true 或 false。 +?> 該參數為可選項。省略或設為 `true` 時,使用者會看到「沒有 XEC/BCH?」的 SideShift 兌換入口。設為 `XEC` 或 `BCH` 可隱藏該入口。設為 `BTC` 將自動開啟 SideShift 流程並預選 BTC。無法識別的幣種代碼與 `true` 行為相同。 **Example:** @@ -994,19 +994,19 @@ apiBaseUrl = "https://paybutton.org" #### ** HTML ** ```html -disable-altpayment="true" +altpayment="BTC" ``` #### ** JavaScript ** ```javascript -disableAltpayment: 'true' +altpayment: 'BTC' ``` #### ** React ** ```react -disableAltpayment = "true" +altpayment = "BTC" ``` diff --git a/docs/zh-tw/_sidebar.md b/docs/zh-tw/_sidebar.md index 8bda09ed..8909d20f 100644 --- a/docs/zh-tw/_sidebar.md +++ b/docs/zh-tw/_sidebar.md @@ -27,7 +27,7 @@ - [disabled](/zh-tw/?id=disabled) - [ws-base-url](/zh-tw/?id=ws-base-url) - [api-base-url](/zh-tw/?id=api-base-url) - - [disable-altpayment](/zh-tw/?id=disable-altpayment) + - [altpayment](/zh-tw/?id=altpayment) - [contribution-offset](/zh-tw/?id=contribution-offset) - [auto-close](/zh-tw/?id=auto-close) - [disable-sound](/zh-tw/?id=disable-sound) diff --git a/paybutton/dev/demo/index.html b/paybutton/dev/demo/index.html index 3ee3f628..4e53f2a3 100644 --- a/paybutton/dev/demo/index.html +++ b/paybutton/dev/demo/index.html @@ -38,7 +38,7 @@ currency="BCH" text="Purchase" on-transaction="myTransactionFunction" size="xl">
+ text="Donate (other dev)" on-success="mySuccessFunction" on-transaction="myTransactionFunction" altpayment="true">
+ theme='{ "palette": { "primary": "#F18F01", "secondary": "#C3F3FD", "tertiary": "#2E4057"} }' altpayment="XEC">
diff --git a/paybutton/dev/demo/paybutton-generator.html b/paybutton/dev/demo/paybutton-generator.html index 7cb9f130..d4b81198 100644 --- a/paybutton/dev/demo/paybutton-generator.html +++ b/paybutton/dev/demo/paybutton-generator.html @@ -170,10 +170,14 @@ false-value="false"> -
- - +
+ +
(undefined); const [altpaymentSocket, setAltpaymentSocket] = useState(undefined); - const [useAltpayment, setUseAltpayment] = useState(false); + const [useAltpayment, setUseAltpayment] = useState(() => parseAltpayment(altpayment).autoStart); const [coins, setCoins] = useState([]); const [loadingPair, setLoadingPair] = useState(false); const [coinPair, setCoinPair] = useState(); @@ -124,8 +125,19 @@ export const PayButton = ({ getCurrencyTypeFromAddress(to), ); + const altpaymentSocketRef = useRef(undefined); + useEffect(() => { + altpaymentSocketRef.current = altpaymentSocket; + }, [altpaymentSocket]); + const disconnectAltpaymentSocket = useCallback(() => { + if (altpaymentSocketRef.current !== undefined) { + altpaymentSocketRef.current.disconnect(); + altpaymentSocketRef.current = undefined; + } + setAltpaymentSocket(undefined); + }, []); useEffect(() => { priceRef.current = price; }, [price]); @@ -251,11 +263,28 @@ export const PayButton = ({ }, []); + const resetAltpaymentState = useCallback(() => { + setUseAltpayment(parseAltpayment(altpayment).autoStart); + setCoins([]); + setCoinPair(undefined); + setLoadingPair(false); + setLoadingShift(false); + setAltpaymentShift(undefined); + setAltpaymentError(undefined); + disconnectAltpaymentSocket(); + }, [altpayment, disconnectAltpaymentSocket]); + const handleCloseDialog = (success?: boolean, paymentId?: string): void => { if (onClose !== undefined) onClose(success, paymentId); setDialogOpen(false); }; + useEffect(() => { + if (!dialogOpen) { + resetAltpaymentState(); + } + }, [dialogOpen, resetAltpaymentState]); + useEffect(() => { setAmount(initialAmount); }, [initialAmount]); @@ -288,56 +317,66 @@ export const PayButton = ({ }, [to]); useEffect(() => { - if (dialogOpen === false) { - return + if (!dialogOpen) { + return; } + + let cancelled = false; + (async () => { - if (txsSocket === undefined) { - const expectedAmount = currencyObj ? currencyObj?.float : undefined - await setupChronikWebSocket({ - address: to, - txsSocket, - apiBaseUrl, - wsBaseUrl, - setTxsSocket, - setNewTxs, - setDialogOpen, - checkSuccessInfo: { - currency, - price, - randomSatoshis: randomSatoshis ?? false, - disablePaymentId, - expectedAmount, - expectedOpReturn: opReturn, - expectedPaymentId: paymentId, - currencyObj, - donationRate + if (txsSocket === undefined) { + const expectedAmount = currencyObj ? currencyObj?.float : undefined + await setupChronikWebSocket({ + address: to, + txsSocket, + apiBaseUrl, + wsBaseUrl, + setTxsSocket, + setNewTxs, + setDialogOpen, + checkSuccessInfo: { + currency, + price, + randomSatoshis: randomSatoshis ?? false, + disablePaymentId, + expectedAmount, + expectedOpReturn: opReturn, + expectedPaymentId: paymentId, + currencyObj, + donationRate + } + }) + } + if (cancelled || !useAltpayment) { + return + } + if (altpaymentSocketRef.current === undefined) { + await setupAltpaymentSocket({ + addressType, + altpaymentSocket: altpaymentSocketRef.current, + wsBaseUrl, + setAltpaymentSocket: (socket: Socket | undefined) => { + altpaymentSocketRef.current = socket + setAltpaymentSocket(socket) + }, + setCoins, + setCoinPair, + setLoadingPair, + setAltpaymentShift, + setLoadingShift, + setAltpaymentError, + }) + if (cancelled) { + disconnectAltpaymentSocket() } - }) - } - if (altpaymentSocket === undefined && useAltpayment) { - await setupAltpaymentSocket({ - addressType, - altpaymentSocket, - wsBaseUrl, - setAltpaymentSocket, - setCoins, - setCoinPair, - setLoadingPair, - setAltpaymentShift, - setLoadingShift, - setAltpaymentError, - }) - } + } })() return () => { - if (altpaymentSocket !== undefined) { - altpaymentSocket.disconnect(); - setAltpaymentSocket(undefined); - } + cancelled = true + disconnectAltpaymentSocket() } - }, [dialogOpen, useAltpayment]); + }, [dialogOpen, useAltpayment, disconnectAltpaymentSocket]); useEffect(() => { if (initialAmount != null && currency) { @@ -435,7 +474,7 @@ export const PayButton = ({ wsBaseUrl={wsBaseUrl} apiBaseUrl={apiBaseUrl} hoverText={hoverText} - disableAltpayment={disableAltpayment} + altpayment={altpayment} contributionOffset={contributionOffset} hideSendButton={hideSendButton} autoClose={autoClose} diff --git a/react/lib/components/PaymentDialog/PaymentDialog.tsx b/react/lib/components/PaymentDialog/PaymentDialog.tsx index f4d868b5..1301722a 100644 --- a/react/lib/components/PaymentDialog/PaymentDialog.tsx +++ b/react/lib/components/PaymentDialog/PaymentDialog.tsx @@ -36,7 +36,7 @@ export interface PaymentDialogProps extends ButtonProps { onTransaction?: (transaction: Transaction) => void; wsBaseUrl?: string; apiBaseUrl?: string; - disableAltpayment?: boolean; + altpayment?: string | boolean; contributionOffset?: number; hideSendButton?: boolean; useAltpayment: boolean @@ -98,7 +98,7 @@ export const PaymentDialog = ({ wsBaseUrl, apiBaseUrl, hoverText, - disableAltpayment, + altpayment, contributionOffset, hideSendButton, autoClose = true, @@ -231,7 +231,7 @@ export const PaymentDialog = ({ wsBaseUrl={wsBaseUrl} apiBaseUrl={apiBaseUrl} hoverText={hoverText} - disableAltpayment={disableAltpayment} + altpayment={altpayment} contributionOffset={contributionOffset} hideSendButton={hideSendButton} useAltpayment={useAltpayment} diff --git a/react/lib/components/Widget/AltpaymentWidget.tsx b/react/lib/components/Widget/AltpaymentWidget.tsx index 3dcd3f8a..e28719a8 100644 --- a/react/lib/components/Widget/AltpaymentWidget.tsx +++ b/react/lib/components/Widget/AltpaymentWidget.tsx @@ -1,5 +1,5 @@ -import React, { Fragment, useEffect, useState } from 'react' -import { TextField, Select, MenuItem, InputLabel, FormControl } from '@mui/material' +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 { resolveNumber, CryptoCurrency, DECIMALS } from '../../util' @@ -25,6 +25,7 @@ interface AltpaymentProps { coinPair?: AltpaymentPair; setCoinPair: Function; altpaymentEditable: boolean; + preselectedCoin?: string; animation?: animation; addressType: CryptoCurrency; to: string; @@ -49,6 +50,7 @@ export const AltpaymentWidget: React.FunctionComponent = props coinPair, setCoinPair, altpaymentEditable, + preselectedCoin, animation, addressType, thisAmount, @@ -65,6 +67,65 @@ export const AltpaymentWidget: React.FunctionComponent = props const [selectedCoinNetwork, setSelectedCoinNetwork] = useState(undefined); const [pairAmountFixedDecimals, setPairAmountFixedDecimals] = useState(undefined); const [pairAmount, setPairAmount] = useState(undefined); + const autoRateRequestedRef = useRef(false); + const autoQuoteRequestedRef = useRef(false); + const prevAltpaymentSocketRef = useRef(undefined); + + const getDepositDecimals = ( + coin: AltpaymentCoin, + network: string, + pair: AltpaymentPair, + ): number => { + const networkTokenDetails = coin.tokenDetails?.[network] + if (networkTokenDetails?.decimals !== undefined) { + return networkTokenDetails.decimals + } + const minFraction = pair.min.split('.')[1] + if (minFraction !== undefined) { + return minFraction.length + } + return DECIMALS[coin.coin] ?? 8 + } + + const computeDepositAmountFromSettle = (): string | undefined => { + if (!coinPair || !selectedCoin || !selectedCoinNetwork || thisAmount == null || thisAmount === '') { + return undefined + } + const settleAmount = +thisAmount + if (Number.isNaN(settleAmount) || settleAmount <= 0) { + return undefined + } + const decimals = getDepositDecimals(selectedCoin, selectedCoinNetwork, coinPair) + return resolveNumber(settleAmount / +coinPair.rate).toFixed(decimals) + } + + useEffect(() => { + if (altpaymentSocket === undefined) { + autoRateRequestedRef.current = false + autoQuoteRequestedRef.current = false + prevAltpaymentSocketRef.current = undefined + return + } + if (altpaymentSocket !== prevAltpaymentSocketRef.current) { + autoRateRequestedRef.current = false + autoQuoteRequestedRef.current = false + setSelectedCoin(undefined) + setSelectedCoinNetwork(undefined) + setPairAmount(undefined) + setPairAmountFixedDecimals(undefined) + prevAltpaymentSocketRef.current = altpaymentSocket + } + }, [altpaymentSocket]) + + useEffect(() => { + if (preselectedCoin && coins.length > 0 && selectedCoin === undefined) { + const coin = coins.find(c => c.coin === preselectedCoin) + if (coin) { + setSelectedCoin(coin) + setSelectedCoinNetwork(coin.networks[0]) + } + } + }, [coins, preselectedCoin, selectedCoin]) useEffect(() => { if (pairAmount && coinPair) { @@ -83,30 +144,26 @@ export const AltpaymentWidget: React.FunctionComponent = props }, [selectedCoin]) useEffect(() => { - if (coinPair && thisAmount && selectedCoin && selectedCoinNetwork) { - const bigNumber = resolveNumber(+thisAmount / +coinPair.rate) - const tokenDetails = selectedCoin.tokenDetails - let decimals: number - if (tokenDetails !== undefined) { - decimals = tokenDetails[selectedCoinNetwork].decimals - } else { - decimals = coinPair.min.split('.')[1].length + if (coinPair && thisAmount != null && thisAmount !== '' && selectedCoin && selectedCoinNetwork) { + const depositAmount = computeDepositAmountFromSettle() + if (depositAmount === undefined) { + return + } + const decimals = getDepositDecimals(selectedCoin, selectedCoinNetwork, coinPair) + setPairAmountFixedDecimals(depositAmount) + if (!altpaymentEditable) { + setPairAmount(depositAmount) } - const amountString = bigNumber.toFixed(decimals) - setPairAmountFixedDecimals(amountString) - - // Besides decimals, account for the non-decimal part of the bigNumber - // plus the '.' character. - const floorAmount = pairAmount ? Math.floor(+pairAmount) : 1 + const floorAmount = Math.floor(+depositAmount) || 1 const nonDecimalCharCount = 1 + Math.ceil(Math.log10(floorAmount + 1)) setPairAmountMaxLength(nonDecimalCharCount + decimals) } - }, [coinPair, selectedCoin, thisAmount, pairAmount, selectedCoinNetwork]) + }, [coinPair, selectedCoin, thisAmount, selectedCoinNetwork, altpaymentEditable]) const requestPairRate = (): void => { - if (selectedCoin !== undefined) { - const from = `${selectedCoin.coin}-${selectedCoin?.networks[0]}` + if (selectedCoin !== undefined && selectedCoinNetwork !== undefined) { + const from = `${selectedCoin.coin}-${selectedCoinNetwork}` const to = addressType === 'XEC' ? `ecash-mainnet` : `bitcoincash-mainnet` if (altpaymentSocket !== undefined) { altpaymentSocket.emit('get-altpayment-rate', {from, to}) @@ -114,6 +171,35 @@ export const AltpaymentWidget: React.FunctionComponent = props } } + useEffect(() => { + if ( + preselectedCoin && + !altpaymentEditable && + selectedCoin !== undefined && + selectedCoinNetwork !== undefined && + coinPair === undefined && + !loadingPair && + !autoRateRequestedRef.current && + altpaymentSocket !== undefined + ) { + autoRateRequestedRef.current = true + setLoadingPair(true) + const from = `${selectedCoin.coin}-${selectedCoinNetwork}` + const to = addressType === 'XEC' ? `ecash-mainnet` : `bitcoincash-mainnet` + altpaymentSocket.emit('get-altpayment-rate', {from, to}) + } + }, [ + preselectedCoin, + altpaymentEditable, + selectedCoin, + selectedCoinNetwork, + coinPair, + loadingPair, + altpaymentSocket, + addressType, + setLoadingPair, + ]) + const handleCoinChange = async (e: React.ChangeEvent<{ name?: string; value: unknown }>) => { const coinName = e.target.value as string const selectedCoin = coins.find(c => c.coin === coinName) @@ -147,24 +233,81 @@ export const AltpaymentWidget: React.FunctionComponent = props } }; - const handleCreateQuoteButtonClick = () => { - if (altpaymentSocket !== undefined && selectedCoin !== undefined) { - setLoadingShift(true) - altpaymentSocket.emit('create-altpayment-quote', { - depositAmount: pairAmountFixedDecimals, - settleCoin: addressType, - depositCoin: selectedCoin?.coin, - depositNetwork: selectedCoinNetwork, - settleAddress: to - }); + const createQuote = (): boolean => { + if (altpaymentSocket === undefined || selectedCoin === undefined || selectedCoinNetwork === undefined) { + return false + } + + const depositAmount = altpaymentEditable + ? pairAmountFixedDecimals + : (pairAmountFixedDecimals ?? computeDepositAmountFromSettle()) + + const quotePayload: Record = { + settleCoin: addressType, + depositCoin: selectedCoin.coin, + depositNetwork: selectedCoinNetwork, + settleAddress: to, + } + + if (depositAmount) { + quotePayload.depositAmount = depositAmount + } else if (thisAmount != null && thisAmount !== '' && !altpaymentEditable) { + const settleDecimals = DECIMALS[addressType] ?? 2 + quotePayload.settleAmount = resolveNumber(+thisAmount).toFixed(settleDecimals) + } else { + return false + } + + setLoadingShift(true) + altpaymentSocket.emit('create-altpayment-quote', quotePayload) + return true + } + + useEffect(() => { + if ( + !preselectedCoin || + altpaymentEditable || + altpaymentSocket === undefined || + selectedCoin === undefined || + selectedCoinNetwork === undefined || + coinPair === undefined || + altpaymentShift !== undefined || + loadingShift || + autoQuoteRequestedRef.current || + altpaymentError + ) { + return } + + autoQuoteRequestedRef.current = createQuote() + }, [ + preselectedCoin, + altpaymentEditable, + altpaymentSocket, + selectedCoin, + selectedCoinNetwork, + coinPair, + altpaymentShift, + loadingShift, + altpaymentError, + thisAmount, + pairAmountFixedDecimals, + ]) + + const handleCreateQuoteButtonClick = () => { + createQuote() } const resetTrade = () => { + autoRateRequestedRef.current = false + autoQuoteRequestedRef.current = false + setSelectedCoin(undefined) + setSelectedCoinNetwork(undefined) setCoinPair(undefined) setAltpaymentError(undefined) setAltpaymentShift(undefined) setPairAmount(undefined) + setPairAmountFixedDecimals(undefined) setShiftCompleted(false) } @@ -200,9 +343,34 @@ export const AltpaymentWidget: React.FunctionComponent = props }; const SideshiftCtn = styled('div')({ - alignItems: 'center', display: 'flex', flexDirection: 'column', - height: 'calc(100% - 45px)', width: '100%', position: 'absolute', - zIndex: 9, top: '0', left: '0', background: '#f5f5f7', paddingTop: '20px' + alignItems: 'center', + justifyContent: 'flex-start', + display: 'flex', + flexDirection: 'column', + boxSizing: 'border-box', + position: 'absolute', + zIndex: 9, + top: 0, + left: 0, + right: 0, + bottom: 0, + background: '#f5f5f7', + overflowY: 'auto', + padding: '24px', + }) + + const LoadingCenter = styled('div')({ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '16px', + textAlign: 'center', + width: 'max-content', + maxWidth: '100%', }) const Header = styled('div')({ @@ -295,6 +463,26 @@ export const AltpaymentWidget: React.FunctionComponent = props return coinString; } + const isAutoStart = Boolean(preselectedCoin) + const isAutoStartLoading = isAutoStart && !altpaymentShift && !altpaymentError + + const renderLoading = (message: string) => ( + + + + {message} + + + ) + return ( {altpaymentError ? ( @@ -302,6 +490,8 @@ export const AltpaymentWidget: React.FunctionComponent = props Error: {altpaymentError.errorMessage} Back + ) : isAutoStartLoading ? ( + renderLoading('Loading SideShift...') ) : ( { @@ -352,13 +542,13 @@ export const AltpaymentWidget: React.FunctionComponent = props ) ) : loadingShift ? ( -

Loading Shift...

- ) : coinPair ? ( + renderLoading('Loading Shift...') + ) : coinPair && selectedCoin ? (

{' '} - 1 {selectedCoin?.name} ~={' '} - {resolveNumber(coinPair.rate).toFixed(DECIMALS[coinPair.settleCoin])} {coinPair.settleCoin}{' '} + 1 {selectedCoin.name} ~={' '} + {resolveNumber(coinPair.rate).toFixed(DECIMALS[coinPair.settleCoin] ?? 8)} {coinPair.settleCoin}{' '}

{altpaymentEditable ? (
@@ -383,29 +573,30 @@ export const AltpaymentWidget: React.FunctionComponent = props !isAboveMinimumAltpaymentAmount || !isBelowMaximumAltpaymentAmount ? {opacity: '0.5', cursor: 'not-allowed'} : {}}>
- {!isAboveMinimumAltpaymentAmount && ( + {pairAmount && !isAboveMinimumAltpaymentAmount && ( Amount is below minimum. )} - {!isBelowMaximumAltpaymentAmount && ( + {pairAmount && !isBelowMaximumAltpaymentAmount && ( Amount is above maximum. )}
) : ( - {coins.length === 0 &&
Loading...
} + {coins.length === 0 && renderLoading('Loading SideShift...')} {coins.length > 0 && (
@@ -414,6 +605,7 @@ export const AltpaymentWidget: React.FunctionComponent = props SideShift
+ {!preselectedCoin ? ( Select a coin = props ))} + ) : null} {selectedCoin && selectedCoin.networks.length > 1 && ( @@ -490,9 +683,6 @@ export const AltpaymentWidget: React.FunctionComponent = props ) // END: Altpayment region } - {coinPair && !loadingShift && ( - Back - )}
)}
diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index b005f61d..407989a8 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -38,6 +38,7 @@ import { isPropsTrue, setupChronikWebSocket, setupAltpaymentSocket, + parseAltpayment, CryptoCurrency, DEFAULT_DONATION_RATE, DEFAULT_MINIMUM_DONATION_AMOUNT, @@ -86,7 +87,7 @@ export interface WidgetProps { apiBaseUrl?: string loading?: boolean hoverText?: string - disableAltpayment?: boolean + altpayment?: string | boolean contributionOffset?: number setAltpaymentShift?: Function altpaymentShift?: AltpaymentShift | undefined @@ -178,7 +179,7 @@ export const Widget: React.FunctionComponent = props => { altpaymentShift, shiftCompleted, setShiftCompleted, - disableAltpayment, + altpayment, contributionOffset, useAltpayment, setUseAltpayment, @@ -251,7 +252,9 @@ export const Widget: React.FunctionComponent = props => { (setAltpaymentShift as ((s: AltpaymentShift | undefined) => void) | undefined) ?? setInternalAltpaymentShift - const [internalUseAltpayment, setInternalUseAltpayment] = useState(false) + const [internalUseAltpayment, setInternalUseAltpayment] = useState( + () => parseAltpayment(altpayment).autoStart + ) const thisUseAltpayment = useAltpayment ?? internalUseAltpayment const setThisUseAltpayment = (setUseAltpayment as ((b: boolean) => void) | undefined) ?? setInternalUseAltpayment @@ -306,6 +309,8 @@ export const Widget: React.FunctionComponent = props => { const [goalPercent, setGoalPercent] = useState(0) const [altpaymentEditable, setAltpaymentEditable] = useState(false) + const altpaymentConfig = useMemo(() => parseAltpayment(altpayment), [altpayment]) + const price = props.price ?? 0 const [hasPrice, setHasPrice] = useState(props.price !== undefined && props.price > 0) @@ -1138,12 +1143,20 @@ export const Widget: React.FunctionComponent = props => { return ( + {!thisUseAltpayment ? ( = props => { })()} + ) : null} {thisUseAltpayment ? ( = props => { coinPair={thisCoinPair} setCoinPair={setThisCoinPair} altpaymentEditable={altpaymentEditable} + preselectedCoin={altpaymentConfig.preselectedCoin} animation={animation} addressType={thisAddressType} to={to} /> ) : null} + {!thisUseAltpayment ? ( {loading && shouldDisplayGoal ? ( = props => { )} - {!isPropsTrue(disableAltpayment) ? ( + {altpaymentConfig.showAltpaymentLink && !thisUseAltpayment ? ( = props => { ) : null} + ) : null} {foot ? ( {foot as any} ) : null} + - - + + Powered by PayButton.org {(() => { + if (thisUseAltpayment) { + return false + } // For fiat conversions, check the converted crypto amount // For crypto-only, check the currency object amount const amountToCheck = hasPrice && convertedCryptoAmount !== undefined @@ -1497,7 +1519,6 @@ export const Widget: React.FunctionComponent = props => { ) : null} - diff --git a/react/lib/components/Widget/WidgetContainer.tsx b/react/lib/components/Widget/WidgetContainer.tsx index 54045b96..7883d879 100644 --- a/react/lib/components/Widget/WidgetContainer.tsx +++ b/react/lib/components/Widget/WidgetContainer.tsx @@ -49,7 +49,7 @@ export interface WidgetContainerProps wsBaseUrl?: string; apiBaseUrl?: string; successText?: string; - disableAltpayment?: boolean + altpayment?: string | boolean contributionOffset?: number setNewTxs?: Function disableSound?: boolean @@ -128,7 +128,7 @@ export const WidgetContainer: React.FunctionComponent = apiBaseUrl = config.apiBaseUrl, successText, hoverText, - disableAltpayment, + altpayment, contributionOffset, altpaymentShift, setAltpaymentShift, @@ -424,7 +424,7 @@ export const WidgetContainer: React.FunctionComponent = setAltpaymentShift={setAltpaymentShift} shiftCompleted={shiftCompleted} setShiftCompleted={setShiftCompleted} - disableAltpayment={disableAltpayment} + altpayment={altpayment} contributionOffset={contributionOffset} transactionText={transactionText} donationAddress={donationAddress} diff --git a/react/lib/tests/util/altpayment.test.ts b/react/lib/tests/util/altpayment.test.ts new file mode 100644 index 00000000..b97ead9b --- /dev/null +++ b/react/lib/tests/util/altpayment.test.ts @@ -0,0 +1,43 @@ +import { parseAltpayment } from '../../util/altpayment' + +describe('parseAltpayment', () => { + const defaultConfig = { showAltpaymentLink: true, autoStart: false } + + it('returns default for undefined, null, empty, and false', () => { + expect(parseAltpayment(undefined)).toEqual(defaultConfig) + expect(parseAltpayment(null)).toEqual(defaultConfig) + expect(parseAltpayment('')).toEqual(defaultConfig) + expect(parseAltpayment(false)).toEqual(defaultConfig) + expect(parseAltpayment('false')).toEqual(defaultConfig) + }) + + it('returns default for true', () => { + expect(parseAltpayment(true)).toEqual(defaultConfig) + expect(parseAltpayment('true')).toEqual(defaultConfig) + }) + + it('disables altpayment link for XEC and BCH', () => { + expect(parseAltpayment('XEC')).toEqual({ showAltpaymentLink: false, autoStart: false }) + expect(parseAltpayment('xec')).toEqual({ showAltpaymentLink: false, autoStart: false }) + expect(parseAltpayment('BCH')).toEqual({ showAltpaymentLink: false, autoStart: false }) + expect(parseAltpayment('bch')).toEqual({ showAltpaymentLink: false, autoStart: false }) + }) + + it('auto-starts with BTC preselected for BTC ticker', () => { + expect(parseAltpayment('BTC')).toEqual({ + showAltpaymentLink: false, + autoStart: true, + preselectedCoin: 'BTC', + }) + expect(parseAltpayment('btc')).toEqual({ + showAltpaymentLink: false, + autoStart: true, + preselectedCoin: 'BTC', + }) + }) + + it('returns default for unrecognized tickers', () => { + expect(parseAltpayment('ETH')).toEqual(defaultConfig) + expect(parseAltpayment('DOGE')).toEqual(defaultConfig) + }) +}) diff --git a/react/lib/util/altpayment.ts b/react/lib/util/altpayment.ts new file mode 100644 index 00000000..9a57bf7c --- /dev/null +++ b/react/lib/util/altpayment.ts @@ -0,0 +1,32 @@ +export type ParsedAltpayment = { + showAltpaymentLink: boolean + autoStart: boolean + preselectedCoin?: string +} + +const DEFAULT_CONFIG: ParsedAltpayment = { + showAltpaymentLink: true, + autoStart: false, +} + +export function parseAltpayment (value?: string | boolean | null): ParsedAltpayment { + if (value === undefined || value === null || value === '' || value === false || value === 'false') { + return DEFAULT_CONFIG + } + + if (value === true || value === 'true') { + return DEFAULT_CONFIG + } + + const ticker = String(value).toUpperCase() + + if (ticker === 'XEC' || ticker === 'BCH') { + return { showAltpaymentLink: false, autoStart: false } + } + + if (ticker === 'BTC') { + return { showAltpaymentLink: false, autoStart: true, preselectedCoin: 'BTC' } + } + + return DEFAULT_CONFIG +} diff --git a/react/lib/util/index.ts b/react/lib/util/index.ts index 06f0b895..99855c38 100644 --- a/react/lib/util/index.ts +++ b/react/lib/util/index.ts @@ -13,4 +13,5 @@ export * from './number'; export * from './currency'; export * from './validate'; export * from './autoClose'; +export * from './altpayment'; From 6cbe83dfc2b0fd1241d9bd9283f5ad34e8a627c6 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 21 Jun 2026 23:58:48 -0700 Subject: [PATCH 2/3] Update react/lib/tests/util/altpayment.test.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- react/lib/tests/util/altpayment.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/react/lib/tests/util/altpayment.test.ts b/react/lib/tests/util/altpayment.test.ts index b97ead9b..9048b2ad 100644 --- a/react/lib/tests/util/altpayment.test.ts +++ b/react/lib/tests/util/altpayment.test.ts @@ -39,5 +39,6 @@ describe('parseAltpayment', () => { it('returns default for unrecognized tickers', () => { expect(parseAltpayment('ETH')).toEqual(defaultConfig) expect(parseAltpayment('DOGE')).toEqual(defaultConfig) + expect(parseAltpayment('blahInvalidTicker')).toEqual(defaultConfig) }) }) From a9fe014fce7e11f6d157689454a28aefd2d58b75 Mon Sep 17 00:00:00 2001 From: David Klakurka Date: Mon, 22 Jun 2026 00:01:08 -0700 Subject: [PATCH 3/3] Fixes --- react/lib/tests/util/altpayment.test.ts | 18 +++++++++++------- react/lib/util/altpayment.ts | 13 +++++++++++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/react/lib/tests/util/altpayment.test.ts b/react/lib/tests/util/altpayment.test.ts index 9048b2ad..92a56e17 100644 --- a/react/lib/tests/util/altpayment.test.ts +++ b/react/lib/tests/util/altpayment.test.ts @@ -2,13 +2,17 @@ import { parseAltpayment } from '../../util/altpayment' describe('parseAltpayment', () => { const defaultConfig = { showAltpaymentLink: true, autoStart: false } + const disabledConfig = { showAltpaymentLink: false, autoStart: false } - it('returns default for undefined, null, empty, and false', () => { + it('returns default for undefined, null, and empty', () => { expect(parseAltpayment(undefined)).toEqual(defaultConfig) expect(parseAltpayment(null)).toEqual(defaultConfig) expect(parseAltpayment('')).toEqual(defaultConfig) - expect(parseAltpayment(false)).toEqual(defaultConfig) - expect(parseAltpayment('false')).toEqual(defaultConfig) + }) + + it('disables altpayment for false', () => { + expect(parseAltpayment(false)).toEqual(disabledConfig) + expect(parseAltpayment('false')).toEqual(disabledConfig) }) it('returns default for true', () => { @@ -17,10 +21,10 @@ describe('parseAltpayment', () => { }) it('disables altpayment link for XEC and BCH', () => { - expect(parseAltpayment('XEC')).toEqual({ showAltpaymentLink: false, autoStart: false }) - expect(parseAltpayment('xec')).toEqual({ showAltpaymentLink: false, autoStart: false }) - expect(parseAltpayment('BCH')).toEqual({ showAltpaymentLink: false, autoStart: false }) - expect(parseAltpayment('bch')).toEqual({ showAltpaymentLink: false, autoStart: false }) + expect(parseAltpayment('XEC')).toEqual(disabledConfig) + expect(parseAltpayment('xec')).toEqual(disabledConfig) + expect(parseAltpayment('BCH')).toEqual(disabledConfig) + expect(parseAltpayment('bch')).toEqual(disabledConfig) }) it('auto-starts with BTC preselected for BTC ticker', () => { diff --git a/react/lib/util/altpayment.ts b/react/lib/util/altpayment.ts index 9a57bf7c..57101ad0 100644 --- a/react/lib/util/altpayment.ts +++ b/react/lib/util/altpayment.ts @@ -9,11 +9,20 @@ const DEFAULT_CONFIG: ParsedAltpayment = { autoStart: false, } +const DISABLED_CONFIG: ParsedAltpayment = { + showAltpaymentLink: false, + autoStart: false, +} + export function parseAltpayment (value?: string | boolean | null): ParsedAltpayment { - if (value === undefined || value === null || value === '' || value === false || value === 'false') { + if (value === undefined || value === null || value === '') { return DEFAULT_CONFIG } + if (value === false || value === 'false') { + return DISABLED_CONFIG + } + if (value === true || value === 'true') { return DEFAULT_CONFIG } @@ -21,7 +30,7 @@ export function parseAltpayment (value?: string | boolean | null): ParsedAltpaym const ticker = String(value).toUpperCase() if (ticker === 'XEC' || ticker === 'BCH') { - return { showAltpaymentLink: false, autoStart: false } + return DISABLED_CONFIG } if (ticker === 'BTC') {