Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
38a0471
chore: single button page to simplified tests
chedieck Dec 1, 2025
28ed757
fix: paymentId on open
chedieck Dec 1, 2025
13a1776
fix: regenerating for undefined
chedieck Dec 1, 2025
8c052f2
test: add test for 13a1776f
chedieck Dec 1, 2025
a1c1612
test: test for not regenerating paymentId across reopenings
chedieck Dec 1, 2025
866a62a
fix: <div> inside <p>
chedieck Dec 1, 2025
77ec5ab
test: add tests for paybutton
chedieck Dec 1, 2025
3abdbdb
fix: open ui immediatly (WIP)
chedieck Dec 1, 2025
030a899
fix: onOpen with correct values, leaving amount updated
chedieck Dec 3, 2025
30f3bea
fix: optional props, wait for loading before being able to click button
chedieck Dec 3, 2025
2143dd5
fix: don't allow copying qrcode before paymentId loads
chedieck Dec 3, 2025
5d7bc59
test: improve widget tests
chedieck Dec 3, 2025
3dab97e
fix: dependecy on effect
chedieck Dec 9, 2025
15e37b2
test: working review of paybutton tests
chedieck Dec 10, 2025
1543843
fix: createPayment import
chedieck Dec 10, 2025
3070474
test: passing paybutton tests
chedieck Dec 11, 2025
689127c
test: fix warning on widget tests
chedieck Dec 11, 2025
0adb6b9
test: add tests for QR code and amount shown
chedieck Dec 11, 2025
c8d8f30
fix: type error
chedieck Dec 11, 2025
c88bbc3
fix: widget is actually widget container
chedieck Dec 11, 2025
165f92f
fix: widget not fetching paymentId for undefined amount
chedieck Dec 11, 2025
d94939a
test: complete widget test for now
chedieck Dec 11, 2025
a780c8b
Merge branch 'master' into fix/paymentId-onopen
chedieck Dec 11, 2025
285e0bc
fix: double editable
chedieck Dec 11, 2025
2505d69
chore: add canvas
chedieck Dec 12, 2025
39dcd22
test: fix mock get price
chedieck Dec 12, 2025
3836eb6
test: faster test time
chedieck Dec 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions paybutton/dev/demo/single.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<title>Paybutton Bundle Test</title>
<style>
body {
padding: auto;
font-family: Arial, sans-serif;
}
</style>
<link rel="stylesheet" type="text/css" href="style.css">
</head>

<body>
<script src="./paybutton.js"></script>

<div class="paybutton" to="ecash:qr64gaal5zx97judme7nmqxpru6gnx8p3uwgvx585h" text="Purchase" amount="1001"
editable="true"
currency="XEC" goal-amount="2.5" success-text="WOW!" on-close="myClose" on-open="myOpen"
on-success="mySuccessFunction" on-transaction="myTransactionFunction" size="lg"></div>
<script>
function mySuccessFunction(tx) {
console.log('Ran mySuccess', { tx })
}

function myTransactionFunction(tx) {
console.log('Ran myTransaction', { tx })
}

function myClose(success, paymentId) {
console.log('Ran myClose', { success, paymentId})
}

function myOpen(amount, to, paymentId) {
console.log('Ran myOpen', { amount, to, paymentId })
}
</script>
</body>
126 changes: 71 additions & 55 deletions react/lib/components/PayButton/PayButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {
setupChronikWebSocket,
CryptoCurrency,
ButtonSize,
DEFAULT_DONATION_RATE
DEFAULT_DONATION_RATE,
createPayment
} from '../../util';
import { createPayment } from '../../util/api-client';
import { PaymentDialog } from '../PaymentDialog';
import { AltpaymentCoin, AltpaymentError, AltpaymentPair, AltpaymentShift } from '../../altpayment';
export interface PayButtonProps extends ButtonProps {
Expand Down Expand Up @@ -129,17 +129,8 @@ export const PayButton = ({
}, [price]);

useEffect(() => {
cryptoAmountRef.current = cryptoAmount;
}, [cryptoAmount]);

const waitPrice = (callback: Function) => {
const intervalId = setInterval(() => {
if (priceRef.current !== 0) {
clearInterval(intervalId);
callback();
}
}, 300);
};
cryptoAmountRef.current = convertedCurrencyObj?.string;
}, [cryptoAmount, convertedCurrencyObj]);


