Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore: refactor containers #5619

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/app/common/page/page.context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ReactNode, createContext, useContext, useEffect, useReducer } from 'react';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm placing this here for now as I'm not sure where it should be located


import { RouteUrls } from '@shared/route-urls';

interface HeaderPayloadState {
title?: string;
isSummaryPage?: boolean;
isSessionLocked?: boolean;
isSettingsVisibleOnSm?: boolean;
onBackLocation?: RouteUrls;
onClose?(): void;
}

interface UpdateAction {
type: 'update';
payload: HeaderPayloadState;
}

interface ResetAction {
type: 'reset';
}
type Action = UpdateAction | ResetAction;

const initialPageState = { isSessionLocked: false, isSettingsVisibleOnSm: true };
const pageReducer = (state: HeaderPayloadState, action: Action): HeaderPayloadState => {
switch (action.type) {
case 'update':
return { ...state, ...action.payload };
case 'reset':
default:
return initialPageState;
}
};

const PageContext = createContext<
{ state: HeaderPayloadState; dispatch: React.Dispatch<Action> } | undefined
>(undefined);

export function PageProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(pageReducer, initialPageState);
const value = { state, dispatch };
return <PageContext.Provider value={value}>{children}</PageContext.Provider>;
}

export const usePageContext = () => {
const context = useContext(PageContext);
if (context === undefined) {
throw new Error('usePageContext must be used within a PageProvider');
}
return context;
};

Copy link
Contributor Author

@pete-watters pete-watters Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using a context to help dynamically update the page header as sometimes it needs to vary based on the page.

That is working well generally apart from on the Send flow when adding the title causes a re-render. I can try and fix that but I wonder if context is the best approach here.

See here

I tried using OutletContext and route state but hit a similar issue. I will keep working on it but maybe someone else has better ideas?

