Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(warning): Add generic gas fee warning component #6385

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -2823,5 +2823,25 @@
"availableBalance": "Available: <0></0>",
"selectToken": "Select token",
"fiatPriceUnavailable": "Price unavailable"
},
"gasFeeWarning": {
"title": "You need more {{tokenSymbol}} for gas fees",
"descriptionMaxAmount": {
"sending": "Add {{tokenSymbol}} for gas fees or lower the amount you're sending",
"swapping": "Add {{tokenSymbol}} for gas fees or lower the amount you're swapping",
"depositing": "Add {{tokenSymbol}} for gas fees or lower the amount you're depositing",
"withdrawing": "Add {{tokenSymbol}} for gas fees or lower the amount you're withdrawing"
},
"descriptionNotEnoughGas_Send": "Adjust the amount you're sending or add {{tokenSymbol}} to continue",
"descriptionNotEnoughGas_Swap": "Adjust the amount you're swapping or add {{tokenSymbol}} to continue",
"descriptionNotEnoughGas_Deposit": "Adjust the amount you're depositing or add {{tokenSymbol}} to continue",
"descriptionNotEnoughGas_Withdraw": "Adjust the amount you're withdrawing or add {{tokenSymbol}} to continue",
"titleDapp": "You have an insufficient gas token balance",
"descriptionDapp": "Add {{tokenSymbol}} to complete this transaction",
"cta": "Buy {{tokenSymbol}}",
"ctaGasToken_Send": "Send smaller amount",
"ctaGasToken_Swap": "Swap smaller amount",
"ctaGasToken_Deposit": "Deposit smaller amount",
"ctaGasToken_Withdraw": "Withdraw smaller amount"
}
}
2 changes: 2 additions & 0 deletions src/analytics/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export enum AppEvents {
in_app_review_error = 'in_app_review_error',

handle_deeplink = 'handle_deeplink',

gas_fee_warning_impression = 'gas_fee_warning_impression',
}

export enum HomeEvents {
Expand Down
6 changes: 6 additions & 0 deletions src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
} from 'src/analytics/types'
import { ErrorMessages } from 'src/app/ErrorMessages'
import { AddAssetsActionType } from 'src/components/AddAssetsBottomSheet'
import { GasFeeWarningFlow } from 'src/components/GasFeeWarning'
import { TokenPickerOrigin } from 'src/components/TokenBottomSheet'
import { DappSection } from 'src/dapps/types'
import { BeforeDepositActionName, EarnActiveMode, SerializableRewardsInfo } from 'src/earn/types'
Expand Down Expand Up @@ -152,6 +153,11 @@ interface AppEventsProperties {
fullPath: string | null
query: string | null
}
[AppEvents.gas_fee_warning_impression]: {
flow: GasFeeWarningFlow
errorType: 'need-decrease-spend-amount-for-gas' | 'not-enough-balance-for-gas'
tokenId: string
}
}

