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])
Comment on lines 140 to 161
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

Fix amount units for server creation and guard against concurrent calls

For fiat or randomized flows, if convertedAmount isn’t ready, wait for price and compute a crypto amount; also guard to prevent duplicate createPayment calls.

+  const creatingPaymentIdRef = useRef(false);
   const getPaymentId = useCallback(async (
     currency: Currency,
     amount: number,
     convertedAmount: number | undefined,
     to: string | undefined,
   ): Promise<string | undefined> => {
-    if (disablePaymentId || !to) return paymentId
+    if (disablePaymentId || !to) return paymentId;
+    if (creatingPaymentIdRef.current) return paymentId;
+    creatingPaymentIdRef.current = true;
     try {
-      const amountToUse =
-        (isFiat(currency) || randomSatoshis) && convertedAmount
-        ? convertedAmount
-        : amount
-
-      console.log('Creating payment ID with amount:', amountToUse)
-      const responsePaymentId = await createPayment(amountToUse, to, apiBaseUrl)
-
-      setPaymentId(responsePaymentId)
-      return responsePaymentId
+      let amountToUse = amount;
+      if (isFiat(currency) || randomSatoshis) {
+        if (convertedAmount !== undefined) {
+          amountToUse = convertedAmount;
+        } else {
+          // Wait until price is available if fiat, then compute crypto amount now
+          if (isFiat(currency) && priceRef.current === 0) {
+            await new Promise<void>((resolve) => waitPrice(resolve));
+          }
+          const addressTypeLocal: Currency = getCurrencyTypeFromAddress(to);
+          const convertedObj = getCurrencyObject(
+            isFiat(currency) && priceRef.current ? amount / priceRef.current : amount,
+            addressTypeLocal,
+            randomSatoshis,
+          );
+          amountToUse = convertedObj.float;
+        }
+      }
+      const responsePaymentId = await createPayment(amountToUse, to, apiBaseUrl);
+      setPaymentId(responsePaymentId);
+      return responsePaymentId;
     } catch (err) {
-      console.error('Error creating payment ID:', err)
-      return undefined
+      console.error('Error creating payment ID:', err);
+      return undefined;
+    } finally {
+      creatingPaymentIdRef.current = false;
     }
-  }, [disablePaymentId, apiBaseUrl, isFiat, randomSatoshis])
+  }, [disablePaymentId, apiBaseUrl, randomSatoshis])

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In react/lib/components/PayButton/PayButton.tsx around lines 139-161, the
current getPaymentId can call createPayment with the wrong units when
convertedAmount is not yet available and it can trigger duplicate concurrent
createPayment calls; fix by (1) if currency is fiat or randomSatoshis and
convertedAmount is undefined, await the price/conversion helper to compute the
proper crypto amount in the server-expected unit before calling createPayment,
(2) add a simple in-flight guard (e.g., an internal promise or boolean lock) so
concurrent calls reuse the same in-flight request rather than issuing duplicate
createPayment requests, (3) ensure you still setPaymentId and return the result,
and (4) update the hook dependency list to include any new helpers/vars you
reference (like paymentId or price helper) so useCallback is stable.


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