export function useUpdatePageHeaderContext(payload: HeaderPayloadState) {
const { dispatch } = usePageContext();

useEffect(() => {
dispatch({ type: 'update', payload });
return () => {
dispatch({ type: 'reset' });
};
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import get from 'lodash.get';

import { Button, Dialog } from '@leather.io/ui';

import { Footer } from '@app/ui/components/containers/footers/footer';
import { DialogHeader } from '@app/ui/components/containers/headers/dialog-header';
import { Footer } from '@app/ui/layout/containers/footers/footer';
import { DialogHeader } from '@app/ui/layout/containers/headers/dialog-header';

export function BroadcastErrorDialog() {
const navigate = useNavigate();
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/request-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { analytics } from '@shared/utils/analytics';
import { useKeyActions } from '@app/common/hooks/use-key-actions';
import { buildEnterKeyEvent } from '@app/common/hooks/use-modifier-key';
import { WaitingMessages, useWaitingMessage } from '@app/common/hooks/use-waiting-message';
import { Footer } from '@app/ui/components/containers/footers/footer';
import { Card } from '@app/ui/layout/card/card';
import { Footer } from '@app/ui/layout/containers/footers/footer';
import { Page } from '@app/ui/layout/page/page.layout';

import { ErrorLabel } from './error-label';
Expand Down
2 changes: 2 additions & 0 deletions src/app/features/add-network/add-network.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Stack, styled } from 'leather-styles/jsx';

import { Button } from '@leather.io/ui';

import { useUpdatePageHeaderContext } from '@app/common/page/page.context';
import { ErrorLabel } from '@app/components/error-label';
import { Card } from '@app/ui/layout/card/card';
import { Page } from '@app/ui/layout/page/page.layout';
Expand All @@ -13,6 +14,7 @@ import { useAddNetwork } from './use-add-network';

export function AddNetwork() {
const { error, initialFormValues, loading, onSubmit } = useAddNetwork();
useUpdatePageHeaderContext({ title: 'Add Network' });

return (
<Page>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function Sip10TokenAssetList({
{tokens.map(token => (
<Sip10TokenAssetItem
balance={token.balance}
key={token.info.name}
key={token.info.name + token.info.contractId}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this as it was throwing a duplicate keys error for ALEX token

info={token.info}
isLoading={isLoading}
marketData={priceAsMarketData(
Expand Down
2 changes: 1 addition & 1 deletion src/app/features/bitcoin-choose-fee/bitcoin-choose-fee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { BitcoinCustomFee } from '@app/components/bitcoin-custom-fee/bitcoin-cus
import { MAX_FEE_RATE_MULTIPLIER } from '@app/components/bitcoin-custom-fee/hooks/use-bitcoin-custom-fee';
import { OnChooseFeeArgs } from '@app/components/bitcoin-fees-list/bitcoin-fees-list';
import { useCurrentBtcCryptoAssetBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks';
import { AvailableBalance } from '@app/ui/components/containers/footers/available-balance';
import { AvailableBalance } from '@app/ui/layout/containers/footers/available-balance';

import { BitcoinChooseFeeLayout } from './components/bitcoin-choose-fee.layout';
import { ChooseFeeSubtitle } from './components/choose-fee-subtitle';
Expand Down
148 changes: 4 additions & 144 deletions src/app/features/container/container.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,24 @@
import { useEffect, useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';

import { ChainID } from '@stacks/transactions';
import { OnboardingSelectors } from '@tests/selectors/onboarding.selectors';
import { SettingsSelectors } from '@tests/selectors/settings.selectors';
import { Box } from 'leather-styles/jsx';

import { Flag, HamburgerIcon, Logo, NetworkModeBadge } from '@leather.io/ui';
import { useEffect } from 'react';
import { Outlet, useLocation } from 'react-router-dom';

import { RouteUrls } from '@shared/route-urls';
import { closeWindow } from '@shared/utils';
import { analytics } from '@shared/utils/analytics';

import { useInitalizeAnalytics } from '@app/common/app-analytics';
import { LoadingSpinner } from '@app/components/loading-spinner';
import { CurrentAccountAvatar } from '@app/features/current-account/current-account-avatar';
import { CurrentAccountName } from '@app/features/current-account/current-account-name';
import { SwitchAccountDialog } from '@app/features/dialogs/switch-account-dialog/switch-account-dialog';
import { InAppMessages } from '@app/features/hiro-messages/in-app-messages';
import { useOnSignOut } from '@app/routes/hooks/use-on-sign-out';
import { useOnWalletLock } from '@app/routes/hooks/use-on-wallet-lock';
import { useHasStateRehydrated } from '@app/store';
import { useCurrentNetworkState } from '@app/store/networks/networks.hooks';
import { ContainerLayout } from '@app/ui/components/containers/container.layout';
import { Header } from '@app/ui/components/containers/headers/header';

import { useRestoreFormState } from '../popup-send-form-restoration/use-restore-form-state';
import { Settings } from '../settings/settings';
import { TotalBalance } from './total-balance';
import {
getDisplayAddresssBalanceOf,
isKnownPopupRoute,
isRpcRoute,
showAccountInfo,
showBalanceInfo,
} from './utils/get-popup-header';
import { getTitleFromUrl } from './utils/get-title-from-url';
import {
canGoBack,
getIsSessionLocked,
getPageVariant,
hideLogo,
hideSettingsOnSm,
isLandingPage,
isNoHeaderPopup,
isSummaryPage,
} from './utils/route-helpers';

export function Container() {
const [isShowingSwitchAccount, setIsShowingSwitchAccount] = useState(false);
const navigate = useNavigate();
const { pathname: locationPathname } = useLocation();
const pathname = locationPathname as RouteUrls;

const hasStateRehydrated = useHasStateRehydrated();
const { chain, name: chainName } = useCurrentNetworkState();

useOnWalletLock(() => closeWindow());
useOnSignOut(() => closeWindow());
Expand All @@ -63,117 +27,13 @@ export function Container() {

useEffect(() => void analytics.page('view', `${pathname}`), [pathname]);

const variant = getPageVariant(pathname);

const displayHeader = !isLandingPage(pathname) && !isNoHeaderPopup(pathname);
const isSessionLocked = getIsSessionLocked(pathname);

// TODO: Refactor? This is very hard to manage with dynamic routes. Temporarily
// added a fix to catch the swap route: '/swap/:base/:quote?'
function getOnGoBackLocation(pathname: RouteUrls) {
if (pathname.includes('/swap')) return navigate(RouteUrls.Home);
switch (pathname) {
case RouteUrls.Fund.replace(':currency', 'STX'):
case RouteUrls.Fund.replace(':currency', 'BTC'):
case RouteUrls.SendCryptoAssetForm.replace(':symbol', 'stx'):
case RouteUrls.SendCryptoAssetForm.replace(':symbol', 'btc'):
return navigate(RouteUrls.Home);
case RouteUrls.SendStxConfirmation:
return navigate(RouteUrls.SendCryptoAssetForm.replace(':symbol', 'stx'));
case RouteUrls.SendBtcConfirmation:
return navigate(RouteUrls.SendCryptoAssetForm.replace(':symbol', 'btc'));
default:
return navigate(-1);
}
}

if (!hasStateRehydrated) return <LoadingSpinner />;

const showLogoSm = variant === 'home' || isSessionLocked || isKnownPopupRoute(pathname);
const hideSettings =
isKnownPopupRoute(pathname) || isSummaryPage(pathname) || variant === 'onboarding';

const isLogoClickable = variant !== 'home' && !isRpcRoute(pathname);
return (
<>
{isShowingSwitchAccount && (
<SwitchAccountDialog
isShowing={isShowingSwitchAccount}
onClose={() => setIsShowingSwitchAccount(false)}
/>
)}

<InAppMessages />
<ContainerLayout
header={
displayHeader ? (
<Header
variant={variant}
onGoBack={canGoBack(pathname) ? () => getOnGoBackLocation(pathname) : undefined}
onClose={isSummaryPage(pathname) ? () => navigate(RouteUrls.Home) : undefined}
settingsMenu={
hideSettings ? null : (
<Settings
triggerButton={
<HamburgerIcon
data-testid={SettingsSelectors.SettingsMenuBtn}
hideBelow={hideSettingsOnSm(pathname) ? 'sm' : undefined}
/>
}
toggleSwitchAccount={() => setIsShowingSwitchAccount(!isShowingSwitchAccount)}
/>
)
}
networkBadge={
<NetworkModeBadge
isTestnetChain={chain.stacks.chainId === ChainID.Testnet}
name={chainName}
/>
}
title={getTitleFromUrl(pathname)}
logo={
!hideLogo(pathname) && (
<Box
height="headerContainerHeight"
margin="auto"
px="space.02"
hideBelow={showLogoSm ? undefined : 'sm'}
hideFrom={isSessionLocked ? 'sm' : undefined}
>
<Logo
data-testid={OnboardingSelectors.LogoRouteToHome}
onClick={isLogoClickable ? () => navigate(RouteUrls.Home) : undefined}
/>
</Box>
)
}
account={
showAccountInfo(pathname) && (
<Flag
align="middle"
img={
<CurrentAccountAvatar
toggleSwitchAccount={() =>
setIsShowingSwitchAccount(!isShowingSwitchAccount)
}
/>
}
>
<CurrentAccountName />
</Flag>
)
}
totalBalance={
showBalanceInfo(pathname) && (
<TotalBalance displayAddresssBalanceOf={getDisplayAddresssBalanceOf(pathname)} />
)
}
/>
) : null
}
>
<Outlet context={{ isShowingSwitchAccount, setIsShowingSwitchAccount }} />
</ContainerLayout>

<Outlet />
</>
);
}
16 changes: 16 additions & 0 deletions src/app/features/container/layouts/components/container.layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Flex } from 'leather-styles/jsx';

interface ContainerLayoutProps {
content?: React.JSX.Element | React.JSX.Element[];
header?: React.JSX.Element | null;
}
export function ContainerLayout({ content, header }: ContainerLayoutProps) {
return (
<Flex flexDirection="column" flexGrow={1} width="100%" height={{ base: '100vh', sm: '100%' }}>
{header}
<Flex className="main-content" flexGrow={1} position="relative" width="100%">
{content}
</Flex>
</Flex>
);
}
53 changes: 53 additions & 0 deletions src/app/features/container/layouts/components/home-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ReactNode, useState } from 'react';

import { SettingsSelectors } from '@tests/selectors/settings.selectors';
import { Flex, Grid, GridItem, HStack, styled } from 'leather-styles/jsx';

import { HamburgerIcon } from '@leather.io/ui';

import { SwitchAccountDialog } from '@app/features/dialogs/switch-account-dialog/switch-account-dialog';

import { Settings } from '../../../settings/settings';

interface HomeHeaderProps {
networkBadge?: ReactNode;
logo?: ReactNode;
}

export function HomeHeader({ networkBadge, logo }: HomeHeaderProps) {
const [isShowingSwitchAccount, setIsShowingSwitchAccount] = useState(false);
return (
<>
{isShowingSwitchAccount && (
<SwitchAccountDialog
isShowing={isShowingSwitchAccount}
onClose={() => setIsShowingSwitchAccount(false)}
/>
)}
<styled.header
justifyContent="center"
margin={{ base: 0, md: 'auto' }}
p="space.04"
bg="transparent"
maxWidth={{ base: '100vw', md: 'fullPageMaxWidth' }}
width="100%"
>
<Grid alignItems="center" gridTemplateColumns="auto" gridAutoFlow="column" width="100%">
<GridItem justifySelf="start">
<Flex py={{ base: 0, md: 'space.01' }}>{logo}</Flex>
</GridItem>

<GridItem>
<HStack alignItems="center" justifyContent="flex-end">
{networkBadge}
<Settings
triggerButton={<HamburgerIcon data-testid={SettingsSelectors.SettingsMenuBtn} />}
toggleSwitchAccount={() => setIsShowingSwitchAccount(!isShowingSwitchAccount)}
/>
</HStack>
</GridItem>
</Grid>
</styled.header>
</>
);
}
Loading
Loading