Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
160 changes: 109 additions & 51 deletions react/lib/components/PayButton/PayButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import {
isValidCashAddress,
isValidXecAddress,
CurrencyObject,
generatePaymentId,
getCurrencyObject,
isPropsTrue,
setupAltpaymentSocket,
setupChronikWebSocket,
CryptoCurrency,
ButtonSize
} 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 @@ -105,18 +105,21 @@ export const PayButton = ({

const [currencyObj, setCurrencyObj] = useState<CurrencyObject | undefined>();
const [cryptoAmount, setCryptoAmount] = useState<string>();
const [convertedAmount, setConvertedAmount] = useState<number>();
const [convertedCurrencyObj, setConvertedCurrencyObj] = useState<CurrencyObject | undefined>();

const [price, setPrice] = useState(0);
const [newTxs, setNewTxs] = useState<Transaction[] | undefined>();
const priceRef = useRef<number>(price);
const cryptoAmountRef = useRef<string | undefined>(cryptoAmount);



const [paymentId] = useState(!disablePaymentId ? generatePaymentId(8) : undefined);
const [paymentId, setPaymentId] = useState<string | undefined>(undefined);
const [addressType, setAddressType] = useState<CryptoCurrency>(
getCurrencyTypeFromAddress(to),
);



Comment on lines +116 to +122
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Reset paymentId when address/currency/randomization inputs change to avoid stale IDs

Otherwise an ID tied to a previous address or unit policy may be reused.

   const [paymentId, setPaymentId] = useState<string | undefined>(undefined);
+  useEffect(() => {
+    setPaymentId(undefined);
+  }, [to, currency, randomSatoshis]);

If the server binds paymentId to the original amount as well, also include amount in the dependency list. Please confirm backend semantics.



---

🏁 Script executed:

```shell
#!/bin/bash
# Locate PayButton.tsx and examine its content
find . -name "PayButton.tsx" -type f 2>/dev/null | head -20

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Search for paymentId usage patterns in the codebase
rg -n "paymentId" --type=ts --type=tsx -C 5 | head -100

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Look for useEffect hooks and state management in PayButton area
fd "PayButton" -type f | xargs -I {} sh -c 'echo "=== {} ===" && cat {}'

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Search for backend API calls related to payment
rg -n "payment" --type=ts --type=tsx -i -A 2 -B 2 | grep -i "api\|fetch\|axios\|request" | head -50

Length of output: 27



---

🏁 Script executed:

```shell
#!/bin/bash
# Read the PayButton.tsx file directly
cat ./react/lib/components/PayButton/PayButton.tsx

Length of output: 12741


🏁 Script executed:

#!/bin/bash
# Search for paymentId usage (corrected syntax)
rg -n "paymentId" -C 5

Length of output: 90419


🏁 Script executed:

#!/bin/bash
# Search for amount usage in payment context
rg -n "amount" -C 3 | head -80

Length of output: 4137


🏁 Script executed:

#!/bin/bash
# Look for useEffect hooks in the codebase
rg -n "useEffect" ./react/lib/components/PayButton/ -C 3

Length of output: 4531


🏁 Script executed:

#!/bin/bash
# Search for API calls or backend requests
rg -n "fetch\|axios\|api" -i ./react/lib/components/PayButton/ -C 3

Length of output: 0


Reset paymentId when address, currency, amount, or randomization inputs change to prevent reuse of stale IDs

Without resetting paymentId when these inputs change, the component can reuse IDs tied to previous addresses, amounts, or unit policies, causing validation mismatches.

   const [paymentId, setPaymentId] = useState<string | undefined>(undefined);
+  useEffect(() => {
+    setPaymentId(undefined);
+  }, [to, currency, amount, randomSatoshis]);

The backend's createPayment API endpoint receives both amount and address and generates an ID bound to those values (per react/lib/util/api-client.ts lines 98–99). Therefore, amount must be included in the dependency list alongside to, currency, and randomSatoshis.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [paymentId, setPaymentId] = useState<string | undefined>(undefined);
const [addressType, setAddressType] = useState<CryptoCurrency>(
getCurrencyTypeFromAddress(to),
);
const [paymentId, setPaymentId] = useState<string | undefined>(undefined);
useEffect(() => {
setPaymentId(undefined);
}, [to, currency, amount, randomSatoshis]);
const [addressType, setAddressType] = useState<CryptoCurrency>(
getCurrencyTypeFromAddress(to),
);
🤖 Prompt for AI Agents
In react/lib/components/PayButton/PayButton.tsx around lines 115 to 121, the
component fails to reset paymentId when key inputs change which can cause reuse
of stale IDs; update the effect/logic that computes addressType and paymentId so
that it includes amount (and already-present dependencies to, currency,
randomSatoshis) in its dependency list and call setPaymentId(undefined) whenever
any of those change, ensuring a new ID is requested for the new
address/amount/unit policy.

useEffect(() => {
priceRef.current = price;
}, [price]);
Expand All @@ -133,16 +136,56 @@ export const PayButton = ({
}
}, 300);
};

