ensure fiat currency selector cannot hang

This commit is contained in:
Blake Kaufman
2026-07-12 11:01:05 -04:00
parent 9032824cc1
commit 535dced95b
12 changed files with 68 additions and 40 deletions
@@ -92,6 +92,7 @@ export default function AnalyticsCreateBudgetPage() {
onSelectCurrency: async code => {
const response = await selectCurrency(code);
if (response?.didWork) setAmountValue('');
return response;
},
});
@@ -659,6 +659,7 @@ export default function useContactPayment({
async code => {
const response = await selectCurrency(code);
if (response?.didWork) setAmountValue('');
return response;
},
[selectCurrency],
);
@@ -22,8 +22,6 @@ import {
INSET_WINDOW_WIDTH,
} from '../../../../constants/theme';
import FullLoadingScreen from '../../../../functions/CustomElements/loadingScreen';
import loadNewFiatData from '../../../../functions/saveAndUpdateFiatData';
import { useKeysContext } from '../../../../../context-store/keys';
export default function DisplayCurrencySelect({
currentCurrency,
@@ -36,7 +34,6 @@ export default function DisplayCurrencySelect({
const { masterInfoObject } = useGlobalContextProvider();
const { theme, darkModeType } = useGlobalThemeContext();
const { screenDimensions } = useAppStatus();
const { contactsPrivateKey, publicKey } = useKeysContext();
const { backgroundColor, backgroundOffset, textColor } = GetThemeColors();
const normalizedCurrentCurrency = normalizeDisplayCurrency(currentCurrency);
const deviceCurrency = (
@@ -67,28 +64,28 @@ export default function DisplayCurrencySelect({
if (hasSelected.current) return;
hasSelected.current = true;
setIsLoadingNewRate(true);
const normalizedCode = normalizeDisplayCurrency(currency);
currency !== 'SATS' &&
(await loadNewFiatData(
normalizedCode,
contactsPrivateKey,
publicKey,
masterInfoObject,
));
// The parent (useDisplayCurrencyController.selectCurrency) owns the single
// fetch, its hard timeout, rate caching, and ErrorScreen navigation on
// failure. We just reflect its result. Because the timeout lives in the hook,
// the hook stops loading at the same moment this await resolves — no state
// mismatch where the picker gives up but the hook keeps loading.
const response = await onSelectCurrency?.(currency);
if (!isMounted.current) return;
handleBackPressFunction(() => {
navigate.goBack();
onSelectCurrency?.(currency);
});
// Failure or timeout: clear the loader and reset so the picker returns to an
// interactive state instead of relying on unmount to hide the loader.
if (!response || response.didWork === false) {
hasSelected.current = false;
setIsLoadingNewRate(false);
return;
}
// Success: dismiss the picker.
handleBackPressFunction(() => navigate.goBack());
},
[
handleBackPressFunction,
navigate,
onSelectCurrency,
contactsPrivateKey,
publicKey,
masterInfoObject,
],
[handleBackPressFunction, navigate, onSelectCurrency],
);
const pinnedRows = useMemo(() => {
@@ -88,6 +88,7 @@ export default function CreateGift(props) {
onSelectCurrency: async code => {
const response = await selectCurrency(code);
if (response?.didWork) setAmountValue('');
return response;
},
});
@@ -81,6 +81,7 @@ export default function PayLinkAmountInput({
onSelectCurrency: async code => {
const response = await selectCurrency(code);
if (response?.didWork) setAmountValue('');
return response;
},
}),
[navigate, displayCurrency, selectCurrency],
@@ -76,6 +76,7 @@ export default function PoolAmountInput({
onSelectCurrency: async code => {
const response = await selectCurrency(code);
if (response?.didWork) setAmountValue('');
return response;
},
}),
[navigate, displayCurrency, selectCurrency],
@@ -289,6 +289,7 @@ export default function ContributeToPoolHalfModal({
onSelectCurrency: async code => {
const response = await selectCurrency(code);
if (response?.didWork) setAmountValue('');
return response;
},
}),
[displayCurrency, navigate, selectCurrency],
@@ -145,6 +145,7 @@ export default function EditReceivePaymentInformation(props) {
onSelectCurrency: async code => {
const response = await selectCurrency(code);
if (response?.didWork) setAmountValue('');
return response;
},
});
}, [displayCurrency, isKeyboardFocused, navigate, selectCurrency]);
@@ -1417,16 +1417,18 @@ export default function SendPaymentScreen(props) {
currentCurrency: displayCurrency,
onSelectCurrency: async code => {
const response = await selectCurrency(code);
if (!response?.didWork) return;
if (!canEditAmount) return;
setPaymentInfo(prev => ({
...prev,
sendAmount: '',
feeQuote: undefined,
swapPaymentQuote: undefined,
paymentFee: 0,
supportFee: 0,
}));
if (!response?.didWork) return response;
if (canEditAmount) {
setPaymentInfo(prev => ({
...prev,
sendAmount: '',
feeQuote: undefined,
swapPaymentQuote: undefined,
paymentFee: 0,
supportFee: 0,
}));
}
return response;
},
});
};
@@ -387,6 +387,7 @@ export default function StablecoinSendScreen() {
onSelectCurrency: async code => {
const response = await selectCurrency(code);
if (response?.didWork) setRawInput('');
return response;
},
});
};
@@ -88,6 +88,7 @@ export default function CustomInputHalfModal(props) {
onSelectCurrency: async code => {
const response = await selectCurrency(code);
if (response?.didWork) setAmountValue('');
return response;
},
}),
[displayCurrency, navigate, selectCurrency],
+27 -7
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigation } from '@react-navigation/native';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
import { useKeysContext } from '../../context-store/keys';
import { useToast } from '../../context-store/toastManager';
@@ -9,6 +9,9 @@ import {
normalizeDisplayCurrency,
} from '../functions/displayCurrency';
// Hard cap on a single rate fetch, well under loadNewFiatData's ~3 min retry budget.
const CURRENCY_RATE_TIMEOUT_MS = 30000;
export default function useDisplayCurrencyController({
initialCurrency,
fiatStats,
@@ -79,12 +82,29 @@ export default function useDisplayCurrencyController({
try {
setIsLoadingRate(true);
const response = await loadNewFiatData(
normalizedCode,
contactsPrivateKey,
publicKey,
masterInfoObject,
);
// Hard-bound the fetch so isLoadingRate (and the currency picker that
// awaits this call) can never stay up for loadNewFiatData's full retry
// budget (~3 min). On timeout we take the same failure path and clear the
// loader here; the abandoned fetch may still warm the rate cache in the
// background. Keeping the timeout in the hook keeps the picker and the hook
// in sync — they resolve from the same bounded promise, no state mismatch.
let timeoutId;
const response = await Promise.race([
loadNewFiatData(
normalizedCode,
contactsPrivateKey,
publicKey,
masterInfoObject,
),
new Promise(resolve => {
timeoutId = setTimeout(
() => resolve({ didWork: false }),
CURRENCY_RATE_TIMEOUT_MS,
);
}),
]);
clearTimeout(timeoutId);
if (!response.didWork) throw new Error('error loading fiat data');