diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75b4cf336b9..6f13d4efd5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,6 @@ jobs: - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 #v1 with: ruby-version: '3.1.6' - bundler-cache: true env: BUNDLE_GEMFILE: ios/Gemfile - run: yarn setup @@ -181,7 +180,6 @@ jobs: name: ios-bundle path: ios/main.jsbundle - - name: Push bundle size to mobile_bundlesize_stats repo run: ./scripts/push-bundle-size.sh env: diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx index c7c3005ad89..6507156a447 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx @@ -9,6 +9,9 @@ import { ChainId, PooledStakingContract } from '@metamask/stake-sdk'; import { Contract } from 'ethers'; import { MOCK_GET_VAULT_RESPONSE } from '../../__mocks__/mockData'; import { toWei } from '../../../../../util/number'; +import { strings } from '../../../../../../locales/i18n'; +// eslint-disable-next-line import/no-namespace +import * as useStakingGasFee from '../../hooks/useStakingGasFee'; function render(Component: React.ComponentType) { return renderScreen( @@ -96,7 +99,6 @@ jest.mock('../../hooks/useStakingGasFee', () => ({ __esModule: true, default: () => ({ estimatedGasFeeWei: mockGasFee, - gasLimit: 70122, isLoadingStakingGasFee: false, isStakingGasFeeError: false, refreshGasValues: jest.fn(), @@ -203,5 +205,31 @@ describe('StakeInputView', () => { screen: Routes.STAKING.MODALS.LEARN_MORE, }); }); + + it('navigates to gas impact modal when gas cost is 30% or more of deposit amount', () => { + jest.spyOn(useStakingGasFee, 'default').mockReturnValue({ + estimatedGasFeeWei: toWei('0.25'), + isLoadingStakingGasFee: false, + isStakingGasFeeError: false, + refreshGasValues: jest.fn(), + }); + + render(StakeInputView); + + fireEvent.press(screen.getByText('25%')); + + fireEvent.press(screen.getByText(strings('stake.review'))); + + expect(mockNavigate).toHaveBeenLastCalledWith('StakeModals', { + screen: Routes.STAKING.MODALS.GAS_IMPACT, + params: { + amountFiat: '750', + amountWei: '375000000000000000', + annualRewardRate: '2.5%', + annualRewardsETH: '0.00938 ETH', + annualRewardsFiat: '18.75 USD', + }, + }); + }); }); }); diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx index 06fdb4ab6ba..56c3cc749ae 100644 --- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx +++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx @@ -48,6 +48,8 @@ const StakeInputView = () => { isLoadingVaultData, handleMax, balanceValue, + isHighGasCostImpact, + isLoadingStakingGasFee, } = useStakingInputHandlers(); const navigateToLearnMoreModal = () => { @@ -57,6 +59,20 @@ const StakeInputView = () => { }; const handleStakePress = useCallback(() => { + if (isHighGasCostImpact()) { + navigation.navigate('StakeModals', { + screen: Routes.STAKING.MODALS.GAS_IMPACT, + params: { + amountWei: amountWei.toString(), + amountFiat: fiatAmount, + annualRewardsETH, + annualRewardsFiat, + annualRewardRate, + }, + }); + return; + } + navigation.navigate('StakeScreens', { screen: Routes.STAKING.STAKE_CONFIRMATION, params: { @@ -77,7 +93,7 @@ const StakeInputView = () => { .build(), ); }, [ - amountEth, + isHighGasCostImpact, navigation, amountWei, fiatAmount, @@ -86,6 +102,7 @@ const StakeInputView = () => { annualRewardRate, trackEvent, createEventBuilder, + amountEth, ]); const handleMaxButtonPress = () => { @@ -173,7 +190,9 @@ const StakeInputView = () => { size={ButtonSize.Lg} labelTextVariant={TextVariant.BodyMDMedium} variant={ButtonVariants.Primary} - isDisabled={isOverMaximum || !isNonZeroAmount} + isDisabled={ + isOverMaximum || !isNonZeroAmount || isLoadingStakingGasFee + } width={ButtonWidthTypes.Full} onPress={handleStakePress} /> diff --git a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.styles.ts b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.styles.ts new file mode 100644 index 00000000000..54b53fd9e90 --- /dev/null +++ b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.styles.ts @@ -0,0 +1,16 @@ +import { StyleSheet } from 'react-native'; + +const styleSheet = () => + StyleSheet.create({ + container: { + paddingHorizontal: 16, + }, + content: { + paddingBottom: 16, + }, + footer: { + paddingBottom: 16, + }, + }); + +export default styleSheet; diff --git a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx new file mode 100644 index 00000000000..7018981373c --- /dev/null +++ b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import renderWithProvider from '../../../../../util/test/renderWithProvider'; +import { GasImpactModalProps } from './GasImpactModal.types'; +import GasImpactModal from './index'; +import { Metrics, SafeAreaProvider } from 'react-native-safe-area-context'; +import { fireEvent } from '@testing-library/react-native'; +import { strings } from '../../../../../../locales/i18n'; + +const mockNavigate = jest.fn(); +const mockGoBack = jest.fn(); + +jest.mock('@react-navigation/native', () => { + const actualReactNavigation = jest.requireActual('@react-navigation/native'); + return { + ...actualReactNavigation, + useNavigation: () => ({ + navigate: mockNavigate, + goBack: mockGoBack, + }), + }; +}); + +const props: GasImpactModalProps = { + route: { + key: '1', + params: { + amountWei: '3210000000000000', + amountFiat: '7.46', + annualRewardRate: '2.5%', + annualRewardsETH: '2.5 ETH', + annualRewardsFiat: '$5000', + }, + name: 'params', + }, +}; + +const initialMetrics: Metrics = { + frame: { x: 0, y: 0, width: 320, height: 640 }, + insets: { top: 0, left: 0, right: 0, bottom: 0 }, +}; + +const renderGasImpactModal = () => + renderWithProvider( + + , + , + ); + +describe('GasImpactModal', () => { + it('render matches snapshot', () => { + const { toJSON } = renderGasImpactModal(); + + expect(toJSON()).toMatchSnapshot(); + }); + + it('navigates to StakeConfirmationView on approval', () => { + const { getByText } = renderGasImpactModal(); + + const proceedAnywayButton = getByText(strings('stake.proceed_anyway')); + + fireEvent.press(proceedAnywayButton); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + }); + + it('closes gas impact modal on cancel', () => { + const { getByText } = renderGasImpactModal(); + + const proceedAnywayButton = getByText(strings('stake.cancel')); + + fireEvent.press(proceedAnywayButton); + + expect(mockGoBack).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts new file mode 100644 index 00000000000..a00204cfbee --- /dev/null +++ b/app/components/UI/Stake/components/GasImpactModal/GasImpactModal.types.ts @@ -0,0 +1,13 @@ +import { RouteProp } from '@react-navigation/native'; + +interface GasImpactModalRouteParams { + amountWei: string; + amountFiat: string; + annualRewardsETH: string; + annualRewardsFiat: string; + annualRewardRate: string; +} + +export interface GasImpactModalProps { + route: RouteProp<{ params: GasImpactModalRouteParams }, 'params'>; +} diff --git a/app/components/UI/Stake/components/GasImpactModal/__snapshots__/GasImpactModal.test.tsx.snap b/app/components/UI/Stake/components/GasImpactModal/__snapshots__/GasImpactModal.test.tsx.snap new file mode 100644 index 00000000000..4050753f14d --- /dev/null +++ b/app/components/UI/Stake/components/GasImpactModal/__snapshots__/GasImpactModal.test.tsx.snap @@ -0,0 +1,342 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GasImpactModal render matches snapshot 1`] = ` + + + + + + + + + + + + + + + + + + Gas cost impact + + + + + + + + + + + + Warning: the transaction gas cost will account for more than 30% of your deposit. + + + + + Cancel + + + + + Proceed anyway + + + + + + + + , + +`; diff --git a/app/components/UI/Stake/components/GasImpactModal/index.tsx b/app/components/UI/Stake/components/GasImpactModal/index.tsx new file mode 100644 index 00000000000..4e348f75426 --- /dev/null +++ b/app/components/UI/Stake/components/GasImpactModal/index.tsx @@ -0,0 +1,103 @@ +import React, { useRef } from 'react'; +import BottomSheet, { + BottomSheetRef, +} from '../../../../../component-library/components/BottomSheets/BottomSheet'; +import Text, { + TextColor, + TextVariant, +} from '../../../../../component-library/components/Texts/Text'; +import { View } from 'react-native'; +import BottomSheetHeader from '../../../../../component-library/components/BottomSheets/BottomSheetHeader'; +import BottomSheetFooter, { + ButtonsAlignment, +} from '../../../../../component-library/components/BottomSheets/BottomSheetFooter'; +import { + ButtonProps, + ButtonSize, + ButtonVariants, +} from '../../../../../component-library/components/Buttons/Button/Button.types'; +import styleSheet from './GasImpactModal.styles'; +import { useStyles } from '../../../../hooks/useStyles'; +import { useNavigation } from '@react-navigation/native'; +import Routes from '../../../../../constants/navigation/Routes'; +import { GasImpactModalProps } from './GasImpactModal.types'; +import { strings } from '../../../../../../locales/i18n'; + +const GasImpactModal = ({ route }: GasImpactModalProps) => { + const { styles } = useStyles(styleSheet, {}); + + const { navigate } = useNavigation(); + + const sheetRef = useRef(null); + + const handleClose = () => { + sheetRef.current?.onCloseBottomSheet(); + }; + + const handleNavigateToStakeReviewScreen = () => { + const { + amountWei, + annualRewardRate, + annualRewardsFiat, + annualRewardsETH, + amountFiat, + } = route.params; + + navigate('StakeScreens', { + screen: Routes.STAKING.STAKE_CONFIRMATION, + params: { + amountWei: amountWei.toString(), + amountFiat, + annualRewardsETH, + annualRewardsFiat, + annualRewardRate, + }, + }); + }; + + const footerButtons: ButtonProps[] = [ + { + variant: ButtonVariants.Secondary, + label: ( + + {strings('stake.cancel')} + + ), + size: ButtonSize.Lg, + onPress: handleClose, + }, + { + variant: ButtonVariants.Primary, + label: ( + + {strings('stake.proceed_anyway')} + + ), + labelTextVariant: TextVariant.BodyMDMedium, + size: ButtonSize.Lg, + onPress: handleNavigateToStakeReviewScreen, + }, + ]; + + return ( + + + + + {strings('stake.gas_cost_impact')} + + + + {strings('stake.gas_cost_impact_warning')} + + + + + ); +}; + +export default GasImpactModal; diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx index 9aafb72b6e9..a014fca6425 100644 --- a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx +++ b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx @@ -43,6 +43,7 @@ import type { TokenI } from '../../../Tokens/types'; import useBalance from '../../hooks/useBalance'; import { NetworkBadgeSource } from '../../../AssetOverview/Balance/Balance'; import { selectChainId } from '../../../../../selectors/networkController'; +import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; export interface StakingBalanceProps { asset: TokenI; @@ -62,6 +63,7 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { exchangeRate, hasStakedPositions, hasEthToUnstake, + isLoadingPooledStakesData, } = usePooledStakes(); const { vaultData } = useVaultData(); const annualRewardRate = vaultData?.apy || ''; @@ -94,6 +96,74 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { return <>; } + const renderStakingContent = () => { + if (isLoadingPooledStakesData) { + return ( + + + + ); + } + + if (!isEligibleForPooledStaking) { + return ( + + ); + } + + return ( + <> + {unstakingRequests.map( + ({ positionTicket, withdrawalTimestamp, assetsToDisplay }) => + assetsToDisplay && ( + + ), + )} + + {hasClaimableEth && ( + + )} + + {!hasStakedPositions && ( + + )} + + + + ); + }; + return ( {hasStakedPositions && ( @@ -120,61 +190,7 @@ const StakingBalanceContent = ({ asset }: StakingBalanceProps) => { )} - - {!isEligibleForPooledStaking ? ( - - ) : ( - <> - {unstakingRequests.map( - ({ positionTicket, withdrawalTimestamp, assetsToDisplay }) => - assetsToDisplay && ( - - ), - )} - - {hasClaimableEth && ( - - )} - - {!hasStakedPositions && ( - - )} - - - - )} - + {renderStakingContent()} ); }; diff --git a/app/components/UI/Stake/hooks/usePoolStakedUnstake/index.ts b/app/components/UI/Stake/hooks/usePoolStakedUnstake/index.ts index 04e8802c5c7..641a2fd9c49 100644 --- a/app/components/UI/Stake/hooks/usePoolStakedUnstake/index.ts +++ b/app/components/UI/Stake/hooks/usePoolStakedUnstake/index.ts @@ -1,3 +1,4 @@ +import { ethers } from 'ethers'; import { PooledStakingContract, ChainId } from '@metamask/stake-sdk'; import { useStakeContext } from '../useStakeContext'; import { @@ -8,6 +9,7 @@ import { import { addTransaction } from '../../../../../util/transaction-controller'; import { ORIGIN_METAMASK } from '@metamask/controller-utils'; import trackErrorAsAnalytics from '../../../../../util/metrics/TrackError/trackErrorAsAnalytics'; +import useBalance from '../useBalance'; const generateUnstakeTxParams = ( activeAccountAddress: string, @@ -23,11 +25,33 @@ const generateUnstakeTxParams = ( }); const attemptUnstakeTransaction = - (pooledStakingContract: PooledStakingContract) => + (pooledStakingContract: PooledStakingContract, stakedBalanceWei: string) => // Note: receiver is the user address attempting to unstake. async (valueWei: string, receiver: string) => { try { - const shares = await pooledStakingContract.convertToShares(valueWei); + // STAKE-867: This is temporary logic for the unstake all action + // if we are unstaking the total assets we send the total shares + // the user has in the vault through getShares contract method + // this is a quick fix for mobile only and will be refactored to cover + // portfolio in the future. We avoid the case where contract level rounding + // error causes 1 wei dust to be left when converting assets to shares + // and attempting to unstake all assets + let shares; + if (valueWei === stakedBalanceWei) { + // create the interface for the getShares method and call getShares to get user shares + const tempInterface = new ethers.utils.Interface([ + 'function getShares(address) returns (uint256)', + ]); + const data = tempInterface.encodeFunctionData('getShares', [receiver]); + const sharesResult = await pooledStakingContract?.contract.provider.call({ + to: pooledStakingContract?.contract.address, + data, + }); + const [sharesBN] = tempInterface.decodeFunctionResult('getShares', sharesResult); + shares = sharesBN.toString(); + } else { + shares = await pooledStakingContract.convertToShares(valueWei); + } const gasLimit = await pooledStakingContract.estimateEnterExitQueueGas( shares.toString(), @@ -64,11 +88,12 @@ const attemptUnstakeTransaction = const usePoolStakedUnstake = () => { const stakeContext = useStakeContext(); + const { stakedBalanceWei } = useBalance(); const stakingContract = stakeContext.stakingContract as PooledStakingContract; return { - attemptUnstakeTransaction: attemptUnstakeTransaction(stakingContract), + attemptUnstakeTransaction: attemptUnstakeTransaction(stakingContract, stakedBalanceWei), }; }; diff --git a/app/components/UI/Stake/hooks/usePoolStakedUnstake/usePoolStakedUnstake.test.tsx b/app/components/UI/Stake/hooks/usePoolStakedUnstake/usePoolStakedUnstake.test.tsx index eb546d23f64..780791f1d0c 100644 --- a/app/components/UI/Stake/hooks/usePoolStakedUnstake/usePoolStakedUnstake.test.tsx +++ b/app/components/UI/Stake/hooks/usePoolStakedUnstake/usePoolStakedUnstake.test.tsx @@ -7,8 +7,9 @@ import { StakingType, ChainId, } from '@metamask/stake-sdk'; -import { Contract } from 'ethers'; +import { BigNumber, Contract, ethers } from 'ethers'; import { Stake } from '../../sdk/stakeSdkProvider'; +import useBalance from '../useBalance'; const MOCK_ADDRESS_1 = '0x0'; @@ -32,6 +33,8 @@ const MOCK_DEPOSIT_CONTRACT_ADDRESS = const MOCK_RECEIVER_ADDRESS = '0x316bde155acd07609872a56bc32ccfb0b13201fa'; const MOCK_UNSTAKE_GAS_LIMIT = 73135; const MOCK_UNSTAKE_VALUE_WEI = '10000000000000000'; // 0.01 ETH +const MOCK_STAKED_BALANCE_VALUE_WEI = '20000000000000000'; // 0.02 ETH +const MOCK_UNSTAKE_ALL_VALUE_WEI = MOCK_STAKED_BALANCE_VALUE_WEI; const ENCODED_TX_UNSTAKE_DATA = { chainId: 1, @@ -82,7 +85,12 @@ jest.mock('../../../../../core/Engine', () => { const mockPooledStakingContractService: PooledStakingContract = { chainId: ChainId.ETHEREUM, connectSignerOrProvider: mockConnectSignerOrProvider, - contract: new Contract('0x0000000000000000000000000000000000000000', []), + contract: { + ...new Contract('0x0000000000000000000000000000000000000000', []), + provider: { + call: jest.fn(), + }, + } as unknown as Contract, convertToShares: mockConvertToShares, encodeClaimExitedAssetsTransactionData: jest.fn(), encodeDepositTransactionData: jest.fn(), @@ -100,10 +108,19 @@ const mockSdkContext: Stake = { setSdkType: jest.fn(), }; +const mockBalance: Pick, 'stakedBalanceWei'> = { + stakedBalanceWei: MOCK_STAKED_BALANCE_VALUE_WEI, +}; + jest.mock('../useStakeContext', () => ({ useStakeContext: () => mockSdkContext, })); +jest.mock('../useBalance', () => ({ + __esModule: true, + default: () => mockBalance, +})); + describe('usePoolStakedUnstake', () => { afterEach(() => { jest.clearAllMocks(); @@ -129,5 +146,27 @@ describe('usePoolStakedUnstake', () => { expect(mockEncodeEnterExitQueueTransactionData).toHaveBeenCalledTimes(1); expect(mockAddTransaction).toHaveBeenCalledTimes(1); }); + + it('attempts to create and submit an unstake all transaction', async () => { + jest.spyOn(ethers.utils, 'Interface').mockImplementation(() => ({ + encodeFunctionData: jest.fn(), + decodeFunctionResult: jest.fn().mockReturnValue([BigNumber.from(MOCK_UNSTAKE_ALL_VALUE_WEI)]), + } as unknown as ethers.utils.Interface)); + + const { result } = renderHookWithProvider(() => usePoolStakedUnstake(), { + state: mockInitialState, + }); + + await result.current.attemptUnstakeTransaction( + MOCK_UNSTAKE_ALL_VALUE_WEI, + MOCK_RECEIVER_ADDRESS, + ); + + expect(mockConvertToShares).toHaveBeenCalledTimes(0); + expect(mockEstimateEnterExitQueueGas).toHaveBeenCalledTimes(1); + expect(mockEncodeEnterExitQueueTransactionData).toHaveBeenCalledTimes(1); + expect(mockEncodeEnterExitQueueTransactionData).toHaveBeenCalledWith(BigNumber.from(MOCK_UNSTAKE_ALL_VALUE_WEI).toString(), MOCK_RECEIVER_ADDRESS, { gasLimit: MOCK_UNSTAKE_GAS_LIMIT }); + expect(mockAddTransaction).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/app/components/UI/Stake/hooks/useStakingInput.ts b/app/components/UI/Stake/hooks/useStakingInput.ts index 3193c1a7002..5920a3f633c 100644 --- a/app/components/UI/Stake/hooks/useStakingInput.ts +++ b/app/components/UI/Stake/hooks/useStakingInput.ts @@ -97,6 +97,17 @@ const useStakingInputHandlers = () => { ? `${balanceETH} ETH` : `${balanceFiatNumber?.toString()} ${currentCurrency.toUpperCase()}`; + const getDepositTxGasPercentage = useCallback( + () => estimatedGasFeeWei.mul(new BN(100)).div(amountWei).toString(), + [amountWei, estimatedGasFeeWei], + ); + + // Gas fee make up 30% or more of the deposit amount. + const isHighGasCostImpact = useCallback( + () => new BN(getDepositTxGasPercentage()).gt(new BN(30)), + [getDepositTxGasPercentage], + ); + return { amountEth, amountWei, @@ -122,6 +133,8 @@ const useStakingInputHandlers = () => { handleMax, isLoadingStakingGasFee, balanceValue, + getDepositTxGasPercentage, + isHighGasCostImpact, }; }; diff --git a/app/components/UI/Stake/routes/index.tsx b/app/components/UI/Stake/routes/index.tsx index c6b98f24d2e..cf1ae55b3cb 100644 --- a/app/components/UI/Stake/routes/index.tsx +++ b/app/components/UI/Stake/routes/index.tsx @@ -8,6 +8,7 @@ import UnstakeInputView from '../Views/UnstakeInputView/UnstakeInputView'; import UnstakeConfirmationView from '../Views/UnstakeConfirmationView/UnstakeConfirmationView'; import { StakeSDKProvider } from '../sdk/stakeSdkProvider'; import MaxInputModal from '../components/MaxInputModal'; +import GasImpactModal from '../components/GasImpactModal'; const Stack = createStackNavigator(); const ModalStack = createStackNavigator(); @@ -57,6 +58,11 @@ const StakeModalStack = () => ( component={MaxInputModal} options={{ headerShown: false }} /> + ); diff --git a/app/components/Views/Wallet/index.test.tsx b/app/components/Views/Wallet/index.test.tsx index a7baa2653b3..a145d9d1c8e 100644 --- a/app/components/Views/Wallet/index.test.tsx +++ b/app/components/Views/Wallet/index.test.tsx @@ -1,14 +1,12 @@ import React from 'react'; import Wallet from './'; import { renderScreen } from '../../../util/test/renderWithProvider'; -import { act, screen } from '@testing-library/react-native'; +import { screen } from '@testing-library/react-native'; import ScrollableTabView from 'react-native-scrollable-tab-view'; import Routes from '../../../constants/navigation/Routes'; import { backgroundState } from '../../../util/test/initial-root-state'; import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils'; import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; -import { useAccountSyncing } from '../../../util/notifications/hooks/useAccountSyncing'; -import { AppState } from 'react-native'; const MOCK_ADDRESS = '0xc4955c0d639d99699bfd7ec54d9fafee40e4d272'; @@ -186,40 +184,4 @@ describe('Wallet', () => { ); expect(accountPicker).toBeDefined(); }); - it('dispatches account syncing on mount', () => { - jest.clearAllMocks(); - //@ts-expect-error we are ignoring the navigation params on purpose because we do not want to mock setOptions to test the navbar - render(Wallet); - expect(useAccountSyncing().dispatchAccountSyncing).toHaveBeenCalledTimes(1); - }); - it('dispatches account syncing when appState switches from inactive|background to active', () => { - jest.clearAllMocks(); - - const addEventListener = jest.spyOn(AppState, 'addEventListener'); - - //@ts-expect-error we are ignoring the navigation params on purpose because we do not want to mock setOptions to test the navbar - render(Wallet); - - expect(addEventListener).toHaveBeenCalledWith( - 'change', - expect.any(Function), - ); - const handleAppStateChange = ( - addEventListener as jest.Mock - ).mock.calls.find(([event]) => event === 'change')[1]; - - act(() => { - handleAppStateChange('background'); - handleAppStateChange('active'); - }); - - expect(useAccountSyncing().dispatchAccountSyncing).toHaveBeenCalledTimes(2); - - act(() => { - handleAppStateChange('inactive'); - handleAppStateChange('active'); - }); - - expect(useAccountSyncing().dispatchAccountSyncing).toHaveBeenCalledTimes(3); - }); }); diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index 2036f343925..d06231fc9b9 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -1,10 +1,4 @@ -import React, { - useEffect, - useRef, - useCallback, - useContext, - useLayoutEffect, -} from 'react'; +import React, { useEffect, useRef, useCallback, useContext } from 'react'; import { ActivityIndicator, StyleSheet, @@ -12,8 +6,6 @@ import { TextStyle, InteractionManager, Linking, - AppState, - AppStateStatus, } from 'react-native'; import type { Theme } from '@metamask/design-tokens'; import { connect, useSelector } from 'react-redux'; @@ -93,9 +85,7 @@ import { selectIsProfileSyncingEnabled, } from '../../../selectors/notifications'; import { ButtonVariants } from '../../../component-library/components/Buttons/Button'; -import { useListNotifications } from '../../../util/notifications/hooks/useNotifications'; import { useAccountName } from '../../hooks/useAccountName'; -import { useAccountSyncing } from '../../../util/notifications/hooks/useAccountSyncing'; import { PortfolioBalance } from '../../UI/Tokens/TokenList/PortfolioBalance'; import useCheckNftAutoDetectionModal from '../../hooks/useCheckNftAutoDetectionModal'; @@ -162,10 +152,7 @@ const Wallet = ({ showNftFetchingLoadingIndicator, hideNftFetchingLoadingIndicator, }: WalletProps) => { - const appState = useRef(AppState.currentState); const { navigate } = useNavigation(); - const { listNotifications } = useListNotifications(); - const { dispatchAccountSyncing } = useAccountSyncing(); const walletRef = useRef(null); const theme = useTheme(); const { toastRef } = useContext(ToastContext); @@ -415,35 +402,6 @@ const Wallet = ({ [navigation, providerConfig.chainId], ); - // Layout effect when component/view is visible - // - fetches notifications - // - dispatches account syncing - useLayoutEffect(() => { - const handleAppStateChange = (nextAppState: AppStateStatus) => { - if ( - appState.current?.match(/inactive|background/) && - nextAppState === 'active' - ) { - listNotifications(); - dispatchAccountSyncing(); - } - - appState.current = nextAppState; - }; - - const subscription = AppState.addEventListener( - 'change', - handleAppStateChange, - ); - - listNotifications(); - dispatchAccountSyncing(); - - return () => { - subscription.remove(); - }; - }, [listNotifications, dispatchAccountSyncing]); - useEffect(() => { navigation.setOptions( getWalletNavbarOptions( @@ -533,12 +491,9 @@ const Wallet = ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any let stakedBalance: any = 0; - const assets = [ - ...(tokens || []), - ]; + const assets = [...(tokens || [])]; if (accountBalanceByChainId) { - balance = renderFromWei(accountBalanceByChainId.balance); const nativeAsset = { // TODO: Add name property to Token interface in controllers. @@ -575,8 +530,8 @@ const Wallet = ({ conversionRate, currentCurrency, ), - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; assets.push(stakedAsset); } diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 008fb429b5d..3b3dfcc7ced 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -155,6 +155,7 @@ const Routes = { MODALS: { LEARN_MORE: 'LearnMore', MAX_INPUT: 'MaxInput', + GAS_IMPACT: 'GasImpact', }, }, ///: BEGIN:ONLY_INCLUDE_IF(external-snaps) diff --git a/app/util/sentry/utils.js b/app/util/sentry/utils.js index 06937e5d3bc..5241f5fa41e 100644 --- a/app/util/sentry/utils.js +++ b/app/util/sentry/utils.js @@ -10,6 +10,7 @@ import { store } from '../../store'; import { Performance } from '../../core/Performance'; import Device from '../device'; import { TraceName } from '../trace'; +import { getTraceTags } from './tags'; /** * This symbol matches all object properties when used in a mask */ @@ -402,15 +403,19 @@ function rewriteReport(report) { */ export function excludeEvents(event) { // This is needed because store starts to initialise before performance observers completes to measure app start time - if (event?.transaction === TraceName.UIStartup && Device.isAndroid()) { - const appLaunchTime = Performance.appLaunchTime; - const formattedAppLaunchTime = (event.start_timestamp = Number( - `${appLaunchTime.toString().slice(0, 10)}.${appLaunchTime - .toString() - .slice(10)}`, - )); - if (event.start_timestamp !== formattedAppLaunchTime) { - event.start_timestamp = formattedAppLaunchTime; + if (event?.transaction === TraceName.UIStartup) { + event.tags = getTraceTags(store.getState()); + + if (Device.isAndroid()) { + const appLaunchTime = Performance.appLaunchTime; + const formattedAppLaunchTime = (event.start_timestamp = Number( + `${appLaunchTime.toString().slice(0, 10)}.${appLaunchTime + .toString() + .slice(10)}`, + )); + if (event.start_timestamp !== formattedAppLaunchTime) { + event.start_timestamp = formattedAppLaunchTime; + } } } //Modify or drop event here diff --git a/bitrise.yml b/bitrise.yml index 124b3e2daf6..eb1a69d7959 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -135,8 +135,8 @@ stages: - run_ios_api_specs: {} - run_tag_smoke_accounts_ios: {} - run_tag_smoke_accounts_android: {} - - run_tag_smoke_notifications_ios: {} - - run_tag_smoke_notifications_android: {} + # - run_tag_smoke_notifications_ios: {} + # - run_tag_smoke_notifications_android: {} # - run_tag_smoke_assets_ios: {} - run_tag_smoke_assets_android: {} - run_tag_smoke_confirmations_ios: {} diff --git a/ios/Gemfile b/ios/Gemfile index 4d245c9f943..e05db0cadde 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,10 +1,10 @@ source 'https://rubygems.org' # Recommended to use http://rbenv.org/ to install and use this version -ruby '>= 3.1.6' +ruby '3.1.6' # Allow minor version updates up to but excluding 2.0.0 -gem 'cocoapods', '~> 1.12' +gem 'cocoapods', '1.16.2' # Allow all version updates up to but excluding 7.1.0 gem 'activesupport', '>= 6.1.7.3', '< 7.1.0' diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index ed3f678b6d1..0f857ed10f5 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -10,18 +10,18 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) base64 (0.2.0) claide (1.1.0) - cocoapods (1.15.2) + cocoapods (1.16.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.15.2) + cocoapods-core (= 1.16.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -35,8 +35,8 @@ GEM molinillo (~> 0.8.0) nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.15.2) + xcodeproj (>= 1.27.0, < 2.0) + cocoapods-core (1.16.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -60,36 +60,36 @@ GEM escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - ffi (1.16.3) + ffi (1.17.0) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86_64-linux-gnu) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) httpclient (2.8.3) i18n (1.14.4) concurrent-ruby (~> 1.0) - json (2.7.2) + json (2.8.1) minitest (5.22.3) molinillo (0.8.0) - nanaimo (0.3.0) + nanaimo (0.4.0) nap (1.1.0) netrc (0.11.0) nkf (0.2.0) public_suffix (4.0.7) - rexml (3.3.6) - strscan + rexml (3.3.9) ruby-macho (2.5.1) - strscan (3.1.0) typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - xcodeproj (1.25.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (>= 3.3.2, < 4.0) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) PLATFORMS arm64-darwin-22 @@ -98,7 +98,7 @@ PLATFORMS DEPENDENCIES activesupport (>= 6.1.7.3, < 7.1.0) - cocoapods (~> 1.12) + cocoapods (= 1.16.2) RUBY VERSION ruby 3.1.6p260 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d2a95947052..84b23bf4f32 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -200,9 +200,9 @@ PODS: - lottie-react-native (5.1.5): - lottie-ios (~> 3.4.0) - React-Core - - MMKV (1.3.9): - - MMKVCore (~> 1.3.9) - - MMKVCore (1.3.9) + - MMKV (2.0.0): + - MMKVCore (~> 2.0.0) + - MMKVCore (2.0.0) - MultiplatformBleAdapter (0.2.0) - nanopb (2.30910.0): - nanopb/decode (= 2.30910.0) @@ -1165,7 +1165,7 @@ SPEC CHECKSUMS: BEMCheckBox: 5ba6e37ade3d3657b36caecc35c8b75c6c2b1a4e boost: 7dcd2de282d72e344012f7d6564d024930a6a440 Branch: 4ac024cb3c29b0ef628048694db3c4cfa679beb0 - BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872 + BVLinearGradient: 44fd36b87f318e7933c4c91a6991442a5e3f5bcf CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: 25cbffbaec517695d376ab4bc428948cd0f08088 @@ -1194,63 +1194,63 @@ SPEC CHECKSUMS: GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 lottie-ios: 016449b5d8be0c3dcbcfa0a9988469999cd04c5d - lottie-react-native: 3e722c63015fdb9c27638b0a77969fc412067c18 - MMKV: 817ba1eea17421547e01e087285606eb270a8dcb - MMKVCore: af055b00e27d88cd92fad301c5fecd1ff9b26dd9 + lottie-react-native: b6776287d7fd31be4fd865059cd890f744242ffd + MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801 + MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390 MultiplatformBleAdapter: b1fddd0d499b96b607e00f0faa8e60648343dc1d nanopb: 438bc412db1928dac798aa6fd75726007be04262 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c - Permission-BluetoothPeripheral: 247e379c9ecb4b1af2b87f73e4a15a00a5bc0c1f + Permission-BluetoothPeripheral: 34ab829f159c6cf400c57bac05f5ba1b0af7a86e PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 + RCT-Folly: 8dc08ca5a393b48b1c523ab6220dfdcc0fe000ad RCTRequired: fb207f74935626041e7308c9e88dcdda680f1073 - RCTSearchApi: d2d38a5a7bffbfb144e2c770fbb30f51b1053067 + RCTSearchApi: 5fc36140c598a74fd831dca924a28ed53bc7aa18 RCTTypeSafety: 146fd11361680250b7580dd1f7f601995cfad1b1 React: f3712351445cc96ba507425675a0cd8d31321d0c React-callinvoker: dcc51a66e02d20a70aeca2abbb1388d4d3011bf8 - React-Codegen: 04b7e88a7f5d3933d058ffb9cea7b0268666de79 - React-Core: ed3aeebf41aeb621de2ab4b58216a2fd5a5fd141 - React-CoreModules: 9d1e6f44bf658431a3b99561c8058b54b5959190 - React-cxxreact: d2d14fc0c0782bd9ed7a556892769b4034ae027c + React-Codegen: ef431087b06572288cd0f789c9cf1d22b37c3019 + React-Core: 88bf9e0d862195fda28723fd95aef3111025f300 + React-CoreModules: 96a557c45f6be644a82d63066c4ac79173bba0ff + React-cxxreact: 3db957f2a0db039b95c1103ea2274e36815b8009 React-debug: 4e90d08c78aa207c064a3860e1540ff252695585 React-jsc: 9ffa4c837c5286366d27c892b6c7c34da3cd5f3d - React-jsi: 020729f637b93456de0018061d44ce36f33c2d8a - React-jsiexecutor: ce8ecfcd3b7dbc9cb65a661110be17f5afd18aa3 + React-jsi: 08cb162e1d192bf197bc0693270ab65d8e9d4d5c + React-jsiexecutor: b71b576b4447d9fed6f2f1b146550de70d49a75a React-jsinspector: b86a8abae760c28d69366bbc1d991561e51341ed - React-logger: ed7c9e01e58529065e7da6bf8318baf15024283e - react-native-aes: 0143040f4e0cb19296b69b4acc7ddd8d3df9d62d - react-native-background-timer: 1b6e6b4e10f1b74c367a1fdc3c72b67c619b222b - react-native-ble-plx: f0557dbb6bd1f26cca75a67b5f33cfc7f7f9abed - react-native-blob-jsi-helper: 13c10135af4614dbc0712afba5960784cd44f043 - react-native-blob-util: 18b510205c080a453574a7d2344d64673d0ad9af - react-native-blur: 507cf3dd4434eb9d5ca5f183e49d8bcccdd66826 - react-native-branch: 4e42fda662d96893afbbd02839806931398e3d2e - react-native-camera: b8cc03e2feec0c04403d0998e37cf519d8fd4c6f - react-native-compat: 8b6a38155e778a20a008aea837efd00e099b6fe8 - react-native-cookies: f54fcded06bb0cda05c11d86788020b43528a26c - react-native-fast-crypto: 5943c42466b86ad70be60d3a5f64bd22251e5d9e - react-native-flipper: 6cfd5991388121f7f96fc5171b93380f97ebb3c6 - react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a - react-native-gzip: c5e87ee9e359f02350e3a2ee52eb35eddc398868 - react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4 - react-native-launch-arguments: 4e0fd58e56dcc7f52eedef9dc8eff81eb73ced7a - react-native-mmkv: e97c0c79403fb94577e5d902ab1ebd42b0715b43 - react-native-netinfo: 48c5f79a84fbc3ba1d28a8b0d04adeda72885fa8 - react-native-performance: ff93f8af3b2ee9519fd7879896aa9b8b8272691d - react-native-quick-base64: 777057ea4286f806b00259ede65dc79c7c706320 - react-native-quick-crypto: 455c1b411db006dba1026a30681ececb19180187 - react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 - react-native-render-html: 984dfe2294163d04bf5fe25d7c9f122e60e05ebe - react-native-safe-area-context: 9e40fb181dac02619414ba1294d6c2a807056ab9 - react-native-slider: f266dd860064138a659a42714e6da47a52a51107 - react-native-video: c26780b224543c62d5e1b2a7244a5cd1b50e8253 - react-native-view-shot: 4475fde003fe8a210053d1f98fb9e06c1d834e1c - react-native-webview-mm: c518409c962c1f0f95c08bb6a700b9f97aff131b - React-NativeModulesApple: 7bab439cb5de9a76299210ed1127698170777a7f + React-logger: 8c0f8173197ad28ac3212c18f8141690209dfe52 + react-native-aes: e8b2e113d532b0efb6449754492aee9c218dd502 + react-native-background-timer: 007ff829f79644caf2ed013e22f0563560336f86 + react-native-ble-plx: c08c34c162509ec466c68a7cdc86b69c12e6efdd + react-native-blob-jsi-helper: bd7509e50b0f906044c53ad7ab767786054424c9 + react-native-blob-util: 6560d6fc4b940ec140f9c3ebe21c8669b1df789b + react-native-blur: 7c03644c321696ccec9778447180e0f9339b3604 + react-native-branch: 76e1f947b40597727e6faa5cba5824d7ecf6c6b0 + react-native-camera: 1e6fefa515d3af8b4aeaca3a8bffa2925252c4ea + react-native-compat: 8050db8973090f2c764807e7fa74f163f78e4c32 + react-native-cookies: d648ab7025833b977c0b19e142503034f5f29411 + react-native-fast-crypto: 6b448866f5310cf203714a21147ef67f735bea8e + react-native-flipper: ca4382a2b6cfd319b6e212539bc1fe7aafe36879 + react-native-get-random-values: 0fd2b6a3129988d701d10e30f0622d5f039531bc + react-native-gzip: 8d602277c2564591f04dd1cec4043acc8350dcc3 + react-native-in-app-review: b3d1eed3d1596ebf6539804778272c4c65e4a400 + react-native-launch-arguments: 7eb321ed3f3ef19b3ec4a2eca71c4f9baee76b41 + react-native-mmkv: 5a46c73e3e12aa872c4485ae0e4414b4040af79a + react-native-netinfo: 26560022f28c06d8ef00a9ff1e03beefbbb60c2d + react-native-performance: 125a96c145e29918b55b45ce25cbba54f1e24dcd + react-native-quick-base64: daf67f19ee076b77f0755bf4056f3425f164e1d8 + react-native-quick-crypto: eff065b704d3f1c6e336cfc612dce63228ab3482 + react-native-randombytes: 3c8f3e89d12487fd03a2f966c288d495415fc116 + react-native-render-html: 5afc4751f1a98621b3009432ef84c47019dcb2bd + react-native-safe-area-context: 667324e20fb3dd9c39c12d6036675ed90099bcd5 + react-native-slider: 6a25a7398addb8478798315a58504efce744009d + react-native-video: 2aad0d963bf3952bd9ebb2f53fab799338e8e202 + react-native-view-shot: bb8934cb93bf8ec740c81ed94f93244778797b6c + react-native-webview-mm: d5f16bf95d45db97b53851ab87c79b2e1d964a13 + React-NativeModulesApple: ee6c836571c874dc879cf87603edff00d8dded46 React-perflogger: 6acc671f527e69c0cd93b8e62821d33d3ddf25ca React-RCTActionSheet: 569bb9db46d85565d14697e15ecf2166e035eb07 React-RCTAnimation: 0eea98143c2938a8751a33722623d3e8a38fe1e4 - React-RCTAppDelegate: 74d38dbb3d8691f72e6dda670006e85d9ea21c91 + React-RCTAppDelegate: 11e6d38c00a34e1025b9ef26bb13968f6d9ed902 React-RCTBlob: 9b3b60e806ce5c9fe5a8ee449f3e41087617441c React-RCTImage: 0220975422a367e784dfd794adfc6454fab23c1f React-RCTLinking: 1abf9834017e080ecbd5b6a28b4fb15eb843a3dd @@ -1261,42 +1261,42 @@ SPEC CHECKSUMS: React-RCTVibration: 372a12b697a170aaee792f8a9999c40e1f2692d0 React-rncore: d1ccbd5adaf4a67703790838b7c62f140e72d32a React-runtimeexecutor: d4f7ff5073fcf87e14dbf89541d434218630246e - React-runtimescheduler: b360635f6f804ec42fa875500620882a6b97d2f5 - React-utils: 8eb3c12fd4a4da6df3824f7d9a961d73a6ed6e5d - ReactCommon: 317bddf4a70fca9e542343e942a504285282971c - ReactNativePayments: db62ee22a825e9e9c3e19c276d8d020881dd0630 - RNCAsyncStorage: 826b603ae9c0f88b5ac4e956801f755109fa4d5c - RNCCheckbox: a3ca9978cb0846b981d28da4e9914bd437403d77 - RNCClipboard: ddd4d291537f1667209c9c405aaa4307297e252e - RNCMaskedView: 090213d32d8b3bb83a4dcb7d12c18f0152591906 - RNDateTimePicker: 4f3c4dbd4f908be32ec8c93f086e8924bd4a2e07 - RNDefaultPreference: 2f8d6d54230edbd78708ada8d63bb275e5a8415b - RNDeviceInfo: 1e3f62b9ec32f7754fac60bd06b8f8a27124e7f0 - RNFBApp: 5f87753a8d8b37d229adf85cd0ff37709ffdf008 - RNFBMessaging: 3fa1114c0868dd21f20dfe186adf42297ea316b1 - RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211 - RNI18n: e2f7e76389fcc6e84f2c8733ea89b92502351fd8 - RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364 - RNKeychain: 4f63aada75ebafd26f4bc2c670199461eab85d94 - RNNotifee: 2b7df6e32a9cc24b9af6b410fa7db1cd2f411d6d - RNOS: 6f2f9a70895bbbfbdad7196abd952e7b01d45027 - RNPermissions: 4e3714e18afe7141d000beae3755e5b5fb2f5e05 - RNReanimated: f8379347f71248607d530a21e31e4140c5910c25 - RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789 - RNSensors: c363d486c879e181905dea84a2535e49af1c2d25 - RNSentry: ae79ba7d46cfdf501be85ef72af1b7c8b1d80a79 - RNShare: f116bbb04f310c665ca483d0bd1e88cf59b3b334 - RNSVG: a48668fd382115bc89761ce291a81c4ca5f2fd2e - RNVectorIcons: 6607bd3a30291d0edb56f9bbe7ae411ee2b928b0 - segment-analytics-react-native: dbdd08d96fec78132e96bda092562e41c2ce0ce0 + React-runtimescheduler: 06b060b5b022f4cdb6bd9fd3405396372179cd9b + React-utils: e50991349b1b749744f35ff93d943343886deb24 + ReactCommon: 394d4d2b27d88bb8ae15fa7f864a4a7525f467f0 + ReactNativePayments: 47056cd9f1dc32dbdd716974de5df700c44f12db + RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11 + RNCCheckbox: 450ce156f3e29e25efa0315c96cfbabe5a39ded1 + RNCClipboard: ba13782f62310ffd4377332497241a1051f6870b + RNCMaskedView: de80352547bd4f0d607bf6bab363d826822bd126 + RNDateTimePicker: 590f2000e4272050b98689cee6c8abc66c25bb22 + RNDefaultPreference: 36fe31684af1f2d14e0664aa9a816d0ec6149cc1 + RNDeviceInfo: e5219d380b51ddb7f97e650ab99a518476b90203 + RNFBApp: 0e66b9f844efdf2ac3fa2b30e64c9db41a263b3d + RNFBMessaging: 70b12c9f22c7c9d5011ac9b12ac2bafbfb081267 + RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 + RNGestureHandler: 6572a5f44759900730562b418da289c373de8d06 + RNI18n: 11ec5086508673ef71b5b567da3e8bcca8a926e1 + RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20 + RNKeychain: 3194f1c9d8599f39e570b4b5ecbcdd8cd610e771 + RNNotifee: 5165d37aaf980031837be3caece2eae5a6d73ae8 + RNOS: d07e5090b5060c6f2b83116d740a32cfdb33afe3 + RNPermissions: bd0d9ca7969ff7b999aa605ee2e5919c12522bfe + RNReanimated: 7a85cf61cf3849efb530a9de2204a119f426636a + RNScreens: f112bc5442a9a0e468809c107a43f55882d6cd98 + RNSensors: 4690be00931bc60be7c7bd457701edefaff965e3 + RNSentry: 984bb0495abef6c419697bef208c581f127891d1 + RNShare: d03cdc71e750246a48b81dcd62bd792bf57a758e + RNSVG: e77adf5edb2302f0f10dd03a09e92bb9420d914e + RNVectorIcons: 24be0b504ce32d5bea38bde6c645f08b9c736392 + segment-analytics-react-native: 885c1703579dc7964b97e7ae11d857669aa9b015 Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - sovran-react-native: 791f2f726b4d57ece59676eda58d6da9dc95ad4e - TcpSockets: a8eb6b5867fe643e6cfed5db2e4de62f4d1e8fd0 + sovran-react-native: e4721a564ee6ef5b5a0d901bc677018cf371ea01 + TcpSockets: 48866ffcb39d7114741919d21069fc90189e474a Yoga: 6f5ab94cd8b1ecd04b6e973d0bc583ede2a598cc YogaKit: f782866e155069a2cca2517aafea43200b01fd5a PODFILE CHECKSUM: e0bcc4eb12d48746028cd4f4161a292fa9ddc627 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/locales/languages/en.json b/locales/languages/en.json index 1345810012d..9c371ef2bde 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3503,7 +3503,10 @@ "description": "Max is the total amount of ETH you have, minus the gas fee required to stake. It’s a good idea to keep some extra ETH in your wallet for future transactions." }, "use_max": "Use max", - "estimated_unstaking_time": "1 to 11 days" + "estimated_unstaking_time": "1 to 11 days", + "proceed_anyway": "Proceed anyway", + "gas_cost_impact": "Gas cost impact", + "gas_cost_impact_warning": "Warning: the transaction gas cost will account for more than 30% of your deposit." }, "default_settings": { "title": "Your Wallet is ready",