const getPaymentId = useCallback(async (
currency: Currency,
amount: number,
convertedAmount: number | undefined,
to: string | undefined,
): Promise<string | undefined> => {
if (disablePaymentId || !to) return paymentId
try {
const amountToUse =
(isFiat(currency) || randomSatoshis) && convertedAmount
? convertedAmount
: amount

const responsePaymentId = await createPayment(amountToUse, to, apiBaseUrl)

setPaymentId(responsePaymentId)
return responsePaymentId
} catch (err) {
console.error('Error creating payment ID:', err)
return undefined
}
}, [disablePaymentId, apiBaseUrl, isFiat, randomSatoshis])

const handleButtonClick = useCallback(async (): Promise<void> => {
if (onOpen !== undefined) {

if (onOpen) {
if (isFiat(currency)) {
void waitPrice(() => { onOpen(cryptoAmountRef.current, to, paymentId) })
void waitPrice(() => onOpen(cryptoAmountRef.current, to, paymentId))
} else {
onOpen(amount, to, paymentId)
}
}
setDialogOpen(true);
}, [cryptoAmount, to, paymentId, price])

if (!disablePaymentId && !paymentId) {
await getPaymentId(currency, Number(amount), convertedAmount, to)
}

setDialogOpen(true)
}, [
onOpen,
isFiat,
currency,
amount,
to,
paymentId,
disablePaymentId,
getPaymentId,
convertedAmount,
])

