diff --git a/react/lib/components/PayButton/PayButton.tsx b/react/lib/components/PayButton/PayButton.tsx index e33a406e..ef56c665 100644 --- a/react/lib/components/PayButton/PayButton.tsx +++ b/react/lib/components/PayButton/PayButton.tsx @@ -13,7 +13,6 @@ import { isValidCashAddress, isValidXecAddress, CurrencyObject, - generatePaymentId, getCurrencyObject, isPropsTrue, setupAltpaymentSocket, @@ -21,6 +20,7 @@ import { 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 { @@ -105,18 +105,21 @@ export const PayButton = ({ const [currencyObj, setCurrencyObj] = useState(); const [cryptoAmount, setCryptoAmount] = useState(); + const [convertedAmount, setConvertedAmount] = useState(); + const [convertedCurrencyObj, setConvertedCurrencyObj] = useState(); + const [price, setPrice] = useState(0); const [newTxs, setNewTxs] = useState(); const priceRef = useRef(price); const cryptoAmountRef = useRef(cryptoAmount); - - - const [paymentId] = useState(!disablePaymentId ? generatePaymentId(8) : undefined); + const [paymentId, setPaymentId] = useState(undefined); const [addressType, setAddressType] = useState( getCurrencyTypeFromAddress(to), ); + + useEffect(() => { priceRef.current = price; }, [price]); @@ -133,16 +136,56 @@ export const PayButton = ({ } }, 300); }; + + const getPaymentId = useCallback(async ( + currency: Currency, + amount: number, + convertedAmount: number | undefined, + to: string | undefined, + ): Promise => { + 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 => { - 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); @@ -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 () => { @@ -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]); @@ -347,6 +403,8 @@ export const PayButton = ({ newTxs={newTxs} disableSound={disableSound} transactionText={transactionText} + convertedCurrencyObj={convertedCurrencyObj} + setConvertedCurrencyObj={setConvertedCurrencyObj} /> {errorMsg && (

{ const [success, setSuccess] = useState(false); @@ -247,6 +251,8 @@ export const PaymentDialog = ({ newTxs={newTxs} disableSound={disableSound} transactionText={transactionText} + convertedCurrencyObj={convertedCurrencyObj} + setConvertedCurrencyObj={setConvertedCurrencyObj} foot={success && ( = props => { altpaymentError, setAltpaymentError, isChild, - } = props + convertedCurrencyObj, + setConvertedCurrencyObj = () => {}, + } = props; const [loading, setLoading] = useState(true) @@ -539,14 +543,28 @@ export const Widget: React.FunctionComponent = 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]) @@ -559,11 +577,17 @@ export const Widget: React.FunctionComponent = 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}`, diff --git a/react/lib/components/Widget/WidgetContainer.tsx b/react/lib/components/Widget/WidgetContainer.tsx index 37ace822..0d467bbc 100644 --- a/react/lib/components/Widget/WidgetContainer.tsx +++ b/react/lib/components/Widget/WidgetContainer.tsx @@ -11,7 +11,6 @@ import { Currency, CurrencyObject, Transaction, - generatePaymentId, getCurrencyTypeFromAddress, isCrypto, isGreaterThanZero, @@ -20,6 +19,7 @@ import { shouldTriggerOnSuccess, isPropsTrue, } from '../../util'; +import { createPayment } from '../../util/api-client'; import Widget, { WidgetProps } from './Widget'; @@ -51,6 +51,7 @@ export interface WidgetContainerProps setNewTxs: Function disableSound?: boolean transactionText?: string + convertedCurrencyObj?: CurrencyObject; } const snackbarOptionsSuccess: OptionsObject = { @@ -101,7 +102,7 @@ export const WidgetContainer: React.FunctionComponent = let { to, opReturn, - disablePaymentId, + disablePaymentId = isPropsTrue(props.disablePaymentId), paymentId, amount, setAmount, @@ -131,6 +132,8 @@ export const WidgetContainer: React.FunctionComponent = isChild, disableSound, transactionText, + convertedCurrencyObj, + setConvertedCurrencyObj, ...widgetProps } = props; const [internalCurrencyObj, setInternalCurrencyObj] = useState(); @@ -141,16 +144,36 @@ export const WidgetContainer: React.FunctionComponent = const thisSetNewTxs = setNewTxs ?? setInternalNewTxs; const [thisPaymentId, setThisPaymentId] = useState(); + const [fetchingPaymentId, setFetchingPaymentId] = useState(); const [thisPrice, setThisPrice] = useState(0); const [usdPrice, setUsdPrice] = useState(0); useEffect(() => { - if ((paymentId === undefined || paymentId === '') && !disablePaymentId) { - const newPaymentId = generatePaymentId(8); - setThisPaymentId(newPaymentId) - } else { - setThisPaymentId(paymentId) + if ( + fetchingPaymentId !== undefined || + thisPaymentId !== undefined + ) { + return } - }, [paymentId, disablePaymentId]); + setFetchingPaymentId(true) + const initializePaymentId = async () => { + if (paymentId === undefined && !disablePaymentId) { + if (to) { + try { + const responsePaymentId = await createPayment(amount, to, apiBaseUrl); + setThisPaymentId(responsePaymentId); + setFetchingPaymentId(false); + } catch (error) { + console.error('Error creating payment ID:', error); + } + } + } else { + setThisPaymentId(paymentId); + setFetchingPaymentId(false); + } + }; + + initializePaymentId(); + }, [paymentId, disablePaymentId, amount, to, apiBaseUrl]); const [success, setSuccess] = useState(false); const { enqueueSnackbar } = useSnackbar(); @@ -316,6 +339,8 @@ export const WidgetContainer: React.FunctionComponent = disableAltpayment={disableAltpayment} contributionOffset={contributionOffset} transactionText={transactionText} + convertedCurrencyObj={convertedCurrencyObj} + setConvertedCurrencyObj={setConvertedCurrencyObj} /> ); diff --git a/react/lib/util/api-client.ts b/react/lib/util/api-client.ts index cdcb1e13..dc3fd8fa 100644 --- a/react/lib/util/api-client.ts +++ b/react/lib/util/api-client.ts @@ -89,6 +89,24 @@ export const getTransactionDetails = async ( return res.json(); }; +export const createPayment = async ( + amount: string | number | undefined, + address: string, + rootUrl = config.apiBaseUrl, +): Promise => { + const { data, status } = await axios.post( + `${rootUrl}/api/payments/paymentId`, + { amount, address } + ); + + if (status === 200) { + return data.paymentId; + } + throw new Error("Failed to generate payment Id.") // WIP + +}; + + export default { getAddressDetails, getTransactionDetails,