Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
79 changes: 62 additions & 17 deletions src/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,55 @@ import { Box } from '@mui/material';
import { useRouter } from 'next/router';
import React, { ReactNode } from 'react';
import AnalyticsConsent from 'src/components/Analytics/AnalyticsConsent';
// import { useModalContext } from 'src/hooks/useModal';
import { useModalContext } from 'src/hooks/useModal';
import { SupportModal } from 'src/layouts/SupportModal';
import { useRootStore } from 'src/store/root';
import { CustomMarket } from 'src/ui-config/marketsConfig';
import { getQueryParameter } from 'src/store/utils/queryParams';
import { CustomMarket, marketsData } from 'src/ui-config/marketsConfig';
import { FORK_ENABLED } from 'src/utils/marketsAndNetworksConfig';
import { useShallow } from 'zustand/shallow';

import { AppFooter } from './AppFooter';
import { AppHeader } from './AppHeader';
import TopBarNotify from './TopBarNotify';
import TopBarNotify, { ButtonAction } from './TopBarNotify';

interface CampaignConfig {
notifyText: string;
buttonText: string;
buttonAction: ButtonAction;
bannerVersion: string;
icon: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

does it make sense to make the icon optional for future solo-text campaings

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes 33af445

}

type CampaignChainId = ChainId.base | ChainId.mainnet | ChainId.arbitrum_one;
Copy link
Contributor

Choose a reason for hiding this comment

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

why not just use generic ChainId if down here we use Partial of chains ids

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah good point 33af445


type CampaignConfigs = Partial<Record<CampaignChainId, CampaignConfig>>;

type NetworkCampaigns = { [chainId: number]: CampaignConfig };

const getIntendedChainId = (): ChainId => {
if (typeof window !== 'undefined') {
// Priority 1: localStorage selectedMarket
const selectedMarket = localStorage.getItem('selectedMarket');
if (selectedMarket && marketsData[selectedMarket as CustomMarket]) {
return marketsData[selectedMarket as CustomMarket].chainId;
}

// Priority 2: URL params marketName
const urlMarket = getQueryParameter('marketName');
if (urlMarket && marketsData[urlMarket as CustomMarket]) {
return marketsData[urlMarket as CustomMarket].chainId;
}
}

// Priority 3: Default to mainnet
return ChainId.mainnet;
};