const handleCloseDialog = (success?: boolean, paymentId?: string): void => {
if (onClose !== undefined) onClose(success, paymentId);
Expand Down Expand Up @@ -185,42 +228,42 @@ export const PayButton = ({
return
}
(async () => {
if (txsSocket === undefined) {
const expectedAmount = currencyObj ? currencyObj?.float : undefined
await setupChronikWebSocket({
address: to,
txsSocket,
apiBaseUrl,
wsBaseUrl,
setTxsSocket,
setNewTxs,
setDialogOpen,
checkSuccessInfo: {
currency,
price,
randomSatoshis: randomSatoshis ?? false,
disablePaymentId,
expectedAmount,
expectedOpReturn: opReturn,
expectedPaymentId: paymentId,
currencyObj,
}
})
}
if (altpaymentSocket === undefined && useAltpayment) {
await setupAltpaymentSocket({
addressType,
altpaymentSocket,
wsBaseUrl,
setAltpaymentSocket,
setCoins,
setCoinPair,
setLoadingPair,
setAltpaymentShift,
setLoadingShift,
setAltpaymentError,
})
}
if (txsSocket === undefined) {
const expectedAmount = currencyObj ? currencyObj?.float : undefined
await setupChronikWebSocket({
address: to,
txsSocket,
apiBaseUrl,
wsBaseUrl,
setTxsSocket,
setNewTxs,
setDialogOpen,
checkSuccessInfo: {
currency,
price,
randomSatoshis: randomSatoshis ?? false,
disablePaymentId,
expectedAmount,
expectedOpReturn: opReturn,
expectedPaymentId: paymentId,
currencyObj,
}
})
}
if (altpaymentSocket === undefined && useAltpayment) {
await setupAltpaymentSocket({
addressType,
altpaymentSocket,
wsBaseUrl,
setAltpaymentSocket,
setCoins,
setCoinPair,
setLoadingPair,
setAltpaymentShift,
setLoadingShift,
setAltpaymentError,
})
}
})()

return () => {
Expand Down Expand Up @@ -255,22 +298,35 @@ export const PayButton = ({

useEffect(() => {
(async () => {
if (isFiat(currency) && price === 0) {
await getPrice();
}
if (isFiat(currency) && price === 0) {
await getPrice();
}
})()
}, [currency, getPrice, to, price]);

useEffect(() => {
if (currencyObj && isFiat(currency) && price) {
const addressType: Currency = getCurrencyTypeFromAddress(to);
if(!convertedCurrencyObj) {
const addressType: Currency = getCurrencyTypeFromAddress(to);
const convertedObj = getCurrencyObject(
currencyObj.float / price,
addressType,
randomSatoshis,
);
setCryptoAmount(convertedObj.string);
setConvertedAmount(convertedObj.float);
setConvertedCurrencyObj(convertedObj);
}
} else if (!isFiat(currency) && randomSatoshis && !convertedAmount){
const convertedObj = getCurrencyObject(
currencyObj.float / price,
amount as number,
addressType,
randomSatoshis,
);
);
setCryptoAmount(convertedObj.string);
} else if (!isFiat(currency)) {
setConvertedAmount(convertedObj.float);
setConvertedCurrencyObj(convertedObj);
} else if (!isFiat(currency) && !randomSatoshis) {
setCryptoAmount(amount?.toString());
}
}, [price, currencyObj, amount, currency, randomSatoshis, to]);
Expand Down Expand Up @@ -347,6 +403,8 @@ export const PayButton = ({
newTxs={newTxs}
disableSound={disableSound}
transactionText={transactionText}
convertedCurrencyObj={convertedCurrencyObj}
setConvertedCurrencyObj={setConvertedCurrencyObj}
/>
{errorMsg && (
<p
Expand Down
6 changes: 6 additions & 0 deletions react/lib/components/PaymentDialog/PaymentDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export interface PaymentDialogProps extends ButtonProps {
autoClose?: boolean | number | string;
disableSound?: boolean;
transactionText?: string
convertedCurrencyObj?: CurrencyObject;
setConvertedCurrencyObj?: Function;
}

export const PaymentDialog = ({
Expand Down Expand Up @@ -121,6 +123,8 @@ export const PaymentDialog = ({
disableSound,
transactionText,
disabled,
convertedCurrencyObj,
setConvertedCurrencyObj,
theme: themeProp,
}: PaymentDialogProps): React.ReactElement => {
const [success, setSuccess] = useState(false);
Expand Down Expand Up @@ -247,6 +251,8 @@ export const PaymentDialog = ({
newTxs={newTxs}
disableSound={disableSound}
transactionText={transactionText}
convertedCurrencyObj={convertedCurrencyObj}
setConvertedCurrencyObj={setConvertedCurrencyObj}
foot={success && (
<ButtonComponent
onClick={handleWidgetClose}
Expand Down
78 changes: 51 additions & 27 deletions react/lib/components/Widget/Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,21 +85,23 @@ export interface WidgetProps {
altpaymentSocket?: Socket
setAltpaymentSocket?: Function
shiftCompleted?: boolean
setShiftCompleted?: Function
setCoins?: Function
coins?: AltpaymentCoin[]
setCoinPair?: Function
coinPair?: AltpaymentPair
setLoadingPair?: Function
loadingPair?: boolean
setLoadingShift?: Function
loadingShift?: boolean
setAltpaymentError?: Function
altpaymentError?: AltpaymentError
addressType?: Currency
setAddressType?: Function
newTxText?: string
transactionText?: string
setShiftCompleted?: Function;
setCoins?: Function;
coins?: AltpaymentCoin[];
setCoinPair?: Function;
coinPair?: AltpaymentPair;
setLoadingPair?: Function;
loadingPair?: boolean;
setLoadingShift?: Function;
loadingShift?: boolean;
setAltpaymentError?: Function;
altpaymentError?: AltpaymentError;
addressType?: Currency,
setAddressType?: Function,
newTxText?: string;
transactionText?: string;
convertedCurrencyObj?: CurrencyObject;
setConvertedCurrencyObj?: Function;
}

interface StyleProps {
Expand Down Expand Up @@ -155,7 +157,9 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
altpaymentError,
setAltpaymentError,
isChild,
} = props
convertedCurrencyObj,
setConvertedCurrencyObj = () => {},
} = props;

const [loading, setLoading] = useState(true)

Expand Down Expand Up @@ -539,14 +543,28 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
}
}
if (userEditedAmount !== undefined && thisAmount && thisAddressType) {
const obj = getCurrencyObject(+thisAmount, currency, false)
setThisCurrencyObject(obj)
if (props.setCurrencyObject) props.setCurrencyObject(obj)
const obj = getCurrencyObject(+thisAmount, currency, false);
setThisCurrencyObject(obj);
if (props.setCurrencyObject) {
props.setCurrencyObject(obj);
}
const convertedAmount = obj.float / price
const convertedObj = price
? getCurrencyObject(
convertedAmount,
thisAddressType,
randomSatoshis,
)
: null;
setConvertedCurrencyObj(convertedObj)
} else if (thisAmount && thisAddressType) {
cleanAmount = +thisAmount
const obj = getCurrencyObject(cleanAmount, currency, randomSatoshis)
setThisCurrencyObject(obj)
if (props.setCurrencyObject) props.setCurrencyObject(obj)
cleanAmount = +thisAmount;

const obj = getCurrencyObject(cleanAmount, currency, randomSatoshis);
setThisCurrencyObject(obj);
if (props.setCurrencyObject) {
props.setCurrencyObject(obj);
}
}
}, [thisAmount, currency, userEditedAmount])

Expand All @@ -559,11 +577,17 @@ export const Widget: React.FunctionComponent<WidgetProps> = props => {
} else {
setWidgetButtonText(`Send with ${thisAddressType} wallet`)
}

if (thisCurrencyObject && hasPrice) {
const convertedAmount = thisCurrencyObject.float / price
const convertedObj = price
? getCurrencyObject(convertedAmount, thisAddressType, randomSatoshis)
: null
// Use convertedAmount prop if available, otherwise calculate locally
const convertedAmount = convertedCurrencyObj ? convertedCurrencyObj.float : thisCurrencyObject.float / price
const convertedObj = convertedCurrencyObj ? convertedCurrencyObj : price
? getCurrencyObject(
convertedAmount,
thisAddressType,
randomSatoshis,
)
: null;
if (convertedObj) {
setText(
`Send ${thisCurrencyObject.string} ${thisCurrencyObject.currency} = ${convertedObj.string} ${thisAddressType}`,
Expand Down
Loading