diff --git a/apps/cowswap-frontend/public/audio/success-winterTheme.mp3 b/apps/cowswap-frontend/public/audio/success-winterTheme.mp3 new file mode 100644 index 0000000000..f35a393f5d Binary files /dev/null and b/apps/cowswap-frontend/public/audio/success-winterTheme.mp3 differ diff --git a/apps/cowswap-frontend/src/common/hooks/useAnnouncements.ts b/apps/cowswap-frontend/src/common/hooks/useAnnouncements.ts new file mode 100644 index 0000000000..f768ad58dc --- /dev/null +++ b/apps/cowswap-frontend/src/common/hooks/useAnnouncements.ts @@ -0,0 +1,61 @@ +import { useAtomValue } from 'jotai' +import { useMemo } from 'react' + +import { isProdLike } from '@cowprotocol/common-utils' +import { Announcement, Announcements, announcementsAtom } from '@cowprotocol/core' +import { CowEnv, SupportedChainId } from '@cowprotocol/cow-sdk' + +function getAnnouncementSpecificity(chainId: SupportedChainId, env: CowEnv, announcement: Announcement): number { + let specificity = 0 + + const matchesChain = announcement.chainIds.some((announcementChain) => announcementChain === chainId) + const matchesEnv = announcement.envs.some((announcementEnv) => announcementEnv === env) + const matchesEveryChain = announcement.chainIds.length === 0 + const matchesEveryEnv = announcement.envs.length === 0 + + if (matchesChain) specificity += 2 + if (matchesEnv) specificity += 2 + if (matchesEveryChain) specificity += 1 + if (matchesEveryEnv) specificity += 1 + + return specificity +} + +function useAnnouncements(chainId: SupportedChainId): Announcements { + const allAnnouncements = useAtomValue(announcementsAtom) + + return useMemo(() => { + const env = isProdLike ? 'prod' : 'staging' + + const filtered = allAnnouncements + .filter((announcement) => { + const showForEveryChain = announcement.chainIds.length === 0 + const showForEveryEnv = announcement.envs.length === 0 + + const matchesChainId = announcement.chainIds.some((announcementChain) => announcementChain === chainId) + const matchesEnv = announcement.envs.some((announcementEnv) => announcementEnv === env) + + return (showForEveryChain || matchesChainId) && (showForEveryEnv || matchesEnv) + }) + .sort((a, b) => { + const specificityA = getAnnouncementSpecificity(chainId, env, a) + const specificityB = getAnnouncementSpecificity(chainId, env, b) + + return specificityB - specificityA + }) + + return filtered + }, [chainId, allAnnouncements]) +} + +export function useCriticalAnnouncements(chainId: SupportedChainId): Announcements { + const announcements = useAnnouncements(chainId) + + return announcements.filter(({ isCritical }) => isCritical) +} + +export function useNonCriticalAnnouncements(chainId: SupportedChainId): Announcements { + const announcements = useAnnouncements(chainId) + + return announcements.filter(({ isCritical }) => !isCritical) +} diff --git a/apps/cowswap-frontend/src/common/hooks/useCmsAnnouncements.ts b/apps/cowswap-frontend/src/common/hooks/useCmsAnnouncements.ts new file mode 100644 index 0000000000..c8b114af84 --- /dev/null +++ b/apps/cowswap-frontend/src/common/hooks/useCmsAnnouncements.ts @@ -0,0 +1,19 @@ +import { CmsAnnouncements, getAnnouncements } from '@cowprotocol/core' + +import ms from 'ms.macro' +import useSWR, { SWRConfiguration } from 'swr' + +const ANNOUNCEMENTS_SWR_CONFIG: SWRConfiguration = { + refreshInterval: ms`5min`, // we do need to show this sort of ASAP + refreshWhenHidden: false, + refreshWhenOffline: false, + revalidateOnFocus: false, +} + +const EMPTY_VALUE: CmsAnnouncements = [] + +export function useCmsAnnouncements() { + const { data } = useSWR('/announcements', getAnnouncements, ANNOUNCEMENTS_SWR_CONFIG) + + return data || EMPTY_VALUE +} diff --git a/apps/cowswap-frontend/src/common/hooks/useCmsSolversInfo.ts b/apps/cowswap-frontend/src/common/hooks/useCmsSolversInfo.ts index 9a0cd053f3..c394899d56 100644 --- a/apps/cowswap-frontend/src/common/hooks/useCmsSolversInfo.ts +++ b/apps/cowswap-frontend/src/common/hooks/useCmsSolversInfo.ts @@ -10,8 +10,10 @@ const SOLVERS_INFO_SWR_CONFIG: SWRConfiguration = { revalidateOnFocus: false, } +const EMPTY_VALUE: CmsSolversInfo = [] + export function useCmsSolversInfo() { const { data } = useSWR('/solvers', getSolversInfo, SOLVERS_INFO_SWR_CONFIG) - return data || [] + return data || EMPTY_VALUE } diff --git a/apps/cowswap-frontend/src/common/updaters/AnnouncementsUpdater.ts b/apps/cowswap-frontend/src/common/updaters/AnnouncementsUpdater.ts new file mode 100644 index 0000000000..0a7849741d --- /dev/null +++ b/apps/cowswap-frontend/src/common/updaters/AnnouncementsUpdater.ts @@ -0,0 +1,20 @@ +import { useSetAtom } from 'jotai' +import { useEffect } from 'react' + +import { announcementsAtom, mapCmsAnnouncementsToAnnouncements } from '@cowprotocol/core' + +import { useCmsAnnouncements } from 'common/hooks/useCmsAnnouncements' + +export function AnnouncementsUpdater() { + const setAnnouncements = useSetAtom(announcementsAtom) + + const cmsAnnouncements = useCmsAnnouncements() + + useEffect(() => { + const announcements = mapCmsAnnouncementsToAnnouncements(cmsAnnouncements) + + announcements && setAnnouncements(announcements) + }, [cmsAnnouncements, setAnnouncements]) + + return null +} diff --git a/apps/cowswap-frontend/src/legacy/components/Header/URLWarning/index.tsx b/apps/cowswap-frontend/src/legacy/components/Header/URLWarning/index.tsx index 11e9f1f33a..848ddefce4 100644 --- a/apps/cowswap-frontend/src/legacy/components/Header/URLWarning/index.tsx +++ b/apps/cowswap-frontend/src/legacy/components/Header/URLWarning/index.tsx @@ -1,54 +1,41 @@ -import { useFetchFile } from '@cowprotocol/common-hooks' -import { hashCode } from '@cowprotocol/common-utils' -import { environmentName } from '@cowprotocol/common-utils' +import { hashCode, isInjectedWidget } from '@cowprotocol/common-utils' import { SupportedChainId as ChainId } from '@cowprotocol/cow-sdk' import { ClosableBanner } from '@cowprotocol/ui' import { useWalletInfo } from '@cowprotocol/wallet' import ReactMarkdown, { Components } from 'react-markdown' +import { useCriticalAnnouncements, useNonCriticalAnnouncements } from 'common/hooks/useAnnouncements' import { GlobalWarning } from 'common/pure/GlobalWarning' import { markdownComponents } from '../../Markdown/components' -// Announcement content: Modify this repository to edit the announcement -const ANNOUNCEMENTS_MARKDOWN_BASE_URL = 'https://raw.githubusercontent.com/cowprotocol/cowswap-banner/main' const BANNER_STORAGE_KEY = 'announcementBannerClosed/' -const PRODUCTION_ENVS: (typeof environmentName)[] = ['production', 'staging', 'ens'] -function getAnnouncementUrl(chainId: number, env?: 'production' | 'barn') { - return `${ANNOUNCEMENTS_MARKDOWN_BASE_URL}${env ? `/${env}` : ''}/announcements-${chainId}.md` -} - -function useGetAnnouncement(chainId: number): string | undefined { - const env = PRODUCTION_ENVS.includes(environmentName) ? 'production' : 'barn' - - // Fetches global announcement - const { file, error } = useFetchFile(getAnnouncementUrl(chainId)) - // Fetches env announcement - const { file: envFile, error: envError } = useFetchFile(getAnnouncementUrl(chainId, env)) - - const announcementText = error ? undefined : file?.trim() - - if (error) { - console.error('[URLWarning] Error getting the announcement text: ', error) - } +function useGetCmsAnnouncement(chainId: number): string | undefined { + const critical = useCriticalAnnouncements(chainId) + const nonCritical = useNonCriticalAnnouncements(chainId) - const envAnnouncementText = envError ? undefined : envFile?.trim() + const isWidget = isInjectedWidget() - if (envError) { - console.error(`[URLWarning] Error getting the env ${env} announcement text: `, envError) + // Critical takes priority + if (critical.length) { + // Assume only one announcement can be displayed for now + return critical[0].text + } else if (!isWidget && nonCritical.length) { + // Non-critical can only be displayed when not in the widget + return nonCritical[0].text } - return announcementText || envAnnouncementText + return } export function URLWarning() { const { chainId = ChainId.MAINNET } = useWalletInfo() - const announcementText = useGetAnnouncement(chainId) + const announcementText = useGetCmsAnnouncement(chainId) const contentHash = announcementText ? hashCode(announcementText).toString() : undefined if (!announcementText) { diff --git a/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx b/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx index ba5a2e8a76..6e41f34912 100644 --- a/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx +++ b/apps/cowswap-frontend/src/modules/account/containers/Transaction/ActivityDetails.tsx @@ -35,6 +35,7 @@ import { useHideReceiverWalletBanner, useIsReceiverWalletBannerHidden, } from 'common/state/receiverWalletBannerVisibility' +import { getIsCustomRecipient } from 'utils/orderUtils/getIsCustomRecipient' import { getUiOrderType } from 'utils/orderUtils/getUiOrderType' import { StatusDetails } from './StatusDetails' @@ -298,7 +299,7 @@ export function ActivityDetails(props: { outputToken = COW[chainId as SupportedChainId] } - const isCustomRecipient = Boolean(order?.receiver && order.owner !== order.receiver) + const isCustomRecipient = !!order && getIsCustomRecipient(order) return ( <> diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx index f20a5420bc..25d04938cd 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/Updaters.tsx @@ -15,10 +15,12 @@ import { EthFlowDeadlineUpdater } from 'modules/swap/state/EthFlow/updaters' import { useOnTokenListAddingError } from 'modules/tokensList' import { TradeType, useTradeTypeInfo } from 'modules/trade' import { UsdPricesUpdater } from 'modules/usdAmount' +import { TaxFreeAssetsUpdater } from 'modules/volumeFee' import { LpTokensWithBalancesUpdater, PoolsInfoUpdater, VampireAttackUpdater } from 'modules/yield/shared' import { ProgressBarV2ExecutingOrdersUpdater } from 'common/hooks/orderProgressBarV2' import { TotalSurplusUpdater } from 'common/state/totalSurplusState' +import { AnnouncementsUpdater } from 'common/updaters/AnnouncementsUpdater' import { FeatureFlagsUpdater } from 'common/updaters/FeatureFlagsUpdater' import { FeesUpdater } from 'common/updaters/FeesUpdater' import { GasUpdater } from 'common/updaters/GasUpdater' @@ -70,6 +72,7 @@ export function Updaters() { + + ) } diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx index 8f47e30a73..caac0b966e 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx +++ b/apps/cowswap-frontend/src/modules/application/containers/App/index.tsx @@ -4,10 +4,11 @@ import { ACTIVE_CUSTOM_THEME, CustomTheme } from '@cowprotocol/common-const' import { useMediaQuery } from '@cowprotocol/common-hooks' import { useFeatureFlags } from '@cowprotocol/common-hooks' import { isInjectedWidget } from '@cowprotocol/common-utils' -import { Color, Footer, GlobalCoWDAOStyles, Media, MenuBar, CowSwapTheme } from '@cowprotocol/ui' +import { Color, Footer, GlobalCoWDAOStyles, Media, MenuBar } from '@cowprotocol/ui' import SVG from 'react-inlinesvg' import { NavLink } from 'react-router-dom' +import Snowfall from 'react-snowfall' import { ThemeProvider } from 'theme' import ErrorBoundary from 'legacy/components/ErrorBoundary' @@ -53,8 +54,7 @@ export function App() { useAnalyticsReporterCowSwap() useInitializeUtm() - const featureFlags = useFeatureFlags() - const { isYieldEnabled } = featureFlags + const { isYieldEnabled, isChristmasEnabled, isHalloweenEnabled } = useFeatureFlags() const isInjectedWidgetMode = isInjectedWidget() const menuItems = useMenuItems() @@ -97,11 +97,14 @@ export function App() { const { pendingActivity } = useCategorizeRecentActivity() const isMobile = useMediaQuery(Media.upToMedium(false)) const customTheme = useMemo(() => { - if (ACTIVE_CUSTOM_THEME === CustomTheme.HALLOWEEN && darkMode && featureFlags.isHalloweenEnabled) { - return 'darkHalloween' as CowSwapTheme + if (ACTIVE_CUSTOM_THEME === CustomTheme.HALLOWEEN && darkMode && isHalloweenEnabled) { + return 'darkHalloween' + } + if (ACTIVE_CUSTOM_THEME === CustomTheme.CHRISTMAS && isChristmasEnabled) { + return darkMode ? 'darkChristmas' : 'lightChristmas' } return undefined - }, [darkMode, featureFlags.isHalloweenEnabled]) + }, [darkMode, isHalloweenEnabled, isChristmasEnabled]) const persistentAdditionalContent = ( @@ -112,6 +115,8 @@ export function App() { ) + const isChristmasTheme = ACTIVE_CUSTOM_THEME === CustomTheme.CHRISTMAS && isChristmasEnabled + return ( }> @@ -151,12 +156,28 @@ export function App() { - - + {!isInjectedWidgetMode && isChristmasTheme && ( + + )} + {!isInjectedWidgetMode && (