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

fix: use location.state to store background route to prevent unmount #4062

Closed
wants to merge 8 commits into from
34 changes: 34 additions & 0 deletions src/app/common/hooks/use-background-location-redirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect } 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 added this hook so that:

  • If you have a modal open
  • Open a new tab
  • It will update the backgroundLocation to be the home page
Kapture.2023-07-28.at.14.28.19.mp4

Without doing this, an empty screen appears behind the modal

Copy link
Collaborator

@kyranjamie kyranjamie Jul 31, 2023

Choose a reason for hiding this comment

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

Thinking on this further: couldn't each modal component handle their own backgroundLocation logic?

Where the sign out modal component has a check in its render that, if there's no backgroundLocation, it should re-router to having a default value of home?

Could work with a similar hook API, but where all the logic is colocated to the modal itself, rather than one big catch-all.

Something like:

// src/app/pages/sign-out-confirm/sign-out-confirm.tsx
useDefaultBackgroundLocation(RouteUrls.Home)

which internally handles the

if (!backgroundLocation) navigate(currentRoute, { 
  state: { 
    backgroundLocation: defaultVal, 
    // this might fix the problem you've commented in the other file re route state
    ...otherRouteState 
  } 
})`

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes we can definitely do that. I just implemented a catch all for now as Home seems like the logical background. I can change it to accept a preferred default route

I can give it a try but I think once you open a new tab the location.state gets wiped completely.

import { useLocation, useNavigate } from 'react-router-dom';

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

import { useLocationState } from '@app/common/hooks/use-location-state';

/*
when modals are opened in a new tab they lose the location.state.backgroundLocation
this hook sets the backgroundLocation to be RouteUrls.Home to improve UX
*/
export function useBackgroundLocationRedirect() {
const { pathname, state } = useLocation();
const navigate = useNavigate();
const backgroundLocation = useLocationState('backgroundLocation');

useEffect(() => {
void (async () => {
switch (true) {
// FIXME ReceiveCollectibleOrdinal loses state?.btcAddressTaproot in a new tab
// this can be improved to try and fetch btcAddressTaproot
case pathname === RouteUrls.ReceiveCollectibleOrdinal && !state?.btcAddressTaproot:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For RouteUrls.ReceiveCollectibleOrdinal, it relies on btcAddressTaproot that is set in location.state.

If you open a new tab this state is lost and although the modal opens, the btcAddressTaproot is lost so it's unusable.

For now I just send users back to RouteUrls.Home. This could probably be improved by refactoring how we get btcAddressTaproot in ReceiveCollectibleOrdinal

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not a big fan of using a switch(true). Why not just use ifs?

This is better for readability imo. My thoughts map to the first comment of the HN post of a guy who wrote an article doing this (https://news.ycombinator.com/item?id=26777090) re favouring early return.

Separately—while identical behaviour of modals when doing a fresh load of the app would be ideal—if we need to add a lot of logic to handle this, then I wouldn't consider it a dealbreaker if the under layer doesn't load correctly. Contextually, in case of sign out, it isn't so important.

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 like using switch(true) as I think its cleaner and more elegant than a lot of if statements. I can change it though and use early returns here and in future to match the code base.

If we are OK with having an empty screen behind all modals opened in a new tab then we can avoid the addition of this logic.

It will look like this, with home tabs but no tab content:

Screenshot 2023-07-31 at 11 16 23

For RouteUrls.ReceiveCollectibleOrdinal we will need to do something though as we lose track of the taproot address when opening in a new tab and the modal shows but is non-functional as there is no address to copy:
Screenshot 2023-07-31 at 11 21 10

return navigate(RouteUrls.Home);

case backgroundLocation === undefined:
return navigate(pathname, {
state: { backgroundLocation: { pathname: RouteUrls.Home } },
});
default:
return false;
}
})();
}, [backgroundLocation, navigate, pathname, state]);
}
2 changes: 2 additions & 0 deletions src/app/common/hooks/use-location-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { isUndefined } from '@shared/utils';

pete-watters marked this conversation as resolved.
Show resolved Hide resolved
export function useLocationState(propName: string): string | undefined;
export function useLocationState(propName: string, defaultValue: string): string;
export function useLocationState(propName: 'accountIndex'): number;
export function useLocationState(propName: 'backgroundLocation'): Location;
Comment on lines +10 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

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

These changes mean useLocationState is now aware of implementation details of some of the cases in which it's used.

Why not extend the function to support a generic type argument, rather than extending it with use-case specific typings?

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 can change this, no problem. I struggled with extending the type initially so opted for this to narrow the type to their key. I'll clean it up

export function useLocationState(propName: string, defaultValue?: string) {
const location = useLocation();
return get(location, `state.${propName}`, defaultValue);
Expand Down
12 changes: 7 additions & 5 deletions src/app/components/receive/receive-collectible.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import toast from 'react-hot-toast';
import { useLocation, useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';

import BitcoinStampImg from '@assets/images/bitcoin-stamp.png';
import { Box, Stack, useClipboard } from '@stacks/ui';
import { HomePageSelectors } from '@tests/selectors/home.selectors';
import get from 'lodash.get';

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

import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useLocationState } from '@app/common/hooks/use-location-state';
import { StxAvatar } from '@app/components/crypto-assets/stacks/components/stx-avatar';
import { OrdinalIcon } from '@app/components/icons/ordinal-icon';
import { useZeroIndexTaprootAddress } from '@app/query/bitcoin/ordinals/use-zero-index-taproot-address';
Expand All @@ -19,9 +19,9 @@ import { ReceiveCollectibleItem } from './receive-collectible-item';

export function ReceiveCollectible() {
const analytics = useAnalytics();
const location = useLocation();
const backgroundLocation = useLocationState('backgroundLocation');
const accountIndex = useLocationState('accountIndex');
const navigate = useNavigate();
const accountIndex = get(location.state, 'accountIndex', undefined);
const btcAddressNativeSegwit = useCurrentAccountNativeSegwitAddressIndexZero();
const btcAddressTaproot = useZeroIndexTaprootAddress(accountIndex);

Expand Down Expand Up @@ -54,7 +54,9 @@ export function ReceiveCollectible() {
data-testid={HomePageSelectors.ReceiveBtcTaprootQrCodeBtn}
onCopyAddress={() => {
void analytics.track('select_inscription_to_add_new_collectible');
navigate(RouteUrls.ReceiveCollectibleOrdinal, { state: { btcAddressTaproot } });
navigate(RouteUrls.ReceiveCollectibleOrdinal, {
state: { btcAddressTaproot, backgroundLocation },
});
}}
title="Ordinal inscription"
/>
Expand Down
3 changes: 1 addition & 2 deletions src/app/features/asset-list/asset-list.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Outlet } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { Outlet, useNavigate } from 'react-router-dom';

import { Box, Stack } from '@stacks/ui';
import { HomePageSelectorsLegacy } from '@tests-legacy/page-objects/home.selectors';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';

import { Inscription as InscriptionType } from '@shared/models/inscription.model';
import { RouteUrls } from '@shared/route-urls';
Expand All @@ -18,10 +18,11 @@ interface InscriptionProps {
export function Inscription({ rawInscription }: InscriptionProps) {
const inscription = convertInscriptionToSupportedInscriptionType(rawInscription);
const navigate = useNavigate();
const location = useLocation();

function openSendInscriptionModal() {
navigate(RouteUrls.SendOrdinalInscription, {
state: { inscription },
state: { inscription, backgroundLocation: location },
});
}

Expand Down
12 changes: 9 additions & 3 deletions src/app/features/settings-dropdown/settings-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ export function SettingsDropdown() {
data-testid={SettingsSelectors.ToggleTheme}
onClick={wrappedCloseCallback(() => {
void analytics.track('click_change_theme_menu_item');
navigate(RouteUrls.ChangeTheme, { relative: 'path' });
navigate(RouteUrls.ChangeTheme, {
state: { backgroundLocation: location },
});
})}
>
Change theme
Expand Down Expand Up @@ -147,7 +149,9 @@ export function SettingsDropdown() {
data-testid={SettingsSelectors.ChangeNetworkAction}
onClick={wrappedCloseCallback(() => {
void analytics.track('click_change_network_menu_item');
navigate(RouteUrls.SelectNetwork, { relative: 'path' });
navigate(RouteUrls.SelectNetwork, {
state: { backgroundLocation: location },
});
})}
>
<Flex width="100%" alignItems="center" justifyContent="space-between">
Expand Down Expand Up @@ -178,7 +182,9 @@ export function SettingsDropdown() {
<MenuItem
color={color('feedback-error')}
onClick={wrappedCloseCallback(() =>
navigate(RouteUrls.SignOutConfirm, { relative: 'path' })
navigate(RouteUrls.SignOutConfirm, {
state: { backgroundLocation: location },
})
)}
data-testid="settings-sign-out"
>
Expand Down
6 changes: 5 additions & 1 deletion src/app/features/theme-drawer/theme-drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { useNavigate } from 'react-router-dom';

import { useBackgroundLocationRedirect } from '@app/common/hooks/use-background-location-redirect';
import { useLocationState } from '@app/common/hooks/use-location-state';
import { BaseDrawer } from '@app/components/drawer/base-drawer';

import { ThemeList } from './theme-list';

export function ThemesDrawer() {
useBackgroundLocationRedirect();
const navigate = useNavigate();
const backgroundLocation = useLocationState('backgroundLocation');
return (
<BaseDrawer title="Select Theme" isShowing onClose={() => navigate('..')}>
<BaseDrawer title="Select Theme" isShowing onClose={() => navigate(backgroundLocation)}>
<ThemeList />
</BaseDrawer>
);
Expand Down
14 changes: 7 additions & 7 deletions src/app/pages/home/components/home-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { StackProps } from '@stacks/ui';

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

import { useLocationState } from '@app/common/hooks/use-location-state';
import { LoadingSpinner } from '@app/components/loading-spinner';
import { Tabs } from '@app/components/tabs';

Expand All @@ -15,7 +16,8 @@ interface HomeTabsProps extends StackProps {
// TODO #4013: Abstract this to generic RouteTab once choose-fee-tab updated
export function HomeTabs({ children }: HomeTabsProps) {
const navigate = useNavigate();
const { pathname } = useLocation();
const location = useLocation();
const backgroundLocation = useLocationState('backgroundLocation');

const tabs = useMemo(
() => [
Expand All @@ -24,17 +26,15 @@ export function HomeTabs({ children }: HomeTabsProps) {
],
[]
);

const getActiveTab = useCallback(
() => tabs.findIndex(tab => tab.slug === pathname),
[tabs, pathname]
);
const getActiveTab = useCallback(() => {
const path = backgroundLocation ? backgroundLocation.pathname : location?.pathname;
return tabs.findIndex(tab => tab.slug === path);
}, [tabs, backgroundLocation, location]);

const setActiveTab = useCallback(
(index: number) => navigate(tabs[index]?.slug),
[navigate, tabs]
);

return (
<Stack flexGrow={1} mt="loose" spacing="extra-loose">
<Tabs
Expand Down
10 changes: 8 additions & 2 deletions src/app/pages/home/components/receive-button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';

import { ButtonProps } from '@stacks/ui';
import { HomePageSelectors } from '@tests/selectors/home.selectors';
Expand All @@ -13,15 +13,21 @@ import { HomeActionButton } from './tx-button';

export function ReceiveButton(props: ButtonProps) {
const navigate = useNavigate();
const location = useLocation();
const isBitcoinEnabled = useConfigBitcoinEnabled();
const receivePath = isBitcoinEnabled ? RouteUrls.Receive : RouteUrls.ReceiveStx;

return (
<HomeActionButton
buttonComponent={PrimaryButton}
data-testid={HomePageSelectors.ReceiveCryptoAssetBtn}
icon={QrCodeIcon}
label="Receive"
onClick={() => navigate(isBitcoinEnabled ? RouteUrls.Receive : RouteUrls.ReceiveStx)}
onClick={() =>
navigate(receivePath, {
state: { backgroundLocation: location },
})
}
{...props}
/>
);
Expand Down
21 changes: 16 additions & 5 deletions src/app/pages/home/home.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { Outlet, useNavigate } from 'react-router-dom';
import { Outlet, Route, Routes, useLocation, useNavigate } from 'react-router-dom';

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

import { useTrackFirstDeposit } from '@app/common/hooks/analytics/transactions-analytics.hooks';
import { useOnboardingState } from '@app/common/hooks/auth/use-onboarding-state';
import { useLocationState } from '@app/common/hooks/use-location-state';
import { useOnMount } from '@app/common/hooks/use-on-mount';
import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { Header } from '@app/components/header';
import { ActivityList } from '@app/features/activity-list/activity-list';
import { AssetsList } from '@app/features/asset-list/asset-list';
import { InAppMessages } from '@app/features/hiro-messages/in-app-messages';
import { SuggestedFirstSteps } from '@app/features/suggested-first-steps/suggested-first-steps';
import { HomeActions } from '@app/pages/home/components/home-actions';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { homeModalRoutes } from '@app/routes/app-routes';

import { CurrentAccount } from './components/account-area';
import { HomeTabs } from './components/home-tabs';
Expand All @@ -19,8 +22,10 @@ import { HomeLayout } from './components/home.layout';
export function Home() {
const { decodedAuthRequest } = useOnboardingState();

const stacksAccount = useCurrentStacksAccount();
const navigate = useNavigate();

const location = useLocation();
const backgroundLocation = useLocationState('backgroundLocation');
useTrackFirstDeposit();

useRouteHeader(
Expand All @@ -33,15 +38,21 @@ export function Home() {
useOnMount(() => {
if (decodedAuthRequest) navigate(RouteUrls.ChooseAccount);
});

return (
<HomeLayout
suggestedFirstSteps={<SuggestedFirstSteps />}
currentAccount={<CurrentAccount />}
actions={<HomeActions />}
>
<HomeTabs>
<Outlet context={{ address: stacksAccount?.address }} />
<>
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 need to declare Routes here so I can manipulate the location depending on whether backgroundLocation is populated or not.

backgroundLocation is the route above which the modal should appear.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't it possible to also read location in app-routes to see if bg location is defined? Nw if not, just curious

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can read the location there but the issue I struggled with is how to then feed said location into how we are creating the routes.

We are using createHashRouter and createRoutesFromElements and I was unable to figure out how to pass the location there.

Eventually I came up with the idea of using <Routes nested in <Home so I could then trick the location.

We can probably improve on that structure with more time and I am open to suggestions but I didn't want to start refactoring the whole of app-routes

<Routes location={backgroundLocation || location}>
<Route path={RouteUrls.Home} element={<AssetsList />} />
<Route path={RouteUrls.Activity} element={<ActivityList />} />
{homeModalRoutes}
</Routes>
{backgroundLocation && <Outlet />}
</>
</HomeTabs>
</HomeLayout>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { useNavigate } from 'react-router-dom';
import { Box, Button, Flex, Text, color } from '@stacks/ui';
import { SharedComponentsSelectors } from '@tests/selectors/shared-component.selectors';

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

import { useBackgroundLocationRedirect } from '@app/common/hooks/use-background-location-redirect';
import { useLocationState } from '@app/common/hooks/use-location-state';
import { AddressDisplayer } from '@app/components/address-displayer/address-displayer';
import { BaseDrawer } from '@app/components/drawer/base-drawer';
import { Title } from '@app/components/typography';
Expand All @@ -21,11 +21,13 @@ interface ReceiveTokensLayoutProps {
hasSubtitle?: boolean;
}
export function ReceiveTokensLayout(props: ReceiveTokensLayoutProps) {
useBackgroundLocationRedirect();
const { address, accountName, onCopyAddressToClipboard, title, warning, hasSubtitle } = props;
const navigate = useNavigate();
const backgroundLocation = useLocationState('backgroundLocation');

return (
<BaseDrawer title={title} isShowing onClose={() => navigate(RouteUrls.Home)}>
<BaseDrawer title={title} isShowing onClose={() => navigate(backgroundLocation?.pathname)}>
<Flex alignItems="center" flexDirection="column" pb={['loose', '48px']} px="loose">
{hasSubtitle && (
<Text color={color('text-caption')} mb="tight" textAlign="left">
Expand Down
7 changes: 5 additions & 2 deletions src/app/pages/receive-tokens/receive-btc.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { useCallback } from 'react';
import toast from 'react-hot-toast';
import { useLocation } from 'react-router-dom';

import { useClipboard } from '@stacks/ui';
import get from 'lodash.get';

import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useBackgroundLocationRedirect } from '@app/common/hooks/use-background-location-redirect';
import { useCurrentAccountIndex } from '@app/store/accounts/account';
import { useNativeSegwitAccountIndexAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';

import { ReceiveBtcModalWarning } from './components/receive-btc-warning';
import { ReceiveTokensLayout } from './components/receive-tokens.layout';

export function ReceiveBtcModal() {
useBackgroundLocationRedirect();
const analytics = useAnalytics();
const { state } = useLocation();

Expand All @@ -23,11 +26,11 @@ export function ReceiveBtcModal() {

const { onCopy } = useClipboard(btcAddress);

function copyToClipboard() {
const copyToClipboard = useCallback(() => {
void analytics.track('copy_btc_address_to_clipboard');
toast.success('Copied to clipboard!');
onCopy();
}
}, [analytics, onCopy]);

return (
<ReceiveTokensLayout
Expand Down
Loading