Skip to content

Commit

Permalink
chore: refactor containers to make them more composable
Browse files Browse the repository at this point in the history
  • Loading branch information
pete-watters committed Jul 11, 2024
1 parent 71f2565 commit 7aead25
Show file tree
Hide file tree
Showing 82 changed files with 872 additions and 771 deletions.
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';

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;
};

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}
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

0 comments on commit 7aead25

Please sign in to comment.