From 561785584f1d1529275b8989987fb03dd660f69c Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 4 Dec 2024 16:28:26 -0300 Subject: [PATCH] [REF] improve not enough funds/fee err msg --- locales/en/translation.json | 1 + .../wallet/components/ErrorMessages.tsx | 21 +- .../wallet/screens/AccountDetails.tsx | 6 - .../wallet/screens/GlobalSelect.tsx | 11 +- .../wallet/screens/SelectInputs.tsx | 6 - .../wallet/screens/SendToOptions.tsx | 6 - .../wallet/screens/TransactionDetails.tsx | 6 - .../wallet/screens/WalletDetails.tsx | 6 - .../screens/send/confirm/BillConfirm.tsx | 15 +- .../wallet/screens/send/confirm/Confirm.tsx | 6 - .../screens/send/confirm/GiftCardConfirm.tsx | 39 +-- .../screens/send/confirm/PayProConfirm.tsx | 51 ++-- src/store/scan/scan.effects.ts | 12 +- src/store/wallet/effects/send/send.ts | 240 ++++++++++++++---- src/utils/helper-methods.ts | 14 + 15 files changed, 286 insertions(+), 154 deletions(-) diff --git a/locales/en/translation.json b/locales/en/translation.json index 9759fd86f..965fb38c7 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -1598,6 +1598,7 @@ "Insufficient confirmed funds": "Insufficient confirmed funds", "You do not have enough confirmed funds to make this payment. Wait for your pending transactions to confirm or enable \"Use unconfirmed funds\" in Advanced Settings.": "You do not have enough confirmed funds to make this payment. Wait for your pending transactions to confirm or enable \"Use unconfirmed funds\" in Advanced Settings.", "Insufficient funds": "Insufficient funds", + "Insufficient funds in your linked wallet to cover the transaction fee.": "Insufficient funds in your linked {{linkedWalletAbbreviation}} wallet to cover the transaction fee.", "No compatible wallets": "No compatible wallets", "You currently don't have any wallets capable of sending this payment. Would you like to import one?": "You currently don't have any wallets capable of sending this payment. Would you like to import one?", "Maybe Later": "Maybe Later", diff --git a/src/navigation/wallet/components/ErrorMessages.tsx b/src/navigation/wallet/components/ErrorMessages.tsx index 5294cee6d..b5d5c11fe 100644 --- a/src/navigation/wallet/components/ErrorMessages.tsx +++ b/src/navigation/wallet/components/ErrorMessages.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components/native'; import {BaseText} from '../../../components/styled/Text'; import {FlatList} from 'react-native'; import {t} from 'i18next'; +import {RootStacks, navigationRef} from '../../../Root'; interface BottomNotificationListType { key: number; @@ -116,25 +117,31 @@ export const WrongPasswordError = (): BottomNotificationConfig => { export const CustomErrorMessage = ({ errMsg, action = () => null, + cta, title, }: { errMsg: string; title?: string; action?: () => void; + cta?: [{text: string; action: () => void; primary: boolean}]; }): BottomNotificationConfig => { + if (!cta) { + // set CTA with action if default + cta = [ + { + text: t('OK'), + action, + primary: true, + }, + ]; + } return { type: 'error', title: title || t('Something went wrong'), message: errMsg, enableBackdropDismiss: true, onBackdropDismiss: action, - actions: [ - { - text: t('OK'), - action, - primary: true, - }, - ], + actions: cta, }; }; diff --git a/src/navigation/wallet/screens/AccountDetails.tsx b/src/navigation/wallet/screens/AccountDetails.tsx index 65debedcb..1f658e34a 100644 --- a/src/navigation/wallet/screens/AccountDetails.tsx +++ b/src/navigation/wallet/screens/AccountDetails.tsx @@ -895,12 +895,6 @@ const AccountDetails: React.FC = ({route}) => { showBottomNotificationModal({ ...errorMessageConfig, enableBackdropDismiss: false, - actions: [ - { - text: t('OK'), - action: () => {}, - }, - ], }), ); } diff --git a/src/navigation/wallet/screens/GlobalSelect.tsx b/src/navigation/wallet/screens/GlobalSelect.tsx index bbbd90877..d8708ca76 100644 --- a/src/navigation/wallet/screens/GlobalSelect.tsx +++ b/src/navigation/wallet/screens/GlobalSelect.tsx @@ -983,6 +983,7 @@ const GlobalSelect: React.FC = ({ dispatch(LogActions.error('[GlobalSelect] ' + errStr)); if (setButtonState) { setButtonState('failed'); + sleep(1000).then(() => setButtonState?.(null)); } else { dispatch(dismissOnGoingProcessModal()); } @@ -994,16 +995,6 @@ const GlobalSelect: React.FC = ({ showBottomNotificationModal({ ...errorMessageConfig, enableBackdropDismiss: false, - actions: [ - { - text: t('OK'), - action: () => { - if (setButtonState) { - setButtonState(undefined); - } - }, - }, - ], }), ); } diff --git a/src/navigation/wallet/screens/SelectInputs.tsx b/src/navigation/wallet/screens/SelectInputs.tsx index e39e88483..e847f1ce4 100644 --- a/src/navigation/wallet/screens/SelectInputs.tsx +++ b/src/navigation/wallet/screens/SelectInputs.tsx @@ -362,12 +362,6 @@ const SelectInputs = () => { showBottomNotificationModal({ ...errorMessageConfig, enableBackdropDismiss: false, - actions: [ - { - text: t('OK'), - action: () => {}, - }, - ], }), ); } diff --git a/src/navigation/wallet/screens/SendToOptions.tsx b/src/navigation/wallet/screens/SendToOptions.tsx index cc4ecdd3a..df72911af 100644 --- a/src/navigation/wallet/screens/SendToOptions.tsx +++ b/src/navigation/wallet/screens/SendToOptions.tsx @@ -250,12 +250,6 @@ const SendToOptions = () => { showBottomNotificationModal({ ...errorMessageConfig, enableBackdropDismiss: false, - actions: [ - { - text: t('OK'), - action: () => {}, - }, - ], }), ); } diff --git a/src/navigation/wallet/screens/TransactionDetails.tsx b/src/navigation/wallet/screens/TransactionDetails.tsx index 54d80cb3a..23836265f 100644 --- a/src/navigation/wallet/screens/TransactionDetails.tsx +++ b/src/navigation/wallet/screens/TransactionDetails.tsx @@ -412,12 +412,6 @@ const TransactionDetails = () => { showBottomNotificationModal({ ...errorMessageConfig, enableBackdropDismiss: false, - actions: [ - { - text: t('OK'), - action: () => {}, - }, - ], }), ); } diff --git a/src/navigation/wallet/screens/WalletDetails.tsx b/src/navigation/wallet/screens/WalletDetails.tsx index 36b86cdf0..d871fddce 100644 --- a/src/navigation/wallet/screens/WalletDetails.tsx +++ b/src/navigation/wallet/screens/WalletDetails.tsx @@ -791,12 +791,6 @@ const WalletDetails: React.FC = ({route}) => { showBottomNotificationModal({ ...errorMessageConfig, enableBackdropDismiss: false, - actions: [ - { - text: t('OK'), - action: () => {}, - }, - ], }), ); } diff --git a/src/navigation/wallet/screens/send/confirm/BillConfirm.tsx b/src/navigation/wallet/screens/send/confirm/BillConfirm.tsx index 7a648c10d..68dc2a723 100644 --- a/src/navigation/wallet/screens/send/confirm/BillConfirm.tsx +++ b/src/navigation/wallet/screens/send/confirm/BillConfirm.tsx @@ -276,15 +276,18 @@ const BillConfirm: React.VFC< const handleBillPayInvoiceOrTxpError = async (err: any) => { await sleep(400); dispatch(dismissOnGoingProcessModal()); + const onDismiss = () => openWalletSelector(400); const [errorConfig] = await Promise.all([ - dispatch(handleCreateTxProposalError(err)), + dispatch(handleCreateTxProposalError(err, onDismiss)), sleep(500), ]); - showError({ - defaultErrorMessage: - err.response?.data?.message || err.message || errorConfig.message, - onDismiss: () => openWalletSelector(400), - }); + dispatch( + AppActions.showBottomNotificationModal({ + ...errorConfig, + errMsg: + err.response?.data?.message || err.message || errorConfig.message, + }), + ); }; const onCoinbaseAccountSelect = async (walletRowProps: WalletRowProps) => { diff --git a/src/navigation/wallet/screens/send/confirm/Confirm.tsx b/src/navigation/wallet/screens/send/confirm/Confirm.tsx index 10dfbb143..b73c624cc 100644 --- a/src/navigation/wallet/screens/send/confirm/Confirm.tsx +++ b/src/navigation/wallet/screens/send/confirm/Confirm.tsx @@ -384,12 +384,6 @@ const Confirm = () => { showBottomNotificationModal({ ...errorMessageConfig, enableBackdropDismiss: false, - actions: [ - { - text: t('OK'), - action: () => {}, - }, - ], }), ); } diff --git a/src/navigation/wallet/screens/send/confirm/GiftCardConfirm.tsx b/src/navigation/wallet/screens/send/confirm/GiftCardConfirm.tsx index dcd272dde..a945234d3 100644 --- a/src/navigation/wallet/screens/send/confirm/GiftCardConfirm.tsx +++ b/src/navigation/wallet/screens/send/confirm/GiftCardConfirm.tsx @@ -1,7 +1,11 @@ import Transport from '@ledgerhq/hw-transport'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -import {useNavigation, useRoute} from '@react-navigation/native'; +import { + useNavigation, + useRoute, + useFocusEffect, +} from '@react-navigation/native'; import styled from 'styled-components/native'; import {Hr} from '../../../../../components/styled/Containers'; import {RouteProp, StackActions} from '@react-navigation/core'; @@ -323,20 +327,23 @@ const Confirm = () => { const handleCreateGiftCardInvoiceOrTxpError = async (err: any) => { await sleep(400); dispatch(dismissOnGoingProcessModal()); + const onDismiss = () => { + if (err.message === GiftCardInvoiceCreationErrors.couponExpired) { + return popToShopHome(); + } + return openWalletSelector(400); + }; const [errorConfig] = await Promise.all([ - dispatch(handleCreateTxProposalError(err)), + dispatch(handleCreateTxProposalError(err, onDismiss)), sleep(500), ]); - showError({ - defaultErrorMessage: - err.response?.data?.message || err.message || errorConfig.message, - onDismiss: () => { - if (err.message === GiftCardInvoiceCreationErrors.couponExpired) { - return popToShopHome(); - } - return openWalletSelector(400); - }, - }); + dispatch( + AppActions.showBottomNotificationModal({ + ...errorConfig, + errMsg: + err.response?.data?.message || err.message || errorConfig.message, + }), + ); }; const onCoinbaseAccountSelect = async (walletRowProps: WalletRowProps) => { @@ -626,9 +633,11 @@ const Confirm = () => { return () => clearTimeout(timer); }, [resetSwipeButton]); - useEffect(() => { - openWalletSelector(100); - }, []); + useFocusEffect( + useCallback(() => { + openWalletSelector(100); + }, []), + ); return ( diff --git a/src/navigation/wallet/screens/send/confirm/PayProConfirm.tsx b/src/navigation/wallet/screens/send/confirm/PayProConfirm.tsx index 28e2144dd..bd67eb1c0 100644 --- a/src/navigation/wallet/screens/send/confirm/PayProConfirm.tsx +++ b/src/navigation/wallet/screens/send/confirm/PayProConfirm.tsx @@ -1,6 +1,10 @@ import Transport from '@ledgerhq/hw-transport'; import {RouteProp, StackActions} from '@react-navigation/core'; -import {useNavigation, useRoute} from '@react-navigation/native'; +import { + useNavigation, + useRoute, + useFocusEffect, +} from '@react-navigation/native'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {useTranslation} from 'react-i18next'; import {WalletScreens, WalletGroupParamList} from '../../../WalletGroup'; @@ -244,28 +248,29 @@ const PayProConfirm = () => { } catch (err: any) { await sleep(400); dispatch(dismissOnGoingProcessModal()); + const onDismiss = () => + wallet ? navigation.goBack() : reshowWalletSelector(); const [errorConfig] = await Promise.all([ - dispatch(handleCreateTxProposalError(err)), + dispatch(handleCreateTxProposalError(err, onDismiss)), sleep(500), ]); dispatch( - AppActions.showBottomNotificationModal( - CustomErrorMessage({ - title: t('Error'), - errMsg: - err.response?.data?.message || err.message || errorConfig.message, - action: () => - wallet ? navigation.goBack() : reshowWalletSelector(), - }), - ), + AppActions.showBottomNotificationModal({ + ...errorConfig, + errMsg: + err.response?.data?.message || err.message || errorConfig.message, + }), ); } }; - useEffect(() => { - wallet ? createTxp(wallet) : setTimeout(() => openKeyWalletSelector(), 500); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + useFocusEffect( + useCallback(() => { + wallet + ? createTxp(wallet) + : setTimeout(() => openKeyWalletSelector(), 500); + }, []), + ); const openKeyWalletSelector = () => { setWalletSelectorVisible(true); @@ -274,19 +279,17 @@ const PayProConfirm = () => { const handleTxpError = async (err: any) => { await sleep(400); dispatch(dismissOnGoingProcessModal()); + const onDismiss = () => reshowWalletSelector(); const [errorConfig] = await Promise.all([ - dispatch(handleCreateTxProposalError(err)), + dispatch(handleCreateTxProposalError(err, onDismiss)), sleep(500), ]); dispatch( - AppActions.showBottomNotificationModal( - CustomErrorMessage({ - title: t('Error'), - errMsg: - err.response?.data?.message || err.message || errorConfig.message, - action: () => reshowWalletSelector(), - }), - ), + AppActions.showBottomNotificationModal({ + ...errorConfig, + errMsg: + err.response?.data?.message || err.message || errorConfig.message, + }), ); }; diff --git a/src/store/scan/scan.effects.ts b/src/store/scan/scan.effects.ts index 0092f246a..4852acb81 100644 --- a/src/store/scan/scan.effects.ts +++ b/src/store/scan/scan.effects.ts @@ -561,6 +561,7 @@ const goToConfirm = } catch (err: any) { if (setButtonState) { setButtonState('failed'); + sleep(1000).then(() => setButtonState?.(null)); } else { dispatch(dismissOnGoingProcessModal()); } @@ -571,17 +572,6 @@ const goToConfirm = dispatch( showBottomNotificationModal({ ...errorMessageConfig, - enableBackdropDismiss: false, - actions: [ - { - text: 'OK', - action: () => { - if (setButtonState) { - setButtonState(undefined); - } - }, - }, - ], }), ); } diff --git a/src/store/wallet/effects/send/send.ts b/src/store/wallet/effects/send/send.ts index 8beef794a..22df9da18 100644 --- a/src/store/wallet/effects/send/send.ts +++ b/src/store/wallet/effects/send/send.ts @@ -45,6 +45,7 @@ import { formatCurrencyAbbreviation, formatFiatAmount, getCWCChain, + getFullLinkedWallet, getRateByCurrencyName, sleep, } from '../../../../utils/helper-methods'; @@ -74,6 +75,7 @@ import { import { GetPrecision, IsERCToken, + IsEVMChain, IsSegwitCoin, IsUtxoChain, } from '../../utils/currency'; @@ -111,6 +113,7 @@ import { } from '../../../../components/modal/import-ledger-wallet/import-account/SelectLedgerCurrency'; import {BitpaySupportedCoins} from '../../../../constants/currencies'; import {getERC20TokenPrice} from '../../../moralis/moralis.effects'; +import {getLinkedWallet} from '@/navigation/wallet/screens/wallet-settings/WalletInformation'; export const createProposalAndBuildTxDetails = ( @@ -1874,59 +1877,206 @@ export const removeTxp = (wallet: Wallet, txp: TransactionProposal) => { }); }; -export const handleCreateTxProposalError = - (proposalErrorProps: ProposalErrorHandlerProps): Effect> => - async dispatch => { - try { - const {err} = proposalErrorProps; +const formatAmounts = ( + dispatch: any, + wallet: any, + amount: number, +): {formatAvailableAmount: string; formatSendingAmount: string} => { + const _formatAvailableAmount = dispatch( + FormatAmountStr( + wallet.currencyAbbreviation, + wallet.chain, + wallet.tokenAddress, + Number(wallet.balance.satSpendable), + false, + ), + ); - switch (getErrorName(err)) { - case 'INSUFFICIENT_FUNDS': - const {tx, txp, getState} = proposalErrorProps; + const formatAvailableAmount = `~${_formatAvailableAmount}`; + const formatSendingAmount = `~${amount.toFixed( + 2, + )} ${formatCurrencyAbbreviation(wallet.currencyAbbreviation)}`; + return {formatAvailableAmount, formatSendingAmount}; +}; - if (!tx || !txp || !getState) { - return GeneralError(); - } +const generateInsufficientFundsError = ( + dispatch: any, + wallet: Wallet, + amount: number, + onDismiss?: () => void, +) => { + const {formatAvailableAmount, formatSendingAmount} = formatAmounts( + dispatch, + wallet, + amount, + ); + let errMsg = IsEVMChain(wallet.chain) + ? t( + 'You are trying to send more funds than you have available.\n\nTrying to send: {{formatSendingAmount}}\nAvailable to send: {{formatAvailableAmount}}', + { + formatSendingAmount, + formatAvailableAmount, + }, + ) + : t( + 'You are trying to send more funds than you have available. Make sure you do not have funds locked by pending transaction proposals.\n\nTrying to send: {{formatSendingAmount}}\nAvailable to send: {{formatAvailableAmount}}', + { + formatSendingAmount, + formatAvailableAmount, + }, + ); + return CustomErrorMessage({ + title: t('Insufficient funds'), + errMsg: errMsg, + action: onDismiss, + cta: [ + { + text: t('Buy Crypto'), + action: () => { + dispatch( + Analytics.track('Clicked Buy Crypto', { + context: 'errorBottomNotification', + }), + ); + navigationRef.navigate('BuyCryptoRoot', { + amount: 100, + fromWallet: wallet, + }); + }, + primary: true, + }, + ], + }); +}; + +const generateInsufficientConfirmedFundsError = (onDismiss?: () => void) => { + return CustomErrorMessage({ + title: t('Insufficient confirmed funds'), + errMsg: t( + 'You do not have enough confirmed funds to make this payment. Wait for your pending transactions to confirm or enable "Use unconfirmed funds" in Advanced Settings.', + ), + action: onDismiss, + cta: [ + { + text: t('APP SETTINGS'), + action: () => { + navigationRef.navigate('SettingsHome'); + }, + primary: true, + }, + ], + }); +}; - const {wallet, amount} = tx; - const {feeLevel} = txp; +const processInsufficientFunds = async ( + proposalErrorProps: ProposalErrorHandlerProps, + dispatch: any, + onDismiss?: () => void, +) => { + const {tx, txp, getState} = proposalErrorProps; - const feeRatePerKb = await getFeeRatePerKb({ - wallet, - feeLevel: feeLevel || FeeLevels.NORMAL, + if (!tx || !txp || !getState) { + return GeneralError(); + } + + const {wallet, amount} = tx; + const {feeLevel} = txp; + + const feeRatePerKb = await getFeeRatePerKb({ + wallet, + feeLevel: feeLevel || FeeLevels.NORMAL, + }); + + const useConfirmedFunds = !getState().WALLET.useUnconfirmedFunds; + const amountSat = dispatch( + ParseAmount( + amount, + wallet.currencyAbbreviation, + wallet.chain, + wallet.tokenAddress, + ), + ).amountSat; + if (useConfirmedFunds && wallet.balance.sat >= amountSat + feeRatePerKb) { + return generateInsufficientConfirmedFundsError(onDismiss); + } else { + return generateInsufficientFundsError(dispatch, wallet, amount, onDismiss); + } +}; + +const handleDefaultError = ( + proposalErrorProps: ProposalErrorHandlerProps, + dispatch: any, + onDismiss?: () => void, +) => { + const {err, tx, txp, getState} = proposalErrorProps; + + if (!tx || !txp || !getState) { + return GeneralError(); + } + + const keys = getState().WALLET.keys; + const {wallet, amount} = tx; + const {feeLevel} = txp; + + const title = IsEVMChain(wallet.chain) + ? t('Not enough gas for transaction') + : t('Insufficient funds for fee.'); + const body = IsERCToken(wallet.currencyAbbreviation, wallet.chain) + ? t( + 'Insufficient funds in your linked wallet to cover the transaction fee.', + { + linkedWalletAbbreviation: + wallet.chain === 'matic' ? 'POL' : wallet.chain.toUpperCase(), + }, + ) + : t( + 'You have enough funds to make this payment, but your wallet doesn’t have enough to cover the network fees.', + ); + return CustomErrorMessage({ + title, + errMsg: body, + action: onDismiss, + cta: [ + { + text: t('Buy Crypto'), + action: () => { + dispatch( + Analytics.track('Clicked Buy Crypto', { + context: 'errorBottomNotification', + }), + ); + navigationRef.navigate('BuyCryptoRoot', { + amount: 100, + fromWallet: IsERCToken(wallet.currencyAbbreviation, wallet.chain) + ? getFullLinkedWallet(keys[wallet.keyId], wallet) + : wallet, }); + }, + primary: true, + }, + ], + }); +}; - if ( - !getState().WALLET.useUnconfirmedFunds && - wallet.balance.sat >= - dispatch( - ParseAmount( - amount, - wallet.currencyAbbreviation, - wallet.chain, - wallet.tokenAddress, - ), - ).amountSat + - feeRatePerKb - ) { - return CustomErrorMessage({ - title: t('Insufficient confirmed funds'), - errMsg: t( - 'You do not have enough confirmed funds to make this payment. Wait for your pending transactions to confirm or enable "Use unconfirmed funds" in Advanced Settings.', - ), - }); - } else { - return CustomErrorMessage({ - title: t('Insufficient funds'), - errMsg: BWCErrorMessage(err), - }); - } +export const handleCreateTxProposalError = + ( + proposalErrorProps: ProposalErrorHandlerProps, + onDismiss?: () => void, + ): Effect> => + async dispatch => { + try { + const {err} = proposalErrorProps; + const errorName = getErrorName(err); + switch (errorName) { + case 'INSUFFICIENT_FUNDS': + return await processInsufficientFunds( + proposalErrorProps, + dispatch, + onDismiss, + ); default: - return CustomErrorMessage({ - title: t('Error'), - errMsg: BWCErrorMessage(err), - }); + return handleDefaultError(proposalErrorProps, dispatch, onDismiss); } } catch (err2) { return GeneralError(); diff --git a/src/utils/helper-methods.ts b/src/utils/helper-methods.ts index e4d607d86..5dfff360e 100644 --- a/src/utils/helper-methods.ts +++ b/src/utils/helper-methods.ts @@ -1022,3 +1022,17 @@ export const camelCaseToUpperWords = (input: string) => { .replace(/([a-z])([A-Z])/g, '$1 $2') .toUpperCase(); }; + +export const getFullLinkedWallet = (key: Key, wallet: Wallet) => { + const { + credentials: {token, walletId}, + } = wallet; + if (token) { + const linkedWallet = key.wallets.find(({tokens}) => + tokens?.includes(walletId), + ); + return linkedWallet; + } + + return; +};