const getPaymentId = useCallback(async (
Expand Down Expand Up @@ -169,67 +160,94 @@ export const PayButton = ({
[disablePaymentId, apiBaseUrl, randomSatoshis, convertedCurrencyObj]
)

const lastPaymentAmount = useRef<number | null | undefined>(undefined)
const lastPaymentAmount = useRef<number | undefined | null>(undefined);
const lastOnOpenPaymentId = useRef<string | undefined>(undefined);
const hasFiredOnOpenRef = useRef(false);

useEffect(() => {
if (!dialogOpen || disablePaymentId || !to) return;

if (
!dialogOpen ||
disablePaymentId ||
!to
) {
return;
}
let effectiveAmount: number | null | undefined;

let effectiveAmount: number | null
if (isFiat(currency)) {
if (!convertedCurrencyObj) {
// Conversion not ready yet – wait for convertedCurrencyObj update
return;
}
effectiveAmount = convertedCurrencyObj.float;
} else if (amount === undefined) {
effectiveAmount = null
effectiveAmount = convertedCurrencyObj?.float ?? null;
} else if (amount !== undefined) {
const parsed = Number(amount);
effectiveAmount = Number.isNaN(parsed) ? null : parsed;
} else {
const amountNumber = Number(amount);
if (Number.isNaN(amountNumber)) {
return;
}
effectiveAmount = amountNumber;
effectiveAmount = null;
}
if (lastPaymentAmount.current === effectiveAmount) {

if (effectiveAmount === lastPaymentAmount.current) {
return;
}

lastPaymentAmount.current = effectiveAmount;

void getPaymentId(
currency,
to,
effectiveAmount ?? undefined,
);
}, [amount, currency, to, dialogOpen, disablePaymentId, paymentId, getPaymentId, convertedCurrencyObj]);
// Clear current paymentId so child widget goes into "waiting" state
setPaymentId(undefined);

void getPaymentId(currency, to, effectiveAmount ?? undefined);
}, [
dialogOpen,
currency,
amount,
convertedCurrencyObj,
disablePaymentId,
to,
getPaymentId,
]);

useEffect(() => {
if (!dialogOpen) {
hasFiredOnOpenRef.current = false;
lastOnOpenPaymentId.current = undefined;
return;
}

if (!onOpen || hasFiredOnOpenRef.current) {
return;
}

const handleButtonClick = useCallback(async (): Promise<void> => {
if (disablePaymentId) {
hasFiredOnOpenRef.current = true;
lastOnOpenPaymentId.current = '__no_paymentid__';

if (onOpen) {
if (isFiat(currency)) {
void waitPrice(() => onOpen(cryptoAmountRef.current, to, paymentId))
onOpen(cryptoAmountRef.current, to, undefined);
} else {
onOpen(amount, to, paymentId)
onOpen(amount, to, undefined);
}
return;
}

setDialogOpen(true)
if (!paymentId) {
return;
}

hasFiredOnOpenRef.current = true;
lastOnOpenPaymentId.current = paymentId;

if (isFiat(currency)) {
onOpen(cryptoAmountRef.current, to, paymentId);
} else {
onOpen(amount, to, paymentId);
}
}, [
dialogOpen,
onOpen,
paymentId,
currency,
amount,
to,
paymentId,
disablePaymentId,
getPaymentId,
])
]);


const handleButtonClick = useCallback((): void => {
setDialogOpen(true);
}, []);


const handleCloseDialog = (success?: boolean, paymentId?: string): void => {
if (onClose !== undefined) onClose(success, paymentId);
Expand Down Expand Up @@ -320,18 +338,16 @@ export const PayButton = ({
}, [dialogOpen, useAltpayment]);

useEffect(() => {
if (dialogOpen === false && initialAmount && currency) {
if (initialAmount != null && currency) {
const obj = getCurrencyObject(
Number(initialAmount),
currency,
randomSatoshis,
);
setTimeout(() => {
setAmount(obj.float);
setCurrencyObj(obj);
}, 300);
setAmount(obj.float);
setCurrencyObj(obj);
}
}, [dialogOpen, initialAmount, currency, randomSatoshis]);
}, [initialAmount, currency, randomSatoshis])

const getPrice = useCallback(
async () => {
Expand Down
27 changes: 19 additions & 8 deletions react/lib/components/Widget/Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ import {
CryptoCurrency,
DEFAULT_DONATION_RATE,
DEFAULT_MINIMUM_DONATION_AMOUNT,
darkMode,
createPayment,
darkMode
} from '../../util';
import AltpaymentWidget from './AltpaymentWidget'
import {
Expand All @@ -53,7 +54,6 @@ import {
MINIMUM_ALTPAYMENT_CAD_AMOUNT,
} from '../../altpayment'

import { createPayment } from '../../util/api-client';

export interface WidgetProps {
to: string
Expand Down Expand Up @@ -182,10 +182,14 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
const inputRef = React.useRef<HTMLInputElement>(null)
const lastEffectiveAmountRef = React.useRef<number | undefined | null>(undefined)

const [standalonePaymentPending, setStandalonePaymentPending] = useState(false)

const isWaitingForPaymentId =
isChild === true &&
!disablePaymentId &&
paymentId === undefined
!disablePaymentId && (
(isChild === true && paymentId === undefined) ||
(isChild !== true && standalonePaymentPending)
)


const qrLoading = loading || isWaitingForPaymentId

Expand Down Expand Up @@ -599,8 +603,8 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
return;
}

// For fiat, wait until we have a converted crypto amount
if (isFiat(currency) && convertedCryptoAmount === undefined) {
// For fiat with defined amount, wait until we have a converted crypto amount
if (isFiat(currency) && convertedCryptoAmount === undefined && thisAmount !== undefined ) {
return;
}

Expand Down Expand Up @@ -631,6 +635,8 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
}
lastEffectiveAmountRef.current = effectiveAmount;

setStandalonePaymentPending(true)

const responsePaymentId = await createPayment(
effectiveAmount ?? undefined,
to,
Expand All @@ -639,6 +645,8 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
setPaymentId(responsePaymentId);
} catch (error) {
console.error('Error creating payment ID:', error);
} finally {
setStandalonePaymentPending(false)
}
};

Expand Down Expand Up @@ -1034,6 +1042,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
<QRCodeSVG
size={300}
level="H"
data-testid="qr-code"
value={url}
bgColor={isDarkMode ? '#1a1a1a' : '#ffffff'}
fgColor={theme.palette.tertiary as unknown as string}
Expand Down Expand Up @@ -1147,6 +1156,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
)}

<Box
data-testid="qr-click-area"
flex={1}
position="relative"
sx={classes.qrCode}
Expand Down Expand Up @@ -1237,6 +1247,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
endAdornment: (
<Box
component="button"
data-testid="confirm-button"
onClick={applyDraftAmount}
sx={{
padding: '4px 10px',
Expand Down Expand Up @@ -1313,7 +1324,7 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
) : null}

<Box py={0.8}>
<Typography sx={classes.footer}>
<Typography component="div" sx={classes.footer}>
<Box>Powered by PayButton.org</Box>

{(() => {
Expand Down
8 changes: 4 additions & 4 deletions react/lib/components/Widget/WidgetContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,21 @@ export interface WidgetContainerProps
currencyObj?: CurrencyObject;
cryptoAmount?: string;
price?: number;
setCurrencyObj: Function;
setCurrencyObj?: Function;
randomSatoshis?: boolean | number;
hideToasts?: boolean;
onSuccess?: (transaction: Transaction) => void;
onTransaction?: (transaction: Transaction) => void;
sound?: boolean;
goalAmount?: number | string;
disabled: boolean;
editable: boolean;
disabled?: boolean;
editable?: boolean;
wsBaseUrl?: string;
apiBaseUrl?: string;
successText?: string;
disableAltpayment?: boolean
contributionOffset?: number
setNewTxs: Function
setNewTxs?: Function
disableSound?: boolean
transactionText?: string
donationAddress?: string
Expand Down
Loading