const getCampaignConfigs = (
// openSwitch: (underlyingAsset: string) => void,
openSwitch: (underlyingAsset: string, chainId: ChainId) => void,
openMarket: (market: CustomMarket) => void
) => ({
): CampaignConfigs => ({
[ChainId.base]: {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit/v2: we can leverage vercel configs or amplitude feature flags to automatically change campaings without commiting new code

notifyText: 'A new incentives campaign is live on the Base market',
buttonText: 'Explore Base',
Expand Down Expand Up @@ -73,16 +107,16 @@ const getCampaignConfigs = (
// icon: '/icons/networks/avalanche.svg',
// },

// [ChainId.arbitrum_one]: {
// notifyText: 'Swap tokens directly in the Aave App',
// buttonText: 'Swap Now',
// buttonAction: {
// type: 'function' as const,
// value: () => openSwitch('', ChainId.arbitrum_one),
// },
// bannerVersion: 'arbitrum-swap-v1',
// icon: '/icons/networks/arbitrum.svg',
// },
[ChainId.arbitrum_one]: {
notifyText: 'Limit orders are now live on Arbitrum',
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure how translations work but can we ensure banner messages will be translated this way? otherwise we can set notifyText type as react node and just use

Copy link
Contributor

Choose a reason for hiding this comment

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

i see
` {currentCampaign.notifyText}

`
just checking if it's enough

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah we dont really use translations much but added b2f5f50

buttonText: 'Swap Now',
buttonAction: {
type: 'function' as const,
value: () => openSwitch('', ChainId.arbitrum_one),
},
bannerVersion: 'arbitrum-swap-v1',
icon: '/icons/networks/arbitrum.svg',
},

// [ChainId.optimism]: {
// notifyText: 'Swap tokens directly in the Aave App',
Expand Down Expand Up @@ -121,17 +155,28 @@ const getCampaignConfigs = (
export function MainLayout({ children }: { children: ReactNode }) {
const router = useRouter();
const setCurrentMarket = useRootStore(useShallow((store) => store.setCurrentMarket));
const { openSwitch } = useModalContext();

const openMarket = (market: CustomMarket) => {
setCurrentMarket(market);
router.push(`/markets/?marketName=${market}`);
};

const campaignConfigs = getCampaignConfigs(openMarket);
const campaignConfigs = getCampaignConfigs(openSwitch, openMarket);
Copy link
Contributor

Choose a reason for hiding this comment

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

would move getCampaignConfigs to a hook useBannerCampaigns so we can get openSwitch (or others in the future) right in there and just pass the chain id

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done in 33af445


const intendedChainId = getIntendedChainId();

const isCampaignChainId = (chainId: ChainId): chainId is CampaignChainId => {
return chainId in campaignConfigs;
};

const filteredCampaigns: NetworkCampaigns = isCampaignChainId(intendedChainId)
? { [intendedChainId]: campaignConfigs[intendedChainId]! }
: {};
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

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

Using non-null assertion operator (!) without proper null checking could cause runtime errors if campaignConfigs[intendedChainId] is undefined. Consider using optional chaining or explicit null checking.

Suggested change
const filteredCampaigns: NetworkCampaigns = isCampaignChainId(intendedChainId)
? { [intendedChainId]: campaignConfigs[intendedChainId]! }
: {};
const campaign = campaignConfigs[intendedChainId];
const filteredCampaigns: NetworkCampaigns =
isCampaignChainId(intendedChainId) && campaign !== undefined
? { [intendedChainId]: campaign }
: {};

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

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

P1 Badge Keep banner campaigns in sync with market changes

The new filteredCampaigns is derived from getIntendedChainId() using only localStorage/URL and is computed once in MainLayout. Because this component subscribes only to setCurrentMarket, it never re-renders when the user switches markets or when a marketName query param overrides a stale localStorage value. Meanwhile TopBarNotify now shows the first entry it receives, so after the initial render the banner stays locked to whatever chain was chosen at load time (e.g. start on mainnet, switch to Base, but the mainnet campaign continues to display). The banner logic therefore regresses compared to before. Consider deriving the campaign from the store’s currentChainId or recomputing filteredCampaigns when the selected market changes.

Useful? React with 👍 / 👎.


return (
<>
<TopBarNotify campaigns={campaignConfigs} />
<TopBarNotify campaigns={filteredCampaigns} />

<AppHeader />
<Box component="main" sx={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
Expand Down
23 changes: 15 additions & 8 deletions src/layouts/TopBarNotify.tsx
Copy link
Contributor

Choose a reason for hiding this comment

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

ui ideas

Image

Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,23 @@ export default function TopBarNotify({ campaigns }: TopBarNotifyProps) {
const mobileDrawerOpen = useRootStore((state) => state.mobileDrawerOpen);

const getCurrentCampaign = (): CampaignConfig | null => {
return campaigns[currentChainId] || null;
const chainIds = Object.keys(campaigns).map(Number);
const firstChainId = chainIds[0];
return firstChainId ? campaigns[firstChainId] || null : null;
Copy link

Copilot AI Oct 2, 2025

Choose a reason for hiding this comment

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

The logic assumes the first chain ID from Object.keys() is the intended campaign, but Object.keys() order is not guaranteed to be consistent. This could lead to unpredictable banner selection when multiple campaigns are available.

Suggested change
const firstChainId = chainIds[0];
return firstChainId ? campaigns[firstChainId] || null : null;
if (chainIds.length === 0) return null;
if (currentChainId && campaigns[currentChainId]) {
return campaigns[currentChainId];
}
// fallback: select the campaign with the lowest chainId
const minChainId = Math.min(...chainIds);
return campaigns[minChainId] || null;

Copilot uses AI. Check for mistakes.
};

const currentCampaign = getCurrentCampaign();
const campaignChainId = currentCampaign
? Object.keys(campaigns)
.map(Number)
.find((chainId) => campaigns[chainId] === currentCampaign) || currentChainId
: currentChainId;

const [showWarning, setShowWarning] = useState(() => {
if (!currentCampaign) return false;

const storedBannerVersion = localStorage.getItem(`bannerVersion_${currentChainId}`);
const warningBarOpen = localStorage.getItem(`warningBarOpen_${currentChainId}`);
const storedBannerVersion = localStorage.getItem(`bannerVersion_${campaignChainId}`);
const warningBarOpen = localStorage.getItem(`warningBarOpen_${campaignChainId}`);

if (storedBannerVersion !== currentCampaign.bannerVersion) {
return true;
Expand All @@ -67,22 +74,22 @@ export default function TopBarNotify({ campaigns }: TopBarNotifyProps) {
useEffect(() => {
if (!currentCampaign) return;

const storedBannerVersion = localStorage.getItem(`bannerVersion_${currentChainId}`);
const storedBannerVersion = localStorage.getItem(`bannerVersion_${campaignChainId}`);

if (storedBannerVersion !== currentCampaign.bannerVersion) {
localStorage.setItem(`bannerVersion_${currentChainId}`, currentCampaign.bannerVersion);
localStorage.setItem(`warningBarOpen_${currentChainId}`, 'true');
localStorage.setItem(`bannerVersion_${campaignChainId}`, currentCampaign.bannerVersion);
localStorage.setItem(`warningBarOpen_${campaignChainId}`, 'true');
setShowWarning(true);
}
}, [currentCampaign, currentChainId]);
}, [currentCampaign, campaignChainId]);

// If no campaign is configured for the current network, don't show anything
if (!currentCampaign) {
return null;
}

const handleClose = () => {
localStorage.setItem(`warningBarOpen_${currentChainId}`, 'false');
localStorage.setItem(`warningBarOpen_${campaignChainId}`, 'false');
setShowWarning(false);
};

Expand Down
Loading