diff --git a/src/app/components/tabBar/index.tsx b/src/app/components/tabBar/index.tsx
index d6fade869..65e096975 100644
--- a/src/app/components/tabBar/index.tsx
+++ b/src/app/components/tabBar/index.tsx
@@ -42,7 +42,7 @@ function BottomTabBar({ tab }:Props) {
};
const handleStackingButtonClick = () => {
-
+ if (tab !== 'stacking') { navigate('/stacking'); }
};
const handleSettingButtonClick = () => {
diff --git a/src/app/hooks/useStackingData.tsx b/src/app/hooks/useStackingData.tsx
new file mode 100644
index 000000000..f30f0dacb
--- /dev/null
+++ b/src/app/hooks/useStackingData.tsx
@@ -0,0 +1,58 @@
+import { useQueries } from '@tanstack/react-query';
+import { useCallback } from 'react';
+import {
+ fetchDelegationState, fetchPoolStackerInfo, fetchStackingPoolInfo, getStacksInfo, StackingData,
+} from '@secretkeylabs/xverse-core';
+import useWalletSelector from './useWalletSelector';
+
+const useStackingData = () => {
+ const { stxAddress, network } = useWalletSelector();
+
+ const results = useQueries({
+ queries: [
+ {
+ queryKey: ['stacking-core-info', network],
+ queryFn: () => getStacksInfo(network.address),
+ },
+ {
+ queryKey: ['stacking-delegation-state', stxAddress, network],
+ queryFn: () => fetchDelegationState(stxAddress, network),
+ },
+ {
+ queryKey: ['stacking-pool-info'],
+ queryFn: () => fetchStackingPoolInfo(),
+ },
+ {
+ queryKey: ['pool-stacker-info', stxAddress],
+ queryFn: () => fetchPoolStackerInfo(stxAddress),
+ },
+ ],
+ });
+
+ const coreInfoData = results[0].data;
+ const delegationStateData = results[1].data;
+ const poolInfoData = results[2].data;
+ const stackerInfoData = results[3].data;
+
+ const isStackingLoading = results.some((result) => result.isLoading);
+ const stackingError = results.find(({ error }) => error != null)?.error;
+ const refetchStackingData = useCallback(() => {
+ results.forEach((result) => result.refetch());
+ }, [results]);
+
+ const stackingData: StackingData = {
+ poolInfo: poolInfoData,
+ delegationInfo: delegationStateData!,
+ coreInfo: coreInfoData!,
+ stackerInfo: stackerInfoData,
+ };
+
+ return {
+ isStackingLoading,
+ stackingError,
+ stackingData,
+ refetchStackingData,
+ };
+};
+
+export default useStackingData;
diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx
index e7f69843d..db1ad2bc5 100644
--- a/src/app/routes/index.tsx
+++ b/src/app/routes/index.tsx
@@ -20,6 +20,7 @@ import Login from '@screens/login';
import RestoreWallet from '@screens/restoreWallet';
import ForgotPassword from '@screens/forgotPassword';
import BackupWalletSteps from '@screens/backupWalletSteps';
+import Stacking from '@screens/stacking';
import NftDashboard from '@screens/nftDashboard';
import NftDetailScreen from '@screens/nftDetail';
import Setting from '@screens/settings';
@@ -119,6 +120,10 @@ const router = createHashRouter([
path: 'backupWalletSteps',
element: ,
},
+ {
+ path: 'stacking',
+ element: ,
+ },
{
path: 'nft-dashboard',
element: ,
diff --git a/src/app/screens/stacking/index.tsx b/src/app/screens/stacking/index.tsx
new file mode 100644
index 000000000..fbbc18e17
--- /dev/null
+++ b/src/app/screens/stacking/index.tsx
@@ -0,0 +1,63 @@
+import { useNavigate } from 'react-router-dom';
+import { Ring } from 'react-spinners-css';
+import { useEffect, useState } from 'react';
+import styled from 'styled-components';
+import useStackingData from '@hooks/useStackingData';
+import useWalletSelector from '@hooks/useWalletSelector';
+import AccountRow from '@components/accountRow';
+import BottomBar from '@components/tabBar';
+import StackingProgress from './stackingProgress';
+import StartStacking from './startStacking';
+
+const SelectedAccountContainer = styled.div((props) => ({
+ marginLeft: props.theme.spacing(8),
+ marginRight: props.theme.spacing(8),
+}));
+
+const LoaderContainer = styled.div((props) => ({
+ display: 'flex',
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginTop: props.theme.spacing(12),
+}));
+
+function Stacking() {
+ const { selectedAccount } = useWalletSelector();
+ const { isStackingLoading, stackingData } = useStackingData();
+ const navigate = useNavigate();
+ const [isStacking, setIsStacking] = useState(false);
+ const handleAccountSelect = () => {
+ navigate('/account-list');
+ };
+
+ useEffect(() => {
+ if (stackingData) {
+ if (stackingData?.stackerInfo?.stacked || stackingData?.delegationInfo?.delegated) {
+ setIsStacking(true);
+ }
+ }
+ }, [stackingData]);
+
+ const showStatus = !isStackingLoading && (
+ isStacking ? :
+ );
+
+ return (
+ <>
+
+
+
+ {isStackingLoading && (
+
+
+
+ ) }
+ {showStatus}
+
+ >
+
+ );
+}
+
+export default Stacking;
diff --git a/src/app/screens/stacking/stackingProgress/index.tsx b/src/app/screens/stacking/stackingProgress/index.tsx
new file mode 100644
index 000000000..57139b10c
--- /dev/null
+++ b/src/app/screens/stacking/stackingProgress/index.tsx
@@ -0,0 +1,36 @@
+import { useTranslation } from 'react-i18next';
+import styled from 'styled-components';
+import StackingStatusTile from './stackingStatusTile';
+
+const Container = styled.div((props) => ({
+ display: 'flex',
+ flex: 1,
+ flexDirection: 'column',
+ margin: props.theme.spacing(8),
+ borderTop: `1px solid ${props.theme.colors.background.elevation3}`,
+}));
+
+const TitleText = styled.h1((props) => ({
+ ...props.theme.headline_s,
+ marginTop: props.theme.spacing(16),
+}));
+
+const StackingDescriptionText = styled.h1((props) => ({
+ ...props.theme.body_m,
+ marginTop: props.theme.spacing(4),
+ marginBottom: props.theme.spacing(18),
+ color: props.theme.colors.white['200'],
+}));
+function StackingProgress() {
+ const { t } = useTranslation('translation', { keyPrefix: 'STACKING_SCREEN' });
+
+ return (
+
+ {t('STACK_FOR_REWARD')}
+ {t('XVERSE_POOL')}
+
+
+ );
+}
+
+export default StackingProgress;
diff --git a/src/app/screens/stacking/stackingProgress/stackingStatusTile.tsx b/src/app/screens/stacking/stackingProgress/stackingStatusTile.tsx
new file mode 100644
index 000000000..a8e3752c8
--- /dev/null
+++ b/src/app/screens/stacking/stackingProgress/stackingStatusTile.tsx
@@ -0,0 +1,106 @@
+import styled from 'styled-components';
+import TokenTicker from '@assets/img/stacking/token_ticker.svg';
+import { useTranslation } from 'react-i18next';
+import useStackingData from '@hooks/useStackingData';
+import { StackingState } from '@secretkeylabs/xverse-core/stacking';
+import { useEffect, useState } from 'react';
+import { XVERSE_WEB_POOL_URL } from '@utils/constants';
+
+const Container = styled.button((props) => ({
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ background: props.theme.colors.background.elevation1,
+ borderRadius: props.theme.radius(2),
+ padding: props.theme.spacing(9),
+}));
+
+const TextContainer = styled.div({
+ display: 'flex',
+ flexDirection: 'row',
+ flex: 1,
+});
+
+const StatusContainer = styled.div((props) => ({
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ background: 'rgba(81, 214, 166, 0.15)',
+ borderRadius: props.theme.radius(7),
+ paddingLeft: props.theme.spacing(6),
+ paddingRight: props.theme.spacing(6),
+ paddingTop: props.theme.spacing(2),
+ paddingBottom: props.theme.spacing(2),
+}));
+
+const Dot = styled.div((props) => ({
+ width: 7,
+ height: 7,
+ borderRadius: props.theme.radius(9),
+ marginRight: props.theme.spacing(4),
+ background: props.theme.colors.feedback.success,
+}));
+
+const ColumnContainer = styled.div((props) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ marginLeft: props.theme.spacing(6),
+}));
+
+const BoldText = styled.h1((props) => ({
+ ...props.theme.body_bold_m,
+ color: props.theme.colors.white['0'],
+}));
+
+const SubText = styled.h1((props) => ({
+ ...props.theme.body_xs,
+ color: props.theme.colors.white['400'],
+}));
+
+const StatusText = styled.h1((props) => ({
+ ...props.theme.body_xs,
+ fontWeight: 500,
+ color: props.theme.colors.white['0'],
+}));
+
+function StackingStatusTile() {
+ const { t } = useTranslation('translation', { keyPrefix: 'STACKING_SCREEN' });
+ const { stackingData } = useStackingData();
+ const [status, setStatus] = useState('Pending');
+
+ useEffect(() => {
+ if (stackingData) {
+ if (!stackingData?.stackerInfo?.stacked && stackingData?.delegationInfo?.delegated) {
+ setStatus('Delegated');
+ } else if (stackingData?.stackerInfo?.stacked) { setStatus('Stacking'); }
+ }
+ }, [stackingData]);
+
+ const handleOnClick = () => {
+ window.open(XVERSE_WEB_POOL_URL);
+ };
+
+ return (
+
+
+
+
+ {t('STACK_STX')}
+ {t('EARN_BTC')}
+
+
+
+
+
+
+ {status === 'Stacking' ? t('IN_PROGRESS') : t('DELEGATED')}
+
+
+
+ );
+}
+
+export default StackingStatusTile;
diff --git a/src/app/screens/stacking/startStacking/index.tsx b/src/app/screens/stacking/startStacking/index.tsx
new file mode 100644
index 000000000..d623a9066
--- /dev/null
+++ b/src/app/screens/stacking/startStacking/index.tsx
@@ -0,0 +1,109 @@
+import styled, { useTheme } from 'styled-components';
+import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { microstacksToStx } from '@secretkeylabs/xverse-core';
+import { Pool } from '@secretkeylabs/xverse-core/types';
+import BigNumber from 'bignumber.js';
+import ArrowSquareOut from '@assets/img/arrow_square_out.svg';
+import ActionButton from '@components/button';
+import useStackingData from '@hooks/useStackingData';
+import { XVERSE_WEB_POOL_URL } from '@utils/constants';
+import StackingInfoTile from './stackInfoTile';
+
+const Container = styled.div((props) => ({
+ margin: props.theme.spacing(8),
+ borderBottom: `1px solid ${props.theme.colors.background.elevation3}`,
+ borderTop: `1px solid ${props.theme.colors.background.elevation3}`,
+}));
+
+const StackingInfoContainer = styled.div((props) => ({
+ marginLeft: props.theme.spacing(8),
+ marginRight: props.theme.spacing(30),
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+}));
+
+const ButtonContainer = styled.div((props) => ({
+ margin: props.theme.spacing(8),
+
+}));
+
+const RowContainer = styled.div((props) => ({
+ marginTop: props.theme.spacing(12),
+ display: 'flex',
+ flexDirection: 'row',
+ flex: 1,
+ justifyContent: 'space-between',
+}));
+
+const TitleText = styled.h1((props) => ({
+ ...props.theme.headline_s,
+ marginTop: props.theme.spacing(16),
+}));
+
+const StackingDescriptionText = styled.h1((props) => ({
+ ...props.theme.body_m,
+ marginTop: props.theme.spacing(4),
+ marginBottom: props.theme.spacing(12),
+ color: props.theme.colors.white['200'],
+}));
+
+function StartStacking() {
+ const { t } = useTranslation('translation', { keyPrefix: 'STACKING_SCREEN' });
+ const [poolAvailable, setPoolAvailable] = useState(false);
+ const { stackingData } = useStackingData();
+
+ const theme = useTheme();
+ const [pool, setPool] = useState(undefined);
+
+ useEffect(() => {
+ if (stackingData) {
+ if (stackingData?.poolInfo?.pools?.length! > 0) {
+ const pools = stackingData?.poolInfo?.pools!;
+ setPool(pools[0]);
+ setPoolAvailable(true);
+ }
+ }
+ }, [stackingData]);
+
+ function getMinimum() {
+ const min = poolAvailable ? pool!.minimum : 0;
+ return microstacksToStx(new BigNumber(min)).toString();
+ }
+
+ function getCycles() {
+ const minCycles = poolAvailable ? Math.min(...pool!.available_cycle_durations) : 0;
+ const maxCycles = poolAvailable ? Math.max(...pool!.available_cycle_durations) : 0;
+ return `${minCycles}-${maxCycles}`;
+ }
+
+ const handleOnClick = () => {
+ window.open(XVERSE_WEB_POOL_URL);
+ };
+
+ return (
+ <>
+
+ {t('STACK_AND_EARN')}
+ {t('STACKING_INFO')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+ );
+}
+
+export default StartStacking;
diff --git a/src/app/screens/stacking/startStacking/stackInfoTile.tsx b/src/app/screens/stacking/startStacking/stackInfoTile.tsx
new file mode 100644
index 000000000..e71d9f04e
--- /dev/null
+++ b/src/app/screens/stacking/startStacking/stackInfoTile.tsx
@@ -0,0 +1,46 @@
+import styled from 'styled-components';
+import BarLoader from '@components/barLoader';
+import { LoaderSize } from '@utils/constants';
+
+const TitleText = styled.h1((props) => ({
+ ...props.theme.headline_category_s,
+ textTransform: 'uppercase',
+ letterSpacing: '0.02em',
+ color: props.theme.colors.white['400'],
+}));
+
+const Container = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+const ValueText = styled.h1((props) => ({
+ ...props.theme.body_bold_l,
+ marginTop: props.theme.spacing(2),
+ color: props.color ?? props.theme.colors.white['0'],
+}));
+
+const BarLoaderContainer = styled.div((props) => ({
+ marginTop: props.theme.spacing(5),
+}));
+
+interface Props {
+ title: string;
+ value: string | undefined;
+ color?:string;
+}
+
+function StackingInfoTile({ title, value, color }: Props) {
+ return (
+
+ {title}
+ {value ? {value} : (
+
+
+
+ )}
+
+ );
+}
+
+export default StackingInfoTile;
diff --git a/src/app/utils/constants.ts b/src/app/utils/constants.ts
index cf70a1165..c6c5c154f 100644
--- a/src/app/utils/constants.ts
+++ b/src/app/utils/constants.ts
@@ -7,6 +7,7 @@ export const PRIVACY_POLICY_LINK = 'https://xverse.app/privacy';
export const SUPPORT_LINK = 'https://support.xverse.app/hc/en-us';
export const BTC_TRANSACTION_STATUS_URL = 'https://www.blockchain.com/btc/tx/';
export const TRANSACTION_STATUS_URL = 'https://explorer.stacks.co/txid/';
+export const XVERSE_WEB_POOL_URL = 'https://pool.xverse.app';
export type CurrencyTypes = 'STX' | 'BTC' | 'FT' | 'NFT';
export enum LoaderSize {
diff --git a/src/assets/img/stacking/token_ticker.svg b/src/assets/img/stacking/token_ticker.svg
new file mode 100644
index 000000000..ee81043f2
--- /dev/null
+++ b/src/assets/img/stacking/token_ticker.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/locales/en.json b/src/locales/en.json
index 018aa42b6..c150d340b 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -169,6 +169,22 @@
"SEED_INPUT_ERROR": "Invalid seed phrase, please try again",
"CONTINUE_BUTTON": "Continue"
},
+ "STACKING_SCREEN": {
+ "STACK_AND_EARN": "Stack STX, earn BTC",
+ "STACKING_INFO": "Pool your STX with other stackers to earn real BTC with low minimums. Rewards are distributed after the end of every 2-week cycle.",
+ "APY": "Estimated APY",
+ "POOL_FEE": "Pool fee",
+ "MINIMUM_AMOUNT": "Minimum amount",
+ "REWARD_CYCLES": "Reward Cycles",
+ "START_STACKNG": "Start stacking",
+ "STACK_FOR_REWARD": "Stack your coins to earn rewards",
+ "XVERSE_POOL": "Xverse delegated pooling allows everyone to participate in Stacking and earn rewards. ",
+ "STACK_STX": "Stack STX",
+ "EARN_BTC":"Earn BTC",
+ "PENDING_DELEGATION": "Pending delegation",
+ "DELEGATED": "Delegated",
+ "IN_PROGRESS": "In progress"
+ },
"NFT_DASHBOARD_SCREEN": {
"COLLECTIBLES": "Number of collectibles",
"NO_COLLECTIBLES": "No collectibles yet",