interface HomeEventsProperties {
Expand Down
1 change: 1 addition & 0 deletions src/analytics/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const eventDocs: Record<AnalyticsEventType, string> = {
[AppEvents.in_app_review_impression]: `User sees an in-app review request`,
[AppEvents.in_app_review_error]: `Error while attempting to display in-app review`,
[AppEvents.handle_deeplink]: `When a deeplink that leads into the app is detected and handled`,
[AppEvents.gas_fee_warning_impression]: `When the gas fee warning is shown to the user`,
[HomeEvents.account_circle_tapped]: `When the account circle used in the tab navigation is tapped`,
[HomeEvents.profile_address_copy]: `When a user copies their wallet address from the profile screen`,
[HomeEvents.notification_scroll]: ``,
Expand Down
126 changes: 126 additions & 0 deletions src/components/GasFeeWarning.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { fireEvent, render } from '@testing-library/react-native'
import BigNumber from 'bignumber.js'
import React from 'react'
import { Provider } from 'react-redux'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { AppEvents } from 'src/analytics/Events'
import GasFeeWarning from 'src/components/GasFeeWarning'
import {
PreparedTransactionsNeedDecreaseSpendAmountForGas,
PreparedTransactionsNotEnoughBalanceForGas,
PreparedTransactionsPossible,
} from 'src/viem/prepareTransactions'
import { createMockStore } from 'test/utils'
import { mockArbEthTokenId, mockCeloTokenId, mockTokenBalances } from 'test/values'

const mockPreparedTransactionPossible: PreparedTransactionsPossible = {
type: 'possible' as const,
transactions: [],
feeCurrency: {
...mockTokenBalances[mockArbEthTokenId],
isNative: true,
balance: new BigNumber(10),
priceUsd: new BigNumber(1),
lastKnownPriceUsd: new BigNumber(1),
},
}

const mockPreparedTransactionNotEnoughCelo: PreparedTransactionsNotEnoughBalanceForGas = {
type: 'not-enough-balance-for-gas' as const,
feeCurrencies: [
{
...mockTokenBalances[mockCeloTokenId],
isNative: true,
balance: new BigNumber(0),
priceUsd: new BigNumber(1500),
lastKnownPriceUsd: new BigNumber(1500),
},
],
}

const mockPreparedTransactionNeedDecreaseEth: PreparedTransactionsNeedDecreaseSpendAmountForGas = {
type: 'need-decrease-spend-amount-for-gas' as const,
feeCurrency: {
...mockTokenBalances[mockArbEthTokenId],
isNative: true,
balance: new BigNumber(0),
priceUsd: new BigNumber(1500),
lastKnownPriceUsd: new BigNumber(1500),
},
maxGasFeeInDecimal: new BigNumber(1),
estimatedGasFeeInDecimal: new BigNumber(1),
decreasedSpendAmount: new BigNumber(1),
}

describe('GasFeeWarning', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('should return null if prepareTransactionsResult is undefined', () => {
const store = createMockStore()
const { queryByTestId } = render(
<Provider store={store}>
<GasFeeWarning flow={'Send'} testIdPrefix={'test'} />
</Provider>
)
expect(queryByTestId('test/GasFeeWarning')).toBeFalsy()
})
it('should return null if prepareTransactionsResult.type is possible', () => {
const store = createMockStore()
const { queryByTestId } = render(
<Provider store={store}>
<GasFeeWarning
flow={'Send'}
testIdPrefix={'test'}
prepareTransactionsResult={mockPreparedTransactionPossible}
/>
</Provider>
)
expect(queryByTestId('test/GasFeeWarning')).toBeFalsy()
})
it.each`
scenario | flow | prepareTransactionsResult | feeCurrencyTokenId | title | description | ctaLabel
${'sending max amount of ETH'} | ${'Send'} | ${mockPreparedTransactionNeedDecreaseEth} | ${mockArbEthTokenId} | ${'gasFeeWarning.title, {"tokenSymbol":"ETH"}'} | ${'gasFeeWarning.descriptionMaxAmount, {"context":"Send","tokenSymbol":"ETH"}'} | ${'gasFeeWarning.ctaGasToken, {"context":"Send"}'}
${'sending with insufficient CELO'} | ${'Send'} | ${mockPreparedTransactionNotEnoughCelo} | ${mockCeloTokenId} | ${'gasFeeWarning.title, {"tokenSymbol":"CELO"}'} | ${'gasFeeWarning.descriptionNotEnoughGas, {"context":"Send","tokenSymbol":"CELO"}'} | ${'gasFeeWarning.cta, {"tokenSymbol":"CELO"}'}
${'swapping max amount of ETH'} | ${'Swap'} | ${mockPreparedTransactionNeedDecreaseEth} | ${mockArbEthTokenId} | ${'gasFeeWarning.title, {"tokenSymbol":"ETH"}'} | ${'gasFeeWarning.descriptionMaxAmount, {"context":"Swap","tokenSymbol":"ETH"}'} | ${'gasFeeWarning.ctaGasToken, {"context":"Swap"}'}
${'swapping with insufficient CELO'} | ${'Swap'} | ${mockPreparedTransactionNotEnoughCelo} | ${mockCeloTokenId} | ${'gasFeeWarning.title, {"tokenSymbol":"CELO"}'} | ${'gasFeeWarning.descriptionNotEnoughGas, {"context":"Swap","tokenSymbol":"CELO"}'} | ${'gasFeeWarning.cta, {"tokenSymbol":"CELO"}'}
${'withdrawing max amount of ETH'} | ${'Withdraw'} | ${mockPreparedTransactionNeedDecreaseEth} | ${mockArbEthTokenId} | ${'gasFeeWarning.title, {"tokenSymbol":"ETH"}'} | ${'gasFeeWarning.descriptionMaxAmount, {"context":"Withdraw","tokenSymbol":"ETH"}'} | ${'gasFeeWarning.ctaGasToken, {"context":"Withdraw"}'}
${'withdrawing with insufficient CELO'} | ${'Withdraw'} | ${mockPreparedTransactionNotEnoughCelo} | ${mockCeloTokenId} | ${'gasFeeWarning.title, {"tokenSymbol":"CELO"}'} | ${'gasFeeWarning.descriptionNotEnoughGas, {"context":"Withdraw","tokenSymbol":"CELO"}'} | ${'gasFeeWarning.cta, {"tokenSymbol":"CELO"}'}
${'depositing max amount of ETH'} | ${'Deposit'} | ${mockPreparedTransactionNeedDecreaseEth} | ${mockArbEthTokenId} | ${'gasFeeWarning.title, {"tokenSymbol":"ETH"}'} | ${'gasFeeWarning.descriptionMaxAmount, {"context":"Deposit","tokenSymbol":"ETH"}'} | ${'gasFeeWarning.ctaGasToken, {"context":"Deposit"}'}
${'depositing with insufficient CELO'} | ${'Deposit'} | ${mockPreparedTransactionNotEnoughCelo} | ${mockCeloTokenId} | ${'gasFeeWarning.title, {"tokenSymbol":"CELO"}'} | ${'gasFeeWarning.descriptionNotEnoughGas, {"context":"Deposit","tokenSymbol":"CELO"}'} | ${'gasFeeWarning.cta, {"tokenSymbol":"CELO"}'}
${'dapp transaction with max amount of ETH'} | ${'Dapp'} | ${mockPreparedTransactionNeedDecreaseEth} | ${mockArbEthTokenId} | ${'gasFeeWarning.titleDapp'} | ${'gasFeeWarning.descriptionDapp, {"tokenSymbol":"ETH"}'} | ${undefined}
${'dapp transaction with insufficient CELO'} | ${'Dapp'} | ${mockPreparedTransactionNotEnoughCelo} | ${mockCeloTokenId} | ${'gasFeeWarning.titleDapp'} | ${'gasFeeWarning.descriptionDapp, {"tokenSymbol":"CELO"}'} | ${undefined}
`(
'renders error correctly when $scenario',
({ flow, prepareTransactionsResult, feeCurrencyTokenId, title, description, ctaLabel }) => {
const store = createMockStore()
const changeInputValueFn = jest.fn()
const { getByTestId, getByText } = render(
<Provider store={store}>
<GasFeeWarning
flow={flow}
testIdPrefix={'test'}
prepareTransactionsResult={prepareTransactionsResult}
changeInputValueFn={changeInputValueFn}
/>
</Provider>
)
expect(getByTestId('test/GasFeeWarning')).toBeTruthy()
expect(AppAnalytics.track).toHaveBeenCalledTimes(1)
expect(AppAnalytics.track).toHaveBeenCalledWith(AppEvents.gas_fee_warning_impression, {
flow,
errorType: prepareTransactionsResult.type,
tokenId: feeCurrencyTokenId,
})
expect(getByText(title)).toBeTruthy()
expect(getByText(description)).toBeTruthy()
expect(ctaLabel ? getByText(ctaLabel) : true).toBeTruthy()
if (ctaLabel) {
fireEvent.press(getByText(ctaLabel))
}
expect(changeInputValueFn).toHaveBeenCalledTimes(
ctaLabel && ctaLabel.includes('ctaGasToken') ? 1 : 0
)
}
)
})
102 changes: 102 additions & 0 deletions src/components/GasFeeWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet } from 'react-native'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { AppEvents } from 'src/analytics/Events'
import InLineNotification, { NotificationVariant } from 'src/components/InLineNotification'
import { CICOFlow } from 'src/fiatExchanges/types'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { Spacing } from 'src/styles/styles'
import { PreparedTransactionsResult } from 'src/viem/prepareTransactions'

