From 5b08454c62a7f77231fde8bbc7b1bd199409bb5a Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 09:58:11 -0600 Subject: [PATCH 01/41] add logic for user editable dev donation --- react/lib/components/Widget/Widget.tsx | 116 +++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 7 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 21e33a3c..5cb48fc4 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -4,6 +4,7 @@ import { Fade, Typography, TextField, + IconButton, } from '@mui/material' import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react' import copyToClipboard from 'copy-to-clipboard' @@ -242,6 +243,9 @@ export const Widget: React.FunctionComponent = props => { const [goalText, setGoalText] = useState('') const [goalPercent, setGoalPercent] = useState(0) const [altpaymentEditable, setAltpaymentEditable] = useState(false) + const [userDonationRate, setUserDonationRate] = useState(donationRate) + const [donationEnabled, setDonationEnabled] = useState(donationRate > 0) + const [previousDonationRate, setPreviousDonationRate] = useState(donationRate) const price = props.price ?? 0 const [url, setUrl] = useState('') @@ -397,6 +401,12 @@ export const Widget: React.FunctionComponent = props => { animation: 'button-slide 0.6s ease-in-out forwards', animationDelay: '0.4s', }, + donationRateContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + gap: '0.5rem', + }, } }, [success, loading, theme, recentlyCopied, copied]) @@ -573,8 +583,8 @@ export const Widget: React.FunctionComponent = props => { if (convertedObj) { let amountToDisplay = thisCurrencyObject.string; let convertedAmountToDisplay = convertedObj.string - if ( donationRate && donationRate >= 5){ - const thisDonationAmount = thisCurrencyObject.float * (donationRate / 100) + if ( donationEnabled && userDonationRate && userDonationRate > 0){ + const thisDonationAmount = thisCurrencyObject.float * (userDonationRate / 100) const amountWithDonation = thisCurrencyObject.float + thisDonationAmount const amountWithDonationObj = getCurrencyObject( amountWithDonation, @@ -583,7 +593,7 @@ export const Widget: React.FunctionComponent = props => { ) amountToDisplay = amountWithDonationObj.string - const convertedDonationAmount = convertedObj.float * (donationRate / 100) + const convertedDonationAmount = convertedObj.float * (userDonationRate / 100) const convertedAmountWithDonation = convertedObj.float + convertedDonationAmount const convertedAmountWithDonationObj = getCurrencyObject( convertedAmountWithDonation, @@ -592,6 +602,9 @@ export const Widget: React.FunctionComponent = props => { ) convertedAmountToDisplay = convertedAmountWithDonationObj.string setDonationAmount(convertedAmountWithDonationObj.float) + } else if (!donationEnabled || !userDonationRate || userDonationRate === 0) { + // Reset donation amount when disabled + setDonationAmount(null) } setText( `Send ${amountToDisplay} ${thisCurrencyObject.currency} = ${convertedAmountToDisplay} ${thisAddressType}`, @@ -612,7 +625,7 @@ export const Widget: React.FunctionComponent = props => { } setUrl(nextUrl ?? '') } - }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable]) + }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationAmount, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType]) useEffect(() => { try { @@ -634,6 +647,41 @@ export const Widget: React.FunctionComponent = props => { setThisAmount(props.amount) }, [props.amount]) + useEffect(() => { + setUserDonationRate(donationRate) + setDonationEnabled(donationRate > 0) + setPreviousDonationRate(donationRate > 0 ? donationRate : previousDonationRate) + }, [donationRate]) + + const handleDonationToggle = () => { + if (donationEnabled) { + // Turning off - save current rate and set to 0 + setPreviousDonationRate(userDonationRate) + setUserDonationRate(0) + setDonationEnabled(false) + } else { + // Turning on - restore previous rate or use default + const rateToRestore = previousDonationRate > 0 ? previousDonationRate : DEFAULT_DONATE_RATE + setUserDonationRate(rateToRestore) + setDonationEnabled(true) + } + } + + const handleDonationRateChange = (value: number) => { + const clampedValue = Math.max(0, Math.min(100, value)) + setUserDonationRate(clampedValue) + if (clampedValue > 0) { + // Auto-enable donation if user enters a value > 0 + if (!donationEnabled) { + setDonationEnabled(true) + } + setPreviousDonationRate(clampedValue) + } else if (clampedValue === 0) { + // Auto-disable donation if user enters 0 + setDonationEnabled(false) + } + } + let cleanGoalAmount: any if (goalAmount) { cleanGoalAmount = +goalAmount @@ -697,12 +745,12 @@ export const Widget: React.FunctionComponent = props => { let thisUrl = `${prefix}:${to.replace(/^.*:/, '')}`; if (amount) { - if (donationAddress && donationRate && Number(donationRate)) { + if (donationAddress && donationEnabled && userDonationRate && Number(userDonationRate)) { const network = Object.entries(CURRENCY_PREFIXES_MAP).find( ([, value]) => value === prefix )?.[0]; const decimals = network ? DECIMALS[network.toUpperCase()] : undefined; - const donationPercent = donationRate / 100 + const donationPercent = userDonationRate / 100 const thisDonationAmount = donationAmount ? donationAmount : amount * donationPercent thisUrl+=`?amount=${amount}` @@ -721,7 +769,7 @@ export const Widget: React.FunctionComponent = props => { return thisUrl; }, - [disabled, to, opReturn] + [disabled, to, opReturn, userDonationRate, donationAddress, donationAmount, donationEnabled] ) const handleAmountChange = (e: React.ChangeEvent) => { @@ -965,6 +1013,60 @@ export const Widget: React.FunctionComponent = props => { ) : null} + + { + const value = parseFloat(e.target.value) || 0 + handleDonationRateChange(value) + }} + inputProps={{ + min: 0, + max: 100, + step: 0.1 + }} + size="small" + fullWidth + disabled={success} + placeholder="Donation %" + sx={{ + opacity: donationEnabled ? 1 : 0.6, + }} + /> + + + + + + + Powered by PayButton.org From 195db0616ececc890a8c0914297c30dc6c950b1f Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:31:36 -0600 Subject: [PATCH 02/41] style edits --- paybutton/dev/demo/index.html | 2 +- react/lib/components/Widget/Widget.tsx | 123 +++++++++++++++---------- 2 files changed, 77 insertions(+), 48 deletions(-) diff --git a/paybutton/dev/demo/index.html b/paybutton/dev/demo/index.html index 3ee3f628..1b9a5798 100644 --- a/paybutton/dev/demo/index.html +++ b/paybutton/dev/demo/index.html @@ -27,7 +27,7 @@
-
= props => { animationDelay: '0.4s', }, donationRateContainer: { + display: 'flex', + flexDirection: 'column', + gap: '0.5rem', + }, + donationRateLabel: { + fontSize: '0.75rem', + color: '#000000', + opacity: 0.7, + marginBottom: '10px', + marginTop: '20px', + textAlign: 'center' + }, + donationRateInputRow: { display: 'flex', alignItems: 'center', - justifyContent: 'flex-end', gap: '0.5rem', }, } @@ -1014,57 +1026,74 @@ export const Widget: React.FunctionComponent = props => { ) : null} - { - const value = parseFloat(e.target.value) || 0 - handleDonationRateChange(value) - }} - inputProps={{ - min: 0, - max: 100, - step: 0.1 - }} - size="small" - fullWidth - disabled={success} - placeholder="Donation %" - sx={{ - opacity: donationEnabled ? 1 : 0.6, - }} - /> - - + Send a dev donation? + + + - - - + + + + + { + const value = parseFloat(e.target.value) || 0 + handleDonationRateChange(value) + }} + inputProps={{ + min: 0, + max: 100, + step: 0.1 + }} + size="small" + fullWidth + disabled={success} + placeholder="0" + sx={{ + opacity: donationEnabled ? 1 : 0.6, + }} + /> + + % + + From 0c98fbb27af5081c5637bc05341e5830116df5c3 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:26:42 -0600 Subject: [PATCH 03/41] styles, animation, tooltip --- react/lib/components/Widget/Widget.tsx | 62 +++++++++++++++----------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index c6065676..e9bcbc03 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -5,6 +5,7 @@ import { Typography, TextField, IconButton, + Tooltip, } from '@mui/material' import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react' import copyToClipboard from 'copy-to-clipboard' @@ -276,6 +277,7 @@ export const Widget: React.FunctionComponent = props => { @keyframes fade-scale { from { opacity: 0; transform: scale(0.3); } 80% { opacity: 1; transform: scale(1.3); } to { opacity: 1; transform: scale(1); } } @keyframes button-slide { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0px); } } @keyframes button-slide-out { from { opacity: 1; transform: translateY(0px); } to { opacity: 0; transform: translateY(20px); } } +@keyframes fade-slide-up { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0px); } } @keyframes copy-qr { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } @keyframes copy-svg { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } } @keyframes copy-icon { 0% { transform: scale(1); } 50% { transform: scale(0.7); } 100% { transform: scale(1); } } @@ -401,23 +403,14 @@ export const Widget: React.FunctionComponent = props => { animation: 'button-slide 0.6s ease-in-out forwards', animationDelay: '0.4s', }, - donationRateContainer: { - display: 'flex', - flexDirection: 'column', - gap: '0.5rem', - }, - donationRateLabel: { - fontSize: '0.75rem', - color: '#000000', - opacity: 0.7, - marginBottom: '10px', - marginTop: '20px', - textAlign: 'center' - }, donationRateInputRow: { display: 'flex', alignItems: 'center', - gap: '0.5rem', + justifyContent: 'center', + marginTop: '15px', + animation: 'fade-slide-up 0.6s ease-out forwards', + animationDelay: '0.7s', + opacity: 0, }, } }, [success, loading, theme, recentlyCopied, copied]) @@ -1025,17 +1018,15 @@ export const Widget: React.FunctionComponent = props => { ) : null} - - - Send a dev donation? - - + {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 ? ( + + = props => { = props => { inputProps={{ min: 0, max: 100, - step: 0.1 + step: 0.1, + style: { + fontSize: '0.75rem', + padding: '4px 8px', + } }} size="small" - fullWidth disabled={success} placeholder="0" sx={{ + width: '50px', opacity: donationEnabled ? 1 : 0.6, + '& .MuiOutlinedInput-root': { + height: '26px', + fontSize: '0.75rem', + '& input': { + padding: '4px 8px', + fontSize: '0.75rem', + textAlign: 'left', + color: '#5c5c5c', + }, + '& fieldset': { + borderWidth: '1px', + }, + }, }} /> % - + + ) : null} From 52cba1e7f59e989f35fc098d7dd4e441a18bf523 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:33:28 -0600 Subject: [PATCH 04/41] add localstorage persistence --- react/lib/components/Widget/Widget.tsx | 58 +++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index e9bcbc03..9a9b6d1f 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -117,6 +117,8 @@ interface StyleProps { copied: boolean } +const DONATION_RATE_STORAGE_KEY = 'paybutton_donation_rate' + export const Widget: React.FunctionComponent = props => { const { to, @@ -244,9 +246,30 @@ export const Widget: React.FunctionComponent = props => { const [goalText, setGoalText] = useState('') const [goalPercent, setGoalPercent] = useState(0) const [altpaymentEditable, setAltpaymentEditable] = useState(false) - const [userDonationRate, setUserDonationRate] = useState(donationRate) - const [donationEnabled, setDonationEnabled] = useState(donationRate > 0) - const [previousDonationRate, setPreviousDonationRate] = useState(donationRate) + + // Load donation rate from localStorage on mount, falling back to prop/default + const getInitialDonationRate = useCallback(() => { + if (typeof window !== 'undefined' && window.localStorage) { + try { + const stored = localStorage.getItem(DONATION_RATE_STORAGE_KEY) + if (stored !== null) { + const parsed = parseFloat(stored) + if (!isNaN(parsed) && parsed >= 0 && parsed <= 100) { + return parsed + } + } + } catch (e) { + // If localStorage is unavailable or parsing fails, fall back to prop/default + console.warn('Failed to load donation rate from localStorage:', e) + } + } + return donationRate + }, [donationRate]) + + const initialDonationRate = useMemo(() => getInitialDonationRate(), [getInitialDonationRate]) + const [userDonationRate, setUserDonationRate] = useState(initialDonationRate) + const [donationEnabled, setDonationEnabled] = useState(initialDonationRate > 0) + const [previousDonationRate, setPreviousDonationRate] = useState(initialDonationRate) const price = props.price ?? 0 const [url, setUrl] = useState('') @@ -652,10 +675,33 @@ export const Widget: React.FunctionComponent = props => { setThisAmount(props.amount) }, [props.amount]) + // Save donation rate to localStorage whenever it changes useEffect(() => { - setUserDonationRate(donationRate) - setDonationEnabled(donationRate > 0) - setPreviousDonationRate(donationRate > 0 ? donationRate : previousDonationRate) + if (typeof window !== 'undefined' && window.localStorage) { + try { + localStorage.setItem(DONATION_RATE_STORAGE_KEY, userDonationRate.toString()) + } catch (e) { + console.warn('Failed to save donation rate to localStorage:', e) + } + } + }, [userDonationRate]) + + // Only sync with prop if there's no localStorage value (on first mount) + useEffect(() => { + if (typeof window !== 'undefined' && window.localStorage) { + const stored = localStorage.getItem(DONATION_RATE_STORAGE_KEY) + if (stored === null) { + // No stored value, use prop/default + setUserDonationRate(donationRate) + setDonationEnabled(donationRate > 0) + setPreviousDonationRate(donationRate > 0 ? donationRate : previousDonationRate) + } + } else { + // localStorage not available, use prop + setUserDonationRate(donationRate) + setDonationEnabled(donationRate > 0) + setPreviousDonationRate(donationRate > 0 ? donationRate : previousDonationRate) + } }, [donationRate]) const handleDonationToggle = () => { From 8995aa8c0a6f2f5c1b0a8a7f8c9050a3973a97df Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:42:49 -0600 Subject: [PATCH 05/41] remove edit to demo index --- paybutton/dev/demo/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paybutton/dev/demo/index.html b/paybutton/dev/demo/index.html index 1b9a5798..3ee3f628 100644 --- a/paybutton/dev/demo/index.html +++ b/paybutton/dev/demo/index.html @@ -27,7 +27,7 @@
-
Date: Fri, 14 Nov 2025 15:40:50 -0600 Subject: [PATCH 06/41] edit functionality --- paybutton/dev/demo/index.html | 2 +- react/lib/components/Widget/Widget.tsx | 246 ++++++++++++------------- 2 files changed, 124 insertions(+), 124 deletions(-) diff --git a/paybutton/dev/demo/index.html b/paybutton/dev/demo/index.html index 3ee3f628..1b9a5798 100644 --- a/paybutton/dev/demo/index.html +++ b/paybutton/dev/demo/index.html @@ -27,7 +27,7 @@
-
= props => { setAltpaymentError, isChild, donationAddress = config.donationAddress, - donationRate = DEFAULT_DONATE_RATE + donationRate: _donationRate = DEFAULT_DONATE_RATE // Unused - we default to 0 (off) if no localStorage value } = props const [loading, setLoading] = useState(true) @@ -247,7 +247,7 @@ export const Widget: React.FunctionComponent = props => { const [goalPercent, setGoalPercent] = useState(0) const [altpaymentEditable, setAltpaymentEditable] = useState(false) - // Load donation rate from localStorage on mount, falling back to prop/default + // Load donation rate from localStorage on mount, defaulting to 0 (off) if not set const getInitialDonationRate = useCallback(() => { if (typeof window !== 'undefined' && window.localStorage) { try { @@ -259,12 +259,13 @@ export const Widget: React.FunctionComponent = props => { } } } catch (e) { - // If localStorage is unavailable or parsing fails, fall back to prop/default + // If localStorage is unavailable or parsing fails, default to off console.warn('Failed to load donation rate from localStorage:', e) } } - return donationRate - }, [donationRate]) + // Default to 0 (donation off) if no localStorage value + return 0 + }, []) const initialDonationRate = useMemo(() => getInitialDonationRate(), [getInitialDonationRate]) const [userDonationRate, setUserDonationRate] = useState(initialDonationRate) @@ -368,6 +369,12 @@ export const Widget: React.FunctionComponent = props => { color: '#a8a8a8', fontWeight: 'normal', userSelect: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + animation: 'fade-slide-up 0.6s ease-out forwards', + animationDelay: '0.7s', + opacity: 0, }, sideShiftLink: { fontSize: '14px', @@ -426,15 +433,6 @@ export const Widget: React.FunctionComponent = props => { animation: 'button-slide 0.6s ease-in-out forwards', animationDelay: '0.4s', }, - donationRateInputRow: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - marginTop: '15px', - animation: 'fade-slide-up 0.6s ease-out forwards', - animationDelay: '0.7s', - opacity: 0, - }, } }, [success, loading, theme, recentlyCopied, copied]) @@ -629,7 +627,7 @@ export const Widget: React.FunctionComponent = props => { randomSatoshis, ) convertedAmountToDisplay = convertedAmountWithDonationObj.string - setDonationAmount(convertedAmountWithDonationObj.float) + setDonationAmount(convertedDonationAmount) } else if (!donationEnabled || !userDonationRate || userDonationRate === 0) { // Reset donation amount when disabled setDonationAmount(null) @@ -645,11 +643,31 @@ export const Widget: React.FunctionComponent = props => { thisCurrencyObject?.float !== undefined && thisCurrencyObject.float > 0 if (!isFiat(currency) && thisCurrencyObject && notZeroValue) { const cur: string = thisCurrencyObject.currency - setText(`Send ${thisCurrencyObject.string} ${cur}`) - nextUrl = resolveUrl(cur, thisCurrencyObject?.float) + let amountToDisplay = thisCurrencyObject.string + let amountToSend = thisCurrencyObject.float + + // Add donation amount if enabled + if (donationEnabled && userDonationRate && userDonationRate > 0 && cur === 'XEC') { + const donationAmountValue = thisCurrencyObject.float * (userDonationRate / 100) + const amountWithDonation = thisCurrencyObject.float + donationAmountValue + const amountWithDonationObj = getCurrencyObject( + amountWithDonation, + cur, + false, + ) + amountToDisplay = amountWithDonationObj.string + amountToSend = amountWithDonationObj.float + setDonationAmount(donationAmountValue) + } else { + setDonationAmount(null) + } + + setText(`Send ${amountToDisplay} ${cur}`) + nextUrl = resolveUrl(cur, amountToSend) } else { setText(`Send any amount of ${thisAddressType}`) nextUrl = resolveUrl(thisAddressType) + setDonationAmount(null) } setUrl(nextUrl ?? '') } @@ -686,23 +704,8 @@ export const Widget: React.FunctionComponent = props => { } }, [userDonationRate]) - // Only sync with prop if there's no localStorage value (on first mount) - useEffect(() => { - if (typeof window !== 'undefined' && window.localStorage) { - const stored = localStorage.getItem(DONATION_RATE_STORAGE_KEY) - if (stored === null) { - // No stored value, use prop/default - setUserDonationRate(donationRate) - setDonationEnabled(donationRate > 0) - setPreviousDonationRate(donationRate > 0 ? donationRate : previousDonationRate) - } - } else { - // localStorage not available, use prop - setUserDonationRate(donationRate) - setDonationEnabled(donationRate > 0) - setPreviousDonationRate(donationRate > 0 ? donationRate : previousDonationRate) - } - }, [donationRate]) + // Don't sync with prop - we default to off (0) if no localStorage value + // This ensures user preference (stored in localStorage) always takes precedence const handleDonationToggle = () => { if (donationEnabled) { @@ -1064,97 +1067,94 @@ export const Widget: React.FunctionComponent = props => { ) : null} - {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 ? ( - - - - - - - - { - const value = parseFloat(e.target.value) || 0 - handleDonationRateChange(value) - }} - inputProps={{ - min: 0, - max: 100, - step: 0.1, - style: { - fontSize: '0.75rem', - padding: '4px 8px', - } - }} - size="small" - disabled={success} - placeholder="0" - sx={{ - width: '50px', - opacity: donationEnabled ? 1 : 0.6, - '& .MuiOutlinedInput-root': { - height: '26px', - fontSize: '0.75rem', - '& input': { - padding: '4px 8px', - fontSize: '0.75rem', - textAlign: 'left', - color: '#5c5c5c', - }, - '& fieldset': { - borderWidth: '1px', - }, - }, - }} - /> - - % - - - - ) : null} - - Powered by PayButton.org + Powered by PayButton.org |{' '} + {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 ? ( + + + + + + + + {donationEnabled ? ( + <> + { + const value = parseFloat(e.target.value) || 0 + handleDonationRateChange(value) + }} + inputProps={{ + min: 0, + max: 100, + step: 1, + style: { + fontSize: '0.75rem', + padding: '4px 8px', + } + }} + size="small" + disabled={success} + placeholder="0" + sx={{ + width: '50px', + '& .MuiOutlinedInput-root': { + height: '26px', + fontSize: '0.75rem', + '& input': { + padding: '4px 8px', + fontSize: '0.75rem', + textAlign: 'left', + color: '#5c5c5c', + }, + '& fieldset': { + borderWidth: '1px', + }, + }, + }} + /> + + % + + + ) : null} + + + ) : null} From 16e67c151181fe6e1d7987c57504e64aeb4f9a0e Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:12:54 -0600 Subject: [PATCH 07/41] style edits --- react/lib/components/Widget/Widget.tsx | 31 +++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 3a439328..22a79a68 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -376,6 +376,10 @@ export const Widget: React.FunctionComponent = props => { animationDelay: '0.7s', opacity: 0, }, + footerSeparator: { + marginLeft: '7px', + marginRight: '4px' + }, sideShiftLink: { fontSize: '14px', cursor: 'pointer', @@ -1069,10 +1073,11 @@ export const Widget: React.FunctionComponent = props => { - Powered by PayButton.org |{' '} + Powered by PayButton.org + | {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 ? ( - + = props => { = props => { min: 0, max: 100, step: 1, - style: { - fontSize: '0.75rem', - padding: '4px 8px', - } }} size="small" disabled={success} placeholder="0" sx={{ - width: '50px', + width: '30px', '& .MuiOutlinedInput-root': { - height: '26px', - fontSize: '0.75rem', + height: '18px', '& input': { - padding: '4px 8px', - fontSize: '0.75rem', + padding: '0px 2px 0px 4px', + fontSize: '0.6rem', textAlign: 'left', color: '#5c5c5c', + lineHeight: '1.5em', }, '& fieldset': { borderWidth: '1px', @@ -1142,10 +1143,10 @@ export const Widget: React.FunctionComponent = props => { % From 62e53446816b7e2b66ff23e85083c218dfda8ec0 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:18:13 -0600 Subject: [PATCH 08/41] fit input better, set limit to 99 --- react/lib/components/Widget/Widget.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 22a79a68..3cc8c51a 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -1117,14 +1117,14 @@ export const Widget: React.FunctionComponent = props => { }} inputProps={{ min: 0, - max: 100, + max: 99, step: 1, }} size="small" disabled={success} placeholder="0" sx={{ - width: '30px', + width: '34px', '& .MuiOutlinedInput-root': { height: '18px', '& input': { From e35014061242191f238d6f5bccbcf1421b013ff4 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:45:47 -0600 Subject: [PATCH 09/41] remove looping dependency array value, set check to 99 instead of 100, use prop value properly --- react/lib/components/Widget/Widget.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 3cc8c51a..59f24b6a 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -165,7 +165,7 @@ export const Widget: React.FunctionComponent = props => { setAltpaymentError, isChild, donationAddress = config.donationAddress, - donationRate: _donationRate = DEFAULT_DONATE_RATE // Unused - we default to 0 (off) if no localStorage value + donationRate = DEFAULT_DONATE_RATE, } = props const [loading, setLoading] = useState(true) @@ -254,7 +254,7 @@ export const Widget: React.FunctionComponent = props => { const stored = localStorage.getItem(DONATION_RATE_STORAGE_KEY) if (stored !== null) { const parsed = parseFloat(stored) - if (!isNaN(parsed) && parsed >= 0 && parsed <= 100) { + if (!isNaN(parsed) && parsed >= 0 && parsed <= 99) { return parsed } } @@ -270,7 +270,10 @@ export const Widget: React.FunctionComponent = props => { const initialDonationRate = useMemo(() => getInitialDonationRate(), [getInitialDonationRate]) const [userDonationRate, setUserDonationRate] = useState(initialDonationRate) const [donationEnabled, setDonationEnabled] = useState(initialDonationRate > 0) - const [previousDonationRate, setPreviousDonationRate] = useState(initialDonationRate) + // Initialize previousDonationRate with prop value so it's available when user first enables donation + const [previousDonationRate, setPreviousDonationRate] = useState( + initialDonationRate > 0 ? initialDonationRate : donationRate + ) const price = props.price ?? 0 const [url, setUrl] = useState('') @@ -675,7 +678,7 @@ export const Widget: React.FunctionComponent = props => { } setUrl(nextUrl ?? '') } - }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationAmount, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType]) + }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType]) useEffect(() => { try { @@ -718,8 +721,8 @@ export const Widget: React.FunctionComponent = props => { setUserDonationRate(0) setDonationEnabled(false) } else { - // Turning on - restore previous rate or use default - const rateToRestore = previousDonationRate > 0 ? previousDonationRate : DEFAULT_DONATE_RATE + // Turning on - restore previous rate or use prop/default + const rateToRestore = previousDonationRate > 0 ? previousDonationRate : donationRate setUserDonationRate(rateToRestore) setDonationEnabled(true) } @@ -803,7 +806,7 @@ export const Widget: React.FunctionComponent = props => { let thisUrl = `${prefix}:${to.replace(/^.*:/, '')}`; if (amount) { - if (donationAddress && donationEnabled && userDonationRate && Number(userDonationRate)) { + if (donationAddress && donationEnabled && userDonationRate) { const network = Object.entries(CURRENCY_PREFIXES_MAP).find( ([, value]) => value === prefix )?.[0]; From 4335f33df2c8536afea12ccb94982ca8aa180727 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:24:57 -0600 Subject: [PATCH 10/41] move localstorage key to constants --- react/lib/components/Widget/Widget.tsx | 3 +-- react/lib/util/constants.ts | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 59f24b6a..2c069837 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -15,6 +15,7 @@ import { Theme, ThemeName, ThemeProvider, useTheme } from '../../themes' import { Button, animation } from '../Button/Button' import BarChart from '../BarChart/BarChart' import config from '../../paybutton-config.json' +import { DONATION_RATE_STORAGE_KEY } from '../../util/constants' import { getAddressBalance, Currency, @@ -117,8 +118,6 @@ interface StyleProps { copied: boolean } -const DONATION_RATE_STORAGE_KEY = 'paybutton_donation_rate' - export const Widget: React.FunctionComponent = props => { const { to, diff --git a/react/lib/util/constants.ts b/react/lib/util/constants.ts index c5e4e5da..861affe5 100644 --- a/react/lib/util/constants.ts +++ b/react/lib/util/constants.ts @@ -21,3 +21,5 @@ export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | "extrasmall" | "smal export const DEFAULT_DONATE_RATE = 2; export const DEFAULT_MINIMUM_DONATE_AMOUNT = 10; + +export const DONATION_RATE_STORAGE_KEY = 'paybutton_donation_rate' From 348a586476ed46c3ef99c5fd1dac9eaa5af63953 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:27:19 -0600 Subject: [PATCH 11/41] remove redundant comments --- react/lib/components/Widget/Widget.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 2c069837..9b84dd5f 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -246,7 +246,7 @@ export const Widget: React.FunctionComponent = props => { const [goalPercent, setGoalPercent] = useState(0) const [altpaymentEditable, setAltpaymentEditable] = useState(false) - // Load donation rate from localStorage on mount, defaulting to 0 (off) if not set + // Load donation rate from localStorage on mount const getInitialDonationRate = useCallback(() => { if (typeof window !== 'undefined' && window.localStorage) { try { @@ -258,11 +258,9 @@ export const Widget: React.FunctionComponent = props => { } } } catch (e) { - // If localStorage is unavailable or parsing fails, default to off console.warn('Failed to load donation rate from localStorage:', e) } } - // Default to 0 (donation off) if no localStorage value return 0 }, []) From 2b796367caa90b61e9cf0ec4073b574e8f042dc3 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:38:26 -0600 Subject: [PATCH 12/41] fix payment url amounts --- react/lib/components/Widget/Widget.tsx | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 9b84dd5f..958eb007 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -289,7 +289,6 @@ export const Widget: React.FunctionComponent = props => { const [thisCurrencyObject, setThisCurrencyObject] = useState(props.currencyObject) const blurCSS = isPropsTrue(disabled) ? { filter: 'blur(5px)' } : {} - const [donationAmount, setDonationAmount] = useState(null) // inject keyframes once (replacement for @global in makeStyles) useEffect(() => { const id = 'paybutton-widget-keyframes' @@ -631,10 +630,6 @@ export const Widget: React.FunctionComponent = props => { randomSatoshis, ) convertedAmountToDisplay = convertedAmountWithDonationObj.string - setDonationAmount(convertedDonationAmount) - } else if (!donationEnabled || !userDonationRate || userDonationRate === 0) { - // Reset donation amount when disabled - setDonationAmount(null) } setText( `Send ${amountToDisplay} ${thisCurrencyObject.currency} = ${convertedAmountToDisplay} ${thisAddressType}`, @@ -648,7 +643,7 @@ export const Widget: React.FunctionComponent = props => { if (!isFiat(currency) && thisCurrencyObject && notZeroValue) { const cur: string = thisCurrencyObject.currency let amountToDisplay = thisCurrencyObject.string - let amountToSend = thisCurrencyObject.float + const baseAmount = thisCurrencyObject.float // Base amount without donation // Add donation amount if enabled if (donationEnabled && userDonationRate && userDonationRate > 0 && cur === 'XEC') { @@ -660,18 +655,14 @@ export const Widget: React.FunctionComponent = props => { false, ) amountToDisplay = amountWithDonationObj.string - amountToSend = amountWithDonationObj.float - setDonationAmount(donationAmountValue) - } else { - setDonationAmount(null) } setText(`Send ${amountToDisplay} ${cur}`) - nextUrl = resolveUrl(cur, amountToSend) + // Pass base amount (without donation) to resolveUrl + nextUrl = resolveUrl(cur, baseAmount) } else { setText(`Send any amount of ${thisAddressType}`) nextUrl = resolveUrl(thisAddressType) - setDonationAmount(null) } setUrl(nextUrl ?? '') } @@ -803,13 +794,14 @@ export const Widget: React.FunctionComponent = props => { let thisUrl = `${prefix}:${to.replace(/^.*:/, '')}`; if (amount) { - if (donationAddress && donationEnabled && userDonationRate) { + if (donationAddress && donationEnabled && userDonationRate && userDonationRate > 0) { const network = Object.entries(CURRENCY_PREFIXES_MAP).find( ([, value]) => value === prefix )?.[0]; const decimals = network ? DECIMALS[network.toUpperCase()] : undefined; const donationPercent = userDonationRate / 100 - const thisDonationAmount = donationAmount ? donationAmount : amount * donationPercent + // Calculate donation amount from base amount + const thisDonationAmount = amount * donationPercent thisUrl+=`?amount=${amount}` if(thisDonationAmount > DEFAULT_MINIMUM_DONATE_AMOUNT){ @@ -827,7 +819,7 @@ export const Widget: React.FunctionComponent = props => { return thisUrl; }, - [disabled, to, opReturn, userDonationRate, donationAddress, donationAmount, donationEnabled] + [disabled, to, opReturn, userDonationRate, donationAddress, donationEnabled] ) const handleAmountChange = (e: React.ChangeEvent) => { From 187e1666880049340b1bbb4923839f8d986d441a Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:38:52 -0600 Subject: [PATCH 13/41] remove change to demo file --- paybutton/dev/demo/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paybutton/dev/demo/index.html b/paybutton/dev/demo/index.html index 1b9a5798..3ee3f628 100644 --- a/paybutton/dev/demo/index.html +++ b/paybutton/dev/demo/index.html @@ -27,7 +27,7 @@
-
Date: Tue, 18 Nov 2025 12:52:49 -0600 Subject: [PATCH 14/41] add check for minimum amount to hide donation ui --- react/lib/components/Widget/Widget.tsx | 151 +++++++++++++------------ 1 file changed, 77 insertions(+), 74 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 958eb007..fad8691a 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -1066,87 +1066,90 @@ export const Widget: React.FunctionComponent = props => { Powered by PayButton.org - | - {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 ? ( - - - - 0 && thisCurrencyObject.float >= 1000 ? ( + <> + | + + + - - - - {donationEnabled ? ( - <> - { - const value = parseFloat(e.target.value) || 0 - handleDonationRateChange(value) - }} - inputProps={{ - min: 0, - max: 99, - step: 1, - }} - size="small" - disabled={success} - placeholder="0" + - - % - - - ) : null} - - + + + + {donationEnabled ? ( + <> + { + const value = parseFloat(e.target.value) || 0 + handleDonationRateChange(value) + }} + inputProps={{ + min: 0, + max: 99, + step: 1, + }} + size="small" + disabled={success} + placeholder="0" + sx={{ + width: '34px', + '& .MuiOutlinedInput-root': { + height: '18px', + '& input': { + padding: '0px 2px 0px 4px', + fontSize: '0.6rem', + textAlign: 'left', + color: '#5c5c5c', + lineHeight: '1.5em', + }, + '& fieldset': { + borderWidth: '1px', + }, + }, + }} + /> + + % + + + ) : null} + + + ) : null} From 6b9d3548d1eb0ce8eb8185c7d4c4b56dae5b4a91 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:58:32 -0600 Subject: [PATCH 15/41] use the DEFAULT_MINIMUM_DONATION_AMOUNT const --- react/lib/components/Widget/Widget.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 1f54d736..ae37c0f1 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -165,7 +165,7 @@ export const Widget: React.FunctionComponent = props => { setAltpaymentError, isChild, donationAddress = config.donationAddress, - donationRate = DEFAULT_DONATE_RATE, + donationRate = DEFAULT_DONATION_RATE, } = props const [loading, setLoading] = useState(true) @@ -803,6 +803,7 @@ export const Widget: React.FunctionComponent = props => { const donationPercent = userDonationRate / 100 // Calculate donation amount from base amount const thisDonationAmount = amount * donationPercent + const minimumDonationAmount = network ? DEFAULT_MINIMUM_DONATION_AMOUNT[network.toUpperCase()] : 10 thisUrl+=`?amount=${amount}` if(thisDonationAmount > minimumDonationAmount){ @@ -1068,7 +1069,7 @@ export const Widget: React.FunctionComponent = props => { Powered by PayButton.org - {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 && thisCurrencyObject.float >= 1000 ? ( + {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 && thisCurrencyObject.float >= DEFAULT_MINIMUM_DONATION_AMOUNT[thisAddressType.toUpperCase()] * 100 ? ( <> | From d429fc8c0583c52f0512a5e1da8ec530775b8115 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 09:58:11 -0600 Subject: [PATCH 16/41] add logic for user editable dev donation --- react/lib/components/Widget/Widget.tsx | 116 +++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 7 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index e77194af..78d373de 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -4,6 +4,7 @@ import { Fade, Typography, TextField, + IconButton, } from '@mui/material' import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react' import copyToClipboard from 'copy-to-clipboard' @@ -243,6 +244,9 @@ export const Widget: React.FunctionComponent = props => { const [goalText, setGoalText] = useState('') const [goalPercent, setGoalPercent] = useState(0) const [altpaymentEditable, setAltpaymentEditable] = useState(false) + const [userDonationRate, setUserDonationRate] = useState(donationRate) + const [donationEnabled, setDonationEnabled] = useState(donationRate > 0) + const [previousDonationRate, setPreviousDonationRate] = useState(donationRate) const price = props.price ?? 0 const [url, setUrl] = useState('') @@ -398,6 +402,12 @@ export const Widget: React.FunctionComponent = props => { animation: 'button-slide 0.6s ease-in-out forwards', animationDelay: '0.4s', }, + donationRateContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + gap: '0.5rem', + }, } }, [success, loading, theme, recentlyCopied, copied]) @@ -574,8 +584,8 @@ export const Widget: React.FunctionComponent = props => { if (convertedObj) { let amountToDisplay = thisCurrencyObject.string; let convertedAmountToDisplay = convertedObj.string - if ( donationRate && donationRate >= DONATION_RATE_FIAT_THRESHOLD){ - const thisDonationAmount = thisCurrencyObject.float * (donationRate / 100) + if ( donationEnabled && userDonationRate >= DONATION_RATE_FIAT_THRESHOLD){ + const thisDonationAmount = thisCurrencyObject.float * (userDonationRate / 100) const amountWithDonation = thisCurrencyObject.float + thisDonationAmount const amountWithDonationObj = getCurrencyObject( amountWithDonation, @@ -584,7 +594,7 @@ export const Widget: React.FunctionComponent = props => { ) amountToDisplay = amountWithDonationObj.string - const convertedDonationAmount = convertedObj.float * (donationRate / 100) + const convertedDonationAmount = convertedObj.float * (userDonationRate / 100) const convertedAmountWithDonation = convertedObj.float + convertedDonationAmount const convertedAmountWithDonationObj = getCurrencyObject( convertedAmountWithDonation, @@ -593,6 +603,9 @@ export const Widget: React.FunctionComponent = props => { ) convertedAmountToDisplay = convertedAmountWithDonationObj.string setDonationAmount(convertedAmountWithDonationObj.float) + } else if (!donationEnabled || !userDonationRate || userDonationRate === 0) { + // Reset donation amount when disabled + setDonationAmount(null) } setText( `Send ${amountToDisplay} ${thisCurrencyObject.currency} = ${convertedAmountToDisplay} ${thisAddressType}`, @@ -613,7 +626,7 @@ export const Widget: React.FunctionComponent = props => { } setUrl(nextUrl ?? '') } - }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable]) + }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationAmount, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType]) useEffect(() => { try { @@ -635,6 +648,41 @@ export const Widget: React.FunctionComponent = props => { setThisAmount(props.amount) }, [props.amount]) + useEffect(() => { + setUserDonationRate(donationRate) + setDonationEnabled(donationRate > 0) + setPreviousDonationRate(donationRate > 0 ? donationRate : previousDonationRate) + }, [donationRate]) + + const handleDonationToggle = () => { + if (donationEnabled) { + // Turning off - save current rate and set to 0 + setPreviousDonationRate(userDonationRate) + setUserDonationRate(0) + setDonationEnabled(false) + } else { + // Turning on - restore previous rate or use default + const rateToRestore = previousDonationRate > 0 ? previousDonationRate : DEFAULT_DONATION_RATE + setUserDonationRate(rateToRestore) + setDonationEnabled(true) + } + } + + const handleDonationRateChange = (value: number) => { + const clampedValue = Math.max(0, Math.min(100, value)) + setUserDonationRate(clampedValue) + if (clampedValue > 0) { + // Auto-enable donation if user enters a value > 0 + if (!donationEnabled) { + setDonationEnabled(true) + } + setPreviousDonationRate(clampedValue) + } else if (clampedValue === 0) { + // Auto-disable donation if user enters 0 + setDonationEnabled(false) + } + } + let cleanGoalAmount: any if (goalAmount) { cleanGoalAmount = +goalAmount @@ -698,12 +746,12 @@ export const Widget: React.FunctionComponent = props => { let thisUrl = `${prefix}:${to.replace(/^.*:/, '')}`; if (amount) { - if (donationAddress && donationRate && Number(donationRate)) { + if (donationAddress && donationEnabled && userDonationRate && Number(userDonationRate)) { const network = Object.entries(CURRENCY_PREFIXES_MAP).find( ([, value]) => value === prefix )?.[0]; const decimals = network ? DECIMALS[network.toUpperCase()] : undefined; - const donationPercent = donationRate / 100 + const donationPercent = userDonationRate / 100 const thisDonationAmount = donationAmount ? donationAmount : amount * donationPercent const minimumDonationAmount = network ? DEFAULT_MINIMUM_DONATION_AMOUNT[network.toUpperCase()] : 0; thisUrl += `?amount=${amount}` @@ -722,7 +770,7 @@ export const Widget: React.FunctionComponent = props => { return thisUrl; }, - [disabled, to, opReturn] + [disabled, to, opReturn, userDonationRate, donationAddress, donationAmount, donationEnabled] ) const handleAmountChange = (e: React.ChangeEvent) => { @@ -966,6 +1014,60 @@ export const Widget: React.FunctionComponent = props => { ) : null} + + { + const value = parseFloat(e.target.value) || 0 + handleDonationRateChange(value) + }} + inputProps={{ + min: 0, + max: 100, + step: 0.1 + }} + size="small" + fullWidth + disabled={success} + placeholder="Donation %" + sx={{ + opacity: donationEnabled ? 1 : 0.6, + }} + /> + + + + + + + Powered by PayButton.org From dd13f54613a230789bd1a11143fd534b075a4954 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:31:36 -0600 Subject: [PATCH 17/41] style edits --- paybutton/dev/demo/index.html | 2 +- react/lib/components/Widget/Widget.tsx | 123 +++++++++++++++---------- 2 files changed, 77 insertions(+), 48 deletions(-) diff --git a/paybutton/dev/demo/index.html b/paybutton/dev/demo/index.html index 3ee3f628..1b9a5798 100644 --- a/paybutton/dev/demo/index.html +++ b/paybutton/dev/demo/index.html @@ -27,7 +27,7 @@
-
= props => { animationDelay: '0.4s', }, donationRateContainer: { + display: 'flex', + flexDirection: 'column', + gap: '0.5rem', + }, + donationRateLabel: { + fontSize: '0.75rem', + color: '#000000', + opacity: 0.7, + marginBottom: '10px', + marginTop: '20px', + textAlign: 'center' + }, + donationRateInputRow: { display: 'flex', alignItems: 'center', - justifyContent: 'flex-end', gap: '0.5rem', }, } @@ -1015,57 +1027,74 @@ export const Widget: React.FunctionComponent = props => { ) : null} - { - const value = parseFloat(e.target.value) || 0 - handleDonationRateChange(value) - }} - inputProps={{ - min: 0, - max: 100, - step: 0.1 - }} - size="small" - fullWidth - disabled={success} - placeholder="Donation %" - sx={{ - opacity: donationEnabled ? 1 : 0.6, - }} - /> - - + Send a dev donation? + + + - - - + + + + + { + const value = parseFloat(e.target.value) || 0 + handleDonationRateChange(value) + }} + inputProps={{ + min: 0, + max: 100, + step: 0.1 + }} + size="small" + fullWidth + disabled={success} + placeholder="0" + sx={{ + opacity: donationEnabled ? 1 : 0.6, + }} + /> + + % + + From ee446c1cffae9315e3606255d4d3efdc731aad08 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:26:42 -0600 Subject: [PATCH 18/41] styles, animation, tooltip --- react/lib/components/Widget/Widget.tsx | 62 +++++++++++++++----------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 9bdeb44d..9166a14e 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -5,6 +5,7 @@ import { Typography, TextField, IconButton, + Tooltip, } from '@mui/material' import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react' import copyToClipboard from 'copy-to-clipboard' @@ -277,6 +278,7 @@ export const Widget: React.FunctionComponent = props => { @keyframes fade-scale { from { opacity: 0; transform: scale(0.3); } 80% { opacity: 1; transform: scale(1.3); } to { opacity: 1; transform: scale(1); } } @keyframes button-slide { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0px); } } @keyframes button-slide-out { from { opacity: 1; transform: translateY(0px); } to { opacity: 0; transform: translateY(20px); } } +@keyframes fade-slide-up { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0px); } } @keyframes copy-qr { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } } @keyframes copy-svg { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } } @keyframes copy-icon { 0% { transform: scale(1); } 50% { transform: scale(0.7); } 100% { transform: scale(1); } } @@ -402,23 +404,14 @@ export const Widget: React.FunctionComponent = props => { animation: 'button-slide 0.6s ease-in-out forwards', animationDelay: '0.4s', }, - donationRateContainer: { - display: 'flex', - flexDirection: 'column', - gap: '0.5rem', - }, - donationRateLabel: { - fontSize: '0.75rem', - color: '#000000', - opacity: 0.7, - marginBottom: '10px', - marginTop: '20px', - textAlign: 'center' - }, donationRateInputRow: { display: 'flex', alignItems: 'center', - gap: '0.5rem', + justifyContent: 'center', + marginTop: '15px', + animation: 'fade-slide-up 0.6s ease-out forwards', + animationDelay: '0.7s', + opacity: 0, }, } }, [success, loading, theme, recentlyCopied, copied]) @@ -1026,17 +1019,15 @@ export const Widget: React.FunctionComponent = props => { ) : null} - - - Send a dev donation? - - + {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 ? ( + + = props => { = props => { inputProps={{ min: 0, max: 100, - step: 0.1 + step: 0.1, + style: { + fontSize: '0.75rem', + padding: '4px 8px', + } }} size="small" - fullWidth disabled={success} placeholder="0" sx={{ + width: '50px', opacity: donationEnabled ? 1 : 0.6, + '& .MuiOutlinedInput-root': { + height: '26px', + fontSize: '0.75rem', + '& input': { + padding: '4px 8px', + fontSize: '0.75rem', + textAlign: 'left', + color: '#5c5c5c', + }, + '& fieldset': { + borderWidth: '1px', + }, + }, }} /> % - + + ) : null} From 542d610ef48876279c0a0b056a7d3a45ad293faf Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:33:28 -0600 Subject: [PATCH 19/41] add localstorage persistence --- react/lib/components/Widget/Widget.tsx | 58 +++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 9166a14e..1ccee47a 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -118,6 +118,8 @@ interface StyleProps { copied: boolean } +const DONATION_RATE_STORAGE_KEY = 'paybutton_donation_rate' + export const Widget: React.FunctionComponent = props => { const { to, @@ -245,9 +247,30 @@ export const Widget: React.FunctionComponent = props => { const [goalText, setGoalText] = useState('') const [goalPercent, setGoalPercent] = useState(0) const [altpaymentEditable, setAltpaymentEditable] = useState(false) - const [userDonationRate, setUserDonationRate] = useState(donationRate) - const [donationEnabled, setDonationEnabled] = useState(donationRate > 0) - const [previousDonationRate, setPreviousDonationRate] = useState(donationRate) + + // Load donation rate from localStorage on mount, falling back to prop/default + const getInitialDonationRate = useCallback(() => { + if (typeof window !== 'undefined' && window.localStorage) { + try { + const stored = localStorage.getItem(DONATION_RATE_STORAGE_KEY) + if (stored !== null) { + const parsed = parseFloat(stored) + if (!isNaN(parsed) && parsed >= 0 && parsed <= 100) { + return parsed + } + } + } catch (e) { + // If localStorage is unavailable or parsing fails, fall back to prop/default + console.warn('Failed to load donation rate from localStorage:', e) + } + } + return donationRate + }, [donationRate]) + + const initialDonationRate = useMemo(() => getInitialDonationRate(), [getInitialDonationRate]) + const [userDonationRate, setUserDonationRate] = useState(initialDonationRate) + const [donationEnabled, setDonationEnabled] = useState(initialDonationRate > 0) + const [previousDonationRate, setPreviousDonationRate] = useState(initialDonationRate) const price = props.price ?? 0 const [url, setUrl] = useState('') @@ -653,10 +676,33 @@ export const Widget: React.FunctionComponent = props => { setThisAmount(props.amount) }, [props.amount]) + // Save donation rate to localStorage whenever it changes useEffect(() => { - setUserDonationRate(donationRate) - setDonationEnabled(donationRate > 0) - setPreviousDonationRate(donationRate > 0 ? donationRate : previousDonationRate) + if (typeof window !== 'undefined' && window.localStorage) { + try { + localStorage.setItem(DONATION_RATE_STORAGE_KEY, userDonationRate.toString()) + } catch (e) { + console.warn('Failed to save donation rate to localStorage:', e) + } + } + }, [userDonationRate]) + + // Only sync with prop if there's no localStorage value (on first mount) + useEffect(() => { + if (typeof window !== 'undefined' && window.localStorage) { + const stored = localStorage.getItem(DONATION_RATE_STORAGE_KEY) + if (stored === null) { + // No stored value, use prop/default + setUserDonationRate(donationRate) + setDonationEnabled(donationRate > 0) + setPreviousDonationRate(donationRate > 0 ? donationRate : previousDonationRate) + } + } else { + // localStorage not available, use prop + setUserDonationRate(donationRate) + setDonationEnabled(donationRate > 0) + setPreviousDonationRate(donationRate > 0 ? donationRate : previousDonationRate) + } }, [donationRate]) const handleDonationToggle = () => { From cc49c67b7c80934a93eccc700fd0b371b3773a21 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:42:49 -0600 Subject: [PATCH 20/41] remove edit to demo index --- paybutton/dev/demo/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paybutton/dev/demo/index.html b/paybutton/dev/demo/index.html index 1b9a5798..3ee3f628 100644 --- a/paybutton/dev/demo/index.html +++ b/paybutton/dev/demo/index.html @@ -27,7 +27,7 @@
-
Date: Fri, 14 Nov 2025 15:40:50 -0600 Subject: [PATCH 21/41] edit functionality --- paybutton/dev/demo/index.html | 2 +- react/lib/components/Widget/Widget.tsx | 244 ++++++++++++------------- 2 files changed, 123 insertions(+), 123 deletions(-) diff --git a/paybutton/dev/demo/index.html b/paybutton/dev/demo/index.html index 3ee3f628..1b9a5798 100644 --- a/paybutton/dev/demo/index.html +++ b/paybutton/dev/demo/index.html @@ -27,7 +27,7 @@
-
= props => { const [goalPercent, setGoalPercent] = useState(0) const [altpaymentEditable, setAltpaymentEditable] = useState(false) - // Load donation rate from localStorage on mount, falling back to prop/default + // Load donation rate from localStorage on mount, defaulting to 0 (off) if not set const getInitialDonationRate = useCallback(() => { if (typeof window !== 'undefined' && window.localStorage) { try { @@ -260,12 +260,13 @@ export const Widget: React.FunctionComponent = props => { } } } catch (e) { - // If localStorage is unavailable or parsing fails, fall back to prop/default + // If localStorage is unavailable or parsing fails, default to off console.warn('Failed to load donation rate from localStorage:', e) } } - return donationRate - }, [donationRate]) + // Default to 0 (donation off) if no localStorage value + return 0 + }, []) const initialDonationRate = useMemo(() => getInitialDonationRate(), [getInitialDonationRate]) const [userDonationRate, setUserDonationRate] = useState(initialDonationRate) @@ -369,6 +370,12 @@ export const Widget: React.FunctionComponent = props => { color: '#a8a8a8', fontWeight: 'normal', userSelect: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + animation: 'fade-slide-up 0.6s ease-out forwards', + animationDelay: '0.7s', + opacity: 0, }, sideShiftLink: { fontSize: '14px', @@ -427,15 +434,6 @@ export const Widget: React.FunctionComponent = props => { animation: 'button-slide 0.6s ease-in-out forwards', animationDelay: '0.4s', }, - donationRateInputRow: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - marginTop: '15px', - animation: 'fade-slide-up 0.6s ease-out forwards', - animationDelay: '0.7s', - opacity: 0, - }, } }, [success, loading, theme, recentlyCopied, copied]) @@ -630,7 +628,7 @@ export const Widget: React.FunctionComponent = props => { randomSatoshis, ) convertedAmountToDisplay = convertedAmountWithDonationObj.string - setDonationAmount(convertedAmountWithDonationObj.float) + setDonationAmount(convertedDonationAmount) } else if (!donationEnabled || !userDonationRate || userDonationRate === 0) { // Reset donation amount when disabled setDonationAmount(null) @@ -646,11 +644,31 @@ export const Widget: React.FunctionComponent = props => { thisCurrencyObject?.float !== undefined && thisCurrencyObject.float > 0 if (!isFiat(currency) && thisCurrencyObject && notZeroValue) { const cur: string = thisCurrencyObject.currency - setText(`Send ${thisCurrencyObject.string} ${cur}`) - nextUrl = resolveUrl(cur, thisCurrencyObject?.float) + let amountToDisplay = thisCurrencyObject.string + let amountToSend = thisCurrencyObject.float + + // Add donation amount if enabled + if (donationEnabled && userDonationRate && userDonationRate > 0 && cur === 'XEC') { + const donationAmountValue = thisCurrencyObject.float * (userDonationRate / 100) + const amountWithDonation = thisCurrencyObject.float + donationAmountValue + const amountWithDonationObj = getCurrencyObject( + amountWithDonation, + cur, + false, + ) + amountToDisplay = amountWithDonationObj.string + amountToSend = amountWithDonationObj.float + setDonationAmount(donationAmountValue) + } else { + setDonationAmount(null) + } + + setText(`Send ${amountToDisplay} ${cur}`) + nextUrl = resolveUrl(cur, amountToSend) } else { setText(`Send any amount of ${thisAddressType}`) nextUrl = resolveUrl(thisAddressType) + setDonationAmount(null) } setUrl(nextUrl ?? '') } @@ -687,23 +705,8 @@ export const Widget: React.FunctionComponent = props => { } }, [userDonationRate]) - // Only sync with prop if there's no localStorage value (on first mount) - useEffect(() => { - if (typeof window !== 'undefined' && window.localStorage) { - const stored = localStorage.getItem(DONATION_RATE_STORAGE_KEY) - if (stored === null) { - // No stored value, use prop/default - setUserDonationRate(donationRate) - setDonationEnabled(donationRate > 0) - setPreviousDonationRate(donationRate > 0 ? donationRate : previousDonationRate) - } - } else { - // localStorage not available, use prop - setUserDonationRate(donationRate) - setDonationEnabled(donationRate > 0) - setPreviousDonationRate(donationRate > 0 ? donationRate : previousDonationRate) - } - }, [donationRate]) + // Don't sync with prop - we default to off (0) if no localStorage value + // This ensures user preference (stored in localStorage) always takes precedence const handleDonationToggle = () => { if (donationEnabled) { @@ -1065,97 +1068,94 @@ export const Widget: React.FunctionComponent = props => { ) : null} - {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 ? ( - - - - - - - - { - const value = parseFloat(e.target.value) || 0 - handleDonationRateChange(value) - }} - inputProps={{ - min: 0, - max: 100, - step: 0.1, - style: { - fontSize: '0.75rem', - padding: '4px 8px', - } - }} - size="small" - disabled={success} - placeholder="0" - sx={{ - width: '50px', - opacity: donationEnabled ? 1 : 0.6, - '& .MuiOutlinedInput-root': { - height: '26px', - fontSize: '0.75rem', - '& input': { - padding: '4px 8px', - fontSize: '0.75rem', - textAlign: 'left', - color: '#5c5c5c', - }, - '& fieldset': { - borderWidth: '1px', - }, - }, - }} - /> - - % - - - - ) : null} - - Powered by PayButton.org + Powered by PayButton.org |{' '} + {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 ? ( + + + + + + + + {donationEnabled ? ( + <> + { + const value = parseFloat(e.target.value) || 0 + handleDonationRateChange(value) + }} + inputProps={{ + min: 0, + max: 100, + step: 1, + style: { + fontSize: '0.75rem', + padding: '4px 8px', + } + }} + size="small" + disabled={success} + placeholder="0" + sx={{ + width: '50px', + '& .MuiOutlinedInput-root': { + height: '26px', + fontSize: '0.75rem', + '& input': { + padding: '4px 8px', + fontSize: '0.75rem', + textAlign: 'left', + color: '#5c5c5c', + }, + '& fieldset': { + borderWidth: '1px', + }, + }, + }} + /> + + % + + + ) : null} + + + ) : null} From 11bde6e3357a6681658eae09d60713ef6929f717 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:12:54 -0600 Subject: [PATCH 22/41] style edits --- react/lib/components/Widget/Widget.tsx | 31 +++++++++++++------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 8ed187ba..f2e896cd 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -377,6 +377,10 @@ export const Widget: React.FunctionComponent = props => { animationDelay: '0.7s', opacity: 0, }, + footerSeparator: { + marginLeft: '7px', + marginRight: '4px' + }, sideShiftLink: { fontSize: '14px', cursor: 'pointer', @@ -1070,10 +1074,11 @@ export const Widget: React.FunctionComponent = props => { - Powered by PayButton.org |{' '} + Powered by PayButton.org + | {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 ? ( - + = props => { = props => { min: 0, max: 100, step: 1, - style: { - fontSize: '0.75rem', - padding: '4px 8px', - } }} size="small" disabled={success} placeholder="0" sx={{ - width: '50px', + width: '30px', '& .MuiOutlinedInput-root': { - height: '26px', - fontSize: '0.75rem', + height: '18px', '& input': { - padding: '4px 8px', - fontSize: '0.75rem', + padding: '0px 2px 0px 4px', + fontSize: '0.6rem', textAlign: 'left', color: '#5c5c5c', + lineHeight: '1.5em', }, '& fieldset': { borderWidth: '1px', @@ -1143,10 +1144,10 @@ export const Widget: React.FunctionComponent = props => { % From 1e05a3a3692bca844b7a28c9aeafe7cc44818f2a Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:18:13 -0600 Subject: [PATCH 23/41] fit input better, set limit to 99 --- react/lib/components/Widget/Widget.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index f2e896cd..33cd458c 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -1118,14 +1118,14 @@ export const Widget: React.FunctionComponent = props => { }} inputProps={{ min: 0, - max: 100, + max: 99, step: 1, }} size="small" disabled={success} placeholder="0" sx={{ - width: '30px', + width: '34px', '& .MuiOutlinedInput-root': { height: '18px', '& input': { From 5a770e412d2b1a07d551a4097110e01ab0905c1f Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:45:47 -0600 Subject: [PATCH 24/41] remove looping dependency array value, set check to 99 instead of 100, use prop value properly --- react/lib/components/Widget/Widget.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 33cd458c..f8f16172 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -255,7 +255,7 @@ export const Widget: React.FunctionComponent = props => { const stored = localStorage.getItem(DONATION_RATE_STORAGE_KEY) if (stored !== null) { const parsed = parseFloat(stored) - if (!isNaN(parsed) && parsed >= 0 && parsed <= 100) { + if (!isNaN(parsed) && parsed >= 0 && parsed <= 99) { return parsed } } @@ -271,7 +271,10 @@ export const Widget: React.FunctionComponent = props => { const initialDonationRate = useMemo(() => getInitialDonationRate(), [getInitialDonationRate]) const [userDonationRate, setUserDonationRate] = useState(initialDonationRate) const [donationEnabled, setDonationEnabled] = useState(initialDonationRate > 0) - const [previousDonationRate, setPreviousDonationRate] = useState(initialDonationRate) + // Initialize previousDonationRate with prop value so it's available when user first enables donation + const [previousDonationRate, setPreviousDonationRate] = useState( + initialDonationRate > 0 ? initialDonationRate : donationRate + ) const price = props.price ?? 0 const [url, setUrl] = useState('') @@ -676,7 +679,7 @@ export const Widget: React.FunctionComponent = props => { } setUrl(nextUrl ?? '') } - }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationAmount, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType]) + }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType]) useEffect(() => { try { @@ -719,8 +722,8 @@ export const Widget: React.FunctionComponent = props => { setUserDonationRate(0) setDonationEnabled(false) } else { - // Turning on - restore previous rate or use default - const rateToRestore = previousDonationRate > 0 ? previousDonationRate : DEFAULT_DONATION_RATE + // Turning on - restore previous rate or use prop/default + const rateToRestore = previousDonationRate > 0 ? previousDonationRate : donationRate setUserDonationRate(rateToRestore) setDonationEnabled(true) } @@ -804,7 +807,7 @@ export const Widget: React.FunctionComponent = props => { let thisUrl = `${prefix}:${to.replace(/^.*:/, '')}`; if (amount) { - if (donationAddress && donationEnabled && userDonationRate && Number(userDonationRate)) { + if (donationAddress && donationEnabled && userDonationRate) { const network = Object.entries(CURRENCY_PREFIXES_MAP).find( ([, value]) => value === prefix )?.[0]; From 21ab925b503162425b78e8eb0ccbd065166a5725 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:24:57 -0600 Subject: [PATCH 25/41] move localstorage key to constants --- react/lib/components/Widget/Widget.tsx | 3 +-- react/lib/util/constants.ts | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index f8f16172..8fcd5cf8 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -15,6 +15,7 @@ import { Theme, ThemeName, ThemeProvider, useTheme } from '../../themes' import { Button, animation } from '../Button/Button' import BarChart from '../BarChart/BarChart' import config from '../../paybutton-config.json' +import { DONATION_RATE_STORAGE_KEY } from '../../util/constants' import { getAddressBalance, Currency, @@ -118,8 +119,6 @@ interface StyleProps { copied: boolean } -const DONATION_RATE_STORAGE_KEY = 'paybutton_donation_rate' - export const Widget: React.FunctionComponent = props => { const { to, diff --git a/react/lib/util/constants.ts b/react/lib/util/constants.ts index 448b32c2..789f1d62 100644 --- a/react/lib/util/constants.ts +++ b/react/lib/util/constants.ts @@ -27,3 +27,5 @@ export const DEFAULT_MINIMUM_DONATION_AMOUNT: { [key: string]: number } = { BCH: 0.00001000, XEC: 10, }; + +export const DONATION_RATE_STORAGE_KEY = 'paybutton_donation_rate' From b124ca7db3d36da079ab55eec818df2e207f675b Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:27:19 -0600 Subject: [PATCH 26/41] remove redundant comments --- react/lib/components/Widget/Widget.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 8fcd5cf8..a79c3496 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -247,7 +247,7 @@ export const Widget: React.FunctionComponent = props => { const [goalPercent, setGoalPercent] = useState(0) const [altpaymentEditable, setAltpaymentEditable] = useState(false) - // Load donation rate from localStorage on mount, defaulting to 0 (off) if not set + // Load donation rate from localStorage on mount const getInitialDonationRate = useCallback(() => { if (typeof window !== 'undefined' && window.localStorage) { try { @@ -259,11 +259,9 @@ export const Widget: React.FunctionComponent = props => { } } } catch (e) { - // If localStorage is unavailable or parsing fails, default to off console.warn('Failed to load donation rate from localStorage:', e) } } - // Default to 0 (donation off) if no localStorage value return 0 }, []) From 93aa4509027da2d8e4b199e1f190f16e6d0776f1 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:38:26 -0600 Subject: [PATCH 27/41] fix payment url amounts --- react/lib/components/Widget/Widget.tsx | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index a79c3496..46bb4762 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -290,7 +290,6 @@ export const Widget: React.FunctionComponent = props => { const [thisCurrencyObject, setThisCurrencyObject] = useState(props.currencyObject) const blurCSS = isPropsTrue(disabled) ? { filter: 'blur(5px)' } : {} - const [donationAmount, setDonationAmount] = useState(null) // inject keyframes once (replacement for @global in makeStyles) useEffect(() => { const id = 'paybutton-widget-keyframes' @@ -632,10 +631,6 @@ export const Widget: React.FunctionComponent = props => { randomSatoshis, ) convertedAmountToDisplay = convertedAmountWithDonationObj.string - setDonationAmount(convertedDonationAmount) - } else if (!donationEnabled || !userDonationRate || userDonationRate === 0) { - // Reset donation amount when disabled - setDonationAmount(null) } setText( `Send ${amountToDisplay} ${thisCurrencyObject.currency} = ${convertedAmountToDisplay} ${thisAddressType}`, @@ -649,7 +644,7 @@ export const Widget: React.FunctionComponent = props => { if (!isFiat(currency) && thisCurrencyObject && notZeroValue) { const cur: string = thisCurrencyObject.currency let amountToDisplay = thisCurrencyObject.string - let amountToSend = thisCurrencyObject.float + const baseAmount = thisCurrencyObject.float // Base amount without donation // Add donation amount if enabled if (donationEnabled && userDonationRate && userDonationRate > 0 && cur === 'XEC') { @@ -661,18 +656,14 @@ export const Widget: React.FunctionComponent = props => { false, ) amountToDisplay = amountWithDonationObj.string - amountToSend = amountWithDonationObj.float - setDonationAmount(donationAmountValue) - } else { - setDonationAmount(null) } setText(`Send ${amountToDisplay} ${cur}`) - nextUrl = resolveUrl(cur, amountToSend) + // Pass base amount (without donation) to resolveUrl + nextUrl = resolveUrl(cur, baseAmount) } else { setText(`Send any amount of ${thisAddressType}`) nextUrl = resolveUrl(thisAddressType) - setDonationAmount(null) } setUrl(nextUrl ?? '') } @@ -804,7 +795,7 @@ export const Widget: React.FunctionComponent = props => { let thisUrl = `${prefix}:${to.replace(/^.*:/, '')}`; if (amount) { - if (donationAddress && donationEnabled && userDonationRate) { + if (donationAddress && donationEnabled && userDonationRate && userDonationRate > 0) { const network = Object.entries(CURRENCY_PREFIXES_MAP).find( ([, value]) => value === prefix )?.[0]; @@ -828,7 +819,7 @@ export const Widget: React.FunctionComponent = props => { return thisUrl; }, - [disabled, to, opReturn, userDonationRate, donationAddress, donationAmount, donationEnabled] + [disabled, to, opReturn, userDonationRate, donationAddress, donationEnabled] ) const handleAmountChange = (e: React.ChangeEvent) => { From 19a14e733578038eacd131886766f01e960e10f5 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:38:52 -0600 Subject: [PATCH 28/41] remove change to demo file --- paybutton/dev/demo/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paybutton/dev/demo/index.html b/paybutton/dev/demo/index.html index 1b9a5798..3ee3f628 100644 --- a/paybutton/dev/demo/index.html +++ b/paybutton/dev/demo/index.html @@ -27,7 +27,7 @@
-
Date: Tue, 18 Nov 2025 12:52:49 -0600 Subject: [PATCH 29/41] add check for minimum amount to hide donation ui --- react/lib/components/Widget/Widget.tsx | 151 +++++++++++++------------ 1 file changed, 77 insertions(+), 74 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 46bb4762..ea8fc5fb 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -1066,87 +1066,90 @@ export const Widget: React.FunctionComponent = props => { Powered by PayButton.org - | - {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 ? ( - - - - 0 && thisCurrencyObject.float >= 1000 ? ( + <> + | + + + - - - - {donationEnabled ? ( - <> - { - const value = parseFloat(e.target.value) || 0 - handleDonationRateChange(value) - }} - inputProps={{ - min: 0, - max: 99, - step: 1, - }} - size="small" - disabled={success} - placeholder="0" + - - % - - - ) : null} - - + + + + {donationEnabled ? ( + <> + { + const value = parseFloat(e.target.value) || 0 + handleDonationRateChange(value) + }} + inputProps={{ + min: 0, + max: 99, + step: 1, + }} + size="small" + disabled={success} + placeholder="0" + sx={{ + width: '34px', + '& .MuiOutlinedInput-root': { + height: '18px', + '& input': { + padding: '0px 2px 0px 4px', + fontSize: '0.6rem', + textAlign: 'left', + color: '#5c5c5c', + lineHeight: '1.5em', + }, + '& fieldset': { + borderWidth: '1px', + }, + }, + }} + /> + + % + + + ) : null} + + + ) : null} From 032cf6469c557bebc2ab9f6ad1ac090996f354ca Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:58:32 -0600 Subject: [PATCH 30/41] use the DEFAULT_MINIMUM_DONATION_AMOUNT const --- react/lib/components/Widget/Widget.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index ea8fc5fb..082ba94c 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -165,9 +165,9 @@ export const Widget: React.FunctionComponent = props => { setAltpaymentError, isChild, donationAddress = config.donationAddress, - donationRate = DEFAULT_DONATION_RATE - } = props; - const [loading, setLoading] = useState(true); + donationRate = DEFAULT_DONATION_RATE, + } = props + const [loading, setLoading] = useState(true) // websockets if standalone const [internalTxsSocket, setInternalTxsSocket] = useState(undefined) @@ -646,8 +646,8 @@ export const Widget: React.FunctionComponent = props => { let amountToDisplay = thisCurrencyObject.string const baseAmount = thisCurrencyObject.float // Base amount without donation - // Add donation amount if enabled - if (donationEnabled && userDonationRate && userDonationRate > 0 && cur === 'XEC') { + // Add donation amount if enabled (for XEC and BCH) + if (donationEnabled && userDonationRate && userDonationRate > 0 && (cur === 'XEC' || cur === 'BCH')) { const donationAmountValue = thisCurrencyObject.float * (userDonationRate / 100) const amountWithDonation = thisCurrencyObject.float + donationAmountValue const amountWithDonationObj = getCurrencyObject( @@ -801,8 +801,10 @@ export const Widget: React.FunctionComponent = props => { )?.[0]; const decimals = network ? DECIMALS[network.toUpperCase()] : undefined; const donationPercent = userDonationRate / 100 - const thisDonationAmount = donationAmount ? donationAmount : amount * donationPercent - const minimumDonationAmount = network ? DEFAULT_MINIMUM_DONATION_AMOUNT[network.toUpperCase()] : 0; + // Calculate donation amount from base amount + const thisDonationAmount = amount * donationPercent + const minimumDonationAmount = network ? DEFAULT_MINIMUM_DONATION_AMOUNT[network.toUpperCase()] : 0 + thisUrl += `?amount=${amount}` if(thisDonationAmount > minimumDonationAmount){ thisUrl += `&addr=${donationAddress}&amount=${thisDonationAmount.toFixed(decimals)}`; @@ -1067,7 +1069,7 @@ export const Widget: React.FunctionComponent = props => { Powered by PayButton.org - {thisAddressType === 'XEC' && thisCurrencyObject?.float && thisCurrencyObject.float > 0 && thisCurrencyObject.float >= 1000 ? ( + {((thisAddressType === 'XEC' || thisAddressType === 'BCH') && thisCurrencyObject?.float && thisCurrencyObject.float > 0 && thisCurrencyObject.float >= (DEFAULT_MINIMUM_DONATION_AMOUNT[thisAddressType.toUpperCase()] || 0) * 100) ? ( <> | From 8db10a1bbf2f019bcc264dd18fcc758fd09327d6 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:47:15 -0600 Subject: [PATCH 31/41] fix minimum donation payment url --- react/lib/components/Widget/Widget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 082ba94c..31efadc8 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -806,7 +806,7 @@ export const Widget: React.FunctionComponent = props => { const minimumDonationAmount = network ? DEFAULT_MINIMUM_DONATION_AMOUNT[network.toUpperCase()] : 0 thisUrl += `?amount=${amount}` - if(thisDonationAmount > minimumDonationAmount){ + if(thisDonationAmount >= minimumDonationAmount){ thisUrl += `&addr=${donationAddress}&amount=${thisDonationAmount.toFixed(decimals)}`; } }else{ From 2004c674afff37ed7945b86f61561015ae949ef5 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:42:32 -0600 Subject: [PATCH 32/41] revert change --- react/lib/components/Widget/Widget.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index df39cc29..1ba704a7 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -165,9 +165,9 @@ export const Widget: React.FunctionComponent = props => { setAltpaymentError, isChild, donationAddress = config.donationAddress, - donationRate = DEFAULT_DONATION_RATE, - } = props - const [loading, setLoading] = useState(true) + donationRate = DEFAULT_DONATION_RATE + } = props; + const [loading, setLoading] = useState(true); // websockets if standalone const [internalTxsSocket, setInternalTxsSocket] = useState(undefined) From 64fa88d66dc6fd05ca2baecb789c1b7fc4d87f5e Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:44:08 -0600 Subject: [PATCH 33/41] check for fiat threshold --- react/lib/components/Widget/Widget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 1ba704a7..5b51d851 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -613,7 +613,7 @@ export const Widget: React.FunctionComponent = props => { if (convertedObj) { let amountToDisplay = thisCurrencyObject.string; let convertedAmountToDisplay = convertedObj.string - if ( donationEnabled && userDonationRate && userDonationRate > 0){ + if ( donationEnabled && userDonationRate && userDonationRate >= DONATION_RATE_FIAT_THRESHOLD){ const thisDonationAmount = thisCurrencyObject.float * (userDonationRate / 100) const amountWithDonation = thisCurrencyObject.float + thisDonationAmount const amountWithDonationObj = getCurrencyObject( From 8006a668c1b34264f90b1717909c2ad24a903480 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:47:19 -0600 Subject: [PATCH 34/41] Fix maximum donation rate inconsistency --- react/lib/components/Widget/Widget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 5b51d851..728a701d 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -718,7 +718,7 @@ export const Widget: React.FunctionComponent = props => { } const handleDonationRateChange = (value: number) => { - const clampedValue = Math.max(0, Math.min(100, value)) + const clampedValue = Math.max(0, Math.min(99, value)) setUserDonationRate(clampedValue) if (clampedValue > 0) { // Auto-enable donation if user enters a value > 0 From 2f80331eb97352ca61a849d3f66dc91f13d3c90d Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:50:17 -0600 Subject: [PATCH 35/41] fix const --- react/lib/util/constants.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/react/lib/util/constants.ts b/react/lib/util/constants.ts index 5c7b7c62..03000ddc 100644 --- a/react/lib/util/constants.ts +++ b/react/lib/util/constants.ts @@ -20,8 +20,6 @@ export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | "extrasmall" | "smal export const DEFAULT_DONATION_RATE = 2; -export const DEFAULT_MINIMUM_DONATE_AMOUNT = 10; - export const DONATION_RATE_STORAGE_KEY = 'paybutton_donation_rate' export const DONATION_RATE_FIAT_THRESHOLD = 5; @@ -30,5 +28,3 @@ export const DEFAULT_MINIMUM_DONATION_AMOUNT: { [key: string]: number } = { BCH: 0.00001000, XEC: 10, }; - -export const DONATION_RATE_STORAGE_KEY = 'paybutton_donation_rate' From c2967d80b95ec62f3aae55ba828fc1984f8092ef Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:04:31 -0600 Subject: [PATCH 36/41] fix inconsistency with min fiat rate --- react/lib/components/Widget/Widget.tsx | 49 ++++++++++++++++++++------ 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 728a701d..cf182c22 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -247,7 +247,29 @@ export const Widget: React.FunctionComponent = props => { const [goalPercent, setGoalPercent] = useState(0) const [altpaymentEditable, setAltpaymentEditable] = useState(false) + const price = props.price ?? 0 + const [hasPrice, setHasPrice] = useState(props.price !== undefined && props.price > 0) + + // Determine if we're in a fiat context (fiat currency or fiat conversion) + // This needs to be calculated early for initialization + const isFiatContext = useMemo(() => { + return hasPrice || isFiat(currency) + }, [hasPrice, currency]) + + // Calculate minimum donation rate based on context + // Fiat requires 5% minimum, crypto allows 1% minimum + const minDonationRate = useMemo(() => { + return isFiatContext ? DONATION_RATE_FIAT_THRESHOLD : 1 + }, [isFiatContext]) + + // Calculate default donation rate based on context + // Fiat defaults to 5%, crypto defaults to prop/default (2%) + const defaultDonationRate = useMemo(() => { + return isFiatContext ? DONATION_RATE_FIAT_THRESHOLD : donationRate + }, [isFiatContext, donationRate]) + // Load donation rate from localStorage on mount + // If stored rate is below minimum for current context, disable donations const getInitialDonationRate = useCallback(() => { if (typeof window !== 'undefined' && window.localStorage) { try { @@ -255,6 +277,10 @@ export const Widget: React.FunctionComponent = props => { if (stored !== null) { const parsed = parseFloat(stored) if (!isNaN(parsed) && parsed >= 0 && parsed <= 99) { + // If stored rate is below minimum for current context, return 0 to disable + if (parsed > 0 && parsed < minDonationRate) { + return 0 + } return parsed } } @@ -263,17 +289,15 @@ export const Widget: React.FunctionComponent = props => { } } return 0 - }, []) + }, [minDonationRate]) const initialDonationRate = useMemo(() => getInitialDonationRate(), [getInitialDonationRate]) const [userDonationRate, setUserDonationRate] = useState(initialDonationRate) const [donationEnabled, setDonationEnabled] = useState(initialDonationRate > 0) // Initialize previousDonationRate with prop value so it's available when user first enables donation const [previousDonationRate, setPreviousDonationRate] = useState( - initialDonationRate > 0 ? initialDonationRate : donationRate + initialDonationRate > 0 ? initialDonationRate : defaultDonationRate ) - - const price = props.price ?? 0 const [url, setUrl] = useState('') const [userEditedAmount, setUserEditedAmount] = useState() const [text, setText] = useState(`Send any amount of ${thisAddressType}`) @@ -286,7 +310,6 @@ export const Widget: React.FunctionComponent = props => { const theme = useTheme(props.theme, isValidXecAddress(to)) const [thisAmount, setThisAmount] = useState(props.amount) - const [hasPrice, setHasPrice] = useState(props.price !== undefined && props.price > 0) const [thisCurrencyObject, setThisCurrencyObject] = useState(props.currencyObject) const blurCSS = isPropsTrue(disabled) ? { filter: 'blur(5px)' } : {} @@ -710,18 +733,22 @@ export const Widget: React.FunctionComponent = props => { setUserDonationRate(0) setDonationEnabled(false) } else { - // Turning on - restore previous rate or use prop/default - const rateToRestore = previousDonationRate > 0 ? previousDonationRate : donationRate + // Turning on - restore previous rate or use context-appropriate default + // If previous rate is below minimum for current context, use default + const rateToRestore = previousDonationRate > 0 && previousDonationRate >= minDonationRate + ? previousDonationRate + : defaultDonationRate setUserDonationRate(rateToRestore) setDonationEnabled(true) } } const handleDonationRateChange = (value: number) => { - const clampedValue = Math.max(0, Math.min(99, value)) + // Enforce minimum based on context (5% for fiat, 1% for crypto) + const clampedValue = Math.max(minDonationRate, Math.min(99, value)) setUserDonationRate(clampedValue) - if (clampedValue > 0) { - // Auto-enable donation if user enters a value > 0 + if (clampedValue >= minDonationRate) { + // Auto-enable donation if user enters a value >= minimum if (!donationEnabled) { setDonationEnabled(true) } @@ -1112,7 +1139,7 @@ export const Widget: React.FunctionComponent = props => { handleDonationRateChange(value) }} inputProps={{ - min: 0, + min: minDonationRate, max: 99, step: 1, }} From e4202ae2064cfabce3ab111f176c9777e9efdc6c Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:15:32 -0600 Subject: [PATCH 37/41] use converted fiat amount for the donation ui logic --- react/lib/components/Widget/Widget.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index cf182c22..cfc8e554 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -304,6 +304,7 @@ export const Widget: React.FunctionComponent = props => { const [widgetButtonText, setWidgetButtonText] = useState('Send Payment') const [opReturn, setOpReturn] = useState() const [isCashtabAvailable, setIsCashtabAvailable] = useState(false) + const [convertedCryptoAmount, setConvertedCryptoAmount] = useState(undefined) const [isAboveMinimumAltpaymentAmount, setIsAboveMinimumAltpaymentAmount] = useState(null) @@ -634,6 +635,8 @@ export const Widget: React.FunctionComponent = props => { ? getCurrencyObject(convertedAmount, thisAddressType, randomSatoshis) : null if (convertedObj) { + // Store converted crypto amount for donation UI visibility check + setConvertedCryptoAmount(convertedObj.float) let amountToDisplay = thisCurrencyObject.string; let convertedAmountToDisplay = convertedObj.string if ( donationEnabled && userDonationRate && userDonationRate >= DONATION_RATE_FIAT_THRESHOLD){ @@ -662,6 +665,8 @@ export const Widget: React.FunctionComponent = props => { setUrl(url ?? "") } } else { + // Clear converted amount when not in fiat conversion mode + setConvertedCryptoAmount(undefined) const notZeroValue = thisCurrencyObject?.float !== undefined && thisCurrencyObject.float > 0 if (!isFiat(currency) && thisCurrencyObject && notZeroValue) { @@ -1096,7 +1101,18 @@ export const Widget: React.FunctionComponent = props => { Powered by PayButton.org - {((thisAddressType === 'XEC' || thisAddressType === 'BCH') && thisCurrencyObject?.float && thisCurrencyObject.float > 0 && thisCurrencyObject.float >= (DEFAULT_MINIMUM_DONATION_AMOUNT[thisAddressType.toUpperCase()] || 0) * 100) ? ( + {(() => { + // For fiat conversions, check the converted crypto amount + // For crypto-only, check the currency object amount + const amountToCheck = hasPrice && convertedCryptoAmount !== undefined + ? convertedCryptoAmount + : thisCurrencyObject?.float + const minDonationAmount = (DEFAULT_MINIMUM_DONATION_AMOUNT[thisAddressType.toUpperCase()] || 0) * 100 + return (thisAddressType === 'XEC' || thisAddressType === 'BCH') && + amountToCheck !== undefined && + amountToCheck > 0 && + amountToCheck >= minDonationAmount + })() ? ( <> | From 76dfd4c00886b32b0e06136e6d6bc67f12884e2e Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Tue, 18 Nov 2025 21:29:59 -0600 Subject: [PATCH 38/41] remove 5% fiat min logic --- react/lib/components/Widget/Widget.tsx | 46 +++++--------------------- 1 file changed, 9 insertions(+), 37 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index cfc8e554..9c2b7200 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -39,7 +39,6 @@ import { CryptoCurrency, DEFAULT_DONATION_RATE, DEFAULT_MINIMUM_DONATION_AMOUNT, - DONATION_RATE_FIAT_THRESHOLD } from '../../util'; import AltpaymentWidget from './AltpaymentWidget' import { @@ -250,26 +249,7 @@ export const Widget: React.FunctionComponent = props => { const price = props.price ?? 0 const [hasPrice, setHasPrice] = useState(props.price !== undefined && props.price > 0) - // Determine if we're in a fiat context (fiat currency or fiat conversion) - // This needs to be calculated early for initialization - const isFiatContext = useMemo(() => { - return hasPrice || isFiat(currency) - }, [hasPrice, currency]) - - // Calculate minimum donation rate based on context - // Fiat requires 5% minimum, crypto allows 1% minimum - const minDonationRate = useMemo(() => { - return isFiatContext ? DONATION_RATE_FIAT_THRESHOLD : 1 - }, [isFiatContext]) - - // Calculate default donation rate based on context - // Fiat defaults to 5%, crypto defaults to prop/default (2%) - const defaultDonationRate = useMemo(() => { - return isFiatContext ? DONATION_RATE_FIAT_THRESHOLD : donationRate - }, [isFiatContext, donationRate]) - // Load donation rate from localStorage on mount - // If stored rate is below minimum for current context, disable donations const getInitialDonationRate = useCallback(() => { if (typeof window !== 'undefined' && window.localStorage) { try { @@ -277,10 +257,6 @@ export const Widget: React.FunctionComponent = props => { if (stored !== null) { const parsed = parseFloat(stored) if (!isNaN(parsed) && parsed >= 0 && parsed <= 99) { - // If stored rate is below minimum for current context, return 0 to disable - if (parsed > 0 && parsed < minDonationRate) { - return 0 - } return parsed } } @@ -289,14 +265,14 @@ export const Widget: React.FunctionComponent = props => { } } return 0 - }, [minDonationRate]) + }, []) const initialDonationRate = useMemo(() => getInitialDonationRate(), [getInitialDonationRate]) const [userDonationRate, setUserDonationRate] = useState(initialDonationRate) const [donationEnabled, setDonationEnabled] = useState(initialDonationRate > 0) // Initialize previousDonationRate with prop value so it's available when user first enables donation const [previousDonationRate, setPreviousDonationRate] = useState( - initialDonationRate > 0 ? initialDonationRate : defaultDonationRate + initialDonationRate > 0 ? initialDonationRate : donationRate ) const [url, setUrl] = useState('') const [userEditedAmount, setUserEditedAmount] = useState() @@ -639,7 +615,7 @@ export const Widget: React.FunctionComponent = props => { setConvertedCryptoAmount(convertedObj.float) let amountToDisplay = thisCurrencyObject.string; let convertedAmountToDisplay = convertedObj.string - if ( donationEnabled && userDonationRate && userDonationRate >= DONATION_RATE_FIAT_THRESHOLD){ + if ( donationEnabled && userDonationRate && userDonationRate > 0){ const thisDonationAmount = thisCurrencyObject.float * (userDonationRate / 100) const amountWithDonation = thisCurrencyObject.float + thisDonationAmount const amountWithDonationObj = getCurrencyObject( @@ -738,22 +714,18 @@ export const Widget: React.FunctionComponent = props => { setUserDonationRate(0) setDonationEnabled(false) } else { - // Turning on - restore previous rate or use context-appropriate default - // If previous rate is below minimum for current context, use default - const rateToRestore = previousDonationRate > 0 && previousDonationRate >= minDonationRate - ? previousDonationRate - : defaultDonationRate + // Turning on - restore previous rate or use prop/default + const rateToRestore = previousDonationRate > 0 ? previousDonationRate : donationRate setUserDonationRate(rateToRestore) setDonationEnabled(true) } } const handleDonationRateChange = (value: number) => { - // Enforce minimum based on context (5% for fiat, 1% for crypto) - const clampedValue = Math.max(minDonationRate, Math.min(99, value)) + const clampedValue = Math.max(0, Math.min(99, value)) setUserDonationRate(clampedValue) - if (clampedValue >= minDonationRate) { - // Auto-enable donation if user enters a value >= minimum + if (clampedValue > 0) { + // Auto-enable donation if user enters a value > 0 if (!donationEnabled) { setDonationEnabled(true) } @@ -1155,7 +1127,7 @@ export const Widget: React.FunctionComponent = props => { handleDonationRateChange(value) }} inputProps={{ - min: minDonationRate, + min: 0, max: 99, step: 1, }} From 74d42b1e029a02a0242510db569c95c73a50ec44 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:10:10 -0600 Subject: [PATCH 39/41] ensure input cannot be below 1 --- react/lib/components/Widget/Widget.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 9c2b7200..68abac62 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -722,17 +722,14 @@ export const Widget: React.FunctionComponent = props => { } const handleDonationRateChange = (value: number) => { - const clampedValue = Math.max(0, Math.min(99, value)) + const clampedValue = Math.max(1, Math.min(99, value)) setUserDonationRate(clampedValue) - if (clampedValue > 0) { + if (clampedValue >= 1) { // Auto-enable donation if user enters a value > 0 if (!donationEnabled) { setDonationEnabled(true) } setPreviousDonationRate(clampedValue) - } else if (clampedValue === 0) { - // Auto-disable donation if user enters 0 - setDonationEnabled(false) } } @@ -1127,7 +1124,7 @@ export const Widget: React.FunctionComponent = props => { handleDonationRateChange(value) }} inputProps={{ - min: 0, + min: 1, max: 99, step: 1, }} From 8eb3b103c81f36643929bb5ebbb5ddffd57d7e58 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:05:05 -0600 Subject: [PATCH 40/41] prevent edge cases of larger saved donation rates getting past min check - only show the ui and apply the donation amount to the qr code if 1% of the button amount is >= the min donation amount --- react/lib/components/Widget/Widget.tsx | 60 +++++++++++++++++--------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index 68abac62..df19e673 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -596,6 +596,27 @@ export const Widget: React.FunctionComponent = props => { } }, [thisAmount, currency, userEditedAmount]) + // Helper function to check if amount meets minimum for donation UI visibility + const shouldShowDonationUI = useCallback((amount: number, currencyType: string): boolean => { + // Normalize currency type to uppercase for comparison + const normalizedCurrency = currencyType.toUpperCase() + if (normalizedCurrency !== 'XEC' && normalizedCurrency !== 'BCH') { + return false + } + // Check if 1% of the amount is >= minimum donation amount + const onePercentOfAmount = amount * 0.01 + const minimumDonationAmount = DEFAULT_MINIMUM_DONATION_AMOUNT[normalizedCurrency] || 0 + return onePercentOfAmount >= minimumDonationAmount + }, []) + + // Helper function to check if donation should be applied + const shouldApplyDonation = useCallback((amount: number, currencyType: string): boolean => { + if (!donationEnabled || !userDonationRate || userDonationRate <= 0) { + return false + } + return shouldShowDonationUI(amount, currencyType) + }, [donationEnabled, userDonationRate, shouldShowDonationUI]) + useEffect(() => { if (to === undefined) return let nextUrl: string | undefined @@ -615,7 +636,9 @@ export const Widget: React.FunctionComponent = props => { setConvertedCryptoAmount(convertedObj.float) let amountToDisplay = thisCurrencyObject.string; let convertedAmountToDisplay = convertedObj.string - if ( donationEnabled && userDonationRate && userDonationRate > 0){ + + // Only apply donation if 1% of converted crypto amount is >= minimum donation amount + if (shouldApplyDonation(convertedObj.float, thisAddressType)) { const thisDonationAmount = thisCurrencyObject.float * (userDonationRate / 100) const amountWithDonation = thisCurrencyObject.float + thisDonationAmount const amountWithDonationObj = getCurrencyObject( @@ -647,13 +670,13 @@ export const Widget: React.FunctionComponent = props => { thisCurrencyObject?.float !== undefined && thisCurrencyObject.float > 0 if (!isFiat(currency) && thisCurrencyObject && notZeroValue) { const cur: string = thisCurrencyObject.currency - let amountToDisplay = thisCurrencyObject.string const baseAmount = thisCurrencyObject.float // Base amount without donation - // Add donation amount if enabled (for XEC and BCH) - if (donationEnabled && userDonationRate && userDonationRate > 0 && (cur === 'XEC' || cur === 'BCH')) { - const donationAmountValue = thisCurrencyObject.float * (userDonationRate / 100) - const amountWithDonation = thisCurrencyObject.float + donationAmountValue + // Only apply donation if 1% of amount is >= minimum donation amount + let amountToDisplay = thisCurrencyObject.string + if (shouldApplyDonation(baseAmount, cur)) { + const donationAmountValue = baseAmount * (userDonationRate / 100) + const amountWithDonation = baseAmount + donationAmountValue const amountWithDonationObj = getCurrencyObject( amountWithDonation, cur, @@ -671,7 +694,7 @@ export const Widget: React.FunctionComponent = props => { } setUrl(nextUrl ?? '') } - }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType]) + }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType, shouldApplyDonation]) useEffect(() => { try { @@ -796,21 +819,18 @@ export const Widget: React.FunctionComponent = props => { let thisUrl = `${prefix}:${to.replace(/^.*:/, '')}`; if (amount) { - if (donationAddress && donationEnabled && userDonationRate && userDonationRate > 0) { - const network = Object.entries(CURRENCY_PREFIXES_MAP).find( - ([, value]) => value === prefix - )?.[0]; - const decimals = network ? DECIMALS[network.toUpperCase()] : undefined; + // Check if donation should be applied (1% of amount >= minimum) + const currencyType = currency.toUpperCase() + + if (donationAddress && shouldApplyDonation(amount, currencyType)) { + const decimals = DECIMALS[currencyType] || DECIMALS.XEC; const donationPercent = userDonationRate / 100 // Calculate donation amount from base amount const thisDonationAmount = amount * donationPercent - const minimumDonationAmount = network ? DEFAULT_MINIMUM_DONATION_AMOUNT[network.toUpperCase()] : 0 thisUrl += `?amount=${amount}` - if(thisDonationAmount >= minimumDonationAmount){ - thisUrl += `&addr=${donationAddress}&amount=${thisDonationAmount.toFixed(decimals)}`; - } - }else{ + thisUrl += `&addr=${donationAddress}&amount=${thisDonationAmount.toFixed(decimals)}`; + } else { thisUrl += `?amount=${amount}` } } @@ -822,7 +842,7 @@ export const Widget: React.FunctionComponent = props => { return thisUrl; }, - [disabled, to, opReturn, userDonationRate, donationAddress, donationEnabled] + [disabled, to, opReturn, userDonationRate, donationAddress, donationEnabled, shouldApplyDonation] ) const handleAmountChange = (e: React.ChangeEvent) => { @@ -1076,11 +1096,11 @@ export const Widget: React.FunctionComponent = props => { const amountToCheck = hasPrice && convertedCryptoAmount !== undefined ? convertedCryptoAmount : thisCurrencyObject?.float - const minDonationAmount = (DEFAULT_MINIMUM_DONATION_AMOUNT[thisAddressType.toUpperCase()] || 0) * 100 + // Show donation UI if amount meets minimum (1% >= 10 XEC), regardless of enabled state return (thisAddressType === 'XEC' || thisAddressType === 'BCH') && amountToCheck !== undefined && amountToCheck > 0 && - amountToCheck >= minDonationAmount + shouldShowDonationUI(amountToCheck, thisAddressType) })() ? ( <> | From 98ad1c2316edd18057e4f3a5ff7f99d296379a31 Mon Sep 17 00:00:00 2001 From: johnkuney <31288518+johnkuney@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:42:00 -0600 Subject: [PATCH 41/41] ensure donation values are clamped to 1-99 range --- react/lib/components/Widget/Widget.tsx | 38 ++++++++++++++++++-------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/react/lib/components/Widget/Widget.tsx b/react/lib/components/Widget/Widget.tsx index df19e673..ce2d8a45 100644 --- a/react/lib/components/Widget/Widget.tsx +++ b/react/lib/components/Widget/Widget.tsx @@ -249,6 +249,12 @@ export const Widget: React.FunctionComponent = props => { const price = props.price ?? 0 const [hasPrice, setHasPrice] = useState(props.price !== undefined && props.price > 0) + // Helper to clamp donation rate to valid range (1-99 if > 0, or 0) + const clampDonationRate = useCallback((value: number): number => { + if (value <= 0) return 0 + return Math.max(1, Math.min(99, value)) + }, []) + // Load donation rate from localStorage on mount const getInitialDonationRate = useCallback(() => { if (typeof window !== 'undefined' && window.localStorage) { @@ -256,8 +262,9 @@ export const Widget: React.FunctionComponent = props => { const stored = localStorage.getItem(DONATION_RATE_STORAGE_KEY) if (stored !== null) { const parsed = parseFloat(stored) - if (!isNaN(parsed) && parsed >= 0 && parsed <= 99) { - return parsed + if (!isNaN(parsed) && parsed >= 0) { + // Clamp to 1-99 range if > 0, or return 0 + return clampDonationRate(parsed) } } } catch (e) { @@ -265,14 +272,17 @@ export const Widget: React.FunctionComponent = props => { } } return 0 - }, []) + }, [clampDonationRate]) + + // Clamp the donationRate prop to ensure it's in valid range + const clampedDonationRateProp = useMemo(() => clampDonationRate(donationRate), [donationRate, clampDonationRate]) const initialDonationRate = useMemo(() => getInitialDonationRate(), [getInitialDonationRate]) const [userDonationRate, setUserDonationRate] = useState(initialDonationRate) const [donationEnabled, setDonationEnabled] = useState(initialDonationRate > 0) - // Initialize previousDonationRate with prop value so it's available when user first enables donation + // Initialize previousDonationRate with clamped prop value so it's available when user first enables donation const [previousDonationRate, setPreviousDonationRate] = useState( - initialDonationRate > 0 ? initialDonationRate : donationRate + initialDonationRate > 0 ? initialDonationRate : clampedDonationRateProp ) const [url, setUrl] = useState('') const [userEditedAmount, setUserEditedAmount] = useState() @@ -732,23 +742,29 @@ export const Widget: React.FunctionComponent = props => { const handleDonationToggle = () => { if (donationEnabled) { - // Turning off - save current rate and set to 0 + // Turning off - save current rate (already clamped) and set to 0 setPreviousDonationRate(userDonationRate) setUserDonationRate(0) setDonationEnabled(false) } else { - // Turning on - restore previous rate or use prop/default - const rateToRestore = previousDonationRate > 0 ? previousDonationRate : donationRate - setUserDonationRate(rateToRestore) + // Turning on - restore previous rate or use clamped prop/default + // Use same clamping logic as handleDonationRateChange to ensure 1-99 range + const rateToRestore = previousDonationRate > 0 ? previousDonationRate : clampedDonationRateProp + const clampedRate = clampDonationRate(rateToRestore) + setUserDonationRate(clampedRate) setDonationEnabled(true) + // Update previousDonationRate to the clamped value + if (clampedRate > 0) { + setPreviousDonationRate(clampedRate) + } } } const handleDonationRateChange = (value: number) => { - const clampedValue = Math.max(1, Math.min(99, value)) + const clampedValue = clampDonationRate(value) setUserDonationRate(clampedValue) if (clampedValue >= 1) { - // Auto-enable donation if user enters a value > 0 + // Auto-enable donation if user enters a value >= 1 if (!donationEnabled) { setDonationEnabled(true) }