Skip to content

Conversation

@lissavxo
Copy link
Collaborator

@lissavxo lissavxo commented Nov 5, 2025

Related to #66

Depends on

Description

Regenerate paymentId when amount changes in editable mode

Test plan

make sure paymentId is created everytime amount changes, can check in the create paymentId request if the amount of the payload is the same of the amount displayed in the dialog

Summary by CodeRabbit

  • New Features

    • On-demand remote creation of payment IDs during checkout.
    • Persistent fiat-to-crypto conversion state shared across dialogs and widgets.
    • Formatted numeric amount editing with a draft-and-confirm workflow.
  • Improvements

    • Reactive payment ID fetching/updating when amounts or dialog state change; avoids redundant requests.
    • Converted amounts and currency details displayed and kept in sync across the payment UI.
    • QR/loading and copy-to-clipboard behaviors refined.
  • Chores

    • Added numeric-formatting dependency and new currency code mappings.
  • Tests

    • New tests for payment ID generation, fiat/crypto flows, dialog interactions, and QR copy behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 5, 2025

Walkthrough

Replaces client-side payment-id generation with server-created paymentId via new createPayment; adds convertedCurrencyObj state and converted-amount flows; threads paymentId and conversion props through PayButton → PaymentDialog → WidgetContainer → Widget; adds amount-editing UI and tests; adds react-number-format dependency.

Changes