export type GasFeeWarningFlow = 'Send' | 'Swap' | 'Withdraw' | 'Deposit' | 'Dapp'

function GasFeeWarning({
prepareTransactionsResult,
flow,
changeInputValueFn,
testIdPrefix,
}: {
prepareTransactionsResult?: PreparedTransactionsResult
flow: GasFeeWarningFlow
changeInputValueFn?: (amount: string) => void
testIdPrefix?: string
}) {
const { t } = useTranslation()

useEffect(() => {
if (prepareTransactionsResult && prepareTransactionsResult.type !== 'possible') {
AppAnalytics.track(AppEvents.gas_fee_warning_impression, {
flow,
errorType: prepareTransactionsResult.type,
tokenId: feeCurrency.tokenId,
})
}
}, [prepareTransactionsResult])

if (!prepareTransactionsResult || prepareTransactionsResult.type === 'possible') {
return false
}

const feeCurrency =
prepareTransactionsResult.type === 'not-enough-balance-for-gas'
? prepareTransactionsResult.feeCurrencies[0]
: prepareTransactionsResult.feeCurrency

const title =
flow === 'Dapp'
? t('gasFeeWarning.titleDapp')
: t('gasFeeWarning.title', { tokenSymbol: feeCurrency.symbol })
const description =
flow === 'Dapp'
? t('gasFeeWarning.descriptionDapp', { tokenSymbol: feeCurrency.symbol })
: prepareTransactionsResult.type === 'not-enough-balance-for-gas'
? t('gasFeeWarning.descriptionNotEnoughGas', {
context: flow,
tokenSymbol: feeCurrency.symbol,
})
: t('gasFeeWarning.descriptionMaxAmount', {
context: flow,
tokenSymbol: feeCurrency.symbol,
})
const ctaLabel =
flow === 'Dapp'
? undefined
: prepareTransactionsResult.type === 'not-enough-balance-for-gas'
? t('gasFeeWarning.cta', { tokenSymbol: feeCurrency.symbol })
: t('gasFeeWarning.ctaGasToken', { context: flow })

const onPressCta = () => {
prepareTransactionsResult.type === 'not-enough-balance-for-gas'
? navigate(Screens.FiatExchangeAmount, {
tokenId: prepareTransactionsResult.feeCurrencies[0].tokenId,
flow: CICOFlow.CashIn,
tokenSymbol: prepareTransactionsResult.feeCurrencies[0].symbol,
})
: changeInputValueFn
? changeInputValueFn(prepareTransactionsResult.decreasedSpendAmount.toString())
: null
}
return (
<InLineNotification
variant={NotificationVariant.Warning}
title={title}
description={description}
ctaLabel={ctaLabel}
onPressCta={onPressCta}
style={styles.warning}
testID={`${testIdPrefix}/GasFeeWarning`}
/>
)
}

