Fix balance rerender (#994)

* remove confirm toast message on send pages

* use balance refs to avoid logic reredners

* remove await on submit
This commit is contained in:
Blake Kaufman
2026-07-05 14:50:58 -04:00
committed by GitHub
parent 0bf6c006a9
commit 248de61c9d
4 changed files with 73 additions and 18 deletions
@@ -87,6 +87,22 @@ export default function ConfirmSplitPayment(props) {
const { t } = useTranslation();
const { bitcoinBalance, dollarBalanceSat, dollarBalanceToken } =
useUserBalanceContext();
// Latest-value ref for balances. The can-pay memo and swap-fee effect read
// balance through this ref instead of the reactive context values so balance
// is dropped from their dependency arrays. A burst of incoming payments then
// can't recompute payment viability or refire simulateSwap mid-send; the
// effect still reads the current balance whenever it legitimately re-runs.
// The actual send validates live balance at send time.
const balanceRef = useRef({
bitcoinBalance,
dollarBalanceSat,
dollarBalanceToken,
});
balanceRef.current.bitcoinBalance = bitcoinBalance;
balanceRef.current.dollarBalanceSat = dollarBalanceSat;
balanceRef.current.dollarBalanceToken = dollarBalanceToken;
const { currentWalletMnemoinc } = useActiveCustodyAccount();
const { contactsPrivateKey } = useKeysContext();
const { sparkInformation } = useSparkWallet();
@@ -186,6 +202,8 @@ export default function ConfirmSplitPayment(props) {
// ── Balance validation ──────────────────────────────────────────────────────
const { canPayBTC, canPayUSD } = useMemo(() => {
// eslint-disable-next-line no-shadow -- read frozen balance, decoupled from live deps
const { bitcoinBalance, dollarBalanceSat } = balanceRef.current;
const price = poolInfoRef.currentPriceAInB;
return validateSplitPayment({
totalSats: totalSplitSats,
@@ -202,8 +220,6 @@ export default function ConfirmSplitPayment(props) {
}, [
totalSplitSats,
paymentCurrency,
bitcoinBalance,
dollarBalanceSat,
swapLimits,
masterInfoObject,
swapUSDPriceDollars,
@@ -270,6 +286,8 @@ export default function ConfirmSplitPayment(props) {
useEffect(() => {
let cancelled = false;
// eslint-disable-next-line no-shadow -- read frozen balance, decoupled from live deps
const { bitcoinBalance, dollarBalanceSat } = balanceRef.current;
const clearSwapFee = () => {
swapFeeKeyRef.current = null;
@@ -405,8 +423,6 @@ export default function ConfirmSplitPayment(props) {
poolInfoRef,
totalSplitSats,
totalSplitDollars,
bitcoinBalance,
dollarBalanceSat,
min_usd_swap_amount,
swapLimits.bitcoin,
currentWalletMnemoinc,
@@ -116,6 +116,21 @@ export default function SendPaymentScreen(props) {
const { t } = useTranslation();
const { bitcoinBalance, dollarBalanceSat, dollarBalanceToken } =
useUserBalanceContext();
// Latest-value ref for balances (same pattern as stablecoinSendScreen). The
// send-decision memos/effects read balance through this ref instead of the
// reactive context values, so balance is dropped from their dependency
// arrays. A burst of incoming payments then can't recompute the payment
// method, flip uiState, or refire fee estimation mid-send. The actual send
// still validates against live balance via sparkInfoRef.current.
const balanceRef = useRef({
bitcoinBalance,
dollarBalanceSat,
dollarBalanceToken,
});
balanceRef.current.bitcoinBalance = bitcoinBalance;
balanceRef.current.dollarBalanceSat = dollarBalanceSat;
balanceRef.current.dollarBalanceToken = dollarBalanceToken;
const { sendWebViewRequest } = useWebView();
const { currentWalletMnemoinc } = useActiveCustodyAccount();
const { accountMnemoinc, contactsPrivateKey, publicKey } = useKeysContext();
@@ -264,6 +279,9 @@ export default function SendPaymentScreen(props) {
(isLightningPayment && paymentInfo?.usingZeroAmountInvoice);
const resolvedPaymentMethod = useMemo(() => {
// eslint-disable-next-line no-shadow -- read frozen balance, decoupled from live deps
const { bitcoinBalance, dollarBalanceSat, dollarBalanceToken } =
balanceRef.current;
if (!paymentInfo || !Object.keys(paymentInfo || {}).length)
return undefined;
@@ -328,9 +346,6 @@ export default function SendPaymentScreen(props) {
enteredPaymentInfo?.inputCurrency,
userPaymentMethod,
paymentInfo,
dollarBalanceSat,
dollarBalanceToken,
bitcoinBalance,
isBitcoinPayment,
isUsingLRC20,
isSparkPayment,
@@ -449,6 +464,8 @@ export default function SendPaymentScreen(props) {
);
const requiresUserMethodSelection = useMemo(() => {
// eslint-disable-next-line no-shadow -- read frozen balance, decoupled from live deps
const { bitcoinBalance, dollarBalanceSat } = balanceRef.current;
if (resolvedPaymentMethod === undefined) return false;
if (
!!preSelectedPaymentMethod ||
@@ -492,8 +509,6 @@ export default function SendPaymentScreen(props) {
didSelectPaymentMethod,
sparkInformation?.didConnectToFlashnet,
paymentInfo?.data?.expectedReceive,
bitcoinBalance,
dollarBalanceSat,
amountViableForSwap,
resolvedPaymentMethod,
receiverExpectsCurrency,
@@ -522,6 +537,9 @@ export default function SendPaymentScreen(props) {
const estimateLightningFee = useCallback(
async (amount, id) => {
// eslint-disable-next-line no-shadow -- read frozen balance, decoupled from live deps
const { bitcoinBalance, dollarBalanceSat, dollarBalanceToken } =
balanceRef.current;
if (!amount || !isLightningPayment || !canEditAmount) {
setIsEstimatingFee(false);
return;
@@ -667,9 +685,6 @@ export default function SendPaymentScreen(props) {
isLightningPayment,
canEditAmount,
resolvedPaymentMethod,
dollarBalanceToken,
dollarBalanceSat,
bitcoinBalance,
paymentInfo,
currentWalletMnemoinc,
masterInfoObject,
@@ -460,7 +460,7 @@ export default function StablecoinSendScreen() {
await bulkUpdateSparkTransactions([pendingTx], 'fullUpdate');
await fetchBackend(
fetchBackend(
'submitFlashnetStablecoinOrder',
{
quoteId: quote.quoteId,
+29 -5
View File
@@ -129,6 +129,23 @@ const SKIP_CONFIRM_NAV_UPDATE_TYPES = new Set([
'incrementalRestore',
]);
// Send screens where an incoming-payment toast would obscure the send UI. Keyed
// by the route names registered in navigation/screens.js.
const BLOCKED_TOAST_ROUTE_NAMES = new Set([
'ConfirmPaymentScreen', // sendPaymentScreen.js
'ConfirmSplitPayment', // confirmSplitPayment.js
'StablecoinSendScreen', // stablecoinSendScreen.js
]);
function isOnSendScreen() {
try {
if (!navigationRef.isReady()) return false;
return BLOCKED_TOAST_ROUTE_NAMES.has(navigationRef.getCurrentRoute()?.name);
} catch {
return false;
}
}
// Initiate context
const SparkWalletManager = createContext(null);
@@ -743,6 +760,11 @@ const SparkWalletProvider = ({ children }) => {
}
handledNavigatedTxs.current.add(parsedTx.sparkID);
if (isOnSendScreen()) {
console.log('On a send screen — suppressing incoming payment toast');
return;
}
// const isOnReceivePage =
// navigationRef
// .getRootState()
@@ -1857,11 +1879,13 @@ const SparkWalletProvider = ({ children }) => {
if (updatedTx.details) {
if (handledNavigatedTxs.current.has(updatedTx.id)) continue;
handledNavigatedTxs.current.add(updatedTx.id);
showToast({
amount: updatedTx.details.amount,
duration: 7000,
type: 'confirmTx',
});
if (!isOnSendScreen()) {
showToast({
amount: updatedTx.details.amount,
duration: 7000,
type: 'confirmTx',
});
}
}
}
}