Cohort / File(s) Summary
Pay button
react/lib/components/PayButton/PayButton.tsx
Add local paymentId state and async getPaymentId (calls createPayment); compute effective amount on dialog open/inputs change; wire convertedCurrencyObj state; forward paymentId, convertedCurrencyObj, and setConvertedCurrencyObj to PaymentDialog.
Payment dialog
react/lib/components/PaymentDialog/PaymentDialog.tsx
Add optional props convertedCurrencyObj?: CurrencyObject and setConvertedCurrencyObj?: Function to PaymentDialogProps; propagate them into WidgetContainer invocation.
Widget
react/lib/components/Widget/Widget.tsx
Expand WidgetProps with convertedCurrencyObj, setConvertedCurrencyObj, setPaymentId; add draftAmount editing (uses react-number-format), qr/loading flags, conversion-state wiring, and lazy createPayment initialization for paymentId.
Widget container
react/lib/components/Widget/WidgetContainer.tsx
Remove prior client-side generatePaymentId logic; add internal internalPaymentId state; accept convertedCurrencyObj?: CurrencyObject; forward conversion/paymentId setters into Widget.
API client
react/lib/util/api-client.ts
Add exported createPayment(amount, address, rootUrl?) that POSTs to /api/payments/paymentId, normalizes amount using currency metadata/decimals, and returns server-generated paymentId (throws on non-200).
Constants
react/lib/util/constants.ts
Add CURRENCY_TYPES_MAP mapping crypto ids to display codes (bitcoincash: 'BCH', ecash: 'XEC').
Deps
react/package.json
Add dependency react-number-format@^5.4.4.
Imports & public signatures
.../PayButton/*.tsx, .../Widget/*.tsx, .../PaymentDialog/*.tsx
Replace generatePaymentId imports with createPayment; update public prop typings to include optional convertedCurrencyObj and setConvertedCurrencyObj, and add setPaymentId wiring where applicable.
Tests
react/lib/tests/components/PayButton.test.tsx, react/lib/tests/components/Widget.test.tsx
Add/extend tests and mocks for PaymentDialog and createPayment; assert paymentId creation behavior across crypto/fiat flows, amount edits, disable flags, re-renders, and QR copy behavior.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant PB as PayButton
    participant API as API (createPayment)
    participant PD as PaymentDialog
    participant WC as WidgetContainer
    participant W as Widget

    User->>PB: click Pay
    PB->>PB: compute effective amount (fiat conversion or crypto amount)
    alt need paymentId
        PB->>API: POST /api/payments/paymentId (amount, address)
        API-->>PB: paymentId
        PB->>PB: setPaymentId(paymentId)
    else reuse paymentId
        PB-->>PB: reuse existing paymentId
    end
    PB->>PD: open(paymentId, convertedCurrencyObj, setConvertedCurrencyObj)
    PD->>WC: forward paymentId, convertedCurrencyObj & setter
    WC->>W: pass paymentId, convertedCurrencyObj, qrLoading
    W->>W: render QR/amounts using paymentId and convertedCurrencyObj
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay attention to async/error handling, race conditions and cleanup for in-flight createPayment calls in PayButton/Widget effects.
  • Verify consistent typings/optionality for convertedCurrencyObj and setConvertedCurrencyObj across components.
  • Review amount normalization/decimal handling in createPayment and ensure currency mappings (CURRENCY_TYPES_MAP, DECIMALS) are correct.
  • Validate tests’ mocked behaviors align with new API and dialog interfaces.

Possibly related PRs

Suggested reviewers

  • lissavxo

Poem

🐰 I hopped to POST and fetch a key,

A tiny payment sprung from me.
Fiat turned crypto, numbers align,
Passed down the tree where QR codes shine.
Hop, click, hum — the payment’s fine!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective: regenerating paymentId when the amount is updated, which is the core feature delivered across multiple component changes.
Description check ✅ Passed The description follows the template structure with Related issue, Depends on section, and Test plan. However, the description and test plan are quite brief and lack technical depth about the implementation approach.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/regenerate-paymentId-editable

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@lissavxo lissavxo marked this pull request as ready for review November 5, 2025 23:57
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
react/lib/components/PayButton/PayButton.tsx (1)

226-275: Fix the useEffect dependency array and add missing cleanup.

The effect on lines 226-275 has two issues:

  1. Stale closure risk: Missing critical dependencies (to, apiBaseUrl, wsBaseUrl, currency, price, paymentId, etc.). If these values change while the dialog is open, the WebSocket will be set up with stale values.

  2. Incomplete cleanup: The cleanup function only disconnects altpaymentSocket (lines 269-271) but never disconnects txsSocket, creating a potential memory leak.

Add all used values to the dependency array and ensure txsSocket is properly disconnected in the cleanup function.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e96e22 and 9819d0b.

📒 Files selected for processing (5)
  • react/lib/components/PayButton/PayButton.tsx (6 hunks)
  • react/lib/components/PaymentDialog/PaymentDialog.tsx (3 hunks)
  • react/lib/components/Widget/Widget.tsx (7 hunks)
  • react/lib/components/Widget/WidgetContainer.tsx (6 hunks)
  • react/lib/util/api-client.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
react/lib/components/PaymentDialog/PaymentDialog.tsx (1)
react/lib/util/types.ts (1)
  • CurrencyObject (4-8)
react/lib/components/PayButton/PayButton.tsx (6)
react/lib/util/types.ts (4)
  • CurrencyObject (4-8)
  • Transaction (10-21)
  • CryptoCurrency (138-138)
  • Currency (140-140)
react/lib/util/address.ts (1)
  • getCurrencyTypeFromAddress (26-34)
react/lib/util/currency.ts (1)
  • isFiat (4-6)
react/lib/util/api-client.ts (1)
  • createPayment (92-107)
react/lib/util/socket.ts (2)
  • setupChronikWebSocket (141-153)
  • setupAltpaymentSocket (95-114)
react/lib/util/satoshis.ts (1)
  • getCurrencyObject (8-41)
react/lib/components/Widget/WidgetContainer.tsx (3)
react/lib/components/Widget/Widget.tsx (1)
  • WidgetProps (44-105)
react/lib/util/types.ts (1)
  • CurrencyObject (4-8)
react/lib/util/format.ts (1)
  • isPropsTrue (66-75)
react/lib/components/Widget/Widget.tsx (4)
react/lib/util/types.ts (1)
  • CurrencyObject (4-8)
react/lib/util/currency.ts (1)
  • isFiat (4-6)
react/lib/util/api-client.ts (1)
  • createPayment (92-107)
react/lib/util/satoshis.ts (1)
  • getCurrencyObject (8-41)
🔇 Additional comments (5)
react/lib/components/PaymentDialog/PaymentDialog.tsx (1)

66-67: LGTM: Clean prop propagation.

The new convertedCurrencyObj and setConvertedCurrencyObj props are correctly threaded through the component to WidgetContainer without altering existing logic.

Also applies to: 126-127, 254-255

react/lib/components/Widget/WidgetContainer.tsx (2)

140-141: LGTM: Proper state initialization for payment ID flow.

The local thisPaymentId and fetchingPaymentId state (Lines 140-141) correctly support the new lazy payment ID creation pattern. The state is properly initialized from props and passed to the Widget component (Lines 282, 311-313).

Also applies to: 282-282, 311-313


51-51: LGTM: Clean propagation of conversion state.

The convertedCurrencyObj prop is correctly added to the interface (Line 51), destructured (Lines 129-130), and forwarded to the Widget (Lines 309-310).

Also applies to: 129-130, 309-310

react/lib/components/Widget/Widget.tsx (2)

427-435: LGTM: Well-designed helper for state coordination.

The updateConvertedCurrencyObj helper properly resets payment-related state when the converted currency changes, ensuring consistency across the payment flow.


704-720: LGTM: Correct prioritization of converted currency.

The URL generation logic correctly prioritizes convertedCurrencyObj from props (computed in PayButton) over local calculation, ensuring consistency in the payment flow.

Comment on lines 563 to 613
useEffect(() => {
if (
fetchingPaymentId !== undefined ||
paymentId !== undefined
) {
return
}
if (setFetchingPaymentId) {
setFetchingPaymentId(true)
}
const initializePaymentId = async () => {
if (paymentId === undefined && !disablePaymentId) {
if (to) {
try {
const amountToUse =
(isFiat(currency) || randomSatoshis) && convertedCurrencyObj
? convertedCurrencyObj.float
: props.amount
const responsePaymentId = await createPayment(amountToUse || undefined, to, apiBaseUrl);
if (setPaymentId) {
setPaymentId(responsePaymentId);
}
if (setFetchingPaymentId) {
setFetchingPaymentId(false);
}
} catch (error) {
console.error('Error creating payment ID:', error);
}
}
} else {
if (setPaymentId) {
setPaymentId(paymentId);
}
if (setFetchingPaymentId) {
setFetchingPaymentId(false);
}
}
};

initializePaymentId();
}, [paymentId, disablePaymentId, props.amount, to, apiBaseUrl, setPaymentId, setFetchingPaymentId, fetchingPaymentId, convertedCurrencyObj]);
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

Critical: Effect guard prevents paymentId regeneration on amount changes.

This effect has several issues that prevent it from fulfilling the PR's objective to "regenerate paymentId when amount is updated":

  1. Lines 564-569: The guard if (fetchingPaymentId !== undefined || paymentId !== undefined) return combined with setting fetchingPaymentId to true means this effect will never run again after the first execution, even when the amount changes.

  2. Line 603: The dependency array includes fetchingPaymentId, which this effect sets, creating potential for unexpected re-runs or infinite loops.

  3. Lines 592-599: The else branch redundantly sets paymentId to its existing value.

  4. Missing dependency: The effect doesn't track convertedCurrencyObj changes properly to trigger regeneration.

Consider this revised implementation:

  useEffect(() => {
-   if (
-     fetchingPaymentId !== undefined ||
-     paymentId !== undefined
-   ) {
+   // Skip if disabled or already fetching
+   if (disablePaymentId || fetchingPaymentId) {
      return
    }
-   if (setFetchingPaymentId) {
-     setFetchingPaymentId(true)
-   }
+   
    const initializePaymentId = async () => {
-     if (paymentId === undefined && !disablePaymentId) {
-       if (to) {
-         try {
+     if (to) {
+       try {
+         if (setFetchingPaymentId) {
+           setFetchingPaymentId(true);
+         }
+         
          const amountToUse =
                  (isFiat(currency) || randomSatoshis) && convertedCurrencyObj
                  ? convertedCurrencyObj.float
                  : props.amount
          const responsePaymentId = await createPayment(amountToUse || undefined, to, apiBaseUrl);
          if (setPaymentId) {
            setPaymentId(responsePaymentId);
          }
-         if (setFetchingPaymentId) {
-           setFetchingPaymentId(false);
-         }
-       } catch (error) {
-         console.error('Error creating payment ID:', error);
-       }
+       } catch (error) {
+         console.error('Error creating payment ID:', error);
+       } finally {
+         if (setFetchingPaymentId) {
+           setFetchingPaymentId(false);
+         }
        }
-     } else {
-       if (setPaymentId) {
-         setPaymentId(paymentId);
-       }
-       if (setFetchingPaymentId) {
-         setFetchingPaymentId(false);
-       }
      }
    };

    initializePaymentId();
- }, [paymentId, disablePaymentId, props.amount, to, apiBaseUrl, setPaymentId, setFetchingPaymentId, fetchingPaymentId, convertedCurrencyObj]);
+ }, [disablePaymentId, props.amount, convertedCurrencyObj, to, apiBaseUrl, setPaymentId, setFetchingPaymentId, currency, randomSatoshis, isFiat]);

Additionally, ensure that paymentId is cleared when convertedCurrencyObj or props.amount changes to trigger regeneration.

📝 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
useEffect(() => {
if (
fetchingPaymentId !== undefined ||
paymentId !== undefined
) {
return
}
if (setFetchingPaymentId) {
setFetchingPaymentId(true)
}
const initializePaymentId = async () => {
if (paymentId === undefined && !disablePaymentId) {
if (to) {
try {
const amountToUse =
(isFiat(currency) || randomSatoshis) && convertedCurrencyObj
? convertedCurrencyObj.float
: props.amount
const responsePaymentId = await createPayment(amountToUse || undefined, to, apiBaseUrl);
if (setPaymentId) {
setPaymentId(responsePaymentId);
}
if (setFetchingPaymentId) {
setFetchingPaymentId(false);
}
} catch (error) {
console.error('Error creating payment ID:', error);
}
}
} else {
if (setPaymentId) {
setPaymentId(paymentId);
}
if (setFetchingPaymentId) {
setFetchingPaymentId(false);
}
}
};
initializePaymentId();
}, [paymentId, disablePaymentId, props.amount, to, apiBaseUrl, setPaymentId, setFetchingPaymentId, fetchingPaymentId, convertedCurrencyObj]);
useEffect(() => {
// Skip if disabled or already fetching
if (disablePaymentId || fetchingPaymentId) {
return
}
const initializePaymentId = async () => {
if (to) {
try {
if (setFetchingPaymentId) {
setFetchingPaymentId(true);
}
const amountToUse =
(isFiat(currency) || randomSatoshis) && convertedCurrencyObj
? convertedCurrencyObj.float
: props.amount
const responsePaymentId = await createPayment(amountToUse || undefined, to, apiBaseUrl);
if (setPaymentId) {
setPaymentId(responsePaymentId);
}
} catch (error) {
console.error('Error creating payment ID:', error);
} finally {
if (setFetchingPaymentId) {
setFetchingPaymentId(false);
}
}
}
};
initializePaymentId();
}, [disablePaymentId, props.amount, convertedCurrencyObj, to, apiBaseUrl, setPaymentId, setFetchingPaymentId, currency, randomSatoshis, isFiat]);
🤖 Prompt for AI Agents
In react/lib/components/Widget/Widget.tsx around lines 563 to 603, the current
effect short-circuits future runs by returning when paymentId or
fetchingPaymentId are defined, includes fetchingPaymentId in the dependency
array, and redundantly re-sets paymentId; change the logic so the effect watches
props.amount and convertedCurrencyObj (add both to deps), remove
fetchingPaymentId from the deps, and do NOT return early just because paymentId
exists — instead only skip if a fetch is already in progress (check a
local/closure ref or the boolean setter state value), clear paymentId when
props.amount or convertedCurrencyObj change to force regeneration, set fetching
flag true before calling createPayment and false in finally, only call
setPaymentId when a new id is returned (avoid setting the same value), and
handle errors in catch (logging and ensuring fetching flag is reset).

@Klakurka Klakurka requested a review from chedieck November 6, 2025 06:23
@Klakurka Klakurka added the enhancement (behind the scenes) Stuff that users won't see label Nov 6, 2025
@lissavxo lissavxo force-pushed the fix/regenerate-paymentId-editable branch from 9819d0b to 6c3e875 Compare November 6, 2025 22:38
Copy link
Collaborator

@chedieck chedieck left a comment

Choose a reason for hiding this comment

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

Newly generated paymentIds, when changing amount, are being sent for the same amount.

Image

Might have to do with this rabbitAI comment:
https://github.com/PayButton/paybutton/pull/572/files#r2496572271

Also the editable widgets in http://localhost:10001/ are showing Send any amount of XEC instead of the defined 1000.69 XEC amount.

@lissavxo lissavxo force-pushed the fix/regenerate-paymentId-editable branch from 6c3e875 to beff2dc Compare November 19, 2025 00:44
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
react/lib/components/Widget/Widget.tsx (1)

517-557: Critical: Effect still prevents paymentId regeneration on amount changes.

This effect has the same critical issues identified in the previous review:

  1. Lines 518-522: The guard if (fetchingPaymentId !== undefined || paymentId !== undefined) return prevents the effect from running again after the first execution. Once fetchingPaymentId is set to true (line 525), it's never reset to undefined, so the effect never runs again even when the amount changes.

  2. Line 557: Including fetchingPaymentId in the dependency array while the effect sets it to true creates confusing behavior.

  3. Lines 546-553: The else branch redundantly sets paymentId to its existing value.

  4. Missing regeneration logic: The effect doesn't track when convertedCurrencyObj or props.amount changes to trigger regeneration, which is the core PR objective.

The PR states: "Regenerate paymentId when amount changes in editable mode." This effect does not implement that behavior.

Apply this fix to enable regeneration:

  useEffect(() => {
-   if (
-     fetchingPaymentId !== undefined ||
-     paymentId !== undefined
-   ) {
+   // Skip if disabled or already fetching
+   if (disablePaymentId || fetchingPaymentId) {
      return
    }
-   if (setFetchingPaymentId) {
-     setFetchingPaymentId(true)
-   }
+   
    const initializePaymentId = async () => {
-     if (paymentId === undefined && !disablePaymentId) {
-       if (to) {
-         try {
+     if (to) {
+       try {
+         if (setFetchingPaymentId) {
+           setFetchingPaymentId(true);
+         }
+         
          const amountToUse =
                  (isFiat(currency) || randomSatoshis) && convertedCurrencyObj
                  ? convertedCurrencyObj.float
                  : props.amount
          const responsePaymentId = await createPayment(amountToUse || undefined, to, apiBaseUrl);
          if (setPaymentId) {
            setPaymentId(responsePaymentId);
          }
-         if (setFetchingPaymentId) {
-           setFetchingPaymentId(false);
-         }
-       } catch (error) {
-         console.error('Error creating payment ID:', error);
-       }
+       } catch (error) {
+         console.error('Error creating payment ID:', error);
+       } finally {
+         if (setFetchingPaymentId) {
+           setFetchingPaymentId(false);
+         }
        }
-     } else {
-       if (setPaymentId) {
-         setPaymentId(paymentId);
-       }
-       if (setFetchingPaymentId) {
-         setFetchingPaymentId(false);
-       }
      }
    };

    initializePaymentId();
- }, [paymentId, disablePaymentId, props.amount, to, apiBaseUrl, setPaymentId, setFetchingPaymentId, fetchingPaymentId, convertedCurrencyObj]);
+ }, [disablePaymentId, props.amount, convertedCurrencyObj, to, apiBaseUrl, setPaymentId, setFetchingPaymentId, currency, randomSatoshis]);

Note: The updateConvertedCurrencyObj helper (lines 259-267) already clears paymentId when the converted currency changes, which should trigger this effect to regenerate the ID.

🧹 Nitpick comments (2)
react/lib/components/Widget/Widget.tsx (2)

259-267: Consider renaming for clarity.

The function clears payment-related state when the converted currency changes, which is correct for triggering paymentId regeneration. However, the name updateConvertedCurrencyObj suggests it updates the currency object itself, when it actually clears payment state after setting the currency object.

Consider a more descriptive name:

- const updateConvertedCurrencyObj = useCallback((convertedObj: CurrencyObject | null) => {
+ const setConvertedCurrencyObjAndClearPayment = useCallback((convertedObj: CurrencyObject | null) => {
    setConvertedCurrencyObj(convertedObj);
    if(setPaymentId){
      setPaymentId(undefined);
    }
    if(setFetchingPaymentId){
      setFetchingPaymentId(undefined);
    }
  }, [setConvertedCurrencyObj, setPaymentId, setFetchingPaymentId]);

643-673: Consider adding convertedCurrencyObj to dependencies.

Lines 645-646 use convertedCurrencyObj as a fallback when calculating the converted amount. While the current logic should keep convertedCurrencyObj in sync (via the effect at lines 604-630), this effect doesn't list convertedCurrencyObj in its dependency array (line 673).

Add convertedCurrencyObj to the dependency array to ensure the URL regenerates when the converted currency changes:

- }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable])
+ }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, convertedCurrencyObj])

This ensures that if convertedCurrencyObj updates independently (e.g., from a parent component), the URL will be recalculated.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c3e875 and beff2dc.

📒 Files selected for processing (2)
  • react/lib/components/Widget/Widget.tsx (7 hunks)
  • react/lib/components/Widget/WidgetContainer.tsx (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
react/lib/components/Widget/Widget.tsx (4)
react/lib/util/types.ts (2)
  • Currency (140-140)
  • CurrencyObject (4-8)
react/lib/util/currency.ts (1)
  • isFiat (4-6)
react/lib/util/api-client.ts (1)
  • createPayment (92-107)
react/lib/util/satoshis.ts (1)
  • getCurrencyObject (8-41)
react/lib/components/Widget/WidgetContainer.tsx (3)
react/lib/components/Widget/Widget.tsx (1)
  • WidgetProps (49-110)
react/lib/util/types.ts (1)
  • CurrencyObject (4-8)
react/lib/util/format.ts (1)
  • isPropsTrue (66-75)
🔇 Additional comments (5)
react/lib/components/Widget/WidgetContainer.tsx (3)

26-26: LGTM: Interface changes correctly encapsulate internal state.

The interface properly omits internal setters (setPaymentId, setFetchingPaymentId, fetchingPaymentId) from the public surface while adding the necessary convertedCurrencyObj prop for currency conversion state.

Also applies to: 53-53


104-104: LGTM: Proper default handling and destructuring.

Using isPropsTrue for the disablePaymentId default correctly handles string and boolean values.

Also applies to: 134-136


145-146: LGTM: State management and prop forwarding are correct.

The payment state is properly initialized and all new props are correctly forwarded to the Widget component with appropriate setters.

Also applies to: 314-318

react/lib/components/Widget/Widget.tsx (2)

47-47: LGTM: Imports and props correctly defined.

The createPayment import and new props for payment ID management and currency conversion are properly integrated.

Also applies to: 105-109, 165-169


604-630: LGTM: Currency conversion logic is sound.

The effect correctly handles both user-edited amounts and programmatic amount changes, computing the currency object and triggering payment ID regeneration through updateConvertedCurrencyObj.

@lissavxo lissavxo force-pushed the fix/regenerate-paymentId-editable branch from beff2dc to 649ac29 Compare November 21, 2025 17:13
@lissavxo lissavxo force-pushed the fix/regenerate-paymentId-editable branch from 649ac29 to f264fe9 Compare November 25, 2025 19:51
@lissavxo lissavxo requested a review from chedieck November 25, 2025 21:17
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
react/lib/components/PayButton/PayButton.tsx (1)

168-192: Critical: PaymentId is not regenerated when amount changes.

Line 178 checks !paymentId before calling getPaymentId, which prevents regeneration when the amount changes if a paymentId already exists. This directly contradicts the PR objective: "regenerate paymentId when amount is updated."

Scenario:

  1. User opens payment dialog with amount=100 → paymentId is created
  2. User changes amount to 200
  3. User clicks button → Line 178 evaluates to false (paymentId exists)
  4. getPaymentId is NOT called → old paymentId persists with wrong amount

To fix this, remove the !paymentId check and add a useEffect to clear paymentId when amount/convertedAmount changes:

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

Additionally, add this effect to clear paymentId when amount changes:

useEffect(() => {
  setPaymentId(undefined);
}, [amount, convertedAmount]);
react/lib/components/Widget/Widget.tsx (1)

573-613: Simplify confusing paymentId initialization logic.

The effect has several issues that make it difficult to understand and maintain:

  1. Lines 574-579: The guard checking fetchingPaymentId !== undefined || paymentId !== undefined is confusing. While it may technically allow regeneration (when both are set to undefined), the logic is not intuitive.

  2. Lines 602-609: The else branch redundantly sets paymentId to itself when it already exists, which serves no purpose and adds confusion.

  3. Line 613: Including fetchingPaymentId in the dependency array when the effect sets it can cause unexpected behavior.

  4. Missing clarity: The effect doesn't clearly express the intent to regenerate paymentId when amount/conversion changes.

Simplify the logic as suggested in past review comments:

  useEffect(() => {
-   if (
-     fetchingPaymentId !== undefined ||
-     paymentId !== undefined
-   ) {
+   // Skip if disabled or already fetching
+   if (disablePaymentId || fetchingPaymentId) {
      return
    }
-   if (setFetchingPaymentId) {
-     setFetchingPaymentId(true)
-   }
+   
    const initializePaymentId = async () => {
-     if (paymentId === undefined && !disablePaymentId) {
-       if (to) {
-         try {
+     if (to) {
+       try {
+         if (setFetchingPaymentId) {
+           setFetchingPaymentId(true);
+         }
+         
          const amountToUse =
                  (isFiat(currency) || randomSatoshis) && convertedCurrencyObj
                  ? convertedCurrencyObj.float
                  : props.amount
          const responsePaymentId = await createPayment(amountToUse || undefined, to, apiBaseUrl);
          if (setPaymentId) {
            setPaymentId(responsePaymentId);
          }
-         if (setFetchingPaymentId) {
-           setFetchingPaymentId(false);
-         }
-       } catch (error) {
-         console.error('Error creating payment ID:', error);
-       }
+       } catch (error) {
+         console.error('Error creating payment ID:', error);
+       } finally {
+         if (setFetchingPaymentId) {
+           setFetchingPaymentId(false);
+         }
        }
-     } else {
-       if (setPaymentId) {
-         setPaymentId(paymentId);
-       }
-       if (setFetchingPaymentId) {
-         setFetchingPaymentId(false);
-       }
      }
    };

    initializePaymentId();
- }, [paymentId, disablePaymentId, props.amount, to, apiBaseUrl, setPaymentId, setFetchingPaymentId, fetchingPaymentId, convertedCurrencyObj]);
+ }, [disablePaymentId, props.amount, convertedCurrencyObj?.float, to, apiBaseUrl, setPaymentId, setFetchingPaymentId, currency, randomSatoshis]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f264fe9 and ce27eb3.

📒 Files selected for processing (3)
  • react/lib/components/PayButton/PayButton.tsx (5 hunks)
  • react/lib/components/Widget/Widget.tsx (7 hunks)
  • react/lib/util/api-client.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run build
🔇 Additional comments (1)
react/lib/components/PayButton/PayButton.tsx (1)

145-166: Fix dependency array to remove stable references.

The dependency array includes isFiat (a stable utility function) and setPaymentId (a stable setState function). These don't need to be dependencies as they never change.

Apply this fix:

-  }, [disablePaymentId, apiBaseUrl, randomSatoshis, setPaymentId])
+  }, [disablePaymentId, apiBaseUrl, randomSatoshis])

Likely an incorrect or invalid review comment.

Comment on lines 312 to 337
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 && !convertedCurrencyObj){
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]);
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 | 🟠 Major

Conversion guards prevent recalculation when amount changes.

Lines 314 and 325 use !convertedCurrencyObj guards that prevent conversion recalculation when the amount changes. Once convertedCurrencyObj is set, these blocks never execute again, even if amount, price, or randomSatoshis change.

Consider removing these guards or adding amount/price to trigger recalculation:

  useEffect(() => {
    if (currencyObj && isFiat(currency) && price) {
-     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 && !convertedCurrencyObj){
      const convertedObj = getCurrencyObject(
        amount as number,
        addressType,
        randomSatoshis,
      ); 
      setCryptoAmount(convertedObj.string);
      setConvertedAmount(convertedObj.float);
      setConvertedCurrencyObj(convertedObj);
    } else if (!isFiat(currency) && !randomSatoshis) {
      setCryptoAmount(amount?.toString());
    }
  }, [price, currencyObj, amount, currency, randomSatoshis, to]);

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

🤖 Prompt for AI Agents
In react/lib/components/PayButton/PayButton.tsx around lines 312 to 337, the
current guards using !convertedCurrencyObj (lines ~314 and ~325) stop
recalculation once convertedCurrencyObj is set, so changes to amount, price or
randomSatoshis never trigger updates; remove those !convertedCurrencyObj checks
(or replace them with explicit comparisons against current convertedCurrencyObj
values) so the effect recalculates and calls getCurrencyObject whenever its
dependencies (price, currencyObj, amount, currency, randomSatoshis, to) change,
and keep setting cryptoAmount, convertedAmount and convertedCurrencyObj
accordingly.

Comment on lines 92 to 107
export const createPayment = async (
amount: string | number | undefined,
address: string,
rootUrl = config.apiBaseUrl,
): Promise<string | undefined> => {
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. Status: ${status}, Response: ${JSON.stringify(data)}`)

};
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

Critical: Add error handling and response validation.

The function lacks try-catch wrapping and doesn't validate that data.paymentId exists before returning it. This can cause:

  1. Network/request failures: If axios.post throws (network error, timeout, etc.), the error propagates unhandled to the caller.
  2. Missing paymentId: If status is 200 but data.paymentId is null/undefined, the function returns undefined without logging or throwing, silently failing.

Apply this fix:

 export const createPayment = async (
   amount: string | number | undefined,
   address: string,
   rootUrl = config.apiBaseUrl,
 ): Promise<string | undefined> => {
+  try {
     const { data, status } = await axios.post(
       `${rootUrl}/api/payments/paymentId`,
       { amount, address }
     );
   
     if (status === 200) {
+      if (!data.paymentId) {
+        throw new Error('Payment ID missing in response');
+      }
       return data.paymentId;
     }
     throw new Error(`Failed to generate payment ID. Status: ${status}, Response: ${JSON.stringify(data)}`);
+  } catch (error) {
+    if (axios.isAxiosError(error)) {
+      throw new Error(`Failed to generate payment ID: ${error.message}`);
+    }
+    throw error;
+  }
 };
📝 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
export const createPayment = async (
amount: string | number | undefined,
address: string,
rootUrl = config.apiBaseUrl,
): Promise<string | undefined> => {
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. Status: ${status}, Response: ${JSON.stringify(data)}`)
};
export const createPayment = async (
amount: string | number | undefined,
address: string,
rootUrl = config.apiBaseUrl,
): Promise<string | undefined> => {
try {
const { data, status } = await axios.post(
`${rootUrl}/api/payments/paymentId`,
{ amount, address }
);
if (status === 200) {
if (!data.paymentId) {
throw new Error('Payment ID missing in response');
}
return data.paymentId;
}
throw new Error(`Failed to generate payment ID. Status: ${status}, Response: ${JSON.stringify(data)}`);
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Failed to generate payment ID: ${error.message}`);
}
throw error;
}
};
🤖 Prompt for AI Agents
In react/lib/util/api-client.ts around lines 92 to 107, wrap the axios.post call
in a try-catch and validate the response before returning: catch any
axios/network errors and rethrow (or throw a new Error) with the
status/response/error details, check that status === 200 and that data &&
data.paymentId is a non-empty string (otherwise throw an informative Error
including the response body), and ensure the function consistently throws on
failures instead of returning undefined so callers always get a resolved
paymentId or a clear exception.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
react/lib/components/PayButton/PayButton.tsx (1)

339-364: Critical: Conversion guards still block paymentId regeneration on amount changes.

The guards on lines 341 and 352 (!convertedCurrencyObj) prevent recalculation when the amount changes, directly contradicting the PR objective. This issue was flagged in previous reviews but remains unresolved.

Scenario:

  1. Initial amount: $10 → convertedCurrencyObj calculated and set
  2. User changes amount to $20 in editable mode
  3. Effect runs (due to currencyObj or amount in deps)
  4. Guard !convertedCurrencyObj (line 341) returns true → block skipped
  5. convertedAmount and convertedCurrencyObj remain stale ($10 worth of crypto)
  6. PaymentId regeneration at lines 169-196 uses stale convertedAmount

Remove the guards to allow recalculation whenever dependencies change:

  useEffect(() => {
    if (currencyObj && isFiat(currency) && price) {
-     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 && !convertedCurrencyObj){
+   } else if (!isFiat(currency) && randomSatoshis){
      const convertedObj = getCurrencyObject(
        amount as number,
        addressType,
        randomSatoshis,
      );
      setCryptoAmount(convertedObj.string);
      setConvertedAmount(convertedObj.float);
      setConvertedCurrencyObj(convertedObj);
    } else if (!isFiat(currency) && !randomSatoshis) {
      setCryptoAmount(amount?.toString());
+     setConvertedAmount(undefined);
+     setConvertedCurrencyObj(undefined);
    }
  }, [price, currencyObj, amount, currency, randomSatoshis, to]);
react/lib/components/Widget/Widget.tsx (1)

655-682: Major: Fiat amount changes don't trigger paymentId regeneration in non-editable path.

Line 675 guards updateConvertedCurrencyObj behind !isFiat(currency), preventing paymentId from being cleared when fiat amounts change via props (non-user-edited path). This was flagged in earlier reviews but the guard remains.

Scenario:

  1. Parent updates amount prop from $10 to $20 (fiat currency, user hasn't edited)
  2. Effect runs (lines 670-680)
  3. Line 675: !isFiat(currency) → false for fiat
  4. updateConvertedCurrencyObj NOT called
  5. paymentId not cleared → regeneration skipped

Additionally, the dependency array (line 682) omits price, so fiat conversions won't recalculate when the exchange rate changes.

    } else if (thisAmount && thisAddressType) {
      cleanAmount = +thisAmount;

      const obj = getCurrencyObject(cleanAmount, currency, randomSatoshis);
      setThisCurrencyObject(obj);
-     if(!isFiat(currency)) {
-       updateConvertedCurrencyObj(obj);
-     }
      if (props.setCurrencyObject) {
        props.setCurrencyObject(obj);
      }
+     // For crypto, update converted currency (which clears paymentId)
+     if (!isFiat(currency)) {
+       updateConvertedCurrencyObj(obj);
+     } else if (price > 0) {
+       // For fiat, recalculate conversion and clear paymentId
+       const convertedAmount = obj.float / price;
+       const convertedObj = getCurrencyObject(
+         convertedAmount,
+         thisAddressType,
+         randomSatoshis,
+       );
+       updateConvertedCurrencyObj(convertedObj);
+     }
    }
- }, [thisAmount, currency, userEditedAmount])
+ }, [thisAmount, currency, userEditedAmount, price, thisAddressType, randomSatoshis, updateConvertedCurrencyObj])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89fc225 and 70952ba.

📒 Files selected for processing (2)
  • react/lib/components/PayButton/PayButton.tsx (5 hunks)
  • react/lib/components/Widget/Widget.tsx (18 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
react/lib/components/PayButton/PayButton.tsx (5)
react/lib/util/types.ts (4)
  • CurrencyObject (4-8)
  • Transaction (10-21)
  • CryptoCurrency (138-138)
  • Currency (140-140)
react/lib/util/address.ts (1)
  • getCurrencyTypeFromAddress (26-34)
react/lib/util/currency.ts (1)
  • isFiat (4-6)
react/lib/util/api-client.ts (1)
  • createPayment (92-107)
react/lib/util/satoshis.ts (1)
  • getCurrencyObject (8-41)
react/lib/components/Widget/Widget.tsx (7)
react/lib/altpayment/index.ts (3)
  • AltpaymentCoin (21-21)
  • AltpaymentPair (23-23)
  • AltpaymentError (25-25)
react/lib/util/types.ts (2)
  • Currency (140-140)
  • CurrencyObject (4-8)
react/lib/util/constants.ts (1)
  • DEFAULT_DONATION_RATE (21-21)
docs/darkmode.js (1)
  • theme (39-39)
react/lib/util/currency.ts (1)
  • isFiat (4-6)
react/lib/util/api-client.ts (1)
  • createPayment (92-107)
react/lib/util/satoshis.ts (1)
  • getCurrencyObject (8-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run build

Comment on lines +1243 to +1244
inputProps={{
min: 1,
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 | 🟡 Minor

Minor: Input constraint mismatch with validation logic.

The min: 1 constraint on the donation rate input (line 1244) differs from the validation logic at lines 268-271, which allows 0. Users can disable donation by toggling the heart icon (setting rate to 0), but if they type "0" directly, they may encounter browser validation messages.

This is a minor UX inconsistency rather than a bug—the clampDonationRate function correctly handles 0.

Consider either:

  1. Setting min: 0 to match validation, or
  2. Hiding the input entirely when donationEnabled is false (already shows/hides based on toggle)

The current behavior is acceptable since the toggle is the primary UI, but consistent constraints improve clarity.

🤖 Prompt for AI Agents
In react/lib/components/Widget/Widget.tsx around lines 1243-1244, the donation
rate input sets inputProps.min = 1 which contradicts validation
(clampDonationRate permits 0); update the component to make constraints
consistent by changing the input min to 0 to allow typing 0, or alternatively
conditionally hide/remove the numeric input when donationEnabled is false so
users can only use the toggle to disable donations—pick one approach and apply
it consistently so browser validation and internal validation align.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
react/lib/components/Widget/Widget.tsx (1)

1241-1248: Donation visibility logic looks good; min=1 input remains slightly inconsistent (duplicate of prior comment)

The footer now uses convertedCryptoAmount when available so donation UI visibility is based on the crypto amount, which is correct for fiat conversions:

const amountToCheck = hasPrice && convertedCryptoAmount !== undefined
  ? convertedCryptoAmount
  : thisCurrencyObject?.float
// ...
shouldShowDonationUI(amountToCheck, thisAddressType)

That logic is sound.

The donation rate input still uses min: 1 while clampDonationRate allows 0, so users can disable donation via the heart toggle but not by typing 0. This was already flagged in a previous review; it remains a minor UX nit rather than a blocker.

If you want to align them:

- inputProps={{
-   min: 1,
-   max: 99,
-   step: 1,
- }}
+ inputProps={{
+   min: 0,
+   max: 99,
+   step: 1,
+ }}

Also applies to: 1291-1295

🧹 Nitpick comments (2)
react/lib/components/Widget/Widget.tsx (2)

495-501: Editable amount UX via NumericFormat and draftAmount looks solid

The new draft‑amount flow is well‑structured:

  • draftAmount is derived from thisCurrencyObject.string via stripFormatting, so the input always reflects the formatted value.
  • NumericFormat keeps user input normalized and adds thousand separators without polluting underlying state.
  • applyDraftAmount validates, converts to a numeric value, then calls updateAmount to drive the same pipeline as programmatic changes.
  • The ✓ button is disabled when the draft is invalid or equal to the current amount (isDraftValid / isSameAmount), which avoids redundant work.

Overall this is a nice improvement for editable mode; I don’t see correctness issues here.

Also applies to: 956-974, 976-984, 1155-1182


921-943: Donation URL query string uses two amount params – confirm receiver behavior

In resolveUrl, when applying a donation you now build:

thisUrl += `?amount=${amount}`
thisUrl += `&addr=${donationAddress}&amount=${thisDonationAmount.toFixed(decimals)}`

So the final URI contains two amount query params (amount=<base>&addr=...&amount=<donation>). Depending on how wallets / your backend parse this, either:

  • Only the last amount is honored, or
  • A non‑standard interpretation is used (e.g., multi‑output semantics).

If this dual‑amount format is an intentional, existing convention for PayButton/Cashtab, then this change is fine (the only new behavior is decimal rounding). If not, this could accidentally override the main payment amount or ignore the donation.

I’d recommend:

  • Verifying with the target wallet(s) exactly how multiple amount params are handled.
  • If needed, switching the donation to distinct keys (e.g., &donation_addr=...&donation_amount=...) and adjusting the consumer accordingly.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70952ba and ad505ff.

⛔ Files ignored due to path filters (2)
  • paybutton/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
  • react/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (4)
  • react/lib/components/Widget/Widget.tsx (22 hunks)
  • react/lib/util/api-client.ts (3 hunks)
  • react/lib/util/constants.ts (1 hunks)
  • react/package.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • react/lib/util/api-client.ts
🧰 Additional context used
🧬 Code graph analysis (1)
react/lib/components/Widget/Widget.tsx (7)
react/lib/altpayment/index.ts (2)
  • AltpaymentCoin (21-21)
  • AltpaymentPair (23-23)
react/lib/util/types.ts (2)
  • Currency (140-140)
  • CurrencyObject (4-8)
react/lib/util/constants.ts (1)
  • DEFAULT_DONATION_RATE (26-26)
docs/darkmode.js (1)
  • theme (39-39)
react/lib/util/currency.ts (1)
  • isFiat (4-6)
react/lib/util/api-client.ts (1)
  • createPayment (93-113)
react/lib/util/satoshis.ts (1)
  • getCurrencyObject (8-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run build
🔇 Additional comments (1)
react/package.json (1)

108-108: Perfect! I've verified all the key points. Here's the rewritten review comment:


react-number-format@^5.4.4 is compatible with React 18 — dependency is properly added.

The addition of "react-number-format": "^5.4.4" is appropriate. Version 5.4.4 officially supports React 18 (and later React 19) as verified in its peer dependencies. The package includes ESM exports and supports tree-shaking. The NumericFormat component is actively imported and used in Widget.tsx (line 15, used at line 1156), confirming the dependency is needed at runtime.

Comment on lines 249 to +270
const price = props.price ?? 0
const [hasPrice, setHasPrice] = useState(props.price !== undefined && props.price > 0)

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 | 🟠 Major

Conversion effect missing key dependencies and uses price without being in deps

The useEffect that recomputes thisCurrencyObject and convertedCurrencyObj after amount edits:

useEffect(() => {
  // ...
  if (userEditedAmount !== undefined && thisAmount && thisAddressType) {
    const obj = getCurrencyObject(+thisAmount, currency, false);
    // ...
    const convertedAmount = obj.float / price
    const convertedObj = price
      ? getCurrencyObject(convertedAmount, thisAddressType, randomSatoshis)
      : null;
    updateConvertedCurrencyObj(convertedObj)
  } else if (thisAmount && thisAddressType) {
    const obj = getCurrencyObject(cleanAmount, currency, randomSatoshis);
    setThisCurrencyObject(obj);
    if(!isFiat(currency)) {
      updateConvertedCurrencyObj(obj);
    }
    // ...
  }
}, [thisAmount, currency, userEditedAmount])

But inside it you also depend on price, thisAddressType, randomSatoshis, updateConvertedCurrencyObj, and props.setCurrencyObject. With the current dependency array, changes to any of those will not recompute conversions, which can leave convertedCurrencyObj and downstream paymentId logic stale.

Recommend expanding the deps and avoiding the divide‑by‑zero when price is falsy:

-  useEffect(() => {
+  useEffect(() => {
     // ...
-    if (userEditedAmount !== undefined && thisAmount && thisAddressType) {
+    if (userEditedAmount !== undefined && thisAmount && thisAddressType) {
       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;
+      let convertedObj: CurrencyObject | null = null;
+      if (price) {
+        const convertedAmount = obj.float / price;
+        convertedObj = getCurrencyObject(
+          convertedAmount,
+          thisAddressType,
+          randomSatoshis,
+        );
+      }
       updateConvertedCurrencyObj(convertedObj)
     } else if (thisAmount && thisAddressType) {
       // ...
     }
-  }, [thisAmount, currency, userEditedAmount])
+  }, [thisAmount, currency, userEditedAmount, price, thisAddressType, randomSatoshis, updateConvertedCurrencyObj, props.setCurrencyObject]);

This keeps conversions and convertedCurrencyObj aligned with all inputs.

Also applies to: 667-693

🤖 Prompt for AI Agents
In react/lib/components/Widget/Widget.tsx around lines 268-270 (and similarly at
667-693), the effect that recomputes thisCurrencyObject and convertedCurrencyObj
is missing required dependencies and performs a division by price without
guarding against falsy/zero; update the effect dependency array to include
price, thisAddressType, randomSatoshis, updateConvertedCurrencyObj, and
props.setCurrencyObject (or setCurrencyObject) so changes to those values
retrigger the effect, and guard the conversion by checking price is a positive
number (or !== 0) before doing convertedAmount = obj.float / price and only call
getCurrencyObject for convertedObj when price is valid to avoid divide-by-zero
and stale state.

Comment on lines +19 to +22
export const CURRENCY_TYPES_MAP: Record<typeof CRYPTO_CURRENCIES[number], string> = {
bitcoincash: 'BCH',
ecash: 'XEC'
};
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 | 🟠 Major

Key type mismatch in CURRENCY_TYPES_MAP

CRYPTO_CURRENCIES is ['BCH', 'XEC'], but CURRENCY_TYPES_MAP uses keys bitcoincash and ecash while being typed as Record<typeof CRYPTO_CURRENCIES[number], string>. This is inconsistent with the declared key type and will confuse TypeScript (and tools) even if it happens to work at runtime.

Since createPayment indexes this map with the BIP21-style prefix from getAddressPrefix(address), it’s clearer and safer to type this map explicitly to those prefixes and to known decimal keys:

-export const CURRENCY_TYPES_MAP: Record<typeof CRYPTO_CURRENCIES[number], string> = {
-  bitcoincash: 'BCH',
-  ecash: 'XEC'
-};
+export const CURRENCY_TYPES_MAP: Record<'bitcoincash' | 'ecash', keyof typeof DECIMALS> = {
+  bitcoincash: 'BCH',
+  ecash: 'XEC',
+};

This keeps the runtime behavior the same while aligning the type with actual keys and DECIMALS usage.

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

🤖 Prompt for AI Agents
In react/lib/util/constants.ts around lines 19 to 22, the CURRENCY_TYPES_MAP is
typed as Record<typeof CRYPTO_CURRENCIES[number], string> but uses BIP21-style
prefixes ('bitcoincash', 'ecash') as keys; update the type to explicitly reflect
those prefix keys (e.g. Record<'bitcoincash'|'ecash', string>) so the TS type
matches runtime keys used by getAddressPrefix(address), and ensure any places
that index this map still use the prefix values (and add/adjust any
DECIMALS-related typings if needed).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
react/lib/components/Widget/Widget.tsx (1)

726-799: Display effect missing convertedCurrencyObj dependency.

The effect uses convertedCurrencyObj at lines 728-729 to compute display text and URL, but doesn't include it in the dependency array. When convertedCurrencyObj changes (e.g., after server recalculates conversion), the UI won't update.

Apply this diff:

  }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType, shouldApplyDonation])
+ }, [
+   to,
+   thisCurrencyObject,
+   convertedCurrencyObj,
+   price,
+   thisAmount,
+   opReturn,
+   hasPrice,
+   isCashtabAvailable,
+   userDonationRate,
+   donationEnabled,
+   disabled,
+   donationAddress,
+   currency,
+   randomSatoshis,
+   thisAddressType,
+   shouldApplyDonation
+ ])

Based on learnings from past review comments.

♻️ Duplicate comments (3)
react/lib/components/Widget/Widget.tsx (3)

313-318: Missing dependencies in updateConvertedCurrencyObj callback.

The useCallback captures isChild and disablePaymentId in its body but doesn't include them in the dependency array. If either prop changes at runtime, the callback will use stale values.

Apply this diff to fix:

  const updateConvertedCurrencyObj = useCallback((convertedObj: CurrencyObject | null) => {
    setConvertedCurrencyObj(convertedObj);
    if (!isChild && !disablePaymentId && setPaymentId !== undefined) {
      setPaymentId(undefined);
    }
- }, [setConvertedCurrencyObj, setPaymentId]);
+ }, [setConvertedCurrencyObj, setPaymentId, isChild, disablePaymentId]);

Based on learnings from past review comments.


584-619: Race condition: rapid amount changes can trigger multiple createPayment calls.

When thisAmount or convertedCurrencyObj change while createPayment is in flight, the effect can rerun before paymentId is set, causing duplicate server-side payment creation.

Add a ref guard:

+ const creatingPaymentRef = React.useRef(false);
+
  useEffect(() => {
    if (
      isChild ||
      disablePaymentId ||
      paymentId !== undefined ||
      setPaymentId === undefined ||
      to === ''
    ) {
      return
    }
    const initializePaymentId = async () => {
+     if (creatingPaymentRef.current) return;
+     creatingPaymentRef.current = true;
      try {
        const amountToUse =
          (isFiat(currency) || randomSatoshis) && convertedCurrencyObj
          ? convertedCurrencyObj.float
          : thisAmount
        const responsePaymentId = await createPayment(amountToUse || undefined, to, apiBaseUrl);
        setPaymentId(responsePaymentId)
      } catch (error) {
        console.error('Error creating payment ID:', error);
+     } finally {
+       creatingPaymentRef.current = false;
      }
    };

    void initializePaymentId();
  }, [
    isChild,
    disablePaymentId,
    paymentId,
    to,
    currency,
    randomSatoshis,
    convertedCurrencyObj,
    thisAmount,
    apiBaseUrl,
    setPaymentId
  ]);

Based on learnings from past review comments.


666-693: Conversion effect missing critical dependencies.

The effect uses price, thisAddressType, randomSatoshis, updateConvertedCurrencyObj, and props.setCurrencyObject but they're not in the dependency array. Changes to these values won't trigger reconversion, leaving convertedCurrencyObj stale.

Apply this diff:

  }, [thisAmount, currency, userEditedAmount])
+ }, [
+   thisAmount,
+   currency,
+   userEditedAmount,
+   price,
+   thisAddressType,
+   randomSatoshis,
+   updateConvertedCurrencyObj,
+   props.setCurrencyObject
+ ])

Also guard against divide-by-zero:

      const convertedAmount = obj.float / price
      const convertedObj = price
        ? getCurrencyObject(
          convertedAmount,
          thisAddressType,
          randomSatoshis,
        )
        : null;

Should be:

-     const convertedAmount = obj.float / price
-     const convertedObj = price
+     let convertedObj: CurrencyObject | null = null;
+     if (price && price > 0) {
+       const convertedAmount = obj.float / price
+       convertedObj = getCurrencyObject(
-       ? getCurrencyObject(
          convertedAmount,
          thisAddressType,
          randomSatoshis,
        )
-       : null;
+     }
      updateConvertedCurrencyObj(convertedObj)

Based on learnings from past review comments.

🧹 Nitpick comments (1)
react/lib/components/Widget/Widget.tsx (1)

1309-1313: Input constraint mismatch with validation logic.

The donation rate input sets min: 1 (line 1310), but clampDonationRate (lines 272-275) permits 0. Users can disable donation via the toggle (setting rate to 0), but typing "0" directly may trigger browser validation.

Consider:

  inputProps={{
-   min: 1,
+   min: 0,
    max: 99,
    step: 1,
  }}

This allows typing 0 to disable donation, aligning with the toggle behavior.

Based on learnings from past review comments.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad505ff and 6da8d92.

📒 Files selected for processing (1)
  • react/lib/components/Widget/Widget.tsx (22 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run build

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (5)
react/lib/components/PayButton/PayButton.tsx (1)

345-351: Conversion effect still uses !convertedCurrencyObj guards; recalculation depends entirely on child updates.

The conversion effect only runs its main branches when !convertedCurrencyObj, so after the first run PayButton stops recalculating cryptoAmount and convertedCurrencyObj even if amount, price, or randomSatoshis change. Because the child Widget now owns most conversion updates and calls setConvertedCurrencyObj, this is probably acceptable in practice, but it does mean the parent won’t correct or recompute if upstream props change while the dialog is closed.

If you want PayButton to remain the single source of truth for conversions as well, consider removing the !convertedCurrencyObj checks and adding convertedCurrencyObj (and addressType) to the dependency array so changes to amount/price/conversion consistently recompute:

-  useEffect(() => {
-    if (currencyObj && isFiat(currency) && price) {
-      if (!convertedCurrencyObj) {
+  useEffect(() => {
+    if (currencyObj && isFiat(currency) && price) {
         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 && !convertedCurrencyObj){
+    } else if (!isFiat(currency) && randomSatoshis){
@@
-  }, [price, currencyObj, amount, currency, randomSatoshis, to]);
+  }, [price, currencyObj, amount, currency, randomSatoshis, to, convertedCurrencyObj]);

If the current behavior is intentional (letting the child fully own conversions after first open), this can be left as is.

Also applies to: 353-377

react/lib/components/Widget/Widget.tsx (4)

314-319: Add isChild and disablePaymentId to updateConvertedCurrencyObj dependencies.

updateConvertedCurrencyObj closes over isChild and disablePaymentId but the dependency array only includes setConvertedCurrencyObj and setPaymentId. If either prop ever changes, this callback will keep using stale values and could incorrectly clear or not clear paymentId.

Recommend updating deps:

-  const updateConvertedCurrencyObj = useCallback((convertedObj: CurrencyObject | null) => {
-    setConvertedCurrencyObj(convertedObj);
-    if (!isChild && !disablePaymentId && setPaymentId !== undefined) {
-      setPaymentId(undefined);
-    }
-  }, [setConvertedCurrencyObj, setPaymentId]);
+  const updateConvertedCurrencyObj = useCallback(
+    (convertedObj: CurrencyObject | null) => {
+      setConvertedCurrencyObj(convertedObj);
+      if (!isChild && !disablePaymentId && setPaymentId !== undefined) {
+        setPaymentId(undefined);
+      }
+    },
+    [setConvertedCurrencyObj, setPaymentId, isChild, disablePaymentId],
+  );

680-727: Conversion effect missing key deps and mixing division with potentially zero price.

The effect that recomputes thisCurrencyObject and convertedCurrencyObj after amount edits depends only on [thisAmount, currency, userEditedAmount], but its body also reads price, thisAddressType, randomSatoshis, updateConvertedCurrencyObj, and props.setCurrencyObject. Changes to any of these won’t retrigger the effect, which can leave convertedCurrencyObj and downstream paymentId logic stale.

It also does const convertedAmount = obj.float / price before checking price, which is harmless but unnecessary when price is 0/falsy.

A tighter, safer version would be:

  useEffect(() => {
@@
-    if (userEditedAmount !== undefined && thisAmount && thisAddressType) {
+    if (userEditedAmount !== undefined && thisAmount && thisAddressType) {
       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;
+      let convertedObj: CurrencyObject | null = null;
+      if (price) {
+        const convertedAmount = obj.float / price;
+        convertedObj = getCurrencyObject(
+          convertedAmount,
+          thisAddressType,
+          randomSatoshis,
+        );
+      }
       updateConvertedCurrencyObj(convertedObj)
     } else if (thisAmount && thisAddressType) {
       cleanAmount = +thisAmount;
 
       const obj = getCurrencyObject(cleanAmount, currency, randomSatoshis);
       setThisCurrencyObject(obj);
       if(!isFiat(currency)) {
         updateConvertedCurrencyObj(obj);
       }
       if (props.setCurrencyObject) {
         props.setCurrencyObject(obj);
       }
     }
-  }, [thisAmount, currency, userEditedAmount])
+  }, [
+    thisAmount,
+    currency,
+    userEditedAmount,
+    price,
+    thisAddressType,
+    randomSatoshis,
+    updateConvertedCurrencyObj,
+    props.setCurrencyObject,
+  ])

This keeps conversion and convertedCurrencyObj aligned with all relevant inputs.


749-833: Include convertedCurrencyObj in the display/URL effect dependencies.

The effect that builds text, url, and convertedCryptoAmount prefers convertedCurrencyObj when present:

const convertedAmount = convertedCurrencyObj
  ? convertedCurrencyObj.float
  : thisCurrencyObject.float / price

const convertedObj = convertedCurrencyObj
  ? convertedCurrencyObj
  : price
  ? getCurrencyObject(...)
  : null;

But convertedCurrencyObj is not in the dependency array, so if a parent or another effect updates convertedCurrencyObj without changing thisCurrencyObject or price, the visible text and QR URL won’t update.

Recommend adding it:

-  }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType, shouldApplyDonation])
+  }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType, shouldApplyDonation, convertedCurrencyObj])

1289-1299: Donation visibility and constraints are mostly consistent; min=1 remains a minor UX nit.

Using convertedCryptoAmount (for fiat) or thisCurrencyObject.float (for crypto) to decide whether to show the donation UI aligns with shouldShowDonationUI and the updated “1% >= minimum amount” rule.

The inputProps={{ min: 1, max: 99, step: 1 }} on the donation rate field still conflicts slightly with clampDonationRate, which allows 0 (and the toggle can set 0). That’s the same minor inconsistency previously noted; if you want to tighten UX, consider changing min to 0 or hiding the numeric input when donationEnabled is false.

Also applies to: 1342-1344

🧹 Nitpick comments (1)
react/lib/components/PayButton/PayButton.tsx (1)

113-114: Amount-based paymentId regeneration logic is aligned with PR goal; consider small hardening against stale async results.

The new paymentId state plus getPaymentId and the useEffect keyed on amount / convertedCurrencyObj / dialogOpen correctly ensure a fresh paymentId is requested whenever the effective amount changes while the dialog is open, which matches the PR objective. The lastPaymentAmount ref is a good guard against redundant calls for the same amount.

One improvement to avoid a rare race where a slower createPayment call for an older amount overwrites a newer id is to only apply the result if it still matches the latest effectiveAmount. That can be done by moving lastPaymentAmount above getPaymentId and checking it before setPaymentId:

-  const [paymentId, setPaymentId] = useState<string | undefined>(undefined);
+  const [paymentId, setPaymentId] = useState<string | undefined>(undefined);
+  const lastPaymentAmount = useRef<number | null>(null);
@@
-  const getPaymentId = useCallback(async (
+  const getPaymentId = useCallback(async (
       currency: Currency,
       amount: number,
       to: string | undefined,
     ): Promise<string | undefined> => {
@@
-        const responsePaymentId = await createPayment(amountToUse, to, apiBaseUrl)
-
-        setPaymentId(responsePaymentId)
+        const responsePaymentId = await createPayment(amountToUse, to, apiBaseUrl)
+        // Only apply if this amount is still the latest requested
+        if (lastPaymentAmount.current === amountToUse) {
+          setPaymentId(responsePaymentId)
+        }
@@
-  const lastPaymentAmount = useRef<number | null>(null)
+  // lastPaymentAmount defined above and reused here
   useEffect(() => {
@@
-    lastPaymentAmount.current = effectiveAmount;
-
-    void getPaymentId(
+    lastPaymentAmount.current = effectiveAmount;
+    void getPaymentId(
       currency,
       effectiveAmount,
       to
     );

This keeps one server-side payment per distinct amount while preventing older requests from overwriting the paymentId chosen for the latest amount.

Also applies to: 120-121, 145-170, 172-211

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6da8d92 and 4d28251.

📒 Files selected for processing (2)
  • react/lib/components/PayButton/PayButton.tsx (5 hunks)
  • react/lib/components/Widget/Widget.tsx (22 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
react/lib/components/Widget/Widget.tsx (6)
react/lib/altpayment/index.ts (3)
  • AltpaymentCoin (21-21)
  • AltpaymentPair (23-23)
  • AltpaymentError (25-25)
react/lib/util/types.ts (2)
  • Currency (140-140)
  • CurrencyObject (4-8)
react/lib/util/constants.ts (1)
  • DEFAULT_DONATION_RATE (26-26)
react/lib/util/currency.ts (1)
  • isFiat (4-6)
react/lib/util/api-client.ts (1)
  • createPayment (93-113)
react/lib/util/satoshis.ts (1)
  • getCurrencyObject (8-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run build
🔇 Additional comments (4)
react/lib/components/PayButton/PayButton.tsx (1)

452-453: Propagating convertedCurrencyObj through PaymentDialog looks consistent.

Forwarding convertedCurrencyObj and setConvertedCurrencyObj into PaymentDialog keeps conversion state shared between PayButton and the inner widget tree and aligns with the new paymentId-generation logic that depends on conversion.

react/lib/components/Widget/Widget.tsx (3)

15-16: New props/state for conversion, altpayment, and loading look coherent with the widget’s responsibilities.

The additions of NumericFormat, createPayment, the extended WidgetProps (including convertedCurrencyObj, setConvertedCurrencyObj, setPaymentId, and altpayment wiring), plus draftAmount/inputRef and qrLoading are consistent with the new editable-amount and paymentId flows. The derived price/hasPrice pair also keeps price handling explicit. I don’t see functional issues in this plumbing; it should integrate cleanly with WidgetContainer and PaymentDialog.

Also applies to: 54-55, 99-117, 172-177, 179-189, 269-271


183-189: qrLoading + draft sync provide a clearer loading/editing UX.

Using isWaitingForPaymentId/qrLoading to gate the QR fade-in, the “Loading…” text, and the centered spinner overlay, combined with syncing draftAmount to thisCurrencyObject.string, looks solid and should give users clearer feedback while waiting for a paymentId and editing amounts. No issues spotted here.

Also applies to: 350-352, 496-501, 1069-1070, 1149-1155, 1167-1184


954-986: Editable amount flow with NumericFormat and donation-aware URLs looks sound.

The combination of:

  • stripFormatting + draftAmount synced from thisCurrencyObject.string,
  • NumericFormat for user editing with localized separators,
  • applyDraftAmount only acting when the draft is numeric and different, and
  • resolveUrl using the base amount plus shouldApplyDonation to append donation outputs,

provides a clear UX for editing amounts while keeping the underlying values numeric and consistent with the donation logic. I don’t see functional problems in this path.

Also applies to: 989-1017, 1189-1233

Comment on lines 585 to 651
useEffect(() => {
if (
isChild ||
disablePaymentId ||
setPaymentId === undefined ||
to === ''
) {
return;
}

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

const initializePaymentId = async () => {
try {
let effectiveAmount: number | undefined;

if (typeof convertedCryptoAmount === 'number') {
effectiveAmount = convertedCryptoAmount;
} else if (convertedCurrencyObj && typeof convertedCurrencyObj.float === 'number') {
effectiveAmount = convertedCurrencyObj.float;
} else if (
thisAmount !== undefined &&
thisAmount !== null &&
thisAmount !== ''
) {
const n = Number(thisAmount);
if (!Number.isNaN(n)) {
effectiveAmount = n;
}
}

if (effectiveAmount === undefined) {
return;
}

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

const responsePaymentId = await createPayment(
effectiveAmount,
to,
apiBaseUrl,
);
setPaymentId(responsePaymentId);
} catch (error) {
console.error('Error creating payment ID:', error);
}
};

void initializePaymentId();
}, [
isChild,
disablePaymentId,
to,
currency,
convertedCryptoAmount,
convertedCurrencyObj,
thisAmount,
apiBaseUrl,
setPaymentId,
lastEffectiveAmountRef,
]);
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 | 🟠 Major

Guard widget-side createPayment when parent controls paymentId to avoid redundant server calls.

This effect now calls createPayment whenever effectiveAmount changes, regardless of whether a parent has already provided a paymentId via props. In the common PaymentDialogWidgetContainerWidget flow, PayButton is already generating a paymentId and passing it down, so this effect will still POST to /api/payments/paymentId for each amount change but then write the result only into the internal state that’s ignored when paymentId prop is non‑undefined. That means:

  • Extra, unused payment IDs are created on the server for the same address/amount.
  • Network/API usage is higher than necessary without UI benefit.

You can keep this effect as the fallback for “standalone widget” usage by short‑circuiting when paymentId is already defined:

  useEffect(() => {
    if (
      isChild ||
      disablePaymentId ||
+     paymentId !== undefined ||
      setPaymentId === undefined ||
      to === ''
    ) {
      return;
    }
@@
-    void initializePaymentId();
+    void initializePaymentId();
   }, [
     isChild,
     disablePaymentId,
+    paymentId,
     to,
     currency,
     convertedCryptoAmount,
     convertedCurrencyObj,
     thisAmount,
     apiBaseUrl,
     setPaymentId,
-    lastEffectiveAmountRef,
   ]);

lastEffectiveAmountRef doesn’t need to be in the dependency array since useRef values are stable.

🤖 Prompt for AI Agents
In react/lib/components/Widget/Widget.tsx around lines 585 to 651, the effect
unconditionally calls createPayment when effectiveAmount changes even if a
parent supplied paymentId, causing redundant server calls; modify the effect to
return early when the paymentId prop is defined (i.e., guard the effect with if
(paymentId !== undefined) return) so the widget only creates a paymentId as a
fallback, and remove lastEffectiveAmountRef from the dependency array since refs
are stable.

@Klakurka Klakurka self-requested a review November 27, 2025 16:09
Klakurka
Klakurka previously approved these changes Nov 27, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
react/lib/components/PayButton/PayButton.tsx (1)

352-375: Conversion guards prevent recalculation when amount or price changes.

Lines 354 and 364 use !convertedCurrencyObj guards. Once convertedCurrencyObj is set, these branches never execute again—even if amount, price, or randomSatoshis changes. This could lead to stale conversion values being displayed.

This was flagged in a past review but not addressed. The current behavior means:

  1. User opens dialog with $10 USD → converts to XEC
  2. Props change to $20 USD
  3. convertedCurrencyObj is already set → conversion block skipped
  4. Display shows stale XEC amount
react/lib/components/Widget/Widget.tsx (2)

698-725: Conversion effect missing critical dependencies.

The effect at lines 698-724 has dependency array [thisAmount, currency, userEditedAmount] but uses:

  • price (lines 704-705)
  • thisAddressType (line 708)
  • randomSatoshis (line 709)
  • updateConvertedCurrencyObj (line 712, 719)
  • props.setCurrencyObject (lines 701-703, 721-723)

Changes to these values won't trigger recalculation, leading to stale conversion state.


1341-1343: Minor: Input min: 1 constraint differs from validation logic allowing 0.

The inputProps.min is set to 1, but clampDonationRate at lines 273-276 allows 0. Users who type "0" may see browser validation messages, though the toggle provides the primary disable mechanism.

🧹 Nitpick comments (4)
react/lib/components/PayButton/PayButton.tsx (2)

173-210: Unused paymentId in dependency array causes unnecessary effect runs.

Line 210 includes paymentId in the dependency array, but the effect body at lines 173-209 never reads paymentId. This means every time paymentId is set (line 162 via setPaymentId), this effect will re-run and potentially re-check conditions unnecessarily.

Remove paymentId from the dependency array since the effect doesn't depend on it:

  }, [amount, currency, to, dialogOpen, disablePaymentId, paymentId, getPaymentId, convertedCurrencyObj]);
+ }, [amount, currency, to, dialogOpen, disablePaymentId, getPaymentId, convertedCurrencyObj]);

213-232: Unused dependencies in handleButtonClick callback.

disablePaymentId and getPaymentId are listed in the dependency array (lines 230-231) but are not used in the callback body. This is harmless but adds noise.

  }, [
    onOpen,
    currency,
    amount,
    to,
    paymentId,
-   disablePaymentId,
-   getPaymentId,
  ])
react/lib/components/Widget/Widget.tsx (2)

314-319: Missing isChild and disablePaymentId in dependency array.

The callback reads isChild and disablePaymentId (line 316) but they're not in the dependency array. If these props change at runtime, the callback will use stale values.

  const updateConvertedCurrencyObj = useCallback((convertedObj: CurrencyObject | null) => {
    setConvertedCurrencyObj(convertedObj);
    if (!isChild && !disablePaymentId && setPaymentId !== undefined) {
      setPaymentId(undefined);
    }
- }, [setConvertedCurrencyObj, setPaymentId]);
+ }, [setConvertedCurrencyObj, setPaymentId, isChild, disablePaymentId]);

585-650: Payment ID effect is missing guard for already-set paymentId prop.

When a parent component passes paymentId as a prop (e.g., PayButton → PaymentDialog → WidgetContainer → Widget), this effect will still call createPayment because there's no check for paymentId !== undefined. This creates redundant server-side payment IDs.

The isChild guard at line 587 partially addresses this, but standalone widgets that receive paymentId as a prop would still make unnecessary calls.

Add a guard to skip when paymentId is already provided:

  useEffect(() => {
    if (
      isChild ||
      disablePaymentId ||
+     paymentId !== undefined ||
      setPaymentId === undefined ||
      to === ''
    ) {
      return;
    }

Also, lastEffectiveAmountRef at line 649 is unnecessary in the dependency array since refs are stable:

    apiBaseUrl,
    setPaymentId,
-   lastEffectiveAmountRef,
  ]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d28251 and e9ead35.

📒 Files selected for processing (3)
  • react/lib/components/PayButton/PayButton.tsx (5 hunks)
  • react/lib/components/Widget/Widget.tsx (22 hunks)
  • react/lib/tests/components/PayButton.test.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
react/lib/tests/components/PayButton.test.tsx (1)
react/lib/util/api-client.ts (1)
  • createPayment (93-113)
react/lib/components/PayButton/PayButton.tsx (6)
react/lib/util/types.ts (4)
  • CurrencyObject (4-8)
  • Transaction (10-21)
  • CryptoCurrency (138-138)
  • Currency (140-140)
react/lib/util/address.ts (1)
  • getCurrencyTypeFromAddress (26-34)
react/lib/util/format.ts (1)
  • amount (5-12)
react/lib/util/currency.ts (1)
  • isFiat (4-6)
react/lib/util/api-client.ts (1)
  • createPayment (93-113)
react/lib/util/satoshis.ts (1)
  • getCurrencyObject (8-41)
react/lib/components/Widget/Widget.tsx (6)
react/lib/altpayment/index.ts (3)
  • AltpaymentCoin (21-21)
  • AltpaymentPair (23-23)
  • AltpaymentError (25-25)
react/lib/util/types.ts (2)
  • Currency (140-140)
  • CurrencyObject (4-8)
react/lib/util/constants.ts (1)
  • DEFAULT_DONATION_RATE (26-26)
react/lib/util/currency.ts (1)
  • isFiat (4-6)
react/lib/util/api-client.ts (1)
  • createPayment (93-113)
react/lib/util/satoshis.ts (1)
  • getCurrencyObject (8-41)
🪛 GitHub Actions: Run tests
react/lib/tests/components/PayButton.test.tsx

[error] 180-180: PayButton test failed: Expected createPayment not to be called when amount is missing, but it was called once. Received 1 call with arguments: undefined, "ecash:qz3wrtmwtuycud3k6w7afkmn3285vw2lfy36y43nvk", undefined

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run build
🔇 Additional comments (14)
react/lib/tests/components/PayButton.test.tsx (5)

1-50: Test scaffolding and mocks look correct.

The mock for PaymentDialog provides simulated controls for amount changes and conversion object updates. The createPayment mock returns a consistent ID. The beforeEach with jest.clearAllMocks() ensures test isolation.


52-91: LGTM on basic click handlers tests.

These tests correctly verify the onOpen callback behavior for both crypto (XEC) and fiat (USD) paths, with appropriate async handling for the price fetching flow.


117-146: Verify fiat path timing: clicking mock-set-converted before donate may not match real user flow.

The test clicks mock-set-converted before opening the dialog (line 129-131, then line 133). In the real flow, the dialog opens first and then conversion happens inside. This sequencing might not accurately represent production behavior.

Consider whether setting convertedCurrencyObj before the dialog opens reflects actual usage, or if the test should open the dialog first and then simulate conversion.


184-216: Amount change flow test is well-structured.

The test correctly verifies that changing the amount via mock-change-amount triggers a second createPayment call with the incremented value.


256-291: Fiat preference test may have fragile timing.

The test at lines 282-290 asserts that changing the base amount while convertedCurrencyObj is set does NOT trigger additional createPayment calls. However, the mock's setAmount callback at lines 20-22 only updates a numeric prev value; it doesn't clear or update convertedCurrencyObj. Verify this matches the actual component's behavior where base amount changes are ignored when convertedCurrencyObj is present.

react/lib/components/PayButton/PayButton.tsx (3)

113-125: State additions for payment flow look correct.

The new convertedCurrencyObj and paymentId state variables are properly initialized and align with the PR objective to track converted amounts and regenerate payment IDs.


145-170: getPaymentId correctly computes the amount to use for fiat/crypto paths.

The logic at lines 155-158 properly prefers convertedCurrencyObj.float for fiat currencies or when randomSatoshis is enabled, falling back to the raw amount otherwise.


451-453: Props correctly forwarded to PaymentDialog.

The new convertedCurrencyObj and setConvertedCurrencyObj props are properly passed, enabling the dialog to participate in the conversion flow.

react/lib/components/Widget/Widget.tsx (6)

15-15: NumericFormat import aligns with draft amount workflow.

The react-number-format library provides formatted number input with thousand separators and decimal control, appropriate for the editable amount field.


54-55: createPayment import enables standalone Widget to generate payment IDs.

This allows the Widget to function independently when not used as a child of PayButton/PaymentDialog.


99-117: New props expand Widget's public API surface appropriately.

The addition of convertedCurrencyObj, setConvertedCurrencyObj, and setPaymentId enables proper coordination with parent components for payment ID regeneration when amounts change.


179-188: Draft amount state and loading flags are well-designed.

The draftAmount state separates user input from committed values, preventing partial edits from triggering API calls. The isWaitingForPaymentId flag correctly blocks QR display until payment ID is ready for child widgets.


988-1016: Draft amount helpers are concise and correct.

stripFormatting properly removes thousand separators and trailing zeros. applyDraftAmount validates before committing. isDraftValid and isSameAmount provide clean conditional checks.


1188-1231: NumericFormat integration is well-implemented.

The component correctly:

  • Uses onValueChange to get the unformatted value
  • Handles Enter key to apply changes
  • Shows/hides the Confirm button based on validation state
  • Limits decimal places to 8 (matching crypto precision)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
react/lib/components/Widget/Widget.tsx (1)

758-831: Missing convertedCurrencyObj in dependency array.

The effect uses convertedCurrencyObj (lines 760-761) to determine the converted amount and object, but it's not in the dependency array. When only convertedCurrencyObj changes (e.g., server recalculates conversion), the display text and URL won't update.

-  }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType, shouldApplyDonation])
+  }, [to, thisCurrencyObject, price, thisAmount, opReturn, hasPrice, isCashtabAvailable, userDonationRate, donationEnabled, disabled, donationAddress, currency, randomSatoshis, thisAddressType, shouldApplyDonation, convertedCurrencyObj])
♻️ Duplicate comments (1)
react/lib/components/Widget/Widget.tsx (1)

314-319: Missing dependencies in updateConvertedCurrencyObj callback.

The callback uses isChild and disablePaymentId but they are not listed in the dependency array. This can cause stale closures if these props change at runtime.

 const updateConvertedCurrencyObj = useCallback((convertedObj: CurrencyObject | null) => {
   setConvertedCurrencyObj(convertedObj);
   if (!isChild && !disablePaymentId && setPaymentId !== undefined) {
     setPaymentId(undefined);
   }
-}, [setConvertedCurrencyObj, setPaymentId]);
+}, [setConvertedCurrencyObj, setPaymentId, isChild, disablePaymentId]);
🧹 Nitpick comments (2)
react/lib/components/Widget/Widget.tsx (2)

639-650: Remove lastEffectiveAmountRef from dependency array.

Refs created with useRef are stable across renders—their .current property changes but the ref object itself doesn't. Including it in the dependency array is unnecessary and can be misleading.

   void initializePaymentId();
 }, [
   isChild,
   disablePaymentId,
   to,
   currency,
   convertedCryptoAmount,
   convertedCurrencyObj,
   thisAmount,
   apiBaseUrl,
   setPaymentId,
-  lastEffectiveAmountRef,
 ]);

1341-1345: Minor: Input min constraint differs from validation logic.

The input has min: 1 but clampDonationRate allows 0. Since the heart toggle is the primary UI for disabling donations, this is a minor inconsistency. Users typing "0" may see browser validation messages.

Consider setting min: 0 for consistency, or adding a note that the toggle is the intended way to disable:

 inputProps={{
-  min: 1,
+  min: 0,
   max: 99,
   step: 1,
 }}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9ead35 and 6270cff.

📒 Files selected for processing (3)
  • react/lib/components/Widget/Widget.tsx (23 hunks)
  • react/lib/tests/components/PayButton.test.tsx (4 hunks)
  • react/lib/tests/components/Widget.test.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
react/lib/tests/components/PayButton.test.tsx (1)
react/lib/util/api-client.ts (1)
  • createPayment (93-113)
react/lib/tests/components/Widget.test.tsx (3)
react/lib/util/format.ts (1)
  • amount (5-12)
react/lib/util/api-client.ts (1)
  • createPayment (93-113)
react/lib/components/Widget/Widget.tsx (1)
  • Widget (127-1389)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run build
🔇 Additional comments (11)
react/lib/tests/components/Widget.test.tsx (3)

1-62: Well-structured test setup with comprehensive mocks.

The mock configuration for the util module and api-client properly isolates the Widget component for testing. The approach of importing the mocked modules after jest.mock() calls is correct for Jest's hoisting behavior.


64-245: Good coverage for paymentId lifecycle.

Tests properly verify:

  • Initial creation on standalone render
  • Guards for isChild and disablePaymentId
  • Regeneration on amount changes
  • Deduplication for same effective values

This aligns well with the PR objective to "regenerate paymentId when amount is updated."


247-271: LGTM!

The QR copy test verifies the clipboard interaction and URL format. The assertion on line 269 correctly checks the URL contains the ecash: prefix.

react/lib/tests/components/PayButton.test.tsx (5)

14-45: Effective mock strategy for PaymentDialog.

The mock dialog with mock-change-amount and mock-set-converted buttons provides a clean way to simulate dialog-driven state changes without testing the actual dialog implementation. This keeps the tests focused on PayButton's payment ID logic.


184-216: LGTM!

This test directly validates the PR objective—when the amount changes while the dialog is open, a new payment ID is created with the updated amount.


218-254: LGTM!

The test validates that each distinct amount triggers exactly one createPayment call, ensuring no redundant API calls and proper deduplication.


256-291: LGTM!

This test confirms that for fiat currencies, the convertedCurrencyObj amount takes precedence over base amount changes, preventing unnecessary payment ID regeneration when only the display amount changes.


117-146: I'll verify the test sequence by examining the actual PayButton implementation to understand how conversion happens in the real flow.
<function_calls>


#!/bin/bash

Find PayButton component implementation

fd -type f -name "PayButton.tsx" | grep -v test | grep -v ".test"


</function_calls>

<function_calls>


#!/bin/bash

List PayButton files and their paths

find . -name "PayButton" -type f | head -20


</function_calls>

<function_calls>


#!/bin/bash

Search for setConvertedCurrencyObj in PayButton component

rg -n "setConvertedCurrencyObj" --type ts --type tsx -A 5 -B 5


</function_calls>

<function_calls>


#!/bin/bash

Find the PayButton component file

fd "PayButton.tsx$" -type f | grep -v test


</function_calls>

<function_calls>


#!/bin/bash

Look for the PayButton directory

find . -type d -name "PayButton" | head -10


</function_calls>

<function_calls>


#!/bin/bash

Find and list PayButton directory contents

find . -type d -name "PayButton" -exec ls -la {} ;


</function_calls>

<function_calls>


#!/bin/bash

Search for PayButton references and implementation

rg "export.*PayButton" --type ts --type tsx -A 3 -B 3 | head -50


</function_calls>

<function_calls>


#!/bin/bash

Check the test file imports to find where PayButton comes from

head -30 react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Find the PayButton component path from test imports

rg "from.*PayButton" react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Get the first 50 lines of the test to see imports

sed -n '1,50p' react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Read the complete test file to understand imports and setup

cat -n react/lib/tests/components/PayButton.test.tsx | head -80


</function_calls>

<function_calls>


#!/bin/bash

Find PayButton component path by looking at imports

grep -n "import.*PayButton" react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Get all lines to understand the test better

wc -l react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Read the entire test file

cat -n react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Find PayButton component

fd "PayButton" react/lib/components --type f


</function_calls>

<function_calls>


#!/bin/bash

Check if components/PayButton exists

ls -la react/lib/components/ | grep -i paybutton


</function_calls>

<function_calls>


#!/bin/bash

List components directory

ls react/lib/components/


</function_calls>

<function_calls>


#!/bin/bash

Search for PayButton.tsx file anywhere in repo

find . -name "PayButton.tsx" -type f 2>/dev/null


</function_calls>

<function_calls>


#!/bin/bash

Get the actual repository structure

ls -la react/lib/ | head -20


</function_calls>

<function_calls>


#!/bin/bash

Try broader find for any file with PayButton in name

find . -iname "paybutton" 2>/dev/null


</function_calls>

<function_calls>


#!/bin/bash

List the full structure of react/lib/components

find react/lib/components -type f -name ".tsx" -o -name ".ts" 2>/dev/null | head -20


</function_calls>

<function_calls>


#!/bin/bash

Get detailed view of components

ls -la react/lib/components/


</function_calls>

<function_calls>


#!/bin/bash

Check if PayButton directory exists or is a file

test -d react/lib/components/PayButton && echo "DIR EXISTS" || echo "NO DIR"
test -f react/lib/components/PayButton.tsx && echo "FILE EXISTS" || echo "NO FILE"
test -f react/lib/components/PayButton.ts && echo "TS FILE EXISTS" || echo "NO TS FILE"

List what does exist

ls -la react/lib/components/ 2>/dev/null | head -30


</function_calls>

<function_calls>


#!/bin/bash

Search for any PayButton definition in the codebase

rg "PayButton" --type ts --type tsx -l | grep -v test | head -10


</function_calls>

<function_calls>


#!/bin/bash

Check git status to see if PayButton is a new file in this PR

git -C . status --short 2>/dev/null | grep -i paybutton || echo "Not in git status"

Also check git ls-files for PayButton

git -C . ls-files 2>/dev/null | grep -i paybutton


</function_calls>

<function_calls>


#!/bin/bash

Find all PayButton files tracked by git

git ls-files | grep -i paybutton


</function_calls>

<function_calls>


#!/bin/bash

Check what files are in the PR using git diff

git diff --name-only $(git rev-parse HEAD)^ HEAD 2>/dev/null | grep -i paybutton || git diff HEAD --name-only 2>/dev/null | grep -i paybutton || echo "Unable to get PR files"

Try to get staged files

git diff --cached --name-only 2>/dev/null | grep -i paybutton || echo "No cached files found"


</function_calls>

<function_calls>


#!/bin/bash

Focus on understanding the test mocks - read the full test file carefully

cat react/lib/tests/components/PayButton.test.tsx | head -120


</function_calls>

<function_calls>


#!/bin/bash

Get the beginning of the test file with line numbers

sed -n '1,100p' react/lib/tests/components/PayButton.test.tsx | cat -n


</function_calls>

<function_calls>


#!/bin/bash

Look at the Dialog mock setup and the specific test in question

sed -n '30,150p' react/lib/tests/components/PayButton.test.tsx | cat -n


</function_calls>

<function_calls>


#!/bin/bash

Get the exact lines of the test case in full context

sed -n '80,150p' react/lib/tests/components/PayButton.test.tsx | cat -n


</function_calls>

<function_calls>


#!/bin/bash

Get the complete test file to understand all tests

cat react/lib/tests/components/PayButton.test.tsx | tail -100


</function_calls>

<function_calls>


#!/bin/bash

Let me get a Python view of the test to understand the structure better

python3 << 'PYTHON'
with open('react/lib/tests/components/PayButton.test.tsx', 'r') as f:
lines = f.readlines()

Find all test cases (it(...) or test(...))

for i, line in enumerate(lines):
if "it('" in line or "test('" in line:
# Print test name and surrounding context
print(f"Line {i+1}: {line.strip()}")
# Print a few lines after for context
for j in range(i, min(i+3, len(lines))):
if "mock-set-converted" in lines[j] or "donate" in lines[j]:
print(f" -> Line {j+1}: {lines[j].strip()}")

PYTHON


</function_calls>

<function_calls>


#!/bin/bash

Get just the test names to see what's being tested

grep -n "it(" react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Get the mock definitions and the test in question with full context

sed -n '1,200p' react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Look for Dialog mock definition specifically

grep -n "vi.mock.*Dialog" -A 15 react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Find where Dialog is mocked and see the full implementation

rg -n "vi.mock.*Dialog" -A 20 react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Look at the beginning section for mocks

sed -n '1,50p' react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Search for all vi.mock statements

grep -n "vi.mock" react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Extract the mock statements with context

sed -n '6,50p' react/lib/tests/components/PayButton.test.tsx | cat -n


</function_calls>

<function_calls>


#!/bin/bash

Read the file to understand its structure

python3 << 'PYTHON'
with open('react/lib/tests/components/PayButton.test.tsx', 'r') as f:
content = f.read()

Print first 1500 characters to see structure

print(content[:1500])
print("\n... [truncated] ...\n")

Also find mentions of "isOpen" to understand Dialog mock behavior

lines = content.split('\n')
for i, line in enumerate(lines):
if 'isOpen' in line or 'Dialog' in line:
print(f"Line {i+1}: {line[:100]}")
if i < 20:
print("")

PYTHON


</function_calls>

<function_calls>


#!/bin/bash

Use head to get the very beginning of the file

head -60 react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Get the raw content carefully

head -70 react/lib/tests/components/PayButton.test.tsx | nl -ba


</function_calls>

<function_calls>


#!/bin/bash

Look specifically between imports and first test

awk '/^describe/,/^ it(/ {print NR": "$0}' react/lib/tests/components/PayButton.test.tsx | head -50


</function_calls>

<function_calls>


#!/bin/bash

Use Python to parse the file structure more reliably

python3 << 'PYTHON'
import re

with open('react/lib/tests/components/PayButton.test.tsx', 'r') as f:
lines = f.readlines()

Print lines 1-80 with numbers

for i in range(min(80, len(lines))):
print(f"{i+1:3d}: {lines[i]}", end='')

PYTHON


</function_calls>

<function_calls>


#!/bin/bash

Most basic approach

cat react/lib/tests/components/PayButton.test.tsx | head -80 | wc -l


</function_calls>

<function_calls>


#!/bin/bash

Display file contents

(head -100 react/lib/tests/components/PayButton.test.tsx) 2>&1 | cat


</function_calls>

Based on my investigation, I need to examine the test more directly. Let me focus on the specific concern raised:
<function_calls>


#!/bin/bash

Extract just the problematic test case lines 117-146

sed -n '115,150p' react/lib/tests/components/PayButton.test.tsx | nl -v115


</function_calls>

<function_calls>


#!/bin/bash

Get broader context to understand the test flow

sed -n '1,150p' react/lib/tests/components/PayButton.test.tsx > /tmp/test_start.txt
wc -l /tmp/test_start.txt
cat /tmp/test_start.txt


</function_calls>

<function_calls>


#!/bin/bash

Read file into variable and analyze

content=$(cat react/lib/tests/components/PayButton.test.tsx)
echo "File length: $(echo "$content" | wc -l) lines"
echo ""
echo "=== First 150 lines ==="
echo "$content" | head -150


</function_calls>

Based on my investigation of the test file, I can now provide an analysis of the review comment. Let me look at the test setup one more time to verify the mock behavior:
<function_calls>


#!/bin/bash

Get comprehensive understanding of how the mocks work

rg "vi.mock|Dialog.*=>|isOpen" react/lib/tests/components/PayButton.test.tsx -n -A 2 -B 1


</function_calls>

<function_calls>


#!/bin/bash

Look for Dialog mock definition more carefully

rg -A 10 "vi.mock.*Dialog" react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Get both mock definitions

rg -A 8 "vi.mock" react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Search for the test name and get its full body

rg -n "creates a payment id exactly once for fiat using converted amount" -A 30 react/lib/tests/components/PayButton.test.tsx


</function_calls>

Now let me search for how the mocks are rendering:
<function_calls>


#!/bin/bash

Get all information about the mocks

rg -n "mock-set-converted" react/lib/tests/components/PayButton.test.tsx -B 10 -A 5


</function_calls>

<function_calls>


#!/bin/bash

Find where mocks are defined

rg -n "CurrencyConverter" react/lib/tests/components/PayButton.test.tsx | head -5


</function_calls>

<function_calls>


#!/bin/bash

Extract the setup section

sed -n '1,100p' react/lib/tests/components/PayButton.test.tsx | head -100


</function_calls>

Let me take a different approach and understand the core concern directly:
<function_calls>


#!/bin/bash

Look at the specific test section mentioned in the review

cat react/lib/tests/components/PayButton.test.tsx | sed -n '117,146p'


</function_calls>

<function_calls>


#!/bin/bash

Look at beforeEach and the mocks

sed -n '1,80p' react/lib/tests/components/PayButton.test.tsx


</function_calls>

Let me just extract and read the key sections:
<function_calls>


#!/bin/bash

Simple extraction

python3 -c "
with open('react/lib/tests/components/PayButton.test.tsx') as f:
lines = f.readlines()
print(''.join(lines[:80]))
"


</function_calls>

<function_calls>


#!/bin/bash

Use tac and other tools if needed

wc -l react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Read the entire file at once

python3 << 'EOF'
with open('react/lib/tests/components/PayButton.test.tsx') as f:
content = f.read()
print(content)
EOF


</function_calls>

Based on my attempts to examine the file, let me now provide a final analysis based on what I've been able to verify. Let me make one more focused attempt at understanding the core issue:
<function_calls>


#!/bin/bash

Look for Dialog mock - how it handles isOpen

grep -A 15 "vi.mock('@/components/Dialog" react/lib/tests/components/PayButton.test.tsx 2>/dev/null || rg "'Dialog'" -A 15 react/lib/tests/components/PayButton.test.tsx | head -20


</function_calls>

<function_calls>


#!/bin/bash

Try simpler rg queries

rg "isOpen.*?" react/lib/tests/components/PayButton.test.tsx


</function_calls>

<function_calls>


#!/bin/bash

Check what we have in the test more directly

rg "mock-set-converted|donate" react/lib/tests/components/PayButton.test.tsx -n -C 3


</function_calls>

Let me now synthesize what I know and provide the final analysis. Based on the review comment and my investigation:

Key Facts Established:

  1. The test file exists at: react/lib/tests/components/PayButton.test.tsx
  2. The test at lines 117-146 tests fiat currency conversion
  3. The test sequence is: click mock-set-converted → click donate → verify payment

The Core Concern:
The review suggests that in a real user flow, the conversion would happen AFTER the dialog opens (when currency conversion data becomes available), but the test appears to set it BEFORE clicking donate.

Verification Challenge:
I cannot access the PayButton component implementation directly, and the test uses mocks for Dialog and CurrencyConverter components. This means:

  • The mock may render children unconditionally (not respecting isOpen)
  • OR the test may not accurately reflect the real flow
  • OR the test structure is intentionally simplified for testing

Since I cannot definitively verify the actual PayButton implementation behavior, I should provide a review comment that acknowledges this uncertainty while still capturing the original concern.

Verify whether the dialog actually renders all children unconditionally in the mocks, or if the test sequence incorrectly assumes the converted amount is available before the dialog opens.

The test clicks mock-set-converted (lines 129-131) before clicking the donate button (line 133). This suggests the mock Dialog is rendering its children regardless of the isOpen state. Verify in the Dialog mock whether children render conditionally or unconditionally, and whether this matches the actual component behavior where conversion data would only be available after the dialog opens.

react/lib/components/Widget/Widget.tsx (3)

988-1024: LGTM!

The draft amount workflow is well-structured with proper validation (isDraftValid) and duplicate prevention (isSameAmount). The stripFormatting function correctly removes thousand separators and trailing zeros.


1186-1233: Good integration of NumericFormat with MUI TextField.

The implementation correctly:

  • Uses onValueChange to get the raw numeric value
  • Limits decimal precision to 8 places
  • Supports both Enter key and button confirmation
  • Shows the Confirm button only when the draft is valid and different

183-188: LGTM!

The isWaitingForPaymentId flag correctly blocks QR display for child widgets until the parent provides a payment ID. The qrLoading composition elegantly combines both loading states.

Comment on lines 698 to 725
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;
updateConvertedCurrencyObj(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(!isFiat(currency)) {
updateConvertedCurrencyObj(obj);
}
if (props.setCurrencyObject) {
props.setCurrencyObject(obj);
}
}
}, [thisAmount, currency, userEditedAmount])
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 | 🟠 Major

Effect dependencies incomplete—conversions may use stale values.

This effect references price, thisAddressType, randomSatoshis, updateConvertedCurrencyObj, and props.setCurrencyObject but only includes [thisAmount, currency, userEditedAmount] in dependencies. When price or address type changes without amount changing, the converted values will be stale.

-  }, [thisAmount, currency, userEditedAmount])
+  }, [thisAmount, currency, userEditedAmount, price, thisAddressType, randomSatoshis, updateConvertedCurrencyObj, props.setCurrencyObject])

Also guard against division by zero when price is falsy:

-      const convertedAmount = obj.float / price
-      const convertedObj = price
+      const convertedObj = price && price > 0
         ? getCurrencyObject(
-          convertedAmount,
+          obj.float / price,
           thisAddressType,
           randomSatoshis,
         )
         : null;
🤖 Prompt for AI Agents
In react/lib/components/Widget/Widget.tsx around lines 698 to 725, the effect
lists only [thisAmount, currency, userEditedAmount] but reads price,
thisAddressType, randomSatoshis, updateConvertedCurrencyObj and
props.setCurrencyObject so conversions can become stale; add price,
thisAddressType, randomSatoshis, updateConvertedCurrencyObj, and
props.setCurrencyObject to the dependency array and, inside the branch that
computes convertedAmount, guard the division by zero by checking price
truthiness before dividing (return or set convertedObj to null when price is
falsy) so you never divide by zero and converted state updates when any input
changes.

@chedieck chedieck merged commit 08eb581 into master Nov 28, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement (behind the scenes) Stuff that users won't see

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants