fix balance float on confirm tx page
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
import React from 'react';
|
||||
import ReactTestRenderer, { act } from 'react-test-renderer';
|
||||
|
||||
const mockNavigate = {
|
||||
popToTop: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
};
|
||||
|
||||
let mockSparkInformation = { tokens: {} };
|
||||
|
||||
jest.mock('@react-navigation/native', () => ({
|
||||
useNavigation: () => mockNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../app/constants', () => {
|
||||
const theme = jest.requireActual('../app/constants/theme');
|
||||
return {
|
||||
...theme,
|
||||
CENTER: {},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('lottie-react-native', () => {
|
||||
const MockReact = require('react');
|
||||
return { __esModule: true, default: MockReact.forwardRef(() => null) };
|
||||
});
|
||||
|
||||
jest.mock('react-native-email-link', () => ({
|
||||
openComposer: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions', () => ({
|
||||
copyToClipboard: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions/lottieViewColorTransformer', () => ({
|
||||
applyErrorAnimationTheme: animation => animation,
|
||||
updateConfirmAnimation: animation => animation,
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions/lrc20/formatTokensBalance', () => ({
|
||||
__esModule: true,
|
||||
default: (amount, decimals) => `tokens-${amount}-${decimals}`,
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions/displayCorrectDenomination', () => ({
|
||||
__esModule: true,
|
||||
default: ({ amount }) => `denom-${amount}`,
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions/customUUID', () => ({
|
||||
__esModule: true,
|
||||
default: () => 'uuid',
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions/cachedImage', () => ({
|
||||
getCachedProfileImage: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../db', () => ({
|
||||
getSingleContact: jest.fn(async () => []),
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions/lnurl/normalizeLNURLAddress', () => ({
|
||||
__esModule: true,
|
||||
default: value => value,
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions/lnurl', () => ({
|
||||
isBlitzLNURLAddress: () => false,
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions/sendBitcoin/getPhonePaymentAddress', () => ({
|
||||
canonicalizePhonePaymentAddress: value => value,
|
||||
}));
|
||||
|
||||
jest.mock('../app/hooks/themeColors', () => () => ({
|
||||
backgroundOffset: '#eeeeee',
|
||||
textColor: '#111111',
|
||||
}));
|
||||
|
||||
jest.mock('../context-store/theme', () => ({
|
||||
useGlobalThemeContext: () => ({ theme: false, darkModeType: false }),
|
||||
}));
|
||||
|
||||
jest.mock('../context-store/toastManager', () => ({
|
||||
useToast: () => ({ showToast: jest.fn() }),
|
||||
}));
|
||||
|
||||
jest.mock('../context-store/sparkContext', () => ({
|
||||
useSparkWallet: () => ({ sparkInformation: mockSparkInformation }),
|
||||
}));
|
||||
|
||||
jest.mock('../context-store/appStatus', () => ({
|
||||
useAppStatus: () => ({ screenDimensions: { width: 400, height: 800 } }),
|
||||
}));
|
||||
|
||||
jest.mock('../context-store/context', () => ({
|
||||
useGlobalContextProvider: () => ({
|
||||
masterInfoObject: {
|
||||
fiatCurrency: 'USD',
|
||||
userBalanceDenomination: 'sats',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../context-store/nodeContext', () => ({
|
||||
useNodeContext: () => ({ fiatStats: { coin: 'USD', value: 100000000 } }),
|
||||
}));
|
||||
|
||||
jest.mock('../context-store/globalContacts', () => ({
|
||||
useGlobalContactsInfo: () => ({ decodedAddedContacts: [] }),
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions/CustomElements', () => {
|
||||
const MockReact = require('react');
|
||||
const RN = require('react-native');
|
||||
return {
|
||||
GlobalThemeView: ({ children }) =>
|
||||
MockReact.createElement(RN.View, null, children),
|
||||
ThemeText: ({ content }) => MockReact.createElement(RN.Text, null, content),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../app/functions/CustomElements/button', () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
jest.mock('../app/functions/CustomElements/dropdownMenu', () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
// FormattedSatText and FormattedBalanceInput are the two amount renderers we
|
||||
// want to distinguish between. Each mock surfaces the props it received via a
|
||||
// `componentProps` prop on a host Text node so tests can inspect them.
|
||||
jest.mock('../app/functions/CustomElements/satTextDisplay', () => {
|
||||
const MockReact = require('react');
|
||||
const RN = require('react-native');
|
||||
return {
|
||||
__esModule: true,
|
||||
default: props =>
|
||||
MockReact.createElement(
|
||||
RN.Text,
|
||||
{ testID: 'formatted-sat-text', componentProps: props },
|
||||
'sat-text',
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../app/functions/CustomElements/formattedBalanceInput', () => {
|
||||
const MockReact = require('react');
|
||||
const RN = require('react-native');
|
||||
return {
|
||||
__esModule: true,
|
||||
default: props =>
|
||||
MockReact.createElement(
|
||||
RN.Text,
|
||||
{ testID: 'formatted-balance-input', componentProps: props },
|
||||
'balance-input',
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const ConfirmTxPage = require('../app/screens/inAccount/confirmTxPage').default;
|
||||
|
||||
async function renderConfirm(routeParams = {}) {
|
||||
let renderer;
|
||||
await act(async () => {
|
||||
renderer = ReactTestRenderer.create(
|
||||
<ConfirmTxPage route={{ params: routeParams }} />,
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
return renderer;
|
||||
}
|
||||
|
||||
function balanceInputProps(renderer) {
|
||||
const nodes = renderer.root.findAllByProps({
|
||||
testID: 'formatted-balance-input',
|
||||
});
|
||||
return nodes.length ? nodes[0].props.componentProps : null;
|
||||
}
|
||||
|
||||
function satTextProps(renderer) {
|
||||
const nodes = renderer.root.findAllByProps({ testID: 'formatted-sat-text' });
|
||||
return nodes.length ? nodes[0].props.componentProps : null;
|
||||
}
|
||||
|
||||
const successfulOutgoingTx = {
|
||||
details: { amount: 1500, direction: 'OUTGOING', paymentType: 'lightning' },
|
||||
};
|
||||
|
||||
const fiatPaymentDisplay = {
|
||||
denomination: 'fiat',
|
||||
forceCurrency: 'EUR',
|
||||
forceFiatStats: { coin: 'EUR', value: 95000000 },
|
||||
};
|
||||
|
||||
describe('ConfirmTxPage amount rendering', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSparkInformation = { tokens: {} };
|
||||
});
|
||||
|
||||
test('does not crash and falls back to FormattedSatText when displayAmount is present but paymentDisplay is missing', async () => {
|
||||
const renderer = await renderConfirm({
|
||||
transaction: successfulOutgoingTx,
|
||||
displayAmount: '10.50',
|
||||
// paymentDisplay intentionally omitted
|
||||
});
|
||||
|
||||
// The displayAmount branch must not dereference an undefined paymentDisplay.
|
||||
expect(balanceInputProps(renderer)).toBeNull();
|
||||
const satText = satTextProps(renderer);
|
||||
expect(satText).not.toBeNull();
|
||||
expect(satText.balance).toBe(1500);
|
||||
expect(satText.globalBalanceDenomination).toBeUndefined();
|
||||
});
|
||||
|
||||
test('renders FormattedBalanceInput with the reviewed displayAmount and paymentDisplay when both are present', async () => {
|
||||
const renderer = await renderConfirm({
|
||||
transaction: successfulOutgoingTx,
|
||||
displayAmount: '10.50',
|
||||
paymentDisplay: fiatPaymentDisplay,
|
||||
});
|
||||
|
||||
expect(satTextProps(renderer)).toBeNull();
|
||||
const props = balanceInputProps(renderer);
|
||||
expect(props).not.toBeNull();
|
||||
expect(props.amountValue).toBe('10.50');
|
||||
expect(props.inputDenomination).toBe('fiat');
|
||||
expect(props.forceCurrency).toBe('EUR');
|
||||
expect(props.maxDecimals).toBe(2);
|
||||
});
|
||||
|
||||
test('falls back to FormattedSatText carrying paymentDisplay when displayAmount is absent', async () => {
|
||||
const renderer = await renderConfirm({
|
||||
transaction: successfulOutgoingTx,
|
||||
paymentDisplay: fiatPaymentDisplay,
|
||||
// displayAmount intentionally omitted (legacy nav without displayAmount)
|
||||
});
|
||||
|
||||
expect(balanceInputProps(renderer)).toBeNull();
|
||||
const props = satTextProps(renderer);
|
||||
expect(props).not.toBeNull();
|
||||
expect(props.balance).toBe(1500);
|
||||
expect(props.globalBalanceDenomination).toBe('fiat');
|
||||
expect(props.forceCurrency).toBe('EUR');
|
||||
});
|
||||
|
||||
test('uses token metadata for an LRC20 send rendered through FormattedBalanceInput', async () => {
|
||||
mockSparkInformation = {
|
||||
tokens: {
|
||||
'token-id': { tokenMetadata: { tokenTicker: 'USDB', decimals: 6 } },
|
||||
},
|
||||
};
|
||||
const renderer = await renderConfirm({
|
||||
transaction: {
|
||||
details: {
|
||||
amount: 5000,
|
||||
direction: 'OUTGOING',
|
||||
isLRC20Payment: true,
|
||||
LRC20Token: 'token-id',
|
||||
},
|
||||
},
|
||||
displayAmount: '12.34',
|
||||
paymentDisplay: { denomination: 'fiat', forceCurrency: 'USD', forceFiatStats: null },
|
||||
});
|
||||
|
||||
const props = balanceInputProps(renderer);
|
||||
expect(props).not.toBeNull();
|
||||
expect(props.customCurrencyCode).toBe('USDB');
|
||||
expect(props.maxDecimals).toBe(6);
|
||||
});
|
||||
});
|
||||
@@ -1279,6 +1279,7 @@ export default function SendPaymentScreen(props) {
|
||||
: undefined,
|
||||
blitzContactInfo: paymentInfo?.blitzContactInfo,
|
||||
paymentDisplay: primaryDisplayRef.current,
|
||||
displayAmount,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -1308,6 +1309,7 @@ export default function SendPaymentScreen(props) {
|
||||
: undefined,
|
||||
blitzContactInfo: paymentInfo?.blitzContactInfo,
|
||||
paymentDisplay: primaryDisplayRef.current,
|
||||
displayAmount,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -1346,6 +1348,7 @@ export default function SendPaymentScreen(props) {
|
||||
resolvedPaymentMethod,
|
||||
fiatValueConvertedSendAmount,
|
||||
paymentValidation,
|
||||
displayAmount,
|
||||
]);
|
||||
|
||||
const handleSelectPaymentMethod = useCallback(
|
||||
|
||||
@@ -498,6 +498,7 @@ export default function StablecoinSendScreen() {
|
||||
params: {
|
||||
transaction: pendingTx,
|
||||
paymentDisplay: primaryDisplayRef.current,
|
||||
displayAmount: rawInput,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -546,6 +547,7 @@ export default function StablecoinSendScreen() {
|
||||
publicKey,
|
||||
clearCountdown,
|
||||
t,
|
||||
rawInput,
|
||||
]);
|
||||
|
||||
const handleEmoji = newDescription => {
|
||||
|
||||
@@ -74,6 +74,7 @@ export function validateSplitPayment({
|
||||
value: swapUSDPriceDollars,
|
||||
coin: 'USD',
|
||||
},
|
||||
forceCurrency: 'USD',
|
||||
}),
|
||||
});
|
||||
} else if (!isUSD && hasUsdForAmount && !aboveUsdSwapMin) {
|
||||
|
||||
@@ -31,6 +31,7 @@ import { HIDDEN_OPACITY, INSET_WINDOW_WIDTH } from '../../constants/theme';
|
||||
import normalizeLNURLAddress from '../../functions/lnurl/normalizeLNURLAddress';
|
||||
import { isBlitzLNURLAddress } from '../../functions/lnurl';
|
||||
import { canonicalizePhonePaymentAddress } from '../../functions/sendBitcoin/getPhonePaymentAddress';
|
||||
import FormattedBalanceInput from '../../functions/CustomElements/formattedBalanceInput';
|
||||
|
||||
const confirmTxAnimation = require('../../assets/confirmTxAnimation.json');
|
||||
const errorTxAnimation = require('../../assets/errorTxAnimation.json');
|
||||
@@ -55,6 +56,7 @@ export default function ConfirmTxPage(props) {
|
||||
const isBlitzAddress = isBlitzLNURLAddress(lnurlAddress);
|
||||
const lnurlUsername = lnurlAddress?.split('@')[0]?.toLowerCase();
|
||||
const blitzContactInfo = props.route.params?.blitzContactInfo;
|
||||
const displayAmount = props.route.params?.displayAmount;
|
||||
// The display currency the user entered/reviewed the payment in (e.g. EUR),
|
||||
// passed from the send screens so the success amount matches what they saw.
|
||||
const paymentDisplay = props.route.params?.paymentDisplay;
|
||||
@@ -289,50 +291,46 @@ export default function ConfirmTxPage(props) {
|
||||
|
||||
{didSucceed && !isLNURLAuth && (
|
||||
<View style={{ marginBottom: 10 }}>
|
||||
<FormattedSatText
|
||||
styles={{
|
||||
fontSize: SIZES.huge,
|
||||
includeFontPadding: false,
|
||||
}}
|
||||
neverHideBalance={true}
|
||||
balance={isLRC20Payment ? formattedTokensBalance : amount}
|
||||
useCustomLabel={isLRC20Payment}
|
||||
customLabel={token?.tokenMetadata?.tokenTicker}
|
||||
useMillionDenomination={true}
|
||||
globalBalanceDenomination={
|
||||
paymentDisplay && !isLRC20Payment
|
||||
? paymentDisplay.denomination
|
||||
: undefined
|
||||
}
|
||||
forceCurrency={
|
||||
paymentDisplay && !isLRC20Payment
|
||||
? paymentDisplay.forceCurrency
|
||||
: null
|
||||
}
|
||||
forceFiatStats={
|
||||
paymentDisplay && !isLRC20Payment
|
||||
? paymentDisplay.forceFiatStats
|
||||
: null
|
||||
}
|
||||
/>
|
||||
{/* {isLRC20Payment && formattedTokensBalance < 1 && (
|
||||
{displayAmount && paymentDisplay ? (
|
||||
<FormattedBalanceInput
|
||||
maxWidth={0.7}
|
||||
amountValue={displayAmount}
|
||||
inputDenomination={paymentDisplay.denomination}
|
||||
forceCurrency={paymentDisplay.forceCurrency}
|
||||
forceFiatStats={paymentDisplay.forceFiatStats}
|
||||
customCurrencyCode={token?.tokenMetadata?.tokenTicker}
|
||||
maxDecimals={
|
||||
isLRC20Payment ? token?.tokenMetadata?.decimals ?? 0 : 2
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<FormattedSatText
|
||||
containerStyles={{
|
||||
...CENTER,
|
||||
}}
|
||||
styles={{
|
||||
fontSize: SIZES.small,
|
||||
fontSize: 45,
|
||||
includeFontPadding: false,
|
||||
}}
|
||||
neverHideBalance={true}
|
||||
balance={formatTokensNumber(
|
||||
amount,
|
||||
token?.tokenMetadata?.decimals,
|
||||
)}
|
||||
balance={isLRC20Payment ? formattedTokensBalance : amount}
|
||||
useCustomLabel={isLRC20Payment}
|
||||
customLabel={token?.tokenMetadata?.tokenTicker}
|
||||
useMillionDenomination={true}
|
||||
globalBalanceDenomination={
|
||||
paymentDisplay && !isLRC20Payment
|
||||
? paymentDisplay.denomination
|
||||
: undefined
|
||||
}
|
||||
forceCurrency={
|
||||
paymentDisplay && !isLRC20Payment
|
||||
? paymentDisplay.forceCurrency
|
||||
: null
|
||||
}
|
||||
forceFiatStats={
|
||||
paymentDisplay && !isLRC20Payment
|
||||
? paymentDisplay.forceFiatStats
|
||||
: null
|
||||
}
|
||||
/>
|
||||
)} */}
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1180,7 +1180,7 @@
|
||||
"receive_amount": "receive amount",
|
||||
"descriptionInputPlaceholder": "Description...",
|
||||
"editAmount": "Edit invoice",
|
||||
"minUSDSwap": "Payments converted to dollars require a minimum amount of:\n{{amount}}"
|
||||
"minUSDSwap": "Payments received as dollars require a minimum amount of:\n{{amount}}"
|
||||
},
|
||||
"buttonContainer": {
|
||||
"format": "Change receive method",
|
||||
@@ -2185,7 +2185,7 @@
|
||||
"generatingInvoice": "Generating Invoice",
|
||||
"amountPlaceholder": "Any amount",
|
||||
"copyInvoice": "Copy Invoice",
|
||||
"usdSwapMinNotice": "Only payments over {{amount}} will be converted to dollars",
|
||||
"usdSwapMinNotice": "Only payments over {{amount}} will be received to dollars",
|
||||
"shareInvoice": "Share Invoice",
|
||||
"walletNotConnected": "Wallet not connected. Please try again.",
|
||||
"paylinkSaveError": "Failed to save paylink",
|
||||
|
||||
Reference in New Issue
Block a user