diff --git a/.js.env.example b/.js.env.example
index 68e8316f034..81d9fea44a1 100644
--- a/.js.env.example
+++ b/.js.env.example
@@ -90,7 +90,10 @@ export MM_ENABLE_SETTINGS_PAGE_DEV_OPTIONS="true"
# The endpoint used to submit errors and tracing data to Sentry for dev environment.
# export MM_SENTRY_DSN_DEV=
-# Multichain Feature flag
-export MULTICHAIN_V1=""
+# Per dapp selected network (Amon Hen) feature flag
+export MM_PER_DAPP_SELECTED_NETWORK=""
+
+export MM_CHAIN_PERMISSIONS=""
+
#Multichain feature flag specific to UI changes
export MM_MULTICHAIN_V1_ENABLED=""
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 80ffad8fa28..31fd8b3afdf 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -174,7 +174,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1450
- versionName "7.32.0"
+ versionName "7.33.0"
testBuildType System.getProperty('testBuildType', 'debug')
missingDimensionStrategy 'react-native-camera', 'general'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/app/components/UI/AssetOverview/Balance/Balance.tsx b/app/components/UI/AssetOverview/Balance/Balance.tsx
index 02c89071051..fed53bd539a 100644
--- a/app/components/UI/AssetOverview/Balance/Balance.tsx
+++ b/app/components/UI/AssetOverview/Balance/Balance.tsx
@@ -34,7 +34,7 @@ interface BalanceProps {
secondaryBalance?: string;
}
-const NetworkBadgeSource = (chainId: string, ticker: string) => {
+export const NetworkBadgeSource = (chainId: string, ticker: string) => {
const isMainnet = isMainnetByChainId(chainId);
const isLineaMainnet = isLineaMainnetByChainId(chainId);
@@ -88,7 +88,9 @@ const Balance = ({ asset, mainBalance, secondaryBalance }: BalanceProps) => {
{asset.name || asset.symbol}
- {isPooledStakingFeatureEnabled() && asset?.isETH && }
+ {isPooledStakingFeatureEnabled() && asset?.isETH && (
+
+ )}
);
};
diff --git a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx
index 8864c78c35c..368e2352d23 100644
--- a/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx
+++ b/app/components/UI/AssetOverview/TokenDetails/TokenDetails.tsx
@@ -1,11 +1,11 @@
import { zeroAddress } from 'ethereumjs-util';
-import React, { useState } from 'react';
+import React from 'react';
import { View } from 'react-native';
import { useSelector } from 'react-redux';
import i18n from '../../../../../locales/i18n';
import { useStyles } from '../../../../component-library/hooks';
import styleSheet from './TokenDetails.styles';
-import { formatAddress, safeToChecksumAddress } from '../../../../util/address';
+import { safeToChecksumAddress } from '../../../../util/address';
import { selectTokenList } from '../../../../selectors/tokenListController';
import { selectContractExchangeRates } from '../../../../selectors/tokenRatesController';
import {
@@ -21,8 +21,8 @@ import Logger from '../../../../util/Logger';
import TokenDetailsList from './TokenDetailsList';
import MarketDetailsList from './MarketDetailsList';
import { TokenI } from '../../Tokens/types';
-import StakingEarnings from '../StakingEarnings';
import { isPooledStakingFeatureEnabled } from '../../Stake/constants';
+import StakingEarnings from '../../Stake/components/StakingEarnings';
export interface TokenDetails {
contractAddress: string | null;
@@ -52,9 +52,6 @@ const TokenDetails: React.FC = ({ asset }) => {
const currentCurrency = useSelector(selectCurrentCurrency);
const tokenContractAddress = safeToChecksumAddress(asset.address);
- // TEMP: Remove once component has been implemented.
- const [hasStakingPositions] = useState(true);
-
let tokenMetadata;
let marketData;
@@ -75,13 +72,12 @@ const TokenDetails: React.FC = ({ asset }) => {
const tokenDetails: TokenDetails = asset.isETH
? {
- contractAddress: formatAddress(zeroAddress(), 'short'),
+ contractAddress: zeroAddress(),
tokenDecimal: 18,
tokenList: '',
}
: {
- contractAddress:
- formatAddress(tokenContractAddress as string, 'short') || null,
+ contractAddress: tokenContractAddress || null,
tokenDecimal: tokenMetadata?.decimals || null,
tokenList: tokenMetadata?.aggregators.join(', ') || null,
};
@@ -127,9 +123,7 @@ const TokenDetails: React.FC = ({ asset }) => {
return (
- {asset.isETH &&
- hasStakingPositions &&
- isPooledStakingFeatureEnabled() && }
+ {asset.isETH && isPooledStakingFeatureEnabled() && }
{(asset.isETH || tokenMetadata) && (
)}
diff --git a/app/components/UI/AssetOverview/TokenDetails/TokenDetailsList/TokenDetailsList.tsx b/app/components/UI/AssetOverview/TokenDetails/TokenDetailsList/TokenDetailsList.tsx
index b218361c5ff..7697440bfbd 100644
--- a/app/components/UI/AssetOverview/TokenDetails/TokenDetailsList/TokenDetailsList.tsx
+++ b/app/components/UI/AssetOverview/TokenDetails/TokenDetailsList/TokenDetailsList.tsx
@@ -17,6 +17,7 @@ import Icon, {
import ClipboardManager from '../../../../../core/ClipboardManager';
import { TokenDetails } from '../TokenDetails';
import TokenDetailsListItem from '../TokenDetailsListItem';
+import { formatAddress } from '../../../../../util/address';
interface TokenDetailsListProps {
tokenDetails: TokenDetails;
@@ -62,7 +63,7 @@ const TokenDetailsList: React.FC = ({
onPress={copyAccountToClipboard}
>
- {tokenDetails.contractAddress}
+ {formatAddress(tokenDetails.contractAddress, 'short')}
{
+ it('should render correctly for network switch', () => {
+ const { toJSON } = renderWithProvider(
+ ,
+ { state: mockInitialState },
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
it('should render correctly', () => {
const { toJSON } = renderWithProvider(
{
onUserAction?.(USER_INTENT.Confirm);
+ onConfirm?.();
};
const cancel = () => {
onUserAction?.(USER_INTENT.Cancel);
+ onCancel?.();
};
const handleEditAccountsButtonPress = () => {
@@ -208,21 +227,33 @@ const PermissionsSummary = ({
{strings('permissions.use_enabled_networks')}
-
-
-
- {strings('permissions.requesting_for')}
-
-
- {networkName}
-
-
-
-
-
-
+ {isNetworkSwitch && (
+ <>
+
+
+
+ {strings('permissions.requesting_for')}
+
+
+ {chainName}
+
+
+
+
+ >
+ )}
+ {!isNetworkSwitch && (
+
+
+
+ )}
{!isNetworkSwitch && renderEndAccessory()}
@@ -247,6 +278,7 @@ const PermissionsSummary = ({
})}
+ {/*TODO These should be conditional upon which permissions are being requested*/}
{!isNetworkSwitch && renderAccountPermissionsRequestInfoCard()}
{renderNetworkPermissionsRequestInfoCard()}
diff --git a/app/components/UI/PermissionsSummary/PermissionsSummary.types.ts b/app/components/UI/PermissionsSummary/PermissionsSummary.types.ts
index c80d27e198a..1be07727a7d 100644
--- a/app/components/UI/PermissionsSummary/PermissionsSummary.types.ts
+++ b/app/components/UI/PermissionsSummary/PermissionsSummary.types.ts
@@ -9,10 +9,16 @@ export interface PermissionsSummaryProps {
onEdit?: () => void;
onEditNetworks?: () => void;
onBack?: () => void;
+ onCancel?: () => void;
+ onConfirm?: () => void;
onUserAction?: React.Dispatch>;
showActionButtons?: boolean;
isAlreadyConnected?: boolean;
isRenderedAsBottomSheet?: boolean;
isDisconnectAllShown?: boolean;
isNetworkSwitch?: boolean;
+ customNetworkInformation?: {
+ chainName: string;
+ chainId: string;
+ };
}
diff --git a/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap b/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap
index 07ce968d48c..b6d0fea2047 100644
--- a/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap
+++ b/app/components/UI/PermissionsSummary/__snapshots__/PermissionsSummary.test.tsx.snap
@@ -500,60 +500,6 @@ exports[`PermissionsSummary should render correctly 1`] = `
}
}
>
-
-
-
- Requesting for
-
-
- Ethereum Main Network
-
-
-
`;
+
+exports[`PermissionsSummary should render correctly for network switch 1`] = `
+
+
+
+
+
+
+
+
+
+ a
+
+
+
+
+
+
+
+
+ app.uniswap.org wants to:
+
+
+
+
+
+
+
+
+
+ Use your enabled networks
+
+
+
+
+
+ Requesting for
+
+
+ Sepolia
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Disconnect all
+
+
+
+
+
+
+ Cancel
+
+
+
+
+ Confirm
+
+
+
+
+
+
+`;
diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx
index a0c36a05079..3f8d3caa69d 100644
--- a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx
+++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx
@@ -67,7 +67,13 @@ describe('StakeConfirmationView', () => {
const props: StakeConfirmationViewProps = {
route: {
key: '1',
- params: { amountWei: '3210000000000000', amountFiat: '7.46' },
+ params: {
+ amountWei: '3210000000000000',
+ amountFiat: '7.46',
+ annualRewardRate: '2.5%',
+ annualRewardsETH: '2.5 ETH',
+ annualRewardsFiat: '$5000',
+ },
name: 'params',
},
};
diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx
index 857752feafb..48a7277df07 100644
--- a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx
+++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx
@@ -9,17 +9,9 @@ import AccountHeaderCard from '../../components/StakingConfirmation/AccountHeade
import RewardsCard from '../../components/StakingConfirmation/RewardsCard/RewardsCard';
import ConfirmationFooter from '../../components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter';
import { StakeConfirmationViewProps } from './StakeConfirmationView.types';
-import { MOCK_GET_VAULT_RESPONSE } from '../../components/StakingBalance/mockData';
import { strings } from '../../../../../../locales/i18n';
import { FooterButtonGroupActions } from '../../components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.types';
-const MOCK_REWARD_DATA = {
- REWARDS: {
- ETH: '0.13 ETH',
- FIAT: '$334.93',
- },
-};
-
const MOCK_STAKING_CONTRACT_NAME = 'MM Pooled Staking';
const StakeConfirmationView = ({ route }: StakeConfirmationViewProps) => {
@@ -47,9 +39,9 @@ const StakeConfirmationView = ({ route }: StakeConfirmationViewProps) => {
diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.types.ts b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.types.ts
index 8c723135f4f..20214a0fc52 100644
--- a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.types.ts
+++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.types.ts
@@ -3,6 +3,9 @@ import { RouteProp } from '@react-navigation/native';
interface StakeConfirmationViewRouteParams {
amountWei: string;
amountFiat: string;
+ annualRewardsETH: string;
+ annualRewardsFiat: string;
+ annualRewardRate: string;
}
export interface StakeConfirmationViewProps {
diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap b/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap
index 9d14c100f63..6d75c275c91 100644
--- a/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap
+++ b/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap
@@ -989,7 +989,7 @@ exports[`StakeConfirmationView render matches snapshot 1`] = `
}
testID="label"
>
- 2.8%
+ 2.5%
@@ -1100,7 +1100,7 @@ exports[`StakeConfirmationView render matches snapshot 1`] = `
}
}
>
- $334.93
+ $5000
- 0.13 ETH
+ 2.5 ETH
diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx
index e75b4a74d83..ebfe8c6be93 100644
--- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx
+++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.test.tsx
@@ -8,6 +8,7 @@ import { BN } from 'ethereumjs-util';
import { Stake } from '../../sdk/stakeSdkProvider';
import { ChainId, PooledStakingContract } from '@metamask/stake-sdk';
import { Contract } from 'ethers';
+import { MOCK_GET_VAULT_RESPONSE } from '../../__mocks__/mockData';
function render(Component: React.ComponentType) {
return renderScreen(
@@ -74,7 +75,8 @@ jest.mock('../../hooks/useStakeContext.ts', () => ({
useStakeContext: jest.fn(() => {
const stakeContext: Stake = {
setSdkType: jest.fn(),
- sdkService: mockPooledStakingContractService,
+ stakingContract: mockPooledStakingContractService,
+ stakingApiService: undefined,
};
return stakeContext;
}),
@@ -89,6 +91,31 @@ jest.mock('../../hooks/useBalance', () => ({
}),
}));
+const mockVaultData = MOCK_GET_VAULT_RESPONSE;
+// Mock hooks
+
+jest.mock('../../hooks/useStakingEligibility', () => ({
+ __esModule: true,
+ default: () => ({
+ isEligible: true,
+ loading: false,
+ error: null,
+ refreshPooledStakingEligibility: jest.fn(),
+ }),
+}));
+
+jest.mock('../../hooks/useVaultData', () => ({
+ __esModule: true,
+ default: () => ({
+ vaultData: mockVaultData,
+ loading: false,
+ error: null,
+ refreshVaultData: jest.fn(),
+ annualRewardRate: '2.5%',
+ annualRewardRateDecimal: 0.025,
+ }),
+}));
+
describe('StakeInputView', () => {
it('render matches snapshot', () => {
render(StakeInputView);
@@ -122,7 +149,7 @@ describe('StakeInputView', () => {
fireEvent.press(screen.getByText('2'));
- expect(screen.getByText('0.052 ETH')).toBeTruthy();
+ expect(screen.getByText('0.05 ETH')).toBeTruthy();
});
});
diff --git a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx
index 782e4d5ab3c..cf70050c558 100644
--- a/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx
+++ b/app/components/UI/Stake/Views/StakeInputView/StakeInputView.tsx
@@ -19,7 +19,6 @@ import styleSheet from './StakeInputView.styles';
import useStakingInputHandlers from '../../hooks/useStakingInput';
import useBalance from '../../hooks/useBalance';
import InputDisplay from '../../components/InputDisplay';
-import { useStakeContext } from '../../hooks/useStakeContext';
const StakeInputView = () => {
const title = strings('stake.stake_eth');
@@ -42,11 +41,12 @@ const StakeInputView = () => {
handleKeypadChange,
calculateEstimatedAnnualRewards,
estimatedAnnualRewards,
+ annualRewardsEth: annualRewardsETH,
+ annualRewardsFiat,
+ annualRewardRate,
+ isLoadingVaultData,
} = useStakingInputHandlers(balanceWei);
-
- const { sdkService } = useStakeContext();
-
const navigateToLearnMoreModal = () => {
navigation.navigate('StakeModals', {
screen: Routes.STAKING.MODALS.LEARN_MORE,
@@ -59,10 +59,19 @@ const StakeInputView = () => {
params: {
amountWei: amountWei.toString(),
amountFiat: fiatAmount,
+ annualRewardsETH,
+ annualRewardsFiat,
+ annualRewardRate,
},
});
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [amountWei, fiatAmount, navigation, sdkService]);
+ }, [
+ navigation,
+ amountWei,
+ fiatAmount,
+ annualRewardsETH,
+ annualRewardsFiat,
+ annualRewardRate,
+ ]);
const balanceText = strings('stake.balance');
@@ -106,6 +115,7 @@ const StakeInputView = () => {
- 2.6%
+ 2.5%
({
selectCurrentCurrency: jest.fn(() => 'USD'),
}));
+const mockVaultData = MOCK_GET_VAULT_RESPONSE;
+const mockPooledStakeData = MOCK_GET_POOLED_STAKES_API_RESPONSE.accounts[0];
+
+jest.mock('../../hooks/useStakingEligibility', () => ({
+ __esModule: true,
+ default: () => ({
+ isEligible: true,
+ loading: false,
+ error: null,
+ refreshPooledStakingEligibility: jest.fn(),
+ }),
+}));
+
+jest.mock('../../hooks/useVaultData', () => ({
+ __esModule: true,
+ default: () => ({
+ vaultData: mockVaultData,
+ loading: false,
+ error: null,
+ annualRewardRate: '2.5%',
+ annualRewardRateDecimal: 0.025,
+ }),
+}));
+
+jest.mock('../../hooks/useBalance', () => ({
+ __esModule: true,
+ default: () => ({
+ stakedBalanceWei: mockPooledStakeData.assets,
+ stakedBalanceFiat: MOCK_STAKED_ETH_ASSET.balanceFiat,
+ }),
+}));
+
describe('UnstakeInputView', () => {
it('render matches snapshot', () => {
render(UnstakeInputView);
@@ -81,7 +118,7 @@ describe('UnstakeInputView', () => {
fireEvent.press(screen.getByText('25%'));
- expect(screen.getByText('1.14999')).toBeTruthy();
+ expect(screen.getByText('1.44783')).toBeTruthy();
});
});
@@ -96,13 +133,14 @@ describe('UnstakeInputView', () => {
render(UnstakeInputView);
fireEvent.press(screen.getByText('1'));
+
expect(screen.getByText('Review')).toBeTruthy();
});
it('displays `Not enough ETH` when input exceeds balance', () => {
render(UnstakeInputView);
- fireEvent.press(screen.getByText('6'));
+ fireEvent.press(screen.getByText('8'));
expect(screen.queryAllByText('Not enough ETH')).toHaveLength(2);
});
});
diff --git a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx
index c109940d3a5..8adb580eade 100644
--- a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx
+++ b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.tsx
@@ -19,13 +19,14 @@ import { View } from 'react-native';
import useStakingInputHandlers from '../../hooks/useStakingInput';
import styleSheet from './UnstakeInputView.styles';
import InputDisplay from '../../components/InputDisplay';
+import useBalance from '../../hooks/useBalance';
const UnstakeInputView = () => {
const title = strings('stake.unstake_eth');
const navigation = useNavigation();
const { styles, theme } = useStyles(styleSheet, {});
- const stakeBalance = '4599964000000000000'; //TODO: Replace with actual balance - STAKE-806
+ const { stakedBalanceWei } = useBalance();
const {
isEth,
@@ -40,10 +41,13 @@ const UnstakeInputView = () => {
handleAmountPress,
handleKeypadChange,
conversionRate,
- } = useStakingInputHandlers(new BN(stakeBalance));
+ } = useStakingInputHandlers(new BN(stakedBalanceWei));
- const stakeBalanceInEth = renderFromWei(stakeBalance, 5);
- const stakeBalanceFiatNumber = weiToFiatNumber(stakeBalance, conversionRate);
+ const stakeBalanceInEth = renderFromWei(stakedBalanceWei, 5);
+ const stakeBalanceFiatNumber = weiToFiatNumber(
+ stakedBalanceWei,
+ conversionRate,
+ );
const stakedBalanceText = strings('stake.staked_balance');
const stakedBalanceValue = isEth
diff --git a/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.types.ts b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.types.ts
new file mode 100644
index 00000000000..e58d20f58af
--- /dev/null
+++ b/app/components/UI/Stake/Views/UnstakeInputView/UnstakeInputView.types.ts
@@ -0,0 +1,9 @@
+import { RouteProp } from '@react-navigation/native';
+
+interface UnstakeInputViewRouteParams {
+ stakedBalanceWei: string;
+}
+
+export interface UnstakeInputViewProps {
+ route: RouteProp<{ params: UnstakeInputViewRouteParams }, 'params'>;
+}
diff --git a/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap b/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap
index 5e7927b0b5c..0e1816f6621 100644
--- a/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap
+++ b/app/components/UI/Stake/Views/UnstakeInputView/__snapshots__/UnstakeInputView.test.tsx.snap
@@ -386,7 +386,7 @@ exports[`UnstakeInputView render matches snapshot 1`] = `
>
Staked balance
:
- 4.59996 ETH
+ 5.79133 ETH
StyleSheet.create({
@@ -42,12 +43,14 @@ const createStyles = (colors: Colors) =>
interface EstimatedAnnualRewardsCardProps {
estimatedAnnualRewards: string;
+ isLoading?: boolean;
onIconPress: () => void;
}
const EstimatedAnnualRewardsCard = ({
estimatedAnnualRewards,
onIconPress,
+ isLoading = false,
}: EstimatedAnnualRewardsCardProps) => {
const { colors } = useTheme();
const styles = createStyles(colors);
@@ -67,9 +70,19 @@ const EstimatedAnnualRewardsCard = ({
-
- {estimatedAnnualRewards}
-
+ {isLoading ? (
+
+
+
+ ) : (
+
+ {estimatedAnnualRewards}
+
+ )}
{
+const StakeButtonContent = ({ asset }: StakeButtonProps) => {
const { colors } = useTheme();
const styles = createStyles(colors);
const navigation = useNavigation();
@@ -40,8 +38,10 @@ export const StakeButton = ({ asset }: StakeButtonProps) => {
const browserTabs = useSelector((state: RootState) => state.browser.tabs);
const chainId = useSelector(selectChainId);
+ const { isEligible } = useStakingEligibility();
+
const onStakeButtonPress = () => {
- if (isPooledStakingFeatureEnabled()) {
+ if (isPooledStakingFeatureEnabled() && isEligible) {
navigation.navigate('StakeScreens', { screen: Routes.STAKING.STAKE });
} else {
const existingStakeTab = browserTabs.find((tab: BrowserTab) =>
@@ -93,3 +93,11 @@ export const StakeButton = ({ asset }: StakeButtonProps) => {
);
};
+
+export const StakeButton = (props: StakeButtonProps) => (
+
+
+
+);
+
+export default StakeButton;
diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx
index 2aae28643bf..4f363fd1d43 100644
--- a/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx
+++ b/app/components/UI/Stake/components/StakingBalance/StakingBalance.test.tsx
@@ -5,6 +5,11 @@ import StakingBalance from './StakingBalance';
import { strings } from '../../../../../../locales/i18n';
import Routes from '../../../../../constants/navigation/Routes';
import { Image } from 'react-native';
+import {
+ MOCK_GET_POOLED_STAKES_API_RESPONSE,
+ MOCK_GET_VAULT_RESPONSE,
+ MOCK_STAKED_ETH_ASSET,
+} from '../../__mocks__/mockData';
jest.mock('../../../../hooks/useIpfsGateway', () => jest.fn());
@@ -24,20 +29,76 @@ jest.mock('@react-navigation/native', () => {
};
});
+const mockPooledStakeData = MOCK_GET_POOLED_STAKES_API_RESPONSE.accounts[0];
+const mockExchangeRate = MOCK_GET_POOLED_STAKES_API_RESPONSE.exchangeRate;
+
+const mockVaultData = MOCK_GET_VAULT_RESPONSE;
+// Mock hooks
+jest.mock('../../hooks/usePooledStakes', () => ({
+ __esModule: true,
+ default: () => ({
+ pooledStakesData: mockPooledStakeData,
+ exchangeRate: mockExchangeRate,
+ loading: false,
+ error: null,
+ refreshPooledStakes: jest.fn(),
+ hasStakedPositions: true,
+ hasEthToUnstake: true,
+ hasNeverStaked: false,
+ hasRewards: true,
+ hasRewardsOnly: false,
+ }),
+}));
+
+jest.mock('../../hooks/useStakingEligibility', () => ({
+ __esModule: true,
+ default: () => ({
+ isEligible: true,
+ loading: false,
+ error: null,
+ refreshPooledStakingEligibility: jest.fn(),
+ }),
+}));
+
+jest.mock('../../hooks/useVaultData', () => ({
+ __esModule: true,
+ default: () => ({
+ vaultData: mockVaultData,
+ loading: false,
+ error: null,
+ annualRewardRate: '2.5%',
+ annualRewardRateDecimal: 0.025,
+ }),
+}));
+
+jest.mock('../../hooks/useBalance', () => ({
+ __esModule: true,
+ default: () => ({
+ stakedBalanceWei: MOCK_STAKED_ETH_ASSET.balance,
+ stakedBalanceFiat: MOCK_STAKED_ETH_ASSET.balanceFiat,
+ }),
+}));
+
afterEach(() => {
jest.clearAllMocks();
});
describe('StakingBalance', () => {
- beforeEach(() => jest.resetAllMocks());
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
it('render matches snapshot', () => {
- const { toJSON } = renderWithProvider();
+ const { toJSON } = renderWithProvider(
+ ,
+ );
expect(toJSON()).toMatchSnapshot();
});
it('redirects to StakeInputView on stake button click', () => {
- const { getByText } = renderWithProvider();
+ const { getByText } = renderWithProvider(
+ ,
+ );
fireEvent.press(getByText(strings('stake.stake_more')));
@@ -48,7 +109,9 @@ describe('StakingBalance', () => {
});
it('redirects to UnstakeInputView on unstake button click', () => {
- const { getByText } = renderWithProvider();
+ const { getByText } = renderWithProvider(
+ ,
+ );
fireEvent.press(getByText(strings('stake.unstake')));
diff --git a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx
index 8884688694b..e70630ab901 100644
--- a/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx
+++ b/app/components/UI/Stake/components/StakingBalance/StakingBalance.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo, useState } from 'react';
+import React, { useMemo } from 'react';
import Badge, {
BadgeVariant,
} from '../../../../../component-library/components/Badges/Badge';
@@ -11,7 +11,6 @@ import AssetElement from '../../../AssetElement';
import NetworkMainAssetLogo from '../../../NetworkMainAssetLogo';
import { selectNetworkName } from '../../../../../selectors/networkInfos';
import { useSelector } from 'react-redux';
-import images from '../../../../../images/image-icons';
import styleSheet from './StakingBalance.styles';
import { View } from 'react-native';
import StakingButtons from './StakingButtons/StakingButtons';
@@ -35,28 +34,48 @@ import {
} from '../../utils/value';
import { multiplyValueByPowerOfTen } from '../../utils/bignumber';
import StakingCta from './StakingCta/StakingCta';
-import {
- MOCK_GET_POOLED_STAKES_API_RESPONSE,
- MOCK_GET_VAULT_RESPONSE,
- MOCK_STAKED_ETH_ASSET,
-} from './mockData';
+import useStakingEligibility from '../../hooks/useStakingEligibility';
+import useStakingChain from '../../hooks/useStakingChain';
+import usePooledStakes from '../../hooks/usePooledStakes';
+import useVaultData from '../../hooks/useVaultData';
+import { StakeSDKProvider } from '../../sdk/stakeSdkProvider';
+import type { TokenI } from '../../../Tokens/types';
+import useBalance from '../../hooks/useBalance';
+import { NetworkBadgeSource } from '../../../AssetOverview/Balance/Balance';
+import { selectChainId } from '../../../../../selectors/networkController';
-const StakingBalance = () => {
- const { styles } = useStyles(styleSheet, {});
+export interface StakingBalanceProps {
+ asset: TokenI;
+}
+const StakingBalanceContent = ({ asset }: StakingBalanceProps) => {
+ const { styles } = useStyles(styleSheet, {});
+ const chainId = useSelector(selectChainId);
const networkName = useSelector(selectNetworkName);
- const [isGeoBlocked] = useState(false);
- const [hasStakedPositions] = useState(false);
+ const { isEligible: isEligibleForPooledStaking } = useStakingEligibility();
- const { unstakingRequests, claimableRequests } = useMemo(
- () =>
- filterExitRequests(
- MOCK_GET_POOLED_STAKES_API_RESPONSE.accounts[0].exitRequests,
- MOCK_GET_POOLED_STAKES_API_RESPONSE.exchangeRate,
- ),
- [],
- );
+ const { isStakingSupportedChain } = useStakingChain();
+
+ const {
+ pooledStakesData,
+ exchangeRate,
+ hasStakedPositions,
+ hasEthToUnstake,
+ isLoadingPooledStakesData,
+ } = usePooledStakes();
+ const { vaultData } = useVaultData();
+ const annualRewardRate = vaultData?.apy || '';
+
+ const {
+ formattedStakedBalanceETH: stakedBalanceETH,
+ formattedStakedBalanceFiat: stakedBalanceFiat,
+ } = useBalance();
+
+ const { unstakingRequests, claimableRequests } = useMemo(() => {
+ const exitRequests = pooledStakesData?.exitRequests ?? [];
+ return filterExitRequests(exitRequests, exchangeRate);
+ }, [pooledStakesData, exchangeRate]);
const claimableEth = useMemo(
() =>
@@ -72,20 +91,24 @@ const StakingBalance = () => {
const hasClaimableEth = !!Number(claimableEth);
+ if (!isStakingSupportedChain || isLoadingPooledStakesData) {
+ return <>>;
+ }
+
return (
- {Boolean(MOCK_STAKED_ETH_ASSET.balance) && !isGeoBlocked && (
+ {hasStakedPositions && isEligibleForPooledStaking && (
}
@@ -93,13 +116,13 @@ const StakingBalance = () => {
- {MOCK_STAKED_ETH_ASSET.name || MOCK_STAKED_ETH_ASSET.symbol}
+ {strings('stake.staked_ethereum')}
)}
- {isGeoBlocked ? (
+ {!isEligibleForPooledStaking ? (
{
{!hasStakedPositions && (
)}
-
+
>
)}
@@ -156,4 +180,10 @@ const StakingBalance = () => {
);
};
+export const StakingBalance = ({ asset }: StakingBalanceProps) => (
+
+
+
+);
+
export default StakingBalance;
diff --git a/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx b/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx
index 51ba3ea15f9..2cec44d4baf 100644
--- a/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx
+++ b/app/components/UI/Stake/components/StakingBalance/StakingButtons/StakingButtons.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React from 'react';
import Button, {
ButtonVariants,
} from '../../../../../../component-library/components/Buttons/Button';
@@ -9,16 +9,23 @@ import styleSheet from './StakingButtons.styles';
import { useNavigation } from '@react-navigation/native';
import Routes from '../../../../../../constants/navigation/Routes';
-interface StakingButtonsProps extends Pick {}
+interface StakingButtonsProps extends Pick {
+ hasStakedPositions: boolean;
+ hasEthToUnstake: boolean;
+}
-const StakingButtons = ({ style }: StakingButtonsProps) => {
- const [hasStakedPosition] = useState(true);
- const [hasEthToUnstake] = useState(true);
+const StakingButtons = ({
+ style,
+ hasStakedPositions,
+ hasEthToUnstake,
+}: StakingButtonsProps) => {
const { navigate } = useNavigation();
const { styles } = useStyles(styleSheet, {});
const onUnstakePress = () =>
- navigate('StakeScreens', { screen: Routes.STAKING.UNSTAKE });
+ navigate('StakeScreens', {
+ screen: Routes.STAKING.UNSTAKE,
+ });
const onStakePress = () =>
navigate('StakeScreens', { screen: Routes.STAKING.STAKE });
@@ -37,7 +44,7 @@ const StakingButtons = ({ style }: StakingButtonsProps) => {
style={styles.balanceActionButton}
variant={ButtonVariants.Secondary}
label={
- hasStakedPosition
+ hasStakedPositions
? strings('stake.stake_more')
: strings('stake.stake')
}
diff --git a/app/components/UI/Stake/components/StakingBalance/__snapshots__/StakingBalance.test.tsx.snap b/app/components/UI/Stake/components/StakingBalance/__snapshots__/StakingBalance.test.tsx.snap
index ee2fbde051b..c161fdb50d6 100644
--- a/app/components/UI/Stake/components/StakingBalance/__snapshots__/StakingBalance.test.tsx.snap
+++ b/app/components/UI/Stake/components/StakingBalance/__snapshots__/StakingBalance.test.tsx.snap
@@ -200,40 +200,7 @@ exports[`StakingBalance render matches snapshot 1`] = `
"flex": 1,
}
}
- >
-
- $13,292.20
-
-
- 4.9999 ETH
-
-
+ />
-
-
- Stake ETH and earn
-
-
-
- Stake your ETH with MetaMask Pool and earn
-
-
- 2.9%
-
-
- annually.
-
-
-
- Learn more.
-
-
-
-
{
it('render matches snapshot', () => {
const props: RewardsCardProps = {
- rewardRate: '2.6',
+ rewardRate: '2.6%',
rewardsEth: '0.13 ETH',
rewardsFiat: '$334.93',
};
@@ -33,7 +33,7 @@ describe('RewardsCard', () => {
,
);
- expect(getByText(`${props.rewardRate}%`)).toBeDefined();
+ expect(getByText(props.rewardRate)).toBeDefined();
expect(getByText(props.rewardsEth)).toBeDefined();
expect(getByText(props.rewardsFiat)).toBeDefined();
@@ -42,7 +42,7 @@ describe('RewardsCard', () => {
it('reward rate tooltip displayed when pressed', () => {
const props: RewardsCardProps = {
- rewardRate: '2.6',
+ rewardRate: '2.6%',
rewardsEth: '0.13 ETH',
rewardsFiat: '$334.93',
};
@@ -69,7 +69,7 @@ describe('RewardsCard', () => {
it('reward frequency tooltip displayed when pressed', () => {
const props: RewardsCardProps = {
- rewardRate: '2.6',
+ rewardRate: '2.6%',
rewardsEth: '0.13 ETH',
rewardsFiat: '$334.93',
};
diff --git a/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.tsx b/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.tsx
index 66b6eacedf5..8068cacd854 100644
--- a/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.tsx
+++ b/app/components/UI/Stake/components/StakingConfirmation/RewardsCard/RewardsCard.tsx
@@ -12,7 +12,6 @@ import { useStyles } from '../../../../../hooks/useStyles';
import Card from '../../../../../../component-library/components/Cards/Card';
import styleSheet from './RewardsCard.styles';
import { RewardsCardProps } from './RewardsCard.types';
-import { fixDisplayAmount } from '../../../utils/value';
const RewardsCard = ({
rewardRate,
@@ -34,7 +33,7 @@ const RewardsCard = ({
}}
value={{
label: {
- text: `${fixDisplayAmount(rewardRate, 1)}%`,
+ text: rewardRate,
color: TextColor.Success,
variant: TextVariant.BodyMD,
},
diff --git a/app/components/UI/AssetOverview/StakingEarnings/StakingEarnings.styles.tsx b/app/components/UI/Stake/components/StakingEarnings/StakingEarnings.styles.tsx
similarity index 92%
rename from app/components/UI/AssetOverview/StakingEarnings/StakingEarnings.styles.tsx
rename to app/components/UI/Stake/components/StakingEarnings/StakingEarnings.styles.tsx
index e5961497522..8d80278c2c8 100644
--- a/app/components/UI/AssetOverview/StakingEarnings/StakingEarnings.styles.tsx
+++ b/app/components/UI/Stake/components/StakingEarnings/StakingEarnings.styles.tsx
@@ -1,5 +1,5 @@
-import { Theme } from '../../../../util/theme/models';
import { StyleSheet, TextStyle } from 'react-native';
+import type { Theme } from '../../../../../util/theme/models';
const styleSheet = (params: { theme: Theme }) => {
const { theme } = params;
diff --git a/app/components/UI/AssetOverview/StakingEarnings/StakingEarnings.test.tsx b/app/components/UI/Stake/components/StakingEarnings/StakingEarnings.test.tsx
similarity index 51%
rename from app/components/UI/AssetOverview/StakingEarnings/StakingEarnings.test.tsx
rename to app/components/UI/Stake/components/StakingEarnings/StakingEarnings.test.tsx
index f9a8c51b5a5..4c8bfc25942 100644
--- a/app/components/UI/AssetOverview/StakingEarnings/StakingEarnings.test.tsx
+++ b/app/components/UI/Stake/components/StakingEarnings/StakingEarnings.test.tsx
@@ -1,9 +1,9 @@
import React from 'react';
import StakingEarnings from './';
-import renderWithProvider from '../../../../util/test/renderWithProvider';
-import { strings } from '../../../../../locales/i18n';
+import renderWithProvider from '../../../../../util/test/renderWithProvider';
+import { strings } from '../../../../../../locales/i18n';
-jest.mock('../../Stake/constants', () => ({
+jest.mock('../../constants', () => ({
isPooledStakingFeatureEnabled: jest.fn().mockReturnValue(true),
}));
@@ -19,6 +19,35 @@ jest.mock('@react-navigation/native', () => {
};
});
+jest.mock('../../hooks/useStakingEligibility', () => ({
+ __esModule: true,
+ default: () => ({
+ isEligible: true,
+ loading: false,
+ error: null,
+ refreshPooledStakingEligibility: jest.fn(),
+ }),
+}));
+
+jest.mock('../../hooks/useStakingEarnings', () => ({
+ __esModule: true,
+ default: () => ({
+ annualRewardRate: '2.6%',
+ lifetimeRewardsETH: '2.5 ETH',
+ lifetimeRewardsFiat: '$5000',
+ estimatedAnnualEarningsETH: '2.5 ETH',
+ estimatedAnnualEarningsFiat: '$5000',
+ isLoadingEarningsData: false,
+ }),
+}));
+
+jest.mock('../../hooks/usePooledStakes', () => ({
+ __esModule: true,
+ default: () => ({
+ hasStakedPositions: true,
+ }),
+}));
+
describe('Staking Earnings', () => {
it('should render correctly', () => {
const { toJSON, getByText } = renderWithProvider();
diff --git a/app/components/UI/AssetOverview/StakingEarnings/__snapshots__/StakingEarnings.test.tsx.snap b/app/components/UI/Stake/components/StakingEarnings/__snapshots__/StakingEarnings.test.tsx.snap
similarity index 98%
rename from app/components/UI/AssetOverview/StakingEarnings/__snapshots__/StakingEarnings.test.tsx.snap
rename to app/components/UI/Stake/components/StakingEarnings/__snapshots__/StakingEarnings.test.tsx.snap
index 3f9bd60d677..17f64f7622b 100644
--- a/app/components/UI/AssetOverview/StakingEarnings/__snapshots__/StakingEarnings.test.tsx.snap
+++ b/app/components/UI/Stake/components/StakingEarnings/__snapshots__/StakingEarnings.test.tsx.snap
@@ -159,7 +159,7 @@ exports[`Staking Earnings should render correctly 1`] = `
}
}
>
- $2
+ $5000
- 0.02151 ETH
+ 2.5 ETH
@@ -231,7 +231,7 @@ exports[`Staking Earnings should render correctly 1`] = `
}
}
>
- $15.93
+ $5000
- 0.0131 ETH
+ 2.5 ETH
diff --git a/app/components/UI/AssetOverview/StakingEarnings/index.tsx b/app/components/UI/Stake/components/StakingEarnings/index.tsx
similarity index 59%
rename from app/components/UI/AssetOverview/StakingEarnings/index.tsx
rename to app/components/UI/Stake/components/StakingEarnings/index.tsx
index d9538aaa50a..4ad6b7b2aaf 100644
--- a/app/components/UI/AssetOverview/StakingEarnings/index.tsx
+++ b/app/components/UI/Stake/components/StakingEarnings/index.tsx
@@ -3,49 +3,60 @@ import { View } from 'react-native';
import Text, {
TextColor,
TextVariant,
-} from '../../../../component-library/components/Texts/Text';
-import { useStyles } from '../../../../component-library/hooks';
+} from '../../../../../component-library/components/Texts/Text';
+import { useStyles } from '../../../../../component-library/hooks';
import styleSheet from './StakingEarnings.styles';
import {
IconColor,
IconName,
-} from '../../../../component-library/components/Icons/Icon';
+} from '../../../../../component-library/components/Icons/Icon';
import ButtonIcon, {
ButtonIconSizes,
-} from '../../../../component-library/components/Buttons/ButtonIcon';
-import useTooltipModal from '../../../../components/hooks/useTooltipModal';
-import { strings } from '../../../../../locales/i18n';
-import { isPooledStakingFeatureEnabled } from '../../Stake/constants';
-
-// TODO: Remove mock data when connecting component to backend.
-const MOCK_DATA = {
- ANNUAL_EARNING_RATE: '2.6%',
- LIFETIME_REWARDS: {
- FIAT: '$2',
- ETH: '0.02151 ETH',
- },
- EST_ANNUAL_EARNINGS: {
- FIAT: '$15.93',
- ETH: '0.0131 ETH',
- },
-};
-
-const StakingEarnings = () => {
- // TODO: Remove mock data when connecting component to backend.
- const { ANNUAL_EARNING_RATE, LIFETIME_REWARDS, EST_ANNUAL_EARNINGS } =
- MOCK_DATA;
+} from '../../../../../component-library/components/Buttons/ButtonIcon';
+import useTooltipModal from '../../../../../components/hooks/useTooltipModal';
+import { strings } from '../../../../../../locales/i18n';
+import { isPooledStakingFeatureEnabled } from '../../../Stake/constants';
+import useStakingEligibility from '../../hooks/useStakingEligibility';
+import useStakingChain from '../../hooks/useStakingChain';
+import { StakeSDKProvider } from '../../sdk/stakeSdkProvider';
+import useStakingEarnings from '../../hooks/useStakingEarnings';
+import usePooledStakes from '../../hooks/usePooledStakes';
+const StakingEarningsContent = () => {
const { styles } = useStyles(styleSheet, {});
const { openTooltipModal } = useTooltipModal();
+ const { hasStakedPositions } = usePooledStakes();
+
+ const {
+ annualRewardRate,
+ lifetimeRewardsETH,
+ lifetimeRewardsFiat,
+ estimatedAnnualEarningsETH,
+ estimatedAnnualEarningsFiat,
+ isLoadingEarningsData,
+ } = useStakingEarnings();
+
const onNavigateToTooltipModal = () =>
openTooltipModal(
strings('stake.annual_rate'),
strings('tooltip_modal.reward_rate.tooltip'),
);
- if (!isPooledStakingFeatureEnabled()) return <>>;
+ const { isEligible, isLoadingEligibility } = useStakingEligibility();
+
+ const { isStakingSupportedChain } = useStakingChain();
+
+ const isLoadingData = isLoadingEligibility || isLoadingEarningsData;
+ if (
+ !isPooledStakingFeatureEnabled() ||
+ !isEligible ||
+ !isStakingSupportedChain ||
+ !hasStakedPositions ||
+ isLoadingData
+ )
+ return <>>;
return (
@@ -74,7 +85,7 @@ const StakingEarnings = () => {
/>
- {ANNUAL_EARNING_RATE}
+ {annualRewardRate}
@@ -87,12 +98,12 @@ const StakingEarnings = () => {
- {LIFETIME_REWARDS.FIAT}
+ {lifetimeRewardsFiat}
- {LIFETIME_REWARDS.ETH}
+ {lifetimeRewardsETH}
@@ -106,12 +117,14 @@ const StakingEarnings = () => {
- {EST_ANNUAL_EARNINGS.FIAT}
+
+ {estimatedAnnualEarningsFiat}
+
- {EST_ANNUAL_EARNINGS.ETH}
+ {estimatedAnnualEarningsETH}
@@ -120,4 +133,10 @@ const StakingEarnings = () => {
);
};
+export const StakingEarnings = () => (
+
+
+
+);
+
export default StakingEarnings;
diff --git a/app/components/UI/Stake/hooks/useBalance.ts b/app/components/UI/Stake/hooks/useBalance.ts
index 2ce1dfc6af4..a1af62ab536 100644
--- a/app/components/UI/Stake/hooks/useBalance.ts
+++ b/app/components/UI/Stake/hooks/useBalance.ts
@@ -9,11 +9,13 @@ import {
import { selectChainId } from '../../../../selectors/networkController';
import {
hexToBN,
+ renderFiat,
renderFromWei,
toHexadecimal,
weiToFiat,
weiToFiatNumber,
} from '../../../../util/number';
+import usePooledStakes from './usePooledStakes';
const useBalance = () => {
const accountsByChainId = useSelector(selectAccountsByChainId);
@@ -48,7 +50,36 @@ const useBalance = () => {
[balanceWei, conversionRate],
);
- return { balance, balanceFiat, balanceWei, balanceFiatNumber, conversionRate, currentCurrency };
+ const { pooledStakesData } = usePooledStakes();
+ const assets = pooledStakesData.assets ?? 0;
+
+ const formattedStakedBalanceETH = useMemo(
+ () => `${renderFromWei(assets)} ETH`,
+ [assets],
+ );
+
+ const stakedBalanceFiatNumber = useMemo(
+ () => weiToFiatNumber(assets, conversionRate),
+ [assets, conversionRate],
+ );
+
+ const formattedStakedBalanceFiat = useMemo(
+ () => renderFiat(stakedBalanceFiatNumber, currentCurrency, 2),
+ [currentCurrency, stakedBalanceFiatNumber],
+ );
+
+ return {
+ balance,
+ balanceFiat,
+ balanceWei,
+ balanceFiatNumber,
+ stakedBalanceWei: assets,
+ formattedStakedBalanceETH,
+ stakedBalanceFiatNumber,
+ formattedStakedBalanceFiat,
+ conversionRate,
+ currentCurrency,
+ };
};
export default useBalance;
diff --git a/app/components/UI/Stake/hooks/usePooledStakes.ts b/app/components/UI/Stake/hooks/usePooledStakes.ts
new file mode 100644
index 00000000000..e6a0fb36217
--- /dev/null
+++ b/app/components/UI/Stake/hooks/usePooledStakes.ts
@@ -0,0 +1,120 @@
+import { useSelector } from 'react-redux';
+import { useState, useEffect, useMemo } from 'react';
+import { selectSelectedInternalAccountChecksummedAddress } from '../../../../selectors/accountsController';
+import { selectChainId } from '../../../../selectors/networkController';
+import { hexToNumber } from '@metamask/utils';
+import { PooledStake } from '@metamask/stake-sdk';
+import { useStakeContext } from './useStakeContext';
+
+export enum StakeAccountStatus {
+ // These statuses are only used internally rather than displayed to a user
+ ACTIVE = 'ACTIVE', // non-zero staked shares
+ NEVER_STAKED = 'NEVER_STAKED',
+ INACTIVE_WITH_EXIT_REQUESTS = 'INACTIVE_WITH_EXIT_REQUESTS', // zero staked shares, unstaking or claimable exit requests
+ INACTIVE_WITH_REWARDS_ONLY = 'INACTIVE_WITH_REWARDS_ONLY', // zero staked shares, no exit requests, previous lifetime rewards
+}
+
+const usePooledStakes = () => {
+ const chainId = useSelector(selectChainId);
+ const selectedAddress =
+ useSelector(selectSelectedInternalAccountChecksummedAddress) || '';
+ const { stakingApiService } = useStakeContext(); // Get the stakingApiService directly from context
+ const [pooledStakesData, setPooledStakesData] = useState({} as PooledStake);
+ const [exchangeRate, setExchangeRate] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [refreshKey, setRefreshKey] = useState(0);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+
+ if (!stakingApiService) {
+ throw new Error('Staking API service is unavailable');
+ }
+
+ const addresses = selectedAddress ? [selectedAddress] : [];
+ const numericChainId = hexToNumber(chainId);
+
+ // Directly calling the stakingApiService
+ const { accounts = [], exchangeRate: fetchedExchangeRate } =
+ await stakingApiService.getPooledStakes(
+ addresses,
+ numericChainId,
+ true,
+ );
+
+ setPooledStakesData(accounts[0] || null);
+ setExchangeRate(fetchedExchangeRate);
+ } catch (err) {
+ setError('Failed to fetch pooled stakes');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [chainId, selectedAddress, stakingApiService, refreshKey]);
+
+ const refreshPooledStakes = () => {
+ setRefreshKey((prevKey) => prevKey + 1); // Increment `refreshKey` to trigger refetch
+ };
+
+ const getStatus = (stake: PooledStake) => {
+ if (stake.assets === '0' && stake.exitRequests.length > 0) {
+ return StakeAccountStatus.INACTIVE_WITH_EXIT_REQUESTS;
+ } else if (stake.assets === '0' && stake.lifetimeRewards !== '0') {
+ return StakeAccountStatus.INACTIVE_WITH_REWARDS_ONLY;
+ } else if (stake.assets === '0') {
+ return StakeAccountStatus.NEVER_STAKED;
+ }
+ return StakeAccountStatus.ACTIVE;
+ };
+
+ const status = useMemo(() => getStatus(pooledStakesData), [pooledStakesData]);
+
+ const hasStakedPositions = useMemo(
+ () =>
+ status === StakeAccountStatus.ACTIVE ||
+ status === StakeAccountStatus.INACTIVE_WITH_EXIT_REQUESTS,
+ [status],
+ );
+
+ const hasRewards = useMemo(
+ () =>
+ status === StakeAccountStatus.INACTIVE_WITH_REWARDS_ONLY ||
+ status === StakeAccountStatus.ACTIVE,
+ [status],
+ );
+
+ const hasRewardsOnly = useMemo(
+ () => status === StakeAccountStatus.INACTIVE_WITH_REWARDS_ONLY,
+ [status],
+ );
+
+ const hasNeverStaked = useMemo(
+ () => status === StakeAccountStatus.NEVER_STAKED,
+ [status],
+ );
+
+ const hasEthToUnstake = useMemo(
+ () => status === StakeAccountStatus.ACTIVE,
+ [status],
+ );
+
+ return {
+ pooledStakesData,
+ exchangeRate,
+ isLoadingPooledStakesData: loading,
+ error,
+ refreshPooledStakes,
+ hasStakedPositions,
+ hasEthToUnstake,
+ hasNeverStaked,
+ hasRewards,
+ hasRewardsOnly,
+ };
+};
+
+export default usePooledStakes;
diff --git a/app/components/UI/Stake/hooks/useStakeContext.ts b/app/components/UI/Stake/hooks/useStakeContext.ts
index 0fc280593da..2e9b915c65e 100644
--- a/app/components/UI/Stake/hooks/useStakeContext.ts
+++ b/app/components/UI/Stake/hooks/useStakeContext.ts
@@ -1,7 +1,10 @@
import { useContext } from 'react';
-import { Stake, StakeContext } from '../sdk/stakeSdkProvider';
+import { StakeContext } from '../sdk/stakeSdkProvider';
export const useStakeContext = () => {
- const context = useContext(StakeContext);
- return context as Stake;
+ const context = useContext(StakeContext);
+ if (!context) {
+ throw new Error('useStakeContext must be used within a StakeProvider');
+ }
+ return context;
};
diff --git a/app/components/UI/Stake/hooks/useStakingChain.ts b/app/components/UI/Stake/hooks/useStakingChain.ts
new file mode 100644
index 00000000000..d5da33c504e
--- /dev/null
+++ b/app/components/UI/Stake/hooks/useStakingChain.ts
@@ -0,0 +1,16 @@
+import { useSelector } from 'react-redux';
+import { getDecimalChainId } from '../../../../util/networks';
+import { selectChainId } from '../../../../selectors/networkController';
+import { isSupportedChain } from '@metamask/stake-sdk';
+
+const useStakingChain = () => {
+ const chainId = useSelector(selectChainId);
+
+ const isStakingSupportedChain = isSupportedChain(getDecimalChainId(chainId));
+
+ return {
+ isStakingSupportedChain,
+ };
+};
+
+export default useStakingChain;
diff --git a/app/components/UI/Stake/hooks/useStakingEarnings.ts b/app/components/UI/Stake/hooks/useStakingEarnings.ts
new file mode 100644
index 00000000000..cf5ccdabf59
--- /dev/null
+++ b/app/components/UI/Stake/hooks/useStakingEarnings.ts
@@ -0,0 +1,56 @@
+import {
+ renderFiat,
+ renderFromWei,
+ weiToFiatNumber,
+} from '../../../../util/number';
+import usePooledStakes from './usePooledStakes';
+import useVaultData from './useVaultData';
+import useBalance from './useBalance';
+import BigNumber from 'bignumber.js';
+
+const useStakingEarnings = () => {
+ const { annualRewardRate, annualRewardRateDecimal, isLoadingVaultData } =
+ useVaultData();
+
+ const { currentCurrency, conversionRate } = useBalance();
+
+ const { pooledStakesData, isLoadingPooledStakesData } = usePooledStakes();
+
+ const lifetimeRewards = pooledStakesData?.lifetimeRewards ?? '0';
+
+ const lifetimeRewardsETH = `${renderFromWei(lifetimeRewards, 5)} ETH`;
+
+ const lifetimeRewardsFiat = renderFiat(
+ weiToFiatNumber(lifetimeRewards, conversionRate),
+ currentCurrency,
+ 2,
+ );
+
+ const assets = pooledStakesData.assets ?? 0;
+ const estimatedAnnualEarnings = new BigNumber(assets)
+ .multipliedBy(annualRewardRateDecimal)
+ .toFixed(0);
+ const estimatedAnnualEarningsETH = `${renderFromWei(
+ estimatedAnnualEarnings.toString(),
+ 5,
+ )} ETH`;
+
+ const estimatedAnnualEarningsFiat = renderFiat(
+ weiToFiatNumber(estimatedAnnualEarnings, conversionRate),
+ currentCurrency,
+ 2,
+ );
+
+ const isLoadingEarningsData = isLoadingVaultData || isLoadingPooledStakesData;
+
+ return {
+ annualRewardRate,
+ lifetimeRewardsETH,
+ lifetimeRewardsFiat,
+ estimatedAnnualEarningsETH,
+ estimatedAnnualEarningsFiat,
+ isLoadingEarningsData,
+ };
+};
+
+export default useStakingEarnings;
diff --git a/app/components/UI/Stake/hooks/useStakingEligibility.ts b/app/components/UI/Stake/hooks/useStakingEligibility.ts
new file mode 100644
index 00000000000..2325145ab64
--- /dev/null
+++ b/app/components/UI/Stake/hooks/useStakingEligibility.ts
@@ -0,0 +1,48 @@
+import { useSelector } from 'react-redux';
+import { useState, useEffect } from 'react';
+import { selectSelectedInternalAccountChecksummedAddress } from '../../../../selectors/accountsController';
+import { useStakeContext } from './useStakeContext';
+
+const useStakingEligibility = () => {
+ const selectedAddress =
+ useSelector(selectSelectedInternalAccountChecksummedAddress) || '';
+ const { stakingApiService } = useStakeContext();
+
+ const [isEligible, setIsEligible] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchStakingEligibility = async () => {
+ try {
+ setLoading(true);
+
+ if (!stakingApiService) {
+ throw new Error('Staking API service is unavailable');
+ }
+
+ const addresses = selectedAddress ? [selectedAddress] : [];
+
+ // Directly calling the stakingApiService to fetch staking eligibility
+ const { eligible } =
+ await stakingApiService.getPooledStakingEligibility(addresses);
+
+ setIsEligible(eligible);
+ } catch (err) {
+ setError('Failed to fetch pooled staking eligibility');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchStakingEligibility();
+ }, [selectedAddress, stakingApiService]);
+
+ return {
+ isEligible,
+ isLoadingEligibility: loading,
+ error,
+ };
+};
+
+export default useStakingEligibility;
diff --git a/app/components/UI/Stake/hooks/useStakingInput.ts b/app/components/UI/Stake/hooks/useStakingInput.ts
index d9d233d19af..cd321d69e0a 100644
--- a/app/components/UI/Stake/hooks/useStakingInput.ts
+++ b/app/components/UI/Stake/hooks/useStakingInput.ts
@@ -15,6 +15,7 @@ import {
renderFiat,
} from '../../../../util/number';
import { strings } from '../../../../../locales/i18n';
+import useVaultData from './useVaultData';
const useStakingInputHandlers = (balance: BN) => {
const [amountEth, setAmountEth] = useState('0');
@@ -32,7 +33,8 @@ const useStakingInputHandlers = (balance: BN) => {
const currentCurrency = useSelector(selectCurrentCurrency);
const conversionRate = useSelector(selectConversionRate) || 1;
- const annualRewardRate = '0.026'; //TODO: Replace with actual value: STAKE-806
+ const { annualRewardRate, annualRewardRateDecimal, isLoadingVaultData } =
+ useVaultData();
const currencyToggleValue = isEth
? `${fiatAmount} ${currentCurrency.toUpperCase()}`
@@ -113,27 +115,42 @@ const useStakingInputHandlers = (balance: BN) => {
[balance, conversionRate],
);
+ const annualRewardsEth = useMemo(
+ () =>
+ limitToMaximumDecimalPlaces(
+ parseFloat(amountEth) * annualRewardRateDecimal,
+ 5,
+ ),
+ [amountEth, annualRewardRateDecimal],
+ );
+
+ const annualRewardsFiat = useMemo(
+ () =>
+ renderFiat(
+ parseFloat(fiatAmount) * annualRewardRateDecimal,
+ currentCurrency,
+ 2,
+ ),
+ [fiatAmount, annualRewardRateDecimal, currentCurrency],
+ );
+
const calculateEstimatedAnnualRewards = useCallback(() => {
if (isNonZeroAmount) {
- // Limiting the decimal places to keep it consistent with other eth values in the input screen
- const ethRewards = limitToMaximumDecimalPlaces(
- parseFloat(amountEth) * parseFloat(annualRewardRate),
- 5,
- );
if (isEth) {
- setEstimatedAnnualRewards(`${ethRewards} ETH`);
+ setEstimatedAnnualRewards(`${annualRewardsEth} ETH`);
} else {
- const fiatRewards = renderFiat(
- parseFloat(fiatAmount) * parseFloat(annualRewardRate),
- currentCurrency,
- 2,
- );
- setEstimatedAnnualRewards(`${fiatRewards}`);
+ setEstimatedAnnualRewards(annualRewardsFiat);
}
} else {
- setEstimatedAnnualRewards(`${Number(annualRewardRate) * 100}%`);
+ setEstimatedAnnualRewards(annualRewardRate);
}
- }, [isNonZeroAmount, amountEth, isEth, fiatAmount, currentCurrency]);
+ }, [
+ isNonZeroAmount,
+ isEth,
+ annualRewardsEth,
+ annualRewardsFiat,
+ annualRewardRate,
+ ]);
return {
amountEth,
@@ -153,6 +170,10 @@ const useStakingInputHandlers = (balance: BN) => {
conversionRate,
estimatedAnnualRewards,
calculateEstimatedAnnualRewards,
+ annualRewardsEth,
+ annualRewardsFiat,
+ annualRewardRate,
+ isLoadingVaultData,
};
};
diff --git a/app/components/UI/Stake/hooks/useVaultData.ts b/app/components/UI/Stake/hooks/useVaultData.ts
new file mode 100644
index 00000000000..1064ab8b73c
--- /dev/null
+++ b/app/components/UI/Stake/hooks/useVaultData.ts
@@ -0,0 +1,60 @@
+import { useSelector } from 'react-redux';
+import { useState, useEffect } from 'react';
+import { selectChainId } from '../../../../selectors/networkController';
+import { hexToNumber } from '@metamask/utils';
+import { VaultData } from '@metamask/stake-sdk';
+import { useStakeContext } from './useStakeContext';
+import { limitToMaximumDecimalPlaces } from '../../../../util/number';
+
+const useVaultData = () => {
+ const chainId = useSelector(selectChainId);
+ const { stakingApiService } = useStakeContext(); // Get the stakingApiService directly from context
+
+ const [vaultData, setVaultData] = useState({} as VaultData);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchVaultData = async () => {
+ try {
+ setLoading(true);
+
+ if (!stakingApiService) {
+ throw new Error('Staking API service is unavailable');
+ }
+
+ const numericChainId = hexToNumber(chainId);
+ const vaultDataResponse = await stakingApiService.getVaultData(
+ numericChainId,
+ );
+
+ setVaultData(vaultDataResponse);
+ } catch (err) {
+ setError('Failed to fetch vault data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchVaultData();
+ }, [chainId, stakingApiService]);
+
+ const apy = vaultData?.apy || '0';
+ const annualRewardRatePercentage = apy ? parseFloat(apy) : 0;
+ const annualRewardRateDecimal = annualRewardRatePercentage / 100;
+
+ const annualRewardRate = `${limitToMaximumDecimalPlaces(
+ annualRewardRatePercentage,
+ 1,
+ )}%`;
+
+ return {
+ vaultData,
+ isLoadingVaultData: loading,
+ error,
+ annualRewardRate,
+ annualRewardRateDecimal,
+ };
+};
+
+export default useVaultData;
diff --git a/app/components/UI/Stake/sdk/__snapshots__/stakeSdkProvider.test.tsx.snap b/app/components/UI/Stake/sdk/__snapshots__/stakeSdkProvider.test.tsx.snap
index 1301d307f16..c9dae8fccd5 100644
--- a/app/components/UI/Stake/sdk/__snapshots__/stakeSdkProvider.test.tsx.snap
+++ b/app/components/UI/Stake/sdk/__snapshots__/stakeSdkProvider.test.tsx.snap
@@ -1155,21 +1155,33 @@ exports[`Stake Modals With Stake Sdk Provider renders stake screen with stake sd
}
}
>
-
- 2.6%
-
+
+
+
+
= {
+ getPooledStakes: jest.fn(),
+ getVaultData: jest.fn(),
+ getPooledStakingEligibility: jest.fn(),
+ fetchFromApi: jest.fn(),
+ baseUrl: 'http://mockApiUrl.com',
+};
+
const mockSDK: Stake = {
- sdkService: mockPooledStakingContractService,
+ stakingContract: mockPooledStakingContractService,
+ stakingApiService: mockStakingApiService as StakingApiService,
sdkType: StakingType.POOLED,
setSdkType: jest.fn(),
};
diff --git a/app/components/UI/Stake/sdk/stakeSdkProvider.tsx b/app/components/UI/Stake/sdk/stakeSdkProvider.tsx
index b6c4964b47e..df4170435d8 100644
--- a/app/components/UI/Stake/sdk/stakeSdkProvider.tsx
+++ b/app/components/UI/Stake/sdk/stakeSdkProvider.tsx
@@ -2,53 +2,72 @@ import {
StakingType,
StakeSdk,
PooledStakingContract,
+ type StakingApiService,
} from '@metamask/stake-sdk';
-import React, { useState, createContext, useMemo } from 'react';
-import { getProviderByChainId } from '../../../../util/notifications';
-import { useSelector } from 'react-redux';
-import { selectChainId } from '../../../../selectors/networkController';
-import { hexToDecimal } from '../../../../util/conversions';
-import { noop } from 'lodash';
+import Logger from '../../../../util/Logger';
+import React, {
+ useState,
+ useEffect,
+ createContext,
+ useMemo,
+ PropsWithChildren,
+} from 'react';
+
+export const SDK = StakeSdk.create({ stakingType: StakingType.POOLED });
export interface Stake {
- sdkService: PooledStakingContract; // to do : facade it for other services implementation
+ sdkError?: Error;
+ stakingContract?: PooledStakingContract;
+ stakingApiService?: StakingApiService;
sdkType?: StakingType;
setSdkType: (stakeType: StakingType) => void;
}
-const initialSdkService = StakeSdk.create({ stakingType: StakingType.POOLED });
+export const StakeContext = StakeSdk.create({ stakingType: StakingType.POOLED });
-export const StakeContext = createContext({
- sdkType: initialSdkService.sdkConfig.stakingType,
- sdkService: initialSdkService.pooledStakingContract,
- setSdkType: noop,
-});
+export interface StakeProviderProps {
+ stakingType?: StakingType;
+}
+export const StakeSDKProvider: React.FC<
+ PropsWithChildren
+> = ({ children }) => {
+ const [stakingContract, setStakingContract] =
+ useState();
+ const [stakingApiService, setStakingApiService] =
+ useState();
-export const StakeSDKProvider: React.FC = ({ children }) => {
+ const [sdkError, setSdkError] = useState();
const [sdkType, setSdkType] = useState(StakingType.POOLED);
- const chainId = useSelector(selectChainId);
-
- const sdkService = useMemo(() => {
- const provider = getProviderByChainId(chainId);
-
- const sdk = StakeSdk.create({
- chainId: parseInt(hexToDecimal(chainId).toString()),
- stakingType: sdkType,
- });
-
- sdk.pooledStakingContract.connectSignerOrProvider(provider);
-
- return sdk.pooledStakingContract;
- }, [chainId, sdkType]);
+ useEffect(() => {
+ (async () => {
+ try {
+ setStakingApiService(SDK.stakingApiService);
+ if (sdkType === StakingType?.POOLED) {
+ setStakingContract(SDK.pooledStakingContract);
+ } else {
+ const notImplementedError = new Error(
+ `StakeSDKProvider SDK.StakingType ${sdkType} not implemented yet`,
+ );
+ Logger.error(notImplementedError);
+ setSdkError(notImplementedError);
+ }
+ } catch (error) {
+ Logger.error(error as Error, `StakeSDKProvider SDK.service failed`);
+ setSdkError(error as Error);
+ }
+ })();
+ }, [sdkType]);
const stakeContextValue = useMemo(
(): Stake => ({
- sdkService,
+ sdkError,
+ stakingContract,
sdkType,
setSdkType,
+ stakingApiService,
}),
- [sdkService, sdkType, setSdkType],
+ [sdkError, stakingContract, sdkType, stakingApiService],
);
return (
diff --git a/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx b/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx
index 7ab0ad470d5..46f5322c377 100644
--- a/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx
+++ b/app/components/UI/Tokens/TokenList/TokenListItem/index.tsx
@@ -46,8 +46,9 @@ import { TokenI } from '../../types';
import { strings } from '../../../../../../locales/i18n';
import { ScamWarningIcon } from '../ScamWarningIcon';
import { ScamWarningModal } from '../ScamWarningModal';
-import { StakeButton } from '../StakeButton';
+import { StakeButton } from '../../../Stake/components/StakeButton';
import { CustomNetworkImgMapping } from '../../../../../util/networks/customNetworks';
+import useStakingChain from '../../../Stake/hooks/useStakingChain';
interface TokenListItemProps {
asset: TokenI;
@@ -151,6 +152,8 @@ export const TokenListItem = ({
const isMainnet = isMainnetByChainId(chainId);
const isLineaMainnet = isLineaMainnetByChainId(chainId);
+ const { isStakingSupportedChain } = useStakingChain();
+
const NetworkBadgeSource = () => {
if (isTestNet(chainId)) return getTestNetImageByChainId(chainId);
@@ -211,7 +214,9 @@ export const TokenListItem = ({
{asset.name || asset.symbol}
{/** Add button link to Portfolio Stake if token is mainnet ETH */}
- {asset.isETH && isMainnet && }
+ {asset.isETH && isStakingSupportedChain && (
+
+ )}
{!isTestNet(chainId) ? (
diff --git a/app/components/Views/AccountConnect/AccountConnect.tsx b/app/components/Views/AccountConnect/AccountConnect.tsx
index 9d3747aed3a..9883878435c 100644
--- a/app/components/Views/AccountConnect/AccountConnect.tsx
+++ b/app/components/Views/AccountConnect/AccountConnect.tsx
@@ -345,7 +345,6 @@ const AccountConnect = (props: AccountConnectProps) => {
},
approvedAccounts: selectedAddresses,
};
-
const connectedAccountLength = selectedAddresses.length;
const activeAddress = selectedAddresses[0];
const activeAccountName = getAccountNameWithENS({
diff --git a/app/components/Views/Settings/NotificationsSettings/AccountsList.test.tsx b/app/components/Views/Settings/NotificationsSettings/AccountsList.test.tsx
new file mode 100644
index 00000000000..aa3d30a0ee7
--- /dev/null
+++ b/app/components/Views/Settings/NotificationsSettings/AccountsList.test.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import renderWithProvider, { DeepPartial } from '../../../../util/test/renderWithProvider';
+import { AccountsList } from './AccountsList';
+import { AvatarAccountType } from '../../../../component-library/components/Avatars/Avatar';
+import { Account } from '../../../../components/hooks/useAccounts/useAccounts.types';
+import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../../util/test/accountsControllerTestUtils';
+import { Hex } from '@metamask/utils';
+import { KeyringTypes } from '@metamask/keyring-controller';
+import { toChecksumAddress } from 'ethereumjs-util';
+import { RootState } from '../../../../reducers';
+import { backgroundState } from '../../../../util/test/initial-root-state';
+
+const MOCK_ACCOUNT_ADDRESSES = Object.values(
+ MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts.accounts,
+).map((account) => account.address);
+
+const MOCK_ACCOUNT_1: Account = {
+ name: 'Account 1',
+ address: toChecksumAddress(MOCK_ACCOUNT_ADDRESSES[0]) as Hex,
+ type: KeyringTypes.hd,
+ yOffset: 0,
+ isSelected: false,
+ assets: {
+ fiatBalance: '\n0 ETH',
+ },
+ balanceError: undefined,
+};
+const MOCK_ACCOUNT_2: Account = {
+ name: 'Account 2',
+ address: toChecksumAddress(MOCK_ACCOUNT_ADDRESSES[1]) as Hex,
+ type: KeyringTypes.hd,
+ yOffset: 78,
+ isSelected: true,
+ assets: {
+ fiatBalance: '\n< 0.00001 ETH',
+ },
+ balanceError: undefined,
+};
+
+const MOCK_ACCOUNTS = [MOCK_ACCOUNT_1, MOCK_ACCOUNT_2];
+
+const mockInitialState: DeepPartial = {
+ engine: {
+ backgroundState: {
+ ...backgroundState,
+ NotificationServicesController: {
+ metamaskNotificationsList: [],
+ },
+ },
+ },
+};
+
+describe('AccountsList', () => {
+ it('matches snapshot', () => {
+
+ const { toJSON } = renderWithProvider(
+ ,
+ {
+ state: mockInitialState,
+ },
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('triggers updateAndfetchAccountSettings on mount', () => {
+ const updateAndfetchAccountSettings = jest.fn();
+ renderWithProvider(
+ ,
+ {
+ state: mockInitialState,
+ },
+ );
+
+ expect(updateAndfetchAccountSettings).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/app/components/Views/Settings/NotificationsSettings/AccountsList.tsx b/app/components/Views/Settings/NotificationsSettings/AccountsList.tsx
new file mode 100644
index 00000000000..8dded276cf9
--- /dev/null
+++ b/app/components/Views/Settings/NotificationsSettings/AccountsList.tsx
@@ -0,0 +1,53 @@
+import React, { useEffect } from 'react';
+import { FlatList, View } from 'react-native';
+import NotificationOptionToggle from './NotificationOptionToggle';
+import { Account } from '../../../../components/hooks/useAccounts/useAccounts.types';
+import { NotificationsToggleTypes } from './NotificationsSettings.constants';
+import { NotificationsAccountsState } from '../../../../core/redux/slices/notifications';
+import { AvatarAccountType } from '../../../../component-library/components/Avatars/Avatar';
+
+export const AccountsList = ({
+ accounts,
+ accountAvatarType,
+ accountSettingsData,
+ updateAndfetchAccountSettings,
+ isUpdatingMetamaskNotificationsAccount,
+}: {
+ accounts: Account[];
+ accountAvatarType: AvatarAccountType;
+ accountSettingsData: NotificationsAccountsState;
+ updateAndfetchAccountSettings: () => Promise | undefined>;
+ isUpdatingMetamaskNotificationsAccount: string[];
+}) => {
+
+ useEffect(() => {
+ const fetchInitialData = async () => {
+ await updateAndfetchAccountSettings();
+ };
+ fetchInitialData();
+ }, [updateAndfetchAccountSettings]);
+
+ return (
+
+ `address-${item.address}`}
+ renderItem={({ item }) => (
+ 0}
+ isLoading={isUpdatingMetamaskNotificationsAccount.includes(
+ item.address.toLowerCase(),
+ )}
+ isEnabled={accountSettingsData?.[item.address.toLowerCase()]}
+ updateAndfetchAccountSettings={updateAndfetchAccountSettings}
+ />
+ )}
+ />
+
+ );
+};
diff --git a/app/components/Views/Settings/NotificationsSettings/NotificationOptionToggle/index.tsx b/app/components/Views/Settings/NotificationsSettings/NotificationOptionToggle/index.tsx
index d04cad3c248..ad6ae7921a3 100644
--- a/app/components/Views/Settings/NotificationsSettings/NotificationOptionToggle/index.tsx
+++ b/app/components/Views/Settings/NotificationsSettings/NotificationOptionToggle/index.tsx
@@ -34,7 +34,7 @@ interface NotificationOptionsToggleProps {
disabledSwitch?: boolean;
isLoading?: boolean;
isEnabled: boolean;
- refetchAccountSettings: () => Promise;
+ updateAndfetchAccountSettings: () => Promise | undefined>;
}
/**
@@ -50,7 +50,7 @@ const NotificationOptionToggle = ({
isEnabled,
disabledSwitch,
isLoading,
- refetchAccountSettings,
+ updateAndfetchAccountSettings,
}: NotificationOptionsToggleProps) => {
const theme = useTheme();
const { colors } = theme;
@@ -59,7 +59,7 @@ const NotificationOptionToggle = ({
const { toggleAccount, loading: isUpdatingAccount } = useUpdateAccountSetting(
address,
- refetchAccountSettings,
+ updateAndfetchAccountSettings,
);
const loading = isLoading || isUpdatingAccount;
@@ -104,7 +104,7 @@ const NotificationOptionToggle = ({
)}
- {isLoading || loading ? (
+ {loading ? (
) : (
},
button: {
alignSelf: 'stretch',
- marginBottom: 16,
+ marginBottom: 48,
+ },
+
+ });
+
+ export const styles = StyleSheet.create({
+ headerLeft: {
+ marginHorizontal: 16,
},
});
diff --git a/app/components/Views/Settings/NotificationsSettings/__snapshots__/AccountsList.test.tsx.snap b/app/components/Views/Settings/NotificationsSettings/__snapshots__/AccountsList.test.tsx.snap
new file mode 100644
index 00000000000..621e0158390
--- /dev/null
+++ b/app/components/Views/Settings/NotificationsSettings/__snapshots__/AccountsList.test.tsx.snap
@@ -0,0 +1,520 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AccountsList matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Account 1
+
+
+ 0xc495...d272
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Account 2
+
+
+ 0xc496...a756
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/app/components/Views/Settings/NotificationsSettings/index.tsx b/app/components/Views/Settings/NotificationsSettings/index.tsx
index e046b203c2e..20456dc6389 100644
--- a/app/components/Views/Settings/NotificationsSettings/index.tsx
+++ b/app/components/Views/Settings/NotificationsSettings/index.tsx
@@ -1,6 +1,5 @@
-/* eslint-disable react-native/no-inline-styles */
/* eslint-disable react/display-name */
-import React, { useEffect, useMemo, useCallback } from 'react';
+import React, { useEffect, useCallback } from 'react';
import { ScrollView, Switch, View, Linking } from 'react-native';
import { useSelector } from 'react-redux';
import { NavigationProp, ParamListBase } from '@react-navigation/native';
@@ -13,6 +12,7 @@ import Text, {
TextVariant,
TextColor,
} from '../../../../component-library/components/Texts/Text';
+import { AccountsList} from './AccountsList';
import { useAccounts } from '../../../../components/hooks/useAccounts';
import { useMetrics } from '../../../../components/hooks/useMetrics';
import { AvatarAccountType } from '../../../../component-library/components/Avatars/Avatar';
@@ -21,9 +21,7 @@ import SwitchLoadingModal from '../../../UI/Notification/SwitchLoadingModal';
import { Props } from './NotificationsSettings.types';
import { useStyles } from '../../../../component-library/hooks';
-import NotificationOptionToggle from './NotificationOptionToggle';
import CustomNotificationsRow from './CustomNotificationsRow';
-import { NotificationsToggleTypes } from './NotificationsSettings.constants';
import {
selectIsFeatureAnnouncementsEnabled,
selectIsMetamaskNotificationsEnabled,
@@ -51,7 +49,7 @@ import {
useAccountSettingsProps,
useSwitchNotifications,
} from '../../../../util/notifications/hooks/useSwitchNotifications';
-import styleSheet from './NotificationsSettings.styles';
+import styleSheet, { styles as navigationOptionsStyles } from './NotificationsSettings.styles';
import AppConstants from '../../../../core/AppConstants';
import notificationsRows from './notificationsRows';
import { IconName } from '../../../../component-library/components/Icons/Icon';
@@ -121,19 +119,6 @@ const NotificationsSettings = ({ navigation, route }: Props) => {
selectIsUpdatingMetamaskNotificationsAccount,
);
- const accountAddresses = useMemo(
- () => accounts.map((a) => a.address),
- [accounts],
- );
-
- const { switchFeatureAnnouncements } = useSwitchNotifications();
-
- // Account Settings
- const accountSettingsProps = useAccountSettingsProps(accountAddresses);
- const refetchAccountSettings = useCallback(async () => {
- await accountSettingsProps.update(accountAddresses);
- }, [accountAddresses, accountSettingsProps]);
-
const {
enableNotifications,
loading: enableLoading,
@@ -146,6 +131,9 @@ const NotificationsSettings = ({ navigation, route }: Props) => {
error: disablingError,
} = useDisableNotifications();
+ const { switchFeatureAnnouncements } = useSwitchNotifications();
+ const { updateAndfetchAccountSettings } = useAccountSettingsProps(accounts);
+
const accountAvatarType = useSelector((state: RootState) =>
state.settings.useBlockieIcon
? AvatarAccountType.Blockies
@@ -157,7 +145,7 @@ const NotificationsSettings = ({ navigation, route }: Props) => {
const [uiNotificationStatus, setUiNotificationStatus] = React.useState(false);
const [platformAnnouncementsState, setPlatformAnnouncementsState] =
React.useState(isFeatureAnnouncementsEnabled);
-
+ const accountSettingsData = useSelector((state: RootState) => state.notifications);
const loading = enableLoading || disableLoading;
const errorText = enablingError || disablingError;
const loadingText = !uiNotificationStatus
@@ -170,7 +158,7 @@ const NotificationsSettings = ({ navigation, route }: Props) => {
const isFullScreenModal = route?.params?.isFullScreenModal;
// Style
const { colors } = theme;
- const { styles } = useStyles(styleSheet, {});
+ const { styles } = useStyles(styleSheet, { theme });
/**
* Initializes the notifications feature.
@@ -220,37 +208,6 @@ const NotificationsSettings = ({ navigation, route }: Props) => {
);
}, [colors, isFullScreenModal, navigation]);
- const renderAccounts = useCallback(
- () =>
- accounts.map((account) => {
- const isEnabled =
- accountSettingsProps.data?.[account.address.toLowerCase()];
- return (
- 0}
- isLoading={accountSettingsProps.accountsBeingUpdated.includes(
- account.address.toLowerCase(),
- )}
- isEnabled={isEnabled ?? false}
- refetchAccountSettings={refetchAccountSettings}
- />
- );
- }),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [
- accountSettingsProps.data,
- accountSettingsProps.accountsBeingUpdated,
- accountAvatarType,
- isUpdatingMetamaskNotificationsAccount.length,
- refetchAccountSettings,
- ],
- );
-
const renderResetNotificationsBtn = useCallback(() => (