Skip to content

Commit c029f4e

Browse files
committed
fix(money): show toast and keep keyboard open when amount update fails on Done
1 parent 981e9a2 commit c029f4e

4 files changed

Lines changed: 115 additions & 83 deletions

File tree

app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.test.tsx

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { act } from 'react';
22
import { merge, noop } from 'lodash';
3+
import { ToastContext } from '../../../../../../component-library/components/Toast';
34
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
45
import {
56
CustomAmountInfo,
@@ -177,32 +178,42 @@ jest.mock('../../../../../UI/Ramp/hooks/useRampsPaymentMethods', () => ({
177178
const TOKEN_ADDRESS_MOCK = '0x123' as Hex;
178179
const CHAIN_ID_MOCK = '0x1' as Hex;
179180

181+
const mockShowToast = jest.fn();
182+
const mockToastRef = {
183+
current: { showToast: mockShowToast, closeToast: jest.fn() },
184+
};
185+
180186
function render(
181187
props: CustomAmountInfoProps & { transactionType?: TransactionType } = {},
182188
) {
183-
return renderWithProvider(<CustomAmountInfo {...props} />, {
184-
state: merge(
185-
{},
186-
simpleSendTransactionControllerMock,
187-
transactionApprovalControllerMock,
188-
otherControllersMock,
189-
{
190-
engine: {
191-
backgroundState: {
192-
TransactionController: {
193-
transactions: [
194-
{
195-
type:
196-
props.transactionType ||
197-
TransactionType.contractInteraction,
198-
},
199-
],
189+
return renderWithProvider(
190+
<ToastContext.Provider value={{ toastRef: mockToastRef } as never}>
191+
<CustomAmountInfo {...props} />
192+
</ToastContext.Provider>,
193+
{
194+
state: merge(
195+
{},
196+
simpleSendTransactionControllerMock,
197+
transactionApprovalControllerMock,
198+
otherControllersMock,
199+
{
200+
engine: {
201+
backgroundState: {
202+
TransactionController: {
203+
transactions: [
204+
{
205+
type:
206+
props.transactionType ||
207+
TransactionType.contractInteraction,
208+
},
209+
],
210+
},
200211
},
201212
},
202213
},
203-
},
204-
),
205-
});
214+
),
215+
},
216+
);
206217
}
207218

208219
describe('CustomAmountInfo', () => {
@@ -261,6 +272,7 @@ describe('CustomAmountInfo', () => {
261272

262273
beforeEach(() => {
263274
jest.resetAllMocks();
275+
mockShowToast.mockReset();
264276

265277
mockUseRampsUserRegion.mockReturnValue({
266278
userRegion: { regionCode: 'us-ca' },
@@ -577,7 +589,7 @@ describe('CustomAmountInfo', () => {
577589
expect(onConfirmMock).toHaveBeenCalledTimes(1);
578590
});
579591

580-
it('still runs UI cleanup and logs the error when updateTokenAmount rejects on Done', async () => {
592+
it('shows toast, keeps keyboard open, and skips onAmountSubmit when updateTokenAmount rejects on Done', async () => {
581593
const error = new Error('update failed');
582594
const updateTokenAmountMock = jest.fn().mockRejectedValue(error);
583595
const mockOnAmountSubmit = jest.fn();
@@ -596,14 +608,24 @@ describe('CustomAmountInfo', () => {
596608
updateTokenAmount: updateTokenAmountMock,
597609
});
598610

599-
const { getByText } = render({ onAmountSubmit: mockOnAmountSubmit });
611+
const { getByText, queryByText } = render({
612+
onAmountSubmit: mockOnAmountSubmit,
613+
});
600614

601615
await act(async () => {
602616
fireEvent.press(getByText(strings('confirm.edit_amount_done')));
603617
});
604618

605619
expect(updateTokenAmountMock).toHaveBeenCalledTimes(1);
606-
expect(mockOnAmountSubmit).toHaveBeenCalledTimes(1);
620+
// onAmountSubmit must NOT be called — keep keyboard visible for retry
621+
expect(mockOnAmountSubmit).not.toHaveBeenCalled();
622+
// Toast must be shown with the transaction-update title
623+
expect(mockShowToast).toHaveBeenCalledTimes(1);
624+
expect(mockShowToast).toHaveBeenCalledWith(
625+
expect.objectContaining({ labelOptions: expect.any(Array) }),
626+
);
627+
// Keyboard stays open: Done button still present
628+
expect(queryByText(strings('confirm.edit_amount_done'))).toBeOnTheScreen();
607629
expect(loggerErrorMock).toHaveBeenCalledWith(
608630
error,
609631
expect.stringContaining('Failed to apply custom amount on Done press'),
@@ -932,7 +954,7 @@ describe('CustomAmountInfo', () => {
932954
);
933955
});
934956

935-
it('does not fire RAMPS_ORDER_PROPOSED when applying the amount throws on Done', async () => {
957+
it('does not fire RAMPS_ORDER_PROPOSED and shows toast when applying the amount throws on Done', async () => {
936958
setMoneyFlow();
937959
const error = new Error('update failed');
938960
const loggerErrorMock = jest.mocked(Logger.error);
@@ -949,8 +971,9 @@ describe('CustomAmountInfo', () => {
949971
fireEvent.press(getByText(strings('confirm.edit_amount_done')));
950972
});
951973

952-
// The Done handler's catch suppresses the commit but logs the error.
974+
// The Done handler's catch shows a toast and returns early without committing.
953975
expect(emittedPayloadFor('RAMPS_ORDER_PROPOSED')).toBeUndefined();
976+
expect(mockShowToast).toHaveBeenCalledTimes(1);
954977
expect(loggerErrorMock).toHaveBeenCalledWith(
955978
error,
956979
expect.stringContaining('Failed to apply custom amount on Done press'),

app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.tsx

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import React, { ReactNode, memo, useCallback, useRef, useState } from 'react';
1+
import React, {
2+
ReactNode,
3+
memo,
4+
useCallback,
5+
useContext,
6+
useRef,
7+
useState,
8+
} from 'react';
29
import { toCaipAssetType } from '@metamask/utils';
310
import { TransactionType } from '@metamask/transaction-controller';
411
import { PayTokenAmount, PayTokenAmountSkeleton } from '../../pay-token-amount';
@@ -65,6 +72,8 @@ import { useConfirmActions } from '../../../hooks/useConfirmActions';
6572
import EngineService from '../../../../../../core/EngineService';
6673
import Engine from '../../../../../../core/Engine';
6774
import Logger from '../../../../../../util/Logger';
75+
import { getTransactionUpdateErrorToastOptions } from '../../../../../../util/confirmation/transactions';
76+
import { ToastContext } from '../../../../../../component-library/components/Toast';
6877
import { ConfirmationFooterSelectorIDs } from '../../../ConfirmationView.testIds';
6978
import { useTransactionPayToken } from '../../../hooks/pay/useTransactionPayToken';
7079
import { useMoneyNoFeeTokens } from '../../../hooks/pay/useMoneyNoFeeTokens';
@@ -161,6 +170,8 @@ export const CustomAmountInfo: React.FC<CustomAmountInfoProps> = memo(
161170
const accountOverride = useTransactionAccountOverride();
162171
const isWithdraw = isTransactionPayWithdraw(transactionMeta);
163172

173+
const { toastRef } = useContext(ToastContext);
174+
164175
const isResultReady = useIsResultReady({ isKeyboardVisible });
165176
const quotes = useTransactionPayQuotes();
166177
const isQuotesLoading = useIsTransactionPayLoading();
@@ -198,18 +209,24 @@ export const CustomAmountInfo: React.FC<CustomAmountInfoProps> = memo(
198209
const handleDone = useCallback(async () => {
199210
try {
200211
if (selectedFiatPaymentMethodId && transactionId) {
201-
Engine.context.TransactionPayController.updateFiatPayment({
202-
transactionId,
203-
callback: (fp) => {
204-
fp.amountFiat = amountFiat;
205-
},
206-
});
207-
208-
// Fiat deposits need nested calldata (approve + deposit) populated
209-
// with approximate amounts now so the transaction is valid at submit
210-
// time. Core will re-encode with exact amounts after settlement.
211212
if (isMoneyAccountDeposit) {
213+
// Update calldata first; only commit the fiat payment amount after
214+
// success so Pay config does not reflect an amount whose calldata
215+
// update failed.
212216
await updateTokenAmount();
217+
Engine.context.TransactionPayController.updateFiatPayment({
218+
transactionId,
219+
callback: (fp) => {
220+
fp.amountFiat = amountFiat;
221+
},
222+
});
223+
} else {
224+
Engine.context.TransactionPayController.updateFiatPayment({
225+
transactionId,
226+
callback: (fp) => {
227+
fp.amountFiat = amountFiat;
228+
},
229+
});
213230
}
214231
} else {
215232
await updateTokenAmount();
@@ -222,16 +239,21 @@ export const CustomAmountInfo: React.FC<CustomAmountInfoProps> = memo(
222239
error as Error,
223240
'Failed to apply custom amount on Done press',
224241
);
225-
} finally {
226-
EngineService.flushState();
227-
setIsKeyboardVisible(false);
228-
onAmountSubmit?.();
242+
toastRef?.current?.showToast(
243+
getTransactionUpdateErrorToastOptions(error),
244+
);
245+
// Keep keyboard visible so the user can retry; do not advance the flow.
246+
return;
229247
}
248+
EngineService.flushState();
249+
setIsKeyboardVisible(false);
250+
onAmountSubmit?.();
230251
}, [
231252
amountFiat,
232253
isMoneyAccountDeposit,
233254
onAmountSubmit,
234255
selectedFiatPaymentMethodId,
256+
toastRef,
235257
trackAmountCommitted,
236258
transactionId,
237259
updateTokenAmount,

app/components/Views/confirmations/hooks/pay/useUpdateTransactionPayAmount.test.ts

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ describe('useUpdateTransactionPayAmount', () => {
179179
expect(updateAtomicBatchDataMock).not.toHaveBeenCalled();
180180
});
181181

182-
it('logs an error when updateAtomicBatchData rejects', async () => {
182+
it('rejects and logs when updateAtomicBatchData rejects', async () => {
183183
const error = new Error('boom');
184184
updateAtomicBatchDataMock.mockRejectedValue(error);
185185
updateMoneyAccountDepositTokenAmountMock.mockResolvedValue([
@@ -188,27 +188,20 @@ describe('useUpdateTransactionPayAmount', () => {
188188

189189
const { result } = runHook({ transactionMeta: moneyAccountDepositMeta });
190190

191-
result.current.updateTransactionPayAmount('1.23');
192-
193-
await flushPromises();
194-
195-
expect(loggerErrorMock).toHaveBeenCalledWith(
196-
error,
197-
expect.stringContaining(
198-
'Failed to update transaction pay amount in nested transaction',
199-
),
200-
);
191+
await expect(
192+
result.current.updateTransactionPayAmount('1.23'),
193+
).rejects.toThrow('boom');
201194
});
202195

203-
it('logs an error when updateMoneyAccountDepositTokenAmount rejects', async () => {
196+
it('rejects and logs when updateMoneyAccountDepositTokenAmount rejects', async () => {
204197
const error = new Error('rpc failure');
205198
updateMoneyAccountDepositTokenAmountMock.mockRejectedValue(error);
206199

207200
const { result } = runHook({ transactionMeta: moneyAccountDepositMeta });
208201

209-
result.current.updateTransactionPayAmount('1.23');
210-
211-
await flushPromises();
202+
await expect(
203+
result.current.updateTransactionPayAmount('1.23'),
204+
).rejects.toThrow('rpc failure');
212205

213206
expect(updateAtomicBatchDataMock).not.toHaveBeenCalled();
214207
expect(loggerErrorMock).toHaveBeenCalledWith(
@@ -281,15 +274,15 @@ describe('useUpdateTransactionPayAmount', () => {
281274
);
282275
});
283276

284-
it('logs an error when updateMoneyAccountWithdrawTokenAmount rejects', async () => {
277+
it('rejects and logs when updateMoneyAccountWithdrawTokenAmount rejects', async () => {
285278
const error = new Error('withdraw rpc failure');
286279
updateMoneyAccountWithdrawTokenAmountMock.mockRejectedValue(error);
287280

288281
const { result } = runHook({ transactionMeta: moneyAccountWithdrawMeta });
289282

290-
result.current.updateTransactionPayAmount('4.56');
291-
292-
await flushPromises();
283+
await expect(
284+
result.current.updateTransactionPayAmount('4.56'),
285+
).rejects.toThrow('withdraw rpc failure');
293286

294287
expect(updateAtomicBatchDataMock).not.toHaveBeenCalled();
295288
expect(loggerErrorMock).toHaveBeenCalledWith(
@@ -408,7 +401,7 @@ describe('useUpdateTransactionPayAmount', () => {
408401
expect(updateTransactionMock).not.toHaveBeenCalled();
409402
});
410403

411-
it('logs an error when updateTransaction throws', async () => {
404+
it('rejects and logs when updateTransaction throws', async () => {
412405
const error = new Error('updateTransaction failed');
413406
updateTransactionMock.mockImplementation(() => {
414407
throw error;
@@ -418,9 +411,9 @@ describe('useUpdateTransactionPayAmount', () => {
418411
transactionMeta: moneyAccountDepositMetaWithRequiredAssets,
419412
});
420413

421-
result.current.updateTransactionPayAmount('1');
422-
423-
await flushPromises();
414+
await expect(
415+
result.current.updateTransactionPayAmount('1'),
416+
).rejects.toThrow('updateTransaction failed');
424417

425418
expect(loggerErrorMock).toHaveBeenCalledWith(
426419
error,

app/components/Views/confirmations/hooks/pay/useUpdateTransactionPayAmount.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,34 +45,27 @@ export function useUpdateTransactionPayAmount() {
4545
return;
4646
}
4747

48+
let updates: UpdateTransactionPayAmountCall[];
4849
try {
49-
const updates = await updater(
50+
updates = await updater(
5051
transactionMeta,
5152
amountHuman,
5253
recipientOverride,
5354
);
54-
55-
const results = await Promise.allSettled(
56-
updates.map(({ nestedTransactionIndex, transactionData }) =>
57-
updateAtomicBatchData({
58-
transactionId: transactionMeta.id,
59-
transactionIndex: nestedTransactionIndex,
60-
transactionData,
61-
}),
62-
),
63-
);
64-
65-
for (const result of results) {
66-
if (result.status === 'rejected') {
67-
Logger.error(
68-
result.reason as Error,
69-
'Failed to update transaction pay amount in nested transaction',
70-
);
71-
}
72-
}
7355
} catch (error) {
7456
Logger.error(error as Error, preparationErrorMessage);
57+
throw error;
7558
}
59+
60+
await Promise.all(
61+
updates.map(({ nestedTransactionIndex, transactionData }) =>
62+
updateAtomicBatchData({
63+
transactionId: transactionMeta.id,
64+
transactionIndex: nestedTransactionIndex,
65+
transactionData,
66+
}),
67+
),
68+
);
7669
},
7770
[transactionMeta],
7871
);
@@ -158,5 +151,6 @@ function syncMoneyAccountDepositRequiredAssets(
158151
error as Error,
159152
'Failed to sync Money Account deposit requiredAssets amount',
160153
);
154+
throw error;
161155
}
162156
}

0 commit comments

Comments
 (0)