[ENG-5004] [ENG-4667] [ENG-3736] [ENG-4654] [ENG-4655] Update the dashboard loader/styles (#612)

* [ENG-5004] Fix clunky loading of dashboard when logging in

* Add animation between /login and /home routes

* Update the dashboard title styles and indents

* Move styles from the CoinDashboard screen to a separate file

* Replace and remove the SmallActionButton component

* Fix dashboard indents

* Update some text style, remove some borders

* Improve the dashboard loading UI

* Stop hiding the bottombar when loading the explore screen

* Remove the old prop use

* Fix routing

* Fix routing

* Fix routing

* Add comments for some routes

* Improve the dashboard loader

* Improve the dashboard loader

* Improve the dashboard loader animation

* Update the dashboard loader animation

* Remove unused import

* Add destructurization

* Change the token tile skeleton height

* Fix e2e tests

* Add the auth guard conditions back

* Add a try-catch for runes balance fetching, shorten the timeout for the balance e2e test

* Update the error handling for hooks used on the Dashboard screen
This commit is contained in:
Den
2024-10-29 12:31:43 +01:00
committed by GitHub
parent 8d8a574598
commit 7010949d6f
46 changed files with 981 additions and 720 deletions

View File

@@ -30,6 +30,14 @@ const StyledIcon = styled.div`
justify-content: center;
`;
const commonToastStyles = {
...Theme.typography.body_medium_m,
position: 'relative' as const,
bottom: Theme.space.xxxxl,
borderRadius: Theme.radius(2),
padding: Theme.space.s,
};
function App(): React.ReactNode {
return (
<>
@@ -46,7 +54,6 @@ function App(): React.ReactNode {
<Toaster
max={1}
position="bottom-center"
containerStyle={{ bottom: 32 }}
toastOptions={{
duration: 2000,
success: {
@@ -56,10 +63,8 @@ function App(): React.ReactNode {
</StyledIcon>
),
style: {
...Theme.typography.body_medium_m,
...commonToastStyles,
backgroundColor: Theme.colors.success_medium,
borderRadius: Theme.radius(2),
padding: Theme.space.s,
color: Theme.colors.elevation0,
},
},
@@ -70,19 +75,15 @@ function App(): React.ReactNode {
</StyledIcon>
),
style: {
...Theme.typography.body_medium_m,
...commonToastStyles,
backgroundColor: Theme.colors.danger_dark,
borderRadius: Theme.radius(2),
padding: Theme.space.s,
color: Theme.colors.white_0,
},
},
blank: {
style: {
...Theme.typography.body_medium_m,
...commonToastStyles,
backgroundColor: Theme.colors.white_0,
borderRadius: Theme.radius(2),
padding: Theme.space.s,
color: Theme.colors.elevation0,
},
},

View File

@@ -2,7 +2,7 @@ import AccountRow from '@components/accountRow';
import useWalletReducer from '@hooks/useWalletReducer';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import styled from 'styled-components';
import OptionsDialog from '@components/optionsDialog/optionsDialog';
@@ -10,14 +10,13 @@ import useSelectedAccount from '@hooks/useSelectedAccount';
import { DotsThreeVertical } from '@phosphor-icons/react';
import { OPTIONS_DIALOG_WIDTH } from '@utils/constants';
const SelectedAccountContainer = styled.div<{ $showBorderBottom?: boolean }>((props) => ({
const SelectedAccountContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
position: 'relative',
alignItems: 'center',
justifyContent: 'space-between',
padding: `${props.theme.space.l} ${props.theme.space.m}`,
borderBottom: props.$showBorderBottom ? `0.5px solid ${props.theme.colors.elevation3}` : 'none',
}));
const OptionsButton = styled.button(() => ({
@@ -58,15 +57,14 @@ const ButtonRow = styled.button`
type Props = {
disableMenuOption?: boolean;
disableAccountSwitch?: boolean;
showBorderBottom?: boolean;
};
function AccountHeaderComponent({
disableMenuOption = false,
disableAccountSwitch = false,
showBorderBottom = true,
}: Props) {
const navigate = useNavigate();
const { pathname } = useLocation();
const selectedAccount = useSelectedAccount();
const { t: optionsDialogTranslation } = useTranslation('translation', {
@@ -80,7 +78,7 @@ function AccountHeaderComponent({
const handleAccountSelect = () => {
if (!disableAccountSwitch) {
navigate('/account-list');
navigate('/account-list', { state: { from: pathname } });
}
};
@@ -102,7 +100,7 @@ function AccountHeaderComponent({
};
return (
<SelectedAccountContainer $showBorderBottom={showBorderBottom}>
<SelectedAccountContainer>
<AccountRow
account={selectedAccount!}
isSelected

View File

@@ -0,0 +1,33 @@
import ScreenContainer from '@components/screenContainer';
import { animated, useSpring } from '@react-spring/web';
import { ANIMATION_EASING } from '@utils/constants';
import { useLocation } from 'react-router-dom';
function AnimatedScreenContainer(): JSX.Element {
const location = useLocation();
const { state } = location;
const shouldAnimate = state?.from === '/login';
const styles = useSpring({
from: {
y: '125%',
},
to: {
y: '0%',
},
delay: shouldAnimate ? 300 : 0,
config: {
duration: shouldAnimate ? 450 : 0,
easing: ANIMATION_EASING,
},
});
return (
<animated.div style={styles}>
<ScreenContainer />
</animated.div>
);
}
export default AnimatedScreenContainer;

View File

@@ -1,4 +1,5 @@
import { LoaderSize } from '@utils/constants';
import { animated, useSpring } from '@react-spring/web';
import { ANIMATION_EASING, LoaderSize } from '@utils/constants';
import ContentLoader from 'react-content-loader';
import styled from 'styled-components';
import Theme from 'theme';
@@ -70,8 +71,9 @@ function BarLoader({ loaderSize }: { loaderSize?: LoaderSize }) {
export default BarLoader;
const StyledContentLoader = styled(ContentLoader)`
padding: ${(props) => props.theme.spacing(1)}px;
padding: ${(props) => props.theme.space.xxxs}px;
`;
export function BetterBarLoader({
width,
height,
@@ -96,3 +98,29 @@ export function BetterBarLoader({
</StyledContentLoader>
);
}
export function BestBarLoader({
width,
height,
className,
}: {
width: number | string;
height: number | string;
className?: string;
}) {
const styles = useSpring({
from: {
backgroundColor: Theme.colors.white_850,
},
to: {
backgroundColor: Theme.colors.white_800,
},
loop: { reverse: true },
config: {
duration: 400,
easing: ANIMATION_EASING,
},
});
return <animated.div style={{ ...styles, width, height }} className={className} />;
}

View File

@@ -137,7 +137,7 @@ export default function AmountWithInscriptionSatribute({
/>
</Range>
{(inscriptionSatributes.length > 0 || inscriptions.length > index + 1) && (
<Divider verticalMargin="s" />
<Divider $verticalMargin="s" />
)}
</>
))}
@@ -150,7 +150,7 @@ export default function AmountWithInscriptionSatribute({
inscriptionSatributes={inscriptionSatributes}
/>
</Range>
{satributesArray.length > 0 && <Divider verticalMargin="s" />}
{satributesArray.length > 0 && <Divider $verticalMargin="s" />}
</>
)}
{satributesArray.map(
@@ -162,7 +162,7 @@ export default function AmountWithInscriptionSatribute({
<Range>
<RareSatRow item={item} />
</Range>
{satributesArray.length > index + 1 && <Divider verticalMargin="s" />}
{satributesArray.length > index + 1 && <Divider $verticalMargin="s" />}
</>
),
)}

View File

@@ -131,12 +131,12 @@ function RareSats({
// eslint-disable-next-line react/jsx-key
<Range>
<RareSatRow item={item} />
{i < satributes.length - 1 && <Divider verticalMargin="s" />}
{i < satributes.length - 1 && <Divider $verticalMargin="s" />}
</Range>
))}
{bundleSize && totalExoticSats !== bundleSize && (
<>
<Divider verticalMargin="s" />
<Divider $verticalMargin="s" />
<Range>
<RareSatRow
item={{

View File

@@ -3,7 +3,7 @@ import useWalletReducer from '@hooks/useWalletReducer';
import useWalletSelector from '@hooks/useWalletSelector';
import Spinner from '@ui-library/spinner';
import { useEffect, type PropsWithChildren } from 'react';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import styled from 'styled-components';
const CenterChildContainer = styled.div`
@@ -24,6 +24,7 @@ const isInitialised = {
function AuthGuard({ children }: PropsWithChildren) {
const navigate = useNavigate();
const { pathname } = useLocation();
const { encryptedSeed, isUnlocked, accountsList } = useWalletSelector();
const { loadWallet, lockWallet } = useWalletReducer();
const seedVault = useSeedVault();
@@ -40,7 +41,7 @@ function AuthGuard({ children }: PropsWithChildren) {
if (encryptedSeed) {
// this is a legacy seed store. If it exists, we need to migrate
// it to the new seed vault which happens on login
navigate('/login');
navigate('/login', { state: { from: pathname } });
return;
}
@@ -63,7 +64,7 @@ function AuthGuard({ children }: PropsWithChildren) {
try {
await seedVault.getSeed();
} catch (error) {
navigate('/login');
navigate('/login', { state: { from: pathname } });
}
await loadWallet(() => {

View File

@@ -1,83 +0,0 @@
import styled from 'styled-components';
interface ButtonProps {
isOpaque?: boolean;
}
const Button = styled.div<ButtonProps>((props) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 16,
backgroundColor: props.isOpaque
? props.theme.colors.elevation2
: props.theme.colors.action.classic,
width: 48,
height: 48,
transition: 'background-color 0.2s ease, opacity 0.2s ease',
':hover': {
backgroundColor: props.isOpaque
? props.theme.colors.elevation2
: props.theme.colors.action.classicLight,
opacity: props.isOpaque ? 0.85 : 0.6,
},
}));
const TransparentButton = styled(Button)`
background-color: transparent;
border: ${(props) => `1px solid ${props.theme.colors.elevation6}`};
`;
const AnimatedTransparentButton = styled(TransparentButton)`
:hover {
background: ${(props) => props.theme.colors.elevation6_800};
}
`;
const ButtonText = styled.h1((props) => ({
...props.theme.body_xs,
fontWeight: 700,
marginTop: props.theme.spacing(4),
color: 'rgba(255, 255, 255, 0.9)',
}));
const ButtonImage = styled.img({
alignSelf: 'center',
transform: 'all',
transition: 'all 0.2s ease',
});
interface ButtonContainerProps {
isDisabled?: boolean;
}
const ButtonContainer = styled.button<ButtonContainerProps>((props) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
background: 'transparent',
opacity: props.isDisabled ? 0.3 : 1,
}));
interface Props {
src?: string;
text: string;
onPress: () => void;
isOpaque?: boolean;
isDisabled?: boolean;
}
function SmallActionButton({ src, text, onPress, isOpaque, isDisabled }: Props) {
const handleOnPress = () => {
if (!isDisabled) onPress();
};
return (
<ButtonContainer data-testid="action-button" isDisabled={isDisabled} onClick={handleOnPress}>
<Button isOpaque={isOpaque}>{src && <ButtonImage src={src} />}</Button>
<ButtonText>{text}</ButtonText>
</ButtonContainer>
);
}
export default SmallActionButton;

View File

@@ -40,7 +40,8 @@ const Wrapper = styled.div<{ $isTransparent?: boolean; $size?: number }>((props)
const Title = styled.span((props) => ({
...props.theme.typography.body_medium_s,
color: props.theme.colors.white_0,
lineHeight: '140%',
color: props.theme.colors.white_200,
textAlign: 'center',
}));

View File

@@ -16,7 +16,6 @@ const RowContainer = styled.div((props) => ({
justifyContent: 'space-between',
paddingLeft: props.theme.space.xl,
paddingRight: props.theme.space.xl,
borderTop: `1px solid ${props.theme.colors.elevation3}`,
}));
const MovingDiv = styled(animated.div)((props) => ({

View File

@@ -2,12 +2,12 @@ import { BetterBarLoader } from '@components/barLoader';
import styled from 'styled-components';
const TilesLoaderContainer = styled.div<{
isGalleryOpen?: boolean;
$isGalleryOpen?: boolean;
}>((props) => ({
width: '100%',
display: 'flex',
justifyContent: 'flex-start',
columnGap: props.isGalleryOpen ? props.theme.spacing(16) : props.theme.spacing(8),
columnGap: props.$isGalleryOpen ? props.theme.space.xl : props.theme.space.m,
}));
const TileLoaderContainer = styled.div({
@@ -16,12 +16,12 @@ const TileLoaderContainer = styled.div({
});
export const StyledBarLoader = styled(BetterBarLoader)<{
withMarginBottom?: boolean;
isGalleryOpen?: boolean;
$withMarginBottom?: boolean;
$isGalleryOpen?: boolean;
}>((props) => ({
padding: 0,
borderRadius: props.theme.radius(props.isGalleryOpen ? 3 : 1),
marginBottom: props.withMarginBottom ? props.theme.spacing(6) : 0,
borderRadius: props.theme.radius(props.$isGalleryOpen ? 3 : 1),
marginBottom: props.$withMarginBottom ? props.theme.space.s : 0,
}));
export function TilesSkeletonLoader({
@@ -34,23 +34,13 @@ export function TilesSkeletonLoader({
isGalleryOpen?: boolean;
}) {
return (
<TilesLoaderContainer className={className} isGalleryOpen={isGalleryOpen}>
<TilesLoaderContainer className={className} $isGalleryOpen={isGalleryOpen}>
<TileLoaderContainer>
<StyledBarLoader
width={tileSize}
height={tileSize}
isGalleryOpen={isGalleryOpen}
withMarginBottom
/>
<StyledBarLoader width={tileSize} height={tileSize} $withMarginBottom />
<StyledBarLoader width={107} height={14} />
</TileLoaderContainer>
<TileLoaderContainer>
<StyledBarLoader
width={tileSize}
height={tileSize}
isGalleryOpen={isGalleryOpen}
withMarginBottom
/>
<StyledBarLoader width={tileSize} height={tileSize} $withMarginBottom />
<StyledBarLoader width={107} height={14} />
</TileLoaderContainer>
</TilesLoaderContainer>

View File

@@ -2,7 +2,7 @@ import IconBitcoin from '@assets/img/dashboard/bitcoin_icon.svg';
import IconStacks from '@assets/img/dashboard/stx_icon.svg';
import OrdinalIcon from '@assets/img/transactions/ordinal.svg';
import RunesIcon from '@assets/img/transactions/runes.svg';
import { StyledBarLoader } from '@components/tilesSkeletonLoader';
import { StyledBarLoader } from '@components/tokenTile/loader';
import useWalletSelector from '@hooks/useWalletSelector';
import type { FungibleToken } from '@secretkeylabs/xverse-core';
import { XVERSE_ORDIVIEW_URL, type CurrencyTypes } from '@utils/constants';
@@ -12,9 +12,9 @@ import styled from 'styled-components';
const DEFAULT_SIZE = 40;
const TickerImage = styled.img<{ size?: number }>((props) => ({
height: props.size ?? DEFAULT_SIZE,
width: props.size ?? DEFAULT_SIZE,
const TickerImage = styled.img<{ $size?: number }>((props) => ({
height: props.$size ?? DEFAULT_SIZE,
width: props.$size ?? DEFAULT_SIZE,
borderRadius: '50%',
}));
@@ -24,12 +24,12 @@ const LoaderImageContainer = styled.div({
justifyContent: 'center',
});
const TickerIconContainer = styled.div<{ size?: number; round?: boolean }>((props) => ({
const TickerIconContainer = styled.div<{ $size?: number; $round?: boolean }>((props) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: props.size ?? DEFAULT_SIZE,
width: props.size ?? DEFAULT_SIZE,
height: props.$size ?? DEFAULT_SIZE,
width: props.$size ?? DEFAULT_SIZE,
borderRadius: '50%',
backgroundColor: props.theme.colors.white_850,
}));
@@ -48,12 +48,12 @@ const TickerProtocolContainer = styled.div`
display: inline-flex;
`;
const ProtocolIcon = styled.div<{ isSquare?: boolean }>((props) => ({
width: props.isSquare ? 18 : 20,
height: props.isSquare ? 18 : 20,
borderRadius: props.isSquare ? 0 : 20,
const ProtocolIcon = styled.div<{ $isSquare?: boolean }>((props) => ({
width: props.$isSquare ? 18 : 20,
height: props.$isSquare ? 18 : 20,
borderRadius: props.$isSquare ? 0 : 20,
position: 'absolute',
right: props.isSquare ? -9 : -10,
right: props.$isSquare ? -9 : -10,
bottom: -2,
backgroundColor: props.theme.colors.elevation0,
padding: 2,
@@ -64,7 +64,7 @@ const ProtocolImage = styled.img({
width: '100%',
});
interface TokenImageProps {
type Props = {
currency?: CurrencyTypes;
fungibleToken?: FungibleToken;
loading?: boolean;
@@ -72,7 +72,7 @@ interface TokenImageProps {
round?: boolean;
showProtocolIcon?: boolean;
customProtocolIcon?: string;
}
};
export default function TokenImage({
currency,
@@ -82,7 +82,7 @@ export default function TokenImage({
round,
showProtocolIcon = true,
customProtocolIcon,
}: TokenImageProps) {
}: Props) {
const { network } = useWalletSelector();
const ftProtocol = fungibleToken?.protocol;
const [imageError, setImageError] = useState(false);
@@ -101,7 +101,7 @@ export default function TokenImage({
(fungibleToken?.name ? getTicker(fungibleToken.name) : fungibleToken?.assetName || '');
const tickerComponent = () => (
<TickerIconContainer size={size} round={round}>
<TickerIconContainer $size={size} $round={round}>
<TickerIconText data-testid="token-image">{ticker.substring(0, 4)}</TickerIconText>
</TickerIconContainer>
);
@@ -129,18 +129,19 @@ export default function TokenImage({
return (
<TickerImage
data-testid="token-image"
size={size}
$size={size}
src={getCurrencyIcon()}
onError={() => setImageError(true)}
/>
);
}
if (fungibleToken.protocol === 'runes') {
if (fungibleToken.runeInscriptionId) {
return (
<TickerImage
data-testid="token-image"
size={size}
$size={size}
src={`${XVERSE_ORDIVIEW_URL(network.type)}/thumbnail/${
fungibleToken.runeInscriptionId
}`}
@@ -148,24 +149,27 @@ export default function TokenImage({
/>
);
}
if (fungibleToken?.runeSymbol) {
return (
<TickerIconContainer size={size} round={round}>
<TickerIconContainer $size={size} $round={round}>
<TickerIconText data-testid="token-image">{fungibleToken.runeSymbol}</TickerIconText>
</TickerIconContainer>
);
}
}
if (fungibleToken?.image) {
return (
<TickerImage
data-testid="token-image"
size={size}
$size={size}
src={fungibleToken.image}
onError={() => setImageError(true)}
/>
);
}
return tickerComponent();
};
@@ -173,7 +177,11 @@ export default function TokenImage({
return (
<TickerProtocolContainer>
<LoaderImageContainer>
<StyledBarLoader width={size ?? DEFAULT_SIZE} height={size ?? DEFAULT_SIZE} />
<StyledBarLoader
width={size ?? DEFAULT_SIZE}
height={size ?? DEFAULT_SIZE}
$borderRadius={100}
/>
</LoaderImageContainer>
</TickerProtocolContainer>
);
@@ -183,7 +191,7 @@ export default function TokenImage({
<TickerProtocolContainer>
{imageError ? tickerComponent() : renderIcon()}
{showProtocolIcon && protocolIcon && (
<ProtocolIcon isSquare={ftProtocol === 'runes'}>{protocolIcon}</ProtocolIcon>
<ProtocolIcon $isSquare={ftProtocol === 'runes'}>{protocolIcon}</ProtocolIcon>
)}
</TickerProtocolContainer>
);

View File

@@ -1,5 +1,5 @@
import { BetterBarLoader } from '@components/barLoader';
import { StyledFiatAmountText } from '@components/fiatAmountText';
import { BestBarLoader } from '@components/barLoader';
import FiatAmountText from '@components/fiatAmountText';
import TokenImage from '@components/tokenImage';
import useSelectedAccountBtcBalance from '@hooks/queries/useSelectedAccountBtcBalance';
import useStxWalletData from '@hooks/queries/useStxWalletData';
@@ -22,16 +22,17 @@ const TileContainer = styled.button((props) => ({
borderRadius: props.theme.radius(2),
}));
const RowContainer = styled.div({
const RowContainer = styled.div((props) => ({
flex: '1 0 auto',
display: 'flex',
});
columnGap: props.theme.space.m,
}));
const TextContainer = styled.div((props) => ({
marginLeft: props.theme.space.m,
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
rowGap: props.theme.space.xxxs,
}));
const AmountContainer = styled.div((props) => ({
@@ -40,6 +41,7 @@ const AmountContainer = styled.div((props) => ({
marginLeft: props.theme.space.xxs,
overflow: 'hidden',
alignItems: 'flex-end',
rowGap: props.theme.space.xxxs,
}));
const LoaderMainContainer = styled.div({
@@ -52,23 +54,29 @@ const LoaderMainContainer = styled.div({
const CoinTickerText = styled.p((props) => ({
...props.theme.typography.body_bold_m,
color: props.theme.colors.white_0,
lineHeight: '140%',
minHeight: 20,
}));
const SubText = styled.p<{ fullWidth: boolean }>((props) => ({
...props.theme.typography.body_medium_s,
const SubText = styled.p<{ $fullWidth: boolean }>((props) => ({
...props.theme.typography.body_medium_m,
color: props.theme.colors.white_200,
minHeight: 20,
lineHeight: '140%',
textAlign: 'left',
maxWidth: props.fullWidth ? 'unset' : 120,
whiteSpace: props.fullWidth ? 'normal' : 'nowrap',
maxWidth: props.$fullWidth ? 'unset' : 120,
whiteSpace: props.$fullWidth ? 'normal' : 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}));
const CoinBalanceText = styled.p((props) => ({
...props.theme.typography.body_medium_m,
...props.theme.typography.body_bold_m,
color: props.theme.colors.white_0,
overflow: 'hidden',
textOverflow: 'ellipsis',
minHeight: 20,
lineHeight: '140%',
maxWidth: '100%',
}));
@@ -79,14 +87,19 @@ const TokenTitleContainer = styled.div({
justifyContent: 'flex-start',
});
const StyledBarLoader = styled(BetterBarLoader)<{
withMarginBottom?: boolean;
const StyledBarLoader = styled(BestBarLoader)<{
$withMarginBottom?: boolean;
}>((props) => ({
padding: 0,
borderRadius: props.theme.radius(1),
marginBottom: props.withMarginBottom ? props.theme.space.xxs : 0,
marginBottom: props.$withMarginBottom ? props.theme.space.xxxs : 0,
}));
const StyledFiatAmountText = styled(FiatAmountText)`
${(props) => props.theme.typography.body_medium_m}
color: ${(props) => props.theme.colors.white_200};
line-height: 140%;
min-height: 20px;
`;
const CoinBalanceContainer = styled.div`
${(props) => props.theme.typography.body_medium_m}
color: ${(props) => props.theme.colors.white_0};
@@ -100,8 +113,8 @@ const FiatAmountContainer = styled.div`
function TokenLoader() {
return (
<LoaderMainContainer>
<StyledBarLoader width={80} height={16} withMarginBottom />
<StyledBarLoader width={70} height={14} />
<StyledBarLoader width={53} height={20} $withMarginBottom />
<StyledBarLoader width={151} height={20} />
</LoaderMainContainer>
);
}
@@ -168,7 +181,7 @@ function TokenTile({
<TextContainer>
<CoinTickerText>{getTickerTitle()}</CoinTickerText>
<TokenTitleContainer>
<SubText aria-label="Token SubTitle" fullWidth={hideSwapBalance}>
<SubText aria-label="Token SubTitle" $fullWidth={hideSwapBalance}>
{title}
</SubText>
</TokenTitleContainer>

View File

@@ -0,0 +1,62 @@
import { BestBarLoader } from '@components/barLoader';
import styled from 'styled-components';
const LoaderContainer = styled.div`
display: flex;
align-items: center;
gap: ${(props) => props.theme.space.s};
padding: ${(props) => props.theme.space.m} 0;
`;
export const StyledBarLoader = styled(BestBarLoader)<{
$withMarginBottom?: boolean;
$borderRadius?: number;
}>((props) => ({
borderRadius: props.$borderRadius,
marginBottom: props.$withMarginBottom ? props.theme.space.s : 0,
}));
const IconContainer = styled.div`
display: flex;
`;
const ContentContainer = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
`;
const LeftColumn = styled.div`
display: flex;
flex-direction: column;
gap: ${(props) => props.theme.space.xxxs};
`;
const RightColumn = styled.div`
display: flex;
flex-direction: column;
align-items: flex-end;
gap: ${(props) => props.theme.space.xxxs};
`;
function TokenTileLoader() {
return (
<LoaderContainer>
<IconContainer>
<StyledBarLoader width={32} height={32} $borderRadius={100} />
</IconContainer>
<ContentContainer>
<LeftColumn>
<StyledBarLoader width={56} height={20} />
<StyledBarLoader width={70} height={20} />
</LeftColumn>
<RightColumn>
<StyledBarLoader width={53} height={20} />
<StyledBarLoader width={151} height={20} />
</RightColumn>
</ContentContainer>
</LoaderContainer>
);
}
export default TokenTileLoader;

View File

@@ -80,6 +80,7 @@ export const useGetBrc20FungibleTokens = (select?: (data: FungibleTokenWithState
queryKey: ['brc20-fungible-tokens', ordinalsAddress, network.type, fiatCurrency],
queryFn,
enabled: Boolean(network && ordinalsAddress),
keepPreviousData: true,
select: selectWithDerivedState,
});
};

View File

@@ -15,7 +15,10 @@ export const fetchRuneBalances =
fiatCurrency: string,
): (() => Promise<FungibleToken[]>) =>
async () => {
const runeBalances = await runesApi.getRuneFungibleTokens(ordinalsAddress, true);
const runeBalances: FungibleToken[] = await runesApi.getRuneFungibleTokens(
ordinalsAddress,
true,
);
if (!Array.isArray(runeBalances) || runeBalances.length === 0) {
return [];
@@ -66,6 +69,7 @@ export const useRuneFungibleTokensQuery = (
select: selectWithDerivedState,
refetchOnWindowFocus: !!backgroundRefetch,
refetchOnReconnect: !!backgroundRefetch,
keepPreviousData: true,
});
};

View File

@@ -72,6 +72,7 @@ export const useGetSip10FungibleTokens = (select?: (data: FungibleTokenWithState
queryKey: ['sip10-fungible-tokens', network.type, stxAddress, fiatCurrency],
queryFn,
enabled: Boolean(network && stxAddress),
keepPreviousData: true,
select: selectWithDerivedState,
});
};

View File

@@ -17,6 +17,7 @@ const useBtcWalletData = () => {
queryFn: fetchBtcWalletData,
enabled: !!btcAddress,
staleTime: 10 * 1000, // 10 secs
keepPreviousData: true,
});
};

View File

@@ -8,16 +8,22 @@ export default function useSelectedAccountBtcBalance() {
data: nativeBalance,
isLoading: nativeLoading,
isRefetching: nativeRefetching,
failureCount: nativeFailureCount,
errorUpdateCount: nativeErrorUpdateCount,
} = useBtcAddressBalance(selectedAccount.btcAddresses.native?.address ?? '');
const {
data: nestedBalance,
isLoading: nestedLoading,
isRefetching: nestedRefetching,
failureCount: nestedFailureCount,
errorUpdateCount: nestedErrorUpdateCount,
} = useBtcAddressBalance(selectedAccount.btcAddresses.nested?.address ?? '');
const {
data: taprootBalance,
isLoading: taprootLoading,
isRefetching: taprootRefetching,
failureCount: taprootFailureCount,
errorUpdateCount: taprootErrorUpdateCount,
} = useBtcAddressBalance(selectedAccount.btcAddresses.taproot.address ?? '');
if (nativeLoading || nestedLoading || taprootLoading) {
@@ -42,5 +48,7 @@ export default function useSelectedAccountBtcBalance() {
taprootBalance,
isLoading: false,
isRefetching: nativeRefetching || nestedRefetching || taprootRefetching,
failureCount: nativeFailureCount || nestedFailureCount || taprootFailureCount,
errorUpdateCount: nativeErrorUpdateCount || nestedErrorUpdateCount || taprootErrorUpdateCount,
};
}

View File

@@ -21,6 +21,7 @@ const useStxWalletData = () => {
queryFn: fetchStxWalletData,
enabled: !!stxAddress,
staleTime: 10 * 1000, // 10 secs
keepPreviousData: true,
});
};

View File

@@ -25,5 +25,6 @@ export default function useBtcAddressBalance(address: string) {
queryKey: ['btc-address-balance', address],
queryFn: fetchBalance,
staleTime: 10 * 1000, // 10 secs
keepPreviousData: true,
});
}

View File

@@ -1,4 +1,5 @@
import RequestsRoutes from '@common/utils/route-urls';
import AnimatedScreenContainer from '@components/animatedScreenContainer';
import ExtendedScreenContainer from '@components/extendedScreenContainer';
import AuthGuard from '@components/guards/auth';
import OnboardingGuard from '@components/guards/onboarding';
@@ -92,6 +93,21 @@ import { createHashRouter } from 'react-router-dom';
import RoutePaths from './paths';
const router = createHashRouter([
{
path: '/',
element: <AnimatedScreenContainer />,
errorElement: <ErrorBoundary />,
children: [
{
index: true,
element: (
<AuthGuard>
<Home />
</AuthGuard>
),
},
],
},
{
path: '/',
element: <ScreenContainer />,
@@ -121,14 +137,6 @@ const router = createHashRouter([
</AuthGuard>
),
},
{
index: true,
element: (
<AuthGuard>
<Home />
</AuthGuard>
),
},
{
path: 'legal',
element: (
@@ -286,7 +294,7 @@ const router = createHashRouter([
element: <SpeedUpTransactionScreen />,
},
{
path: 'create-inscription',
path: 'create-inscription', // used by our legacy style inscriptions methods for the inscription service
element: (
<AuthGuard>
<CreateInscription />
@@ -294,7 +302,7 @@ const router = createHashRouter([
),
},
{
path: 'create-repeat-inscriptions',
path: 'create-repeat-inscriptions', // used by our legacy style inscriptions methods for the inscription service
element: (
<AuthGuard>
<CreateInscription />

View File

@@ -57,7 +57,7 @@ const Title = styled.div((props) => ({
function AccountList(): JSX.Element {
const { t } = useTranslation('translation', { keyPrefix: 'ACCOUNT_SCREEN' });
const navigate = useNavigate();
const { search } = useLocation();
const { search, state } = useLocation();
const params = new URLSearchParams(search);
const selectedAccount = useSelectedAccount();
const { network, accountsList, ledgerAccountsList } = useWalletSelector();
@@ -72,7 +72,7 @@ function AccountList(): JSX.Element {
}, [accountsList, ledgerAccountsList, network]);
const handleBackButtonClick = () => {
navigate(-1);
navigate(state?.from || -1);
};
const handleAccountSelect = async (account: Account, goBack = true) => {

View File

@@ -3,8 +3,8 @@ import styled from 'styled-components';
export const Container = styled.div((props) => ({
display: 'flex',
flexDirection: 'column',
paddingLeft: props.theme.spacing(8),
paddingRight: props.theme.spacing(8),
paddingLeft: props.theme.space.m,
paddingRight: props.theme.space.m,
}));
export const RowContainer = styled.div({
@@ -20,7 +20,7 @@ export const ProtocolText = styled.p((props) => ({
height: 15,
marginTop: props.theme.spacing(3),
textTransform: 'uppercase',
marginLeft: props.theme.spacing(2),
marginLeft: props.theme.space.xxs,
backgroundColor: props.theme.colors.white_400,
padding: '1px 6px 1px',
color: props.theme.colors.elevation0,
@@ -51,7 +51,7 @@ export const FiatAmountText = styled.p((props) => ({
...props.theme.headline_category_s,
color: props.theme.colors.white_200,
fontSize: '0.875rem',
marginTop: props.theme.spacing(2),
marginTop: props.theme.space.xxs,
textAlign: 'center',
cursor: 'pointer',
}));
@@ -60,14 +60,14 @@ export const BalanceTitleText = styled.p((props) => ({
...props.theme.typography.body_medium_m,
color: props.theme.colors.white_400,
textAlign: 'center',
marginTop: props.theme.spacing(4),
marginTop: props.theme.space.xs,
}));
export const RowButtonContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
marginTop: props.theme.spacing(11),
marginTop: props.theme.space.l,
columnGap: props.theme.space.l,
}));
@@ -75,8 +75,8 @@ export const HeaderSeparator = styled.div((props) => ({
border: `0.5px solid ${props.theme.colors.white_400}`,
width: '50%',
alignSelf: 'center',
marginTop: props.theme.spacing(8),
marginBottom: props.theme.spacing(8),
marginTop: props.theme.space.m,
marginBottom: props.theme.space.m,
}));
export const StxLockedText = styled.p((props) => ({
@@ -100,7 +100,7 @@ export const AvailableStxContainer = styled.div((props) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginTop: props.theme.spacing(4),
marginTop: props.theme.space.xs,
span: {
color: props.theme.colors.white_400,
marginRight: props.theme.spacing(3),
@@ -108,13 +108,13 @@ export const AvailableStxContainer = styled.div((props) => ({
}));
export const VerifyOrViewContainer = styled.div((props) => ({
margin: props.theme.spacing(8),
marginTop: props.theme.spacing(16),
marginBottom: props.theme.spacing(20),
margin: props.theme.space.m,
marginTop: props.theme.space.xl,
marginBottom: props.theme.space.xxl,
}));
export const VerifyButtonContainer = styled.div((props) => ({
marginBottom: props.theme.spacing(6),
marginBottom: props.theme.space.s,
}));
export const StacksLockedInfoText = styled.span((props) => ({

View File

@@ -6,7 +6,7 @@ import ArrowSwap from '@assets/img/icons/ArrowSwap.svg';
import Lock from '@assets/img/transactions/Lock.svg';
import BottomModal from '@components/bottomModal';
import ActionButton from '@components/button';
import SmallActionButton from '@components/smallActionButton';
import SquareButton from '@components/squareButton';
import TokenImage from '@components/tokenImage';
import useSelectedAccountBtcBalance from '@hooks/queries/useSelectedAccountBtcBalance';
import useStxWalletData from '@hooks/queries/useStxWalletData';
@@ -283,24 +283,18 @@ export default function CoinHeader({ currency, fungibleToken }: Props) {
</BalanceInfoContainer>
{renderStackingBalances()}
<RowButtonContainer>
<SmallActionButton src={ArrowUp} text={t('SEND')} onPress={goToSendScreen} />
<SmallActionButton src={ArrowDown} text={t('RECEIVE')} onPress={navigateToReceive} />
{showSwaps && (
<SmallActionButton src={ArrowSwap} text={t('SWAP')} onPress={navigateToSwaps} />
)}
<SquareButton src={ArrowUp} text={t('SEND')} onPress={goToSendScreen} />
<SquareButton src={ArrowDown} text={t('RECEIVE')} onPress={navigateToReceive} />
{showSwaps && <SquareButton src={ArrowSwap} text={t('SWAP')} onPress={navigateToSwaps} />}
{showRunesListing && (
<SmallActionButton
<SquareButton
src={List}
text={t('LIST')}
onPress={() => navigate(`/list-rune/${fungibleToken.principal}`)}
/>
)}
{!fungibleToken && (
<SmallActionButton
src={Buy}
text={t('BUY')}
onPress={() => navigate(`/buy/${currency}`)}
/>
<SquareButton src={Buy} text={t('BUY')} onPress={() => navigate(`/buy/${currency}`)} />
)}
</RowButtonContainer>
<BottomModal

View File

@@ -105,7 +105,7 @@ function AuthenticationRequest() {
const [isTxRejected, setIsTxRejected] = useState(false);
const { t } = useTranslation('translation', { keyPrefix: 'AUTH_REQUEST_SCREEN' });
const navigate = useNavigate();
const { search } = useLocation();
const { search, pathname } = useLocation();
const params = new URLSearchParams(search);
const authRequestToken = params.get('authRequest') ?? '';
const authRequest = decodeToken(authRequestToken) as unknown as AuthRequest;
@@ -267,7 +267,7 @@ function AuthenticationRequest() {
};
const handleSwitchAccount = () => {
navigate('/account-list?hideListActions=true');
navigate('/account-list?hideListActions=true', { state: { from: pathname } });
};
const handleAddStxLedgerAccount = async () => {

View File

@@ -13,7 +13,7 @@ import Spinner from '@ui-library/spinner';
import { trackMixPanel } from '@utils/mixpanel';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import AddressPurposeBox from '../addressPurposeBox';
import PermissionsList from '../permissionsList';
import {
@@ -33,6 +33,7 @@ import useBtcAddressRequest from './useBtcAddressRequest';
function BtcSelectAddressScreen() {
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { pathname } = useLocation();
const { t } = useTranslation('translation', { keyPrefix: 'SELECT_BTC_ADDRESS_SCREEN' });
const selectedAccount = useSelectedAccount();
const { network } = useWalletSelector();
@@ -165,7 +166,7 @@ function BtcSelectAddressScreen() {
);
const handleSwitchAccount = () => {
navigate('/account-list?hideListActions=true');
navigate('/account-list?hideListActions=true', { state: { from: pathname } });
};
if (isLoadingIcon) {

View File

@@ -11,7 +11,7 @@ import { StickyHorizontalSplitButtonContainer } from '@ui-library/common.styled'
import Spinner from '@ui-library/spinner';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import AddressPurposeBox from '../addressPurposeBox';
import {
AddressBoxContainer,
@@ -29,6 +29,7 @@ import PermissionsList from '../permissionsList';
function StxSelectAccountScreen() {
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { pathname } = useLocation();
const { t } = useTranslation('translation', { keyPrefix: 'SELECT_BTC_ADDRESS_SCREEN' });
const selectedAccount = useSelectedAccount();
const { network } = useWalletSelector();
@@ -85,7 +86,7 @@ function StxSelectAccountScreen() {
}, [origin]);
const handleSwitchAccount = () => {
navigate('/account-list?hideListActions=true');
navigate('/account-list?hideListActions=true', { state: { from: pathname } });
};
if (isLoadingIcon) {

View File

@@ -11,7 +11,7 @@ import { StickyHorizontalSplitButtonContainer } from '@ui-library/common.styled'
import Spinner from '@ui-library/spinner';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import AddressPurposeBox from '../addressPurposeBox';
import {
AddressBoxContainer,
@@ -29,10 +29,11 @@ import PermissionsList from '../permissionsList';
function StxSelectAddressScreen() {
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { pathname } = useLocation();
const { t } = useTranslation('translation', { keyPrefix: 'SELECT_BTC_ADDRESS_SCREEN' });
const selectedAccount = useSelectedAccount();
const { network } = useWalletSelector();
const [appIcon, setAppIcon] = useState<string>('');
const [appIcon, setAppIcon] = useState('');
const [isLoadingIcon, setIsLoadingIcon] = useState(false);
const { payload, origin, approveStxAddressRequest, cancelAddressRequest } =
useStxAddressRequest();
@@ -85,7 +86,7 @@ function StxSelectAddressScreen() {
}, [origin]);
const handleSwitchAccount = () => {
navigate('/account-list?hideListActions=true');
navigate('/account-list?hideListActions=true', { state: { from: pathname } });
};
if (isLoadingIcon) {

View File

@@ -1,14 +1,15 @@
import useSelectedAccount from '@hooks/useSelectedAccount';
import SelectAccount from '@screens/connect/selectAccount';
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
/* eslint-disable import/prefer-default-export */
export function SelectAccountPrompt() {
const selectedAccount = useSelectedAccount();
const navigate = useNavigate();
const { pathname } = useLocation();
const handleSwitchAccount = () => {
navigate('/account-list?hideListActions=true');
navigate('/account-list?hideListActions=true', { state: { from: pathname } });
};
return <SelectAccount account={selectedAccount} handlePressAccount={handleSwitchAccount} />;

View File

@@ -75,26 +75,28 @@ function ExploreScreen() {
const category = recommended?.filter((r) => r.category === activeTab);
return isLoading ? (
<LoaderContainer>
<Spinner color="white" size={30} />
</LoaderContainer>
) : (
return (
<>
<Container>
<StyledHeading typography="headline_l">{t('TITLE')}</StyledHeading>
<ExternalLink href={XVERSE_EXPLORE_URL} target="_blank" rel="noreferrer">
<ArrowsOut size={16} />
{t('EXPAND_VIEW')}
</ExternalLink>
<Subheader>
{t('FEATURED')}
<SwiperNavigation />
</Subheader>
{!!featured?.length && <FeaturedCardCarousel items={featured} />}
{tabs && <Tabs tabs={tabs} activeTab={activeTab} onTabClick={setActiveTab} />}
{!!category?.length && <RecommendedApps items={category} />}
</Container>
{isLoading ? (
<LoaderContainer>
<Spinner color="white" size={30} />
</LoaderContainer>
) : (
<Container>
<StyledHeading typography="headline_l">{t('TITLE')}</StyledHeading>
<ExternalLink href={XVERSE_EXPLORE_URL} target="_blank" rel="noreferrer">
<ArrowsOut size={16} />
{t('EXPAND_VIEW')}
</ExternalLink>
<Subheader>
{t('FEATURED')}
<SwiperNavigation />
</Subheader>
{!!featured?.length && <FeaturedCardCarousel items={featured} />}
{tabs && <Tabs tabs={tabs} activeTab={activeTab} onTabClick={setActiveTab} />}
{!!category?.length && <RecommendedApps items={category} />}
</Container>
)}
<BottomBar tab="explore" />
</>
);

View File

@@ -1,4 +1,4 @@
import BarLoader from '@components/barLoader';
import { BestBarLoader } from '@components/barLoader';
import { useVisibleBrc20FungibleTokens } from '@hooks/queries/ordinals/useGetBrc20FungibleTokens';
import { useVisibleRuneFungibleTokens } from '@hooks/queries/runes/useRuneFungibleTokensQuery';
import { useVisibleSip10FungibleTokens } from '@hooks/queries/stx/useGetSip10FungibleTokens';
@@ -8,47 +8,53 @@ import useStxWalletData from '@hooks/queries/useStxWalletData';
import useSupportedCoinRates from '@hooks/queries/useSupportedCoinRates';
import useSelectedAccount from '@hooks/useSelectedAccount';
import useWalletSelector from '@hooks/useWalletSelector';
import { animated, useTransition } from '@react-spring/web';
import { currencySymbolMap } from '@secretkeylabs/xverse-core';
import { setBalanceHiddenToggleAction } from '@stores/wallet/actions/actionCreators';
import Spinner from '@ui-library/spinner';
import { HIDDEN_BALANCE_LABEL, LoaderSize } from '@utils/constants';
import { ANIMATION_EASING, HIDDEN_BALANCE_LABEL } from '@utils/constants';
import { calculateTotalBalance, getAccountBalanceKey } from '@utils/helper';
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { NumericFormat } from 'react-number-format';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
const Container = styled.div`
position: relative;
min-height: 62px; // it's the height of RowContainer + BalanceContainer + indent between them
margin-top: ${({ theme }) => theme.space.m};
`;
const RowContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginTop: props.theme.spacing(11),
marginBottom: props.theme.space.xs,
columnGap: props.theme.space.xxs,
minHeight: 20,
}));
const BalanceHeadingText = styled.h3((props) => ({
...props.theme.headline_category_s,
const BalanceHeadingText = styled.p((props) => ({
...props.theme.typography.body_medium_m,
color: props.theme.colors.white_200,
textTransform: 'uppercase',
opacity: 0.7,
lineHeight: '140%',
}));
const CurrencyText = styled.label((props) => ({
...props.theme.headline_category_s,
...props.theme.typography.body_medium_m,
color: props.theme.colors.white_0,
fontSize: 13,
}));
const BalanceAmountText = styled.p((props) => ({
...props.theme.headline_xl,
...props.theme.typography.headline_l,
lineHeight: '1',
color: props.theme.colors.white_0,
}));
const BarLoaderContainer = styled.div((props) => ({
const BarLoaderContainer = styled.div({
display: 'flex',
maxWidth: 300,
marginTop: props.theme.spacing(5),
}));
});
const CurrencyCard = styled.div((props) => ({
display: 'flex',
@@ -56,7 +62,6 @@ const CurrencyCard = styled.div((props) => ({
backgroundColor: props.theme.colors.elevation3,
width: 45,
borderRadius: 30,
marginLeft: props.theme.spacing(4),
}));
const BalanceContainer = styled.div((props) => ({
@@ -66,15 +71,23 @@ const BalanceContainer = styled.div((props) => ({
width: 'fit-content',
alignItems: 'center',
gap: props.theme.spacing(5),
minHeight: 34,
cursor: 'pointer',
}));
interface BalanceCardProps {
const ContentWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
`;
type Props = {
isLoading: boolean;
isRefetching: boolean;
}
};
function BalanceCard(props: BalanceCardProps) {
function BalanceCard({ isLoading, isRefetching }: Props) {
const { t } = useTranslation('translation', { keyPrefix: 'DASHBOARD_SCREEN' });
const selectedAccount = useSelectedAccount();
const dispatch = useDispatch();
@@ -84,7 +97,6 @@ function BalanceCard(props: BalanceCardProps) {
const { data: stxData } = useStxWalletData();
const { btcFiatRate, stxBtcRate } = useSupportedCoinRates();
const { setAccountBalance } = useAccountBalance();
const { isLoading, isRefetching } = props;
// TODO: refactor this into a hook
const oldTotalBalance = accountBalances[getAccountBalanceKey(selectedAccount)];
const { data: sip10CoinsList } = useVisibleSip10FungibleTokens();
@@ -138,46 +150,85 @@ function BalanceCard(props: BalanceCardProps) {
if (event.key === 'Enter') onClickBalance();
};
const isInitialMount = useRef(true);
useEffect(() => {
isInitialMount.current = false;
}, []);
const loaderTransitions = useTransition(isLoading, {
from: { opacity: isInitialMount.current ? 1 : 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
config: {
duration: 400,
easing: ANIMATION_EASING,
},
exitBeforeEnter: true, // This ensures the leave animation completes before the enter animation starts
});
return (
<>
<RowContainer>
<BalanceHeadingText>{t('TOTAL_BALANCE')}</BalanceHeadingText>
<CurrencyCard>
<CurrencyText data-testid="currency-text">{fiatCurrency}</CurrencyText>
</CurrencyCard>
</RowContainer>
{isLoading ? (
<BarLoaderContainer>
<BarLoader loaderSize={LoaderSize.LARGE} />
</BarLoaderContainer>
) : (
<BalanceContainer onClick={onClickBalance} role="button" tabIndex={0} onKeyDown={onKeyDown}>
{balanceHidden && (
<BalanceAmountText data-testid="total-balance-value">
{HIDDEN_BALANCE_LABEL}
</BalanceAmountText>
)}
{!balanceHidden && (
<>
<NumericFormat
value={balance}
displayType="text"
prefix={`${currencySymbolMap[fiatCurrency]}`}
thousandSeparator
renderText={(value: string) => (
<BalanceAmountText data-testid="total-balance-value">{value}</BalanceAmountText>
)}
/>
{isRefetching && (
<div>
<Spinner color="white" size={16} />
</div>
)}
</>
)}
</BalanceContainer>
)}
</>
<Container>
{loaderTransitions((style, loading) => (
<ContentWrapper>
<animated.div style={style}>
{loading ? (
<>
<RowContainer>
<BarLoaderContainer>
<BestBarLoader width={76.5} height={20} />
</BarLoaderContainer>
</RowContainer>
<BarLoaderContainer>
<BestBarLoader width={244} height={34} />
</BarLoaderContainer>
</>
) : (
<>
<RowContainer>
<BalanceHeadingText>{t('TOTAL_BALANCE')}</BalanceHeadingText>
<CurrencyCard>
<CurrencyText data-testid="currency-text">{fiatCurrency}</CurrencyText>
</CurrencyCard>
</RowContainer>
<BalanceContainer
onClick={onClickBalance}
role="button"
tabIndex={0}
onKeyDown={onKeyDown}
>
{balanceHidden && (
<BalanceAmountText data-testid="total-balance-value">
{HIDDEN_BALANCE_LABEL}
</BalanceAmountText>
)}
{!balanceHidden && (
<>
<NumericFormat
value={balance}
displayType="text"
prefix={`${currencySymbolMap[fiatCurrency]}`}
thousandSeparator
renderText={(value: string) => (
<BalanceAmountText data-testid="total-balance-value">
{value}
</BalanceAmountText>
)}
/>
{isRefetching && (
<div>
<Spinner color="white" size={16} />
</div>
)}
</>
)}
</BalanceContainer>
</>
)}
</animated.div>
</ContentWrapper>
))}
</Container>
);
}

View File

@@ -13,8 +13,8 @@ import Banner from './banner';
const CarouselContainer = styled.div`
position: relative;
margin-top: ${({ theme }) => theme.space.xxs};
margin-bottom: ${({ theme }) => theme.space.xxs};
padding-top: ${({ theme }) => theme.space.xxs};
padding-bottom: ${({ theme }) => theme.space.xxs};
.swiper {
padding: 0;

View File

@@ -17,7 +17,7 @@ export const ColumnContainer = styled.div((props) => ({
alignItems: 'space-between',
justifyContent: 'space-between',
marginTop: props.theme.space.xs,
marginBottom: props.theme.space.s,
marginBottom: props.theme.space.l,
}));
export const ReceiveContainer = styled.div((props) => ({
@@ -26,44 +26,40 @@ export const ReceiveContainer = styled.div((props) => ({
gap: props.theme.space.m,
}));
export const TokenListButtonContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
marginBottom: props.theme.space.xxxl,
}));
export const TokenListButton = styled.button((props) => ({
...props.theme.typography.body_medium_m,
cursor: props.disabled ? 'not-allowed' : 'pointer',
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
gap: props.theme.space.xs,
borderRadius: props.theme.radius(1),
backgroundColor: 'transparent',
opacity: 0.8,
marginTop: props.theme.spacing(5),
cursor: props.disabled ? 'not-allowed' : 'pointer',
}));
export const ButtonText = styled.div((props) => ({
...props.theme.typography.body_s,
fontWeight: 700,
color: props.theme.colors.white_0,
textAlign: 'center',
}));
export const ButtonImage = styled.img((props) => ({
marginRight: props.theme.spacing(3),
alignSelf: 'center',
transform: 'all',
opacity: 0.8,
transition: 'opacity 0.1s ease',
'&:hover': {
opacity: 1,
},
'&:active, &:disabled': {
opacity: 0.6,
},
}));
export const RowButtonContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
marginTop: props.theme.spacing(11),
columnGap: props.theme.spacing(11),
}));
export const TokenListButtonContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
marginTop: props.theme.space.s,
marginBottom: props.theme.spacing(22),
marginTop: props.theme.space.l,
marginBottom: props.theme.space.xl,
columnGap: props.theme.space.l,
}));
export const StyledTokenTile = styled(TokenTile)`
@@ -155,7 +151,10 @@ export const IconBackground = styled.div((props) => ({
alignItems: 'center',
}));
export const StyledDivider = styled(Divider)<{ $noMarginBottom?: boolean }>`
export const StyledDivider = styled(Divider)<{
$noMarginBottom?: boolean;
$noMarginTop?: boolean;
}>`
flex: 1 0 auto;
width: calc(100% + ${(props) => props.theme.space.xl});
margin-left: -${(props) => props.theme.space.m};
@@ -166,10 +165,11 @@ export const StyledDivider = styled(Divider)<{ $noMarginBottom?: boolean }>`
`
margin-bottom: 0;
`}
`;
export const StyledDividerSingle = styled(StyledDivider)`
margin-bottom: 0;
${(props) =>
props.$noMarginTop &&
`
margin-top: 0;
`}
`;
export const SpacedCallout = styled(Callout)((props) => ({

View File

@@ -1,9 +1,9 @@
import dashboardIcon from '@assets/img/dashboard-icon.svg';
import ListDashes from '@assets/img/dashboard/list_dashes.svg';
import ArrowSwap from '@assets/img/icons/ArrowSwap.svg';
import AccountHeaderComponent from '@components/accountHeader';
import BottomModal from '@components/bottomModal';
import BottomBar from '@components/tabBar';
import TokenTileLoader from '@components/tokenTile/loader';
import {
useGetBrc20FungibleTokens,
useVisibleBrc20FungibleTokens,
@@ -28,7 +28,7 @@ import useNotificationBanners from '@hooks/useNotificationBanners';
import useSelectedAccount from '@hooks/useSelectedAccount';
import useTrackMixPanelPageViewed from '@hooks/useTrackMixPanelPageViewed';
import useWalletSelector from '@hooks/useWalletSelector';
import { ArrowDown, ArrowUp, Plus } from '@phosphor-icons/react';
import { ArrowDown, ArrowUp, ListDashes, Plus } from '@phosphor-icons/react';
import { animated, useTransition } from '@react-spring/web';
import CoinSelectModal from '@screens/home/coinSelectModal';
import {
@@ -46,11 +46,11 @@ import {
} from '@stores/wallet/actions/actionCreators';
import Button from '@ui-library/button';
import SnackBar from '@ui-library/snackBar';
import type { CurrencyTypes } from '@utils/constants';
import { ANIMATION_EASING, type CurrencyTypes } from '@utils/constants';
import { isInOptions, isLedgerAccount } from '@utils/helper';
import { optInMixPanel, optOutMixPanel, trackMixPanel } from '@utils/mixpanel';
import { sortFtByFiatBalance } from '@utils/tokens';
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import toast from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
@@ -61,8 +61,6 @@ import AnnouncementModal from './announcementModal';
import BalanceCard from './balanceCard';
import BannerCarousel from './bannerCarousel';
import {
ButtonImage,
ButtonText,
ColumnContainer,
Container,
ModalButtonContainer,
@@ -73,7 +71,6 @@ import {
ModalTitle,
RowButtonContainer,
StyledDivider,
StyledDividerSingle,
StyledTokenTile,
TokenListButton,
TokenListButtonContainer,
@@ -93,33 +90,108 @@ function Home() {
const [openReceiveModal, setOpenReceiveModal] = useState(false);
const [openSendModal, setOpenSendModal] = useState(false);
const [openBuyModal, setOpenBuyModal] = useState(false);
const { isLoading: loadingBtcWalletData, isRefetching: refetchingBtcWalletData } =
useSelectedAccountBtcBalance();
const { isInitialLoading: loadingStxWalletData, isRefetching: refetchingStxWalletData } =
useStxWalletData();
const {
isLoading: isInitialLoadingBtc,
isRefetching: refetchingBtcWalletData,
failureCount: failureCountBtc,
errorUpdateCount: errorUpdateCountBtc,
} = useSelectedAccountBtcBalance();
const { btcFiatRate, stxBtcRate } = useSupportedCoinRates();
const { data: notificationBannersArr, isFetching: isFetchingNotificationBannersArr } =
useNotificationBanners();
// Fetching balances
const { data: fullSip10CoinsList } = useGetSip10FungibleTokens();
const { data: fullBrc20CoinsList } = useGetBrc20FungibleTokens();
const { data: fullRunesCoinsList } = useRuneFungibleTokensQuery();
const {
isInitialLoading: isInitialLoadingStx,
isRefetching: refetchingStxWalletData,
failureCount: failureCountStx,
errorUpdateCount: errorUpdateCountStx,
} = useStxWalletData();
const {
data: sip10CoinsList,
isInitialLoading: loadingStxCoinData,
isInitialLoading: isInitialLoadingSip10,
isRefetching: refetchingStxCoinData,
failureCount: failureCountSip10,
errorUpdateCount: errorUpdateCountSip10,
} = useVisibleSip10FungibleTokens();
const {
data: brc20CoinsList,
isInitialLoading: loadingBrcCoinData,
isInitialLoading: isInitialLoadingBrc20,
isRefetching: refetchingBrcCoinData,
failureCount: failureCountBrc20,
errorUpdateCount: errorUpdateCountBrc20,
} = useVisibleBrc20FungibleTokens();
const {
data: runesCoinsList,
isInitialLoading: loadingRunesData,
isInitialLoading: isInitialLoadingRunes,
isRefetching: refetchingRunesData,
errorUpdateCount: errorUpdateCountRunes,
failureCount: failureCountRunes,
} = useVisibleRuneFungibleTokens();
const isInitialLoadingTokens =
(isInitialLoadingSip10 && failureCountSip10 === 0 && errorUpdateCountSip10 === 0) ||
(isInitialLoadingBrc20 && failureCountBrc20 === 0 && errorUpdateCountBrc20 === 0) ||
(isInitialLoadingRunes && failureCountRunes === 0 && errorUpdateCountRunes === 0);
const isInitialLoading =
(isInitialLoadingBtc && failureCountBtc === 0 && errorUpdateCountBtc === 0) ||
(isInitialLoadingStx && failureCountStx === 0 && errorUpdateCountStx === 0) ||
isInitialLoadingTokens;
const isRefetching =
refetchingBtcWalletData ||
refetchingStxWalletData ||
refetchingStxCoinData ||
refetchingBrcCoinData ||
refetchingRunesData;
useEffect(() => {
const errorChecks = [
{
condition: failureCountBtc === 1 && errorUpdateCountBtc === 0,
message: 'ERRORS.BTC_BALANCE',
},
{
condition: failureCountStx === 1 && errorUpdateCountStx === 0,
message: 'ERRORS.STX_BALANCE',
},
{
condition: failureCountSip10 === 1 && errorUpdateCountSip10 === 0,
message: 'ERRORS.SIP10_BALANCE',
},
{
condition: failureCountBrc20 === 1 && errorUpdateCountBrc20 === 0,
message: 'ERRORS.BRC20_BALANCE',
},
{
condition: failureCountRunes === 1 && errorUpdateCountRunes === 0,
message: 'ERRORS.RUNES_BALANCE',
},
];
errorChecks.forEach(({ condition, message }) => {
if (condition) {
toast.error(t(message));
}
});
}, [
t,
failureCountBtc,
errorUpdateCountBtc,
failureCountStx,
errorUpdateCountStx,
failureCountSip10,
errorUpdateCountSip10,
failureCountBrc20,
errorUpdateCountBrc20,
failureCountRunes,
errorUpdateCountRunes,
]);
useFeeMultipliers();
useAppConfig();
useAvatarCleanup();
@@ -296,38 +368,42 @@ function Home() {
dispatch(changeShowDataCollectionAlertAction(false));
};
const isInitialMount = useRef(true);
useEffect(() => {
isInitialMount.current = false;
}, []);
const isCrossChainSwapsEnabled = useHasFeature(FeatureId.CROSS_CHAIN_SWAPS);
const showSwaps = isCrossChainSwapsEnabled;
const transitions = useTransition(showBannerCarousel, {
from: { maxHeight: '1000px', opacity: 0.5 },
enter: { maxHeight: '1000px', opacity: 1 },
const loaderTransitions = useTransition(isInitialLoading, {
from: { opacity: isInitialMount.current ? 1 : 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
config: {
duration: 400,
easing: ANIMATION_EASING,
},
exitBeforeEnter: true, // This ensures the leave animation completes before the enter animation starts
});
const bannerTransitions = useTransition(showBannerCarousel && !isInitialLoading, {
from: { maxHeight: '102px', opacity: isInitialMount.current ? 1 : 0 },
enter: { maxHeight: '102px', opacity: 1 },
leave: { maxHeight: '0px', opacity: 0 },
config: (item, index, phase) =>
phase === 'leave'
? {
duration: 300,
easing: (progress) => 1 - (1 - progress) ** 4,
}
: {
duration: 200,
},
config: {
duration: 400,
easing: ANIMATION_EASING,
},
exitBeforeEnter: true, // This ensures the leave animation completes before the enter animation starts
});
return (
<>
<AccountHeaderComponent />
<Container>
<BalanceCard
isLoading={loadingStxWalletData || loadingBtcWalletData}
isRefetching={
refetchingBtcWalletData ||
refetchingStxWalletData ||
refetchingStxCoinData ||
refetchingBrcCoinData ||
refetchingRunesData
}
/>
<BalanceCard isLoading={isInitialLoading} isRefetching={isRefetching} />
<RowButtonContainer data-testid="transaction-buttons-row">
<SquareButton
icon={<ArrowUp weight="regular" size="20" />}
@@ -347,62 +423,69 @@ function Home() {
/>
</RowButtonContainer>
{transitions((style, item) =>
{bannerTransitions((style, item) =>
item ? (
<animated.div style={style}>
<br />
<StyledDivider color="white_850" verticalMargin="m" />
<StyledDivider color="white_850" $verticalMargin="m" $noMarginTop />
<BannerCarousel items={filteredNotificationBannersArr} />
<StyledDivider
color="white_850"
verticalMargin="m"
$verticalMargin="m"
$noMarginBottom={filteredNotificationBannersArr.length === 1}
/>
</animated.div>
) : (
<animated.div style={style}>
<StyledDividerSingle color="elevation3" verticalMargin="xl" />
<StyledDivider color="elevation3" />
</animated.div>
),
)}
<ColumnContainer>
{btcAddress && (
<StyledTokenTile
title={t('BITCOIN')}
currency="BTC"
loading={loadingBtcWalletData}
onPress={handleTokenPressed}
/>
{loaderTransitions((style, loading) =>
loading ? (
<animated.div style={style}>
<TokenTileLoader />
<TokenTileLoader />
<TokenTileLoader />
<TokenTileLoader />
</animated.div>
) : (
<animated.div style={style}>
{btcAddress && (
<StyledTokenTile
title={t('BITCOIN')}
currency="BTC"
loading={isInitialLoadingBtc}
onPress={handleTokenPressed}
/>
)}
{stxAddress && !hideStx && (
<StyledTokenTile
title={t('STACKS')}
currency="STX"
loading={isInitialLoadingStx}
onPress={handleTokenPressed}
/>
)}
{combinedFtList.map((coin) => (
<StyledTokenTile
key={coin.principal}
title={coin.name}
currency="FT"
loading={isInitialLoadingTokens}
fungibleToken={coin}
onPress={handleTokenPressed}
/>
))}
</animated.div>
),
)}
{stxAddress && !hideStx && (
<StyledTokenTile
title={t('STACKS')}
currency="STX"
loading={loadingStxWalletData}
onPress={handleTokenPressed}
/>
)}
{combinedFtList.map((coin: FungibleTokenWithStates) => {
const isLoading = loadingStxCoinData || loadingBrcCoinData || loadingRunesData;
return (
<StyledTokenTile
key={coin.principal}
title={coin.name}
currency="FT"
loading={isLoading}
fungibleToken={coin}
onPress={handleTokenPressed}
/>
);
})}
</ColumnContainer>
<TokenListButtonContainer>
<TokenListButton onClick={handleManageTokenListOnClick}>
<>
<ButtonImage src={ListDashes} />
<ButtonText>{t('MANAGE_TOKEN')}</ButtonText>
</>
<ListDashes size={20} />
{t('MANAGE_TOKEN')}
</TokenListButton>
</TokenListButtonContainer>
@@ -416,7 +499,7 @@ function Home() {
visible={openSendModal}
coins={combinedFtList}
title={t('SEND')}
loadingWalletData={loadingStxWalletData || loadingBtcWalletData}
loadingWalletData={isInitialLoadingStx || isInitialLoadingBtc}
/>
<CoinSelectModal
onSelectBitcoin={onBuyBtcClick}
@@ -426,7 +509,7 @@ function Home() {
visible={openBuyModal}
coins={[]}
title={t('BUY')}
loadingWalletData={loadingStxWalletData || loadingBtcWalletData}
loadingWalletData={isInitialLoadingStx || isInitialLoadingBtc}
/>
<AnnouncementModal />
</Container>

View File

@@ -0,0 +1,187 @@
import Button from '@ui-library/button';
import styled, { keyframes } from 'styled-components';
const slideYAndOpacity = keyframes`
0% {
opacity: 0;
transform: translateY(49px);
}
100% {
opacity: 1;
transform: translateY(0);
}
`;
const slideY = keyframes`
0% {
transform: translateY(49px);
}
100% {
transform: translateY(0);
}
`;
const slideOpacity = keyframes`
0% {
opacity: 0;
}
100% {
opacity: 1;
}
`;
const slideRightAnimation = keyframes`
0% {
opacity: 0;
transform: translateX(240px);
}
100% {
opacity: 1;
transform: translateX(0);
}
`;
const slideLeftAnimation = keyframes`
0% {
opacity: 0;
transform: translateX(-240px);
}
100% {
opacity: 1;
transform: translateX(0);
}
`;
export const Container = styled.div`
display: flex;
flex-direction: column;
flex: 1;
&::-webkit-scrollbar {
display: none;
}
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
overflow: hidden;
`;
export const AppVersion = styled.p((props) => ({
...props.theme.typography.body_s,
color: props.theme.colors.white_0,
textAlign: 'right',
marginRight: props.theme.spacing(9),
marginTop: props.theme.spacing(8),
position: 'relative',
}));
export const ArrowContainer = styled.div`
position: relative;
top: 209px;
margin-left: ${(props) => props.theme.space.s};
margin-right: ${(props) => props.theme.space.s};
justify-content: space-between;
display: flex;
flex-direction: row;
animation: ${() => slideOpacity} 0.2s ease-out;
z-index: 1;
`;
export const CaretButton = styled.button<{ disabled: boolean }>((props) => ({
backgroundColor: 'transparent',
cursor: props.disabled ? 'default' : 'pointer',
svg: {
opacity: props.disabled ? 0.6 : 1,
transition: 'opacity 0.1s ease',
},
'&:hover:enabled, &:focus:enabled': {
svg: {
opacity: 0.8,
},
},
}));
export const AnimationContainer = styled.div({
marginTop: '60%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
});
const LandingSectionContainer = styled.div({
marginTop: '50%',
marginBottom: '88px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
});
export const InitialTransitionLandingSectionContainer = styled(LandingSectionContainer)`
animation: ${() => slideY} 0.2s ease-out;
`;
export const TransitionLandingSectionContainer = styled(LandingSectionContainer)<{
$direction: 'left' | 'right';
}>((props) => ({
animation: `${
props.$direction === 'left' ? slideLeftAnimation : slideRightAnimation
} 0.3s cubic-bezier(0, 0, 0.58, 1) forwards`,
}));
export const Logo = styled.img`
width: 135px;
height: 25px;
`;
export const LandingTitle = styled.h1`
${(props) => props.theme.typography.body_medium_l}
margin-top: ${(props) => props.theme.space.m};
color: ${(props) => props.theme.colors.white_200};
text-align: center;
`;
const OnboardingContainer = styled.div((props) => ({
marginBottom: props.theme.space.l,
}));
export const TransitionOnboardingContainer = styled(OnboardingContainer)<{
$direction: 'left' | 'right';
}>((props) => ({
animation: `${
props.$direction === 'left' ? slideLeftAnimation : slideRightAnimation
} 0.3s cubic-bezier(0, 0, 0.58, 1) forwards`,
}));
export const OnBoardingImage = styled.img(() => ({
marginTop: -26,
alignSelf: 'center',
transform: 'all',
}));
export const OnBoardingContent = styled.div((props) => ({
marginTop: props.theme.spacing(24),
padding: `0 22px`,
}));
export const OnboardingTitle = styled.h1<{ needPadding: boolean }>((props) => ({
...props.theme.typography.headline_xs,
textAlign: 'center',
paddingLeft: props.needPadding ? 20 : 0,
paddingRight: props.needPadding ? 20 : 0,
}));
export const BottomContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-left: ${(props) => props.theme.space.m};
margin-right: ${(props) => props.theme.space.m};
animation: ${() => slideYAndOpacity} 0.2s ease-out;
`;
export const CreateButton = styled(Button)((props) => ({
marginTop: props.theme.space.l,
}));
export const RestoreButton = styled(Button)((props) => ({
marginTop: props.theme.space.s,
}));

View File

@@ -4,203 +4,35 @@ import onboarding1 from '@assets/img/landing/onboarding1.svg';
import onboarding2 from '@assets/img/landing/onboarding2.svg';
import Dots from '@components/dots';
import { CaretLeft, CaretRight } from '@phosphor-icons/react';
import Button from '@ui-library/button';
import { isInOptions } from '@utils/helper';
import { getIsTermsAccepted } from '@utils/localStorage';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Lottie from 'react-lottie';
import { useNavigate } from 'react-router-dom';
import styled, { keyframes, useTheme } from 'styled-components';
const slideYAndOpacity = keyframes`
0% {
opacity: 0;
transform: translateY(49px);
}
100% {
opacity: 1;
transform: translateY(0);
}
`;
const slideY = keyframes`
0% {
transform: translateY(49px);
}
100% {
transform: translateY(0);
}
`;
const slideOpacity = keyframes`
0% {
opacity: 0;
}
100% {
opacity: 1;
}
`;
const slideRightAnimation = keyframes`
0% {
opacity: 0;
transform: translateX(240px);
}
100% {
opacity: 1;
transform: translateX(0);
}
`;
const slideLeftAnimation = keyframes`
0% {
opacity: 0;
transform: translateX(-240px);
}
100% {
opacity: 1;
transform: translateX(0);
}
`;
const Container = styled.div`
display: flex;
flex-direction: column;
flex: 1;
&::-webkit-scrollbar {
display: none;
}
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
overflow: hidden;
`;
const AppVersion = styled.p((props) => ({
...props.theme.typography.body_s,
color: props.theme.colors.white_0,
textAlign: 'right',
marginRight: props.theme.spacing(9),
marginTop: props.theme.spacing(8),
position: 'relative',
}));
const ArrowContainer = styled.div`
position: relative;
top: 209px;
margin-left: ${(props) => props.theme.space.s};
margin-right: ${(props) => props.theme.space.s};
justify-content: space-between;
display: flex;
flex-direction: row;
animation: ${() => slideOpacity} 0.2s ease-out;
z-index: 1;
`;
const CaretButton = styled.button<{ disabled: boolean }>((props) => ({
backgroundColor: 'transparent',
cursor: props.disabled ? 'default' : 'pointer',
svg: {
opacity: props.disabled ? 0.6 : 1,
transition: 'opacity 0.1s ease',
},
'&:hover:enabled, &:focus:enabled': {
svg: {
opacity: 0.8,
},
},
}));
const AnimationContainer = styled.div({
marginTop: '60%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
});
const LandingSectionContainer = styled.div({
marginTop: '50%',
marginBottom: '88px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
});
const InitialTransitionLandingSectionContainer = styled(LandingSectionContainer)`
animation: ${() => slideY} 0.2s ease-out;
`;
const TransitionLandingSectionContainer = styled(LandingSectionContainer)<{
$direction: 'left' | 'right';
}>((props) => ({
animation: `${
props.$direction === 'left' ? slideLeftAnimation : slideRightAnimation
} 0.3s cubic-bezier(0, 0, 0.58, 1) forwards`,
}));
const Logo = styled.img`
width: 135px;
height: 25px;
`;
const LandingTitle = styled.h1`
${(props) => props.theme.typography.body_medium_l}
margin-top: ${(props) => props.theme.space.m};
color: ${(props) => props.theme.colors.white_200};
text-align: center;
`;
const OnboardingContainer = styled.div((props) => ({
marginBottom: props.theme.space.l,
}));
const TransitionOnboardingContainer = styled(OnboardingContainer)<{
$direction: 'left' | 'right';
}>((props) => ({
animation: `${
props.$direction === 'left' ? slideLeftAnimation : slideRightAnimation
} 0.3s cubic-bezier(0, 0, 0.58, 1) forwards`,
}));
const OnBoardingImage = styled.img(() => ({
marginTop: -26,
alignSelf: 'center',
transform: 'all',
}));
const OnBoardingContent = styled.div((props) => ({
marginTop: props.theme.spacing(24),
padding: `0 22px`,
}));
const OnboardingTitle = styled.h1<{ needPadding: boolean }>((props) => ({
...props.theme.typography.headline_xs,
textAlign: 'center',
paddingLeft: props.needPadding ? 20 : 0,
paddingRight: props.needPadding ? 20 : 0,
}));
const BottomContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-left: ${(props) => props.theme.space.m};
margin-right: ${(props) => props.theme.space.m};
animation: ${() => slideYAndOpacity} 0.2s ease-out;
`;
const CreateButton = styled(Button)((props) => ({
marginTop: props.theme.space.l,
}));
const RestoreButton = styled(Button)((props) => ({
marginTop: props.theme.space.s,
}));
import { useTheme } from 'styled-components';
import {
AnimationContainer,
AppVersion,
ArrowContainer,
BottomContainer,
CaretButton,
Container,
CreateButton,
InitialTransitionLandingSectionContainer,
LandingTitle,
Logo,
OnBoardingContent,
OnBoardingImage,
OnboardingTitle,
RestoreButton,
TransitionLandingSectionContainer,
TransitionOnboardingContainer,
} from './index.styled';
function Landing() {
const { t } = useTranslation('translation', { keyPrefix: 'LANDING_SCREEN' });
const [currentStepIndex, setCurrentStepIndex] = useState<number>(0);
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [animationComplete, setAnimationComplete] = useState(
process.env.SKIP_ANIMATION_WALLET_STARTUP === 'true',
);

View File

@@ -0,0 +1,104 @@
import { animated } from '@react-spring/web';
import styled from 'styled-components';
export const Logo = styled.img({
width: 57,
height: 57,
});
export const IconButton = styled.button({
background: 'none',
});
export const ScreenContainer = styled(animated.div)((props) => ({
display: 'flex',
flexDirection: 'column',
flex: 1,
paddingLeft: props.theme.spacing(9),
paddingRight: props.theme.spacing(9),
overflowY: 'auto',
'&::-webkit-scrollbar': {
display: 'none',
},
}));
export const ContentContainer = styled(animated.div)({
display: 'flex',
flexDirection: 'column',
flex: 1,
});
export const AppVersion = styled.p((props) => ({
...props.theme.typography.body_s,
color: props.theme.colors.white_0,
textAlign: 'right',
marginTop: props.theme.space.m,
}));
export const TopSectionContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
marginTop: props.theme.spacing(60),
marginBottom: props.theme.spacing(30),
}));
export const PasswordInputLabel = styled.h2((props) => ({
...props.theme.typography.body_medium_m,
textAlign: 'left',
marginTop: props.theme.spacing(15.5),
}));
export const PasswordInputContainer = styled.div((props) => ({
display: 'flex',
alignItems: 'center',
width: '100%',
border: `1px solid ${props.theme.colors.elevation3}`,
paddingLeft: props.theme.space.m,
paddingRight: props.theme.space.m,
borderRadius: props.theme.radius(1),
marginTop: props.theme.space.xs,
}));
export const PasswordInput = styled.input((props) => ({
...props.theme.typography.body_medium_m,
height: 44,
backgroundColor: props.theme.colors.elevation0,
color: props.theme.colors.white_0,
width: '100%',
border: 'none',
}));
export const LandingTitle = styled.h1((props) => ({
...props.theme.tile_text,
paddingTop: props.theme.spacing(15),
paddingLeft: props.theme.spacing(34),
paddingRight: props.theme.spacing(34),
color: props.theme.colors.white_200,
textAlign: 'center',
}));
export const ButtonContainer = styled.div((props) => ({
marginTop: props.theme.space.m,
width: '100%',
}));
export const ErrorMessage = styled.h2((props) => ({
...props.theme.typography.body_medium_m,
textAlign: 'left',
color: props.theme.colors.feedback.error,
marginTop: props.theme.space.xs,
}));
export const ForgotPasswordButton = styled.button((props) => ({
...props.theme.typography.body_m,
textAlign: 'center',
marginTop: props.theme.space.l,
color: props.theme.colors.white_0,
textDecoration: 'underline',
backgroundColor: 'transparent',
':hover': {
textDecoration: 'none',
},
}));

View File

@@ -4,121 +4,34 @@ import logo from '@assets/img/xverse_logo.svg';
import useSeedVault from '@hooks/useSeedVault';
import useSeedVaultMigration from '@hooks/useSeedVaultMigration';
import useWalletReducer from '@hooks/useWalletReducer';
import { animated, useSpring } from '@react-spring/web';
import { useSpring } from '@react-spring/web';
import MigrationConfirmation from '@screens/migrationConfirmation';
import { AnalyticsEvents } from '@secretkeylabs/xverse-core';
import Button from '@ui-library/button';
import { trackMixPanel } from '@utils/mixpanel';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
const Logo = styled.img({
width: 57,
height: 57,
});
const IconButton = styled.button({
background: 'none',
});
const ScreenContainer = styled(animated.div)((props) => ({
display: 'flex',
flexDirection: 'column',
flex: 1,
paddingLeft: props.theme.spacing(9),
paddingRight: props.theme.spacing(9),
overflowY: 'auto',
'&::-webkit-scrollbar': {
display: 'none',
},
}));
const ContentContainer = styled(animated.div)({
display: 'flex',
flexDirection: 'column',
flex: 1,
});
const AppVersion = styled.p((props) => ({
...props.theme.typography.body_s,
color: props.theme.colors.white_0,
textAlign: 'right',
marginTop: props.theme.space.m,
}));
const TopSectionContainer = styled.div((props) => ({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
marginTop: props.theme.spacing(60),
marginBottom: props.theme.spacing(30),
}));
const PasswordInputLabel = styled.h2((props) => ({
...props.theme.typography.body_medium_m,
textAlign: 'left',
marginTop: props.theme.spacing(15.5),
}));
const PasswordInputContainer = styled.div((props) => ({
display: 'flex',
alignItems: 'center',
width: '100%',
border: `1px solid ${props.theme.colors.elevation3}`,
paddingLeft: props.theme.space.m,
paddingRight: props.theme.space.m,
borderRadius: props.theme.radius(1),
marginTop: props.theme.space.xs,
}));
const PasswordInput = styled.input((props) => ({
...props.theme.typography.body_medium_m,
height: 44,
backgroundColor: props.theme.colors.elevation0,
color: props.theme.colors.white_0,
width: '100%',
border: 'none',
}));
const LandingTitle = styled.h1((props) => ({
...props.theme.tile_text,
paddingTop: props.theme.spacing(15),
paddingLeft: props.theme.spacing(34),
paddingRight: props.theme.spacing(34),
color: props.theme.colors.white_200,
textAlign: 'center',
}));
const ButtonContainer = styled.div((props) => ({
marginTop: props.theme.space.m,
width: '100%',
}));
const ErrorMessage = styled.h2((props) => ({
...props.theme.typography.body_medium_m,
textAlign: 'left',
color: props.theme.colors.feedback.error,
marginTop: props.theme.space.xs,
}));
const ForgotPasswordButton = styled.button((props) => ({
...props.theme.typography.body_m,
textAlign: 'center',
marginTop: props.theme.space.l,
color: props.theme.colors.white_0,
textDecoration: 'underline',
backgroundColor: 'transparent',
':hover': {
textDecoration: 'none',
},
}));
import { useLocation, useNavigate } from 'react-router-dom';
import {
AppVersion,
ButtonContainer,
ContentContainer,
ErrorMessage,
ForgotPasswordButton,
IconButton,
LandingTitle,
Logo,
PasswordInput,
PasswordInputContainer,
PasswordInputLabel,
ScreenContainer,
TopSectionContainer,
} from './index.styled';
function Login(): JSX.Element {
const { t } = useTranslation('translation', { keyPrefix: 'LOGIN_SCREEN' });
const navigate = useNavigate();
const { state } = useLocation();
const { unlockWallet } = useWalletReducer();
const { hasSeed } = useSeedVault();
const { migrateCachedStorage, isVaultUpdated } = useSeedVaultMigration();
@@ -160,6 +73,7 @@ function Login(): JSX.Element {
const handleTogglePasswordView = () => {
setIsPasswordVisible(!isPasswordVisible);
};
const handlePasswordChange = (event: React.FormEvent<HTMLInputElement>) => {
if (error) {
setError('');
@@ -175,11 +89,19 @@ function Login(): JSX.Element {
setShowMigration(true);
} else {
setIsVerifying(false);
navigate(-1);
if (state?.from) {
navigate(state?.from, { state: { from: '/login' } }); // this is needed for AnimatedScreenContainer where we check the state.from
} else {
navigate(-1);
}
}
} catch (err) {
setIsVerifying(false);
navigate(-1);
if (state?.from) {
navigate(state?.from, { state: { from: '/login' } }); // this is needed for AnimatedScreenContainer where we check the state.from
} else {
navigate(-1);
}
}
};

View File

@@ -200,7 +200,7 @@ function NftDashboardHidden() {
return (
<>
{isGalleryOpen ? (
<AccountHeaderComponent disableMenuOption={isGalleryOpen} showBorderBottom={false} />
<AccountHeaderComponent disableMenuOption={isGalleryOpen} />
) : (
<TopRow
onClick={handleBackButtonClick}

View File

@@ -89,7 +89,7 @@ function NftDashboard() {
{isOrdinalReceiveAlertVisible && (
<ShowOrdinalReceiveAlert onOrdinalReceiveAlertClose={onOrdinalReceiveAlertClose} />
)}
<AccountHeaderComponent disableMenuOption={isGalleryOpen} showBorderBottom={false} />
<AccountHeaderComponent disableMenuOption={isGalleryOpen} />
<Container>
<PageHeader>
<CollectibleContainer>

View File

@@ -2,15 +2,15 @@ import styled from 'styled-components';
import Theme from 'theme';
const Divider = styled.div<{
verticalMargin: keyof typeof Theme.space;
color?: keyof typeof Theme.colors;
$verticalMargin?: keyof typeof Theme.space;
$color?: keyof typeof Theme.colors;
}>((props) => ({
display: 'flex',
width: '100%',
height: 1,
backgroundColor: props.color
? String(props.theme.colors[props.color])
backgroundColor: props.$color
? String(props.theme.colors[props.$color])
: props.theme.colors.white_900,
margin: `${props.theme.space[props.verticalMargin]} 0`,
margin: props.$verticalMargin ? `${props.theme.space[props.$verticalMargin]} 0` : 0,
}));
export default Divider;

View File

@@ -66,6 +66,7 @@ export const DEFAULT_TRANSITION_OPTIONS = {
export const MAX_ACC_NAME_LENGTH = 20;
// UI
export const ANIMATION_EASING = (progress: number) => 1 - (1 - progress) ** 3; // ease out (0, 0, 0.58, 1)
export const EMPTY_LABEL = '--';
export const HIDDEN_BALANCE_LABEL = '●●●●●●';
export const OPTIONS_DIALOG_WIDTH = 179;

View File

@@ -1,8 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 4H13.5" stroke="white" stroke-opacity="0.8" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.00049 8H13.5001" stroke="white" stroke-opacity="0.8" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.00049 12H13.5001" stroke="white" stroke-opacity="0.8" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.5 4H3.5" stroke="white" stroke-opacity="0.8" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.50049 8H3.5001" stroke="white" stroke-opacity="0.8" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.50049 12H3.5001" stroke="white" stroke-opacity="0.8" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 884 B

View File

@@ -207,7 +207,7 @@
"AMOUNT": "Amount"
},
"DASHBOARD_SCREEN": {
"TOTAL_BALANCE": "total balance",
"TOTAL_BALANCE": "Total Balance",
"BITCOIN": "Bitcoin",
"STACKS": "Stacks",
"ACCOUNT_NAME": "Account",
@@ -235,6 +235,13 @@
},
"TOKEN_HIDDEN": "Token hidden and reported",
"UNDO": "Undo",
"ERRORS": {
"BTC_BALANCE": "Failed to load Bitcoin balance",
"STX_BALANCE": "Failed to load Stacks balance",
"SIP10_BALANCE": "Failed to load SIP10 token balances",
"BRC20_BALANCE": "Failed to load BRC20 token balances",
"RUNES_BALANCE": "Failed to load Rune balances"
},
"ANNOUNCEMENTS": {
"NATIVE_SEGWIT": {
"TITLE": "Native SegWit is here!",

View File

@@ -483,9 +483,9 @@ export default class Wallet {
this.backToGallery = page.getByTestId('back-to-gallery');
this.itemCollection = page.getByTestId('collection-item');
this.buttonSend = page.getByRole('button', { name: 'Send' });
this.buttonShare = page.getByRole('button', { name: 'Share' });
this.buttonReceive = page.getByRole('button', { name: 'Receive', exact: true });
this.buttonSend = page.locator('button').filter({ hasText: 'Send' });
this.buttonShare = page.locator('button').filter({ hasText: 'Share' });
this.buttonReceive = page.locator('button').filter({ hasText: 'Receive' });
this.buttonOpenOrdinalViewer = page.getByRole('button', { name: 'Open in Ordinal Viewer' });
this.labelBundle = page.locator('h1').filter({ hasText: 'Bundle' });
this.labelSatsValue = page.locator('h1').filter({ hasText: 'Sats value' });
@@ -620,6 +620,12 @@ export default class Wallet {
}
async checkVisualsStartpage() {
// Wait for the balance element to be present in the DOM
await this.page.waitForSelector('[data-testid="total-balance-value"]', { state: 'attached' });
// Wait for a short duration to allow the animation to complete
await this.page.waitForTimeout(400);
await expect(this.balance).toBeVisible();
await expect(this.manageTokenButton).toBeVisible();