const styles = StyleSheet.create({
warning: {
marginTop: Spacing.Regular16,
paddingHorizontal: Spacing.Regular16,
borderRadius: 16,
},
})

export default GasFeeWarning
10 changes: 3 additions & 7 deletions src/earn/EarnEnterAmount.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,7 @@ describe('EarnEnterAmount', () => {
})
})

it('should track analytics and navigate correctly when tapping cta to add gas', async () => {
it('should show gas warning error when prepareTransactionsResult is type not-enough-balance-for-gas, and tapping cta behaves as expected', async () => {
jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({
prepareTransactionsResult: {
prepareTransactionsResult: mockPreparedTransactionNotEnough,
Expand All @@ -934,12 +934,8 @@ describe('EarnEnterAmount', () => {
</Provider>
)

await waitFor(() => expect(getByTestId('EarnEnterAmount/NotEnoughForGasWarning')).toBeTruthy())
fireEvent.press(
getByText(
'earnFlow.enterAmount.notEnoughBalanceForGasWarning.noGasCta, {"feeTokenSymbol":"ETH","network":"Arbitrum Sepolia"}'
)
)
await waitFor(() => expect(getByTestId('EarnEnterAmount/GasFeeWarning')).toBeTruthy())
fireEvent.press(getByText('gasFeeWarning.cta, {"tokenSymbol":"ETH"}'))
expect(AppAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_deposit_add_gas_press, {
gasTokenId: mockArbEthTokenId,
networkId: NetworkId['arbitrum-sepolia'],
Expand Down
46 changes: 7 additions & 39 deletions src/earn/EarnEnterAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { EarnEvents, SendEvents } from 'src/analytics/Events'
import BackButton from 'src/components/BackButton'
import BottomSheet, { BottomSheetModalRefType } from 'src/components/BottomSheet'
import Button, { BtnSizes, BtnTypes } from 'src/components/Button'
import GasFeeWarning from 'src/components/GasFeeWarning'
import InLineNotification, { NotificationVariant } from 'src/components/InLineNotification'
import KeyboardAwareScrollView from 'src/components/KeyboardAwareScrollView'
import { LabelWithInfo } from 'src/components/LabelWithInfo'
Expand All @@ -27,7 +28,6 @@ import EarnDepositBottomSheet from 'src/earn/EarnDepositBottomSheet'
import { usePrepareEnterAmountTransactionsCallback } from 'src/earn/hooks'
import { depositStatusSelector } from 'src/earn/selectors'
import { getSwapToAmountInDecimals } from 'src/earn/utils'
import { CICOFlow } from 'src/fiatExchanges/types'
import ArrowRightThick from 'src/icons/ArrowRightThick'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
Expand All @@ -36,7 +36,6 @@ import { hooksApiUrlSelector, positionsWithBalanceSelector } from 'src/positions
import { EarnPosition, Position } from 'src/positions/types'
import { useSelector } from 'src/redux/hooks'
import EnterAmountOptions from 'src/send/EnterAmountOptions'
import { NETWORK_NAMES } from 'src/shared/conts'
import { getFeatureGate } from 'src/statsig'
import { StatsigFeatureGates } from 'src/statsig/types'
import Colors from 'src/styles/colors'
Expand Down Expand Up @@ -269,10 +268,6 @@ export default function EarnEnterAmount({ route }: Props) {

const showLowerAmountError =
processedAmounts.token.bignum && processedAmounts.token.bignum.gt(inputToken.balance)
const showNotEnoughBalanceForGasWarning =
!showLowerAmountError &&
prepareTransactionsResult &&
prepareTransactionsResult.type === 'not-enough-balance-for-gas'
const transactionIsPossible =
!showLowerAmountError &&
prepareTransactionsResult &&
Expand Down Expand Up @@ -405,39 +400,12 @@ export default function EarnEnterAmount({ route }: Props) {
/>
)}
</View>

{showNotEnoughBalanceForGasWarning && (
<InLineNotification
variant={NotificationVariant.Warning}
title={t('earnFlow.enterAmount.notEnoughBalanceForGasWarning.title', {
feeTokenSymbol: prepareTransactionsResult.feeCurrencies[0].symbol,
})}
description={t('earnFlow.enterAmount.notEnoughBalanceForGasWarning.description', {
feeTokenSymbol: prepareTransactionsResult.feeCurrencies[0].symbol,
network: NETWORK_NAMES[prepareTransactionsResult.feeCurrencies[0].networkId],
})}
ctaLabel={t('earnFlow.enterAmount.notEnoughBalanceForGasWarning.noGasCta', {
feeTokenSymbol: feeCurrencies[0].symbol,
network: NETWORK_NAMES[prepareTransactionsResult.feeCurrencies[0].networkId],
})}
onPressCta={() => {
AppAnalytics.track(EarnEvents.earn_deposit_add_gas_press, {
gasTokenId: feeCurrencies[0].tokenId,
depositTokenId: pool.dataProps.depositTokenId,
networkId: pool.networkId,
providerId: pool.appId,
poolId: pool.positionId,
})
navigate(Screens.FiatExchangeAmount, {
tokenId: prepareTransactionsResult.feeCurrencies[0].tokenId,
flow: CICOFlow.CashIn,
tokenSymbol: prepareTransactionsResult.feeCurrencies[0].symbol,
})
}}
style={styles.warning}
testID="EarnEnterAmount/NotEnoughForGasWarning"
/>
)}
<GasFeeWarning
prepareTransactionsResult={prepareTransactionsResult}
flow={'Deposit'}
changeInputValueFn={handleAmountInputChange}
testIdPrefix={'EarnEnterAmount'}
/>
{showLowerAmountError && (
<InLineNotification
variant={NotificationVariant.Warning}
Expand Down
Loading
Loading