diff --git a/packages/synapse-interface/components/Maintenance/Events/template/MaintenanceEvent.tsx b/packages/synapse-interface/components/Maintenance/Events/template/MaintenanceEvent.tsx deleted file mode 100644 index 01ffbf6dc6..0000000000 --- a/packages/synapse-interface/components/Maintenance/Events/template/MaintenanceEvent.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useBridgeState } from '@/slices/bridge/hooks' -import { useIntervalTimer } from '@/utils/hooks/useIntervalTimer' -import { OPTIMISM, BASE, BLAST, METIS } from '@/constants/chains/master' -import { - useEventCountdownProgressBar, - getCountdownTimeStatus, -} from '../../EventCountdownProgressBar' -import { AnnouncementBanner } from '../../AnnouncementBanner' -import { WarningMessage } from '../../../Warning' - -/** - * Edit this file for Website Maintenance, components already placed on Bridge page - * - * If require multiple maintenance events, create another file using this file as a template - * and add another instance of components on relevant pages - */ - -/** Banner start time */ -const MAINTENANCE_BANNERS_START = new Date(Date.UTC(2024, 3, 15, 0, 0, 0)) -/** Countdown Progress Bar, Bridge Warning Message + Bridge Pause start time */ -const MAINTENANCE_START_DATE = new Date(Date.UTC(2024, 3, 15, 0, 0, 0)) -/** Ends Banner, Countdown Progress Bar, Bridge Warning Message, Bridge Pause */ -const MAINTENANCE_END_DATE = new Date(Date.UTC(2024, 3, 15, 0, 0, 0)) - - -export const MaintenanceBanner = () => { - const { isComplete } = getCountdownTimeStatus( - MAINTENANCE_BANNERS_START, // Banner will automatically appear after start time - MAINTENANCE_END_DATE // Banner will automatically disappear when end time is reached - ) - - useIntervalTimer(60000, isComplete) - - return ( - -

Bridging on Optimism is temporarily paused.

- - } - startDate={MAINTENANCE_BANNERS_START} - endDate={MAINTENANCE_END_DATE} - /> - ) -} - -export const MaintenanceWarningMessage = () => { - const { fromChainId, toChainId } = useBridgeState() - - const isWarningChain = isChainIncluded( - [fromChainId, toChainId], - [OPTIMISM.id] // Update for Chains to show warning on - ) - - const { isComplete } = getCountdownTimeStatus( - MAINTENANCE_BANNERS_START, // Banner will automatically appear after start time - MAINTENANCE_END_DATE // Banner will automatically disappear when end time is reached - ) - - if (isComplete) return null - - if (isWarningChain) { - return ( - -

Bridging on Optimism is temporarily paused.

- - } - /> - ) - } - - return null -} - -export const useMaintenanceCountdownProgress = () => { - const { fromChainId, toChainId } = useBridgeState() - - const isCurrentChain = isChainIncluded( - [fromChainId, toChainId], - [OPTIMISM.id] // Update for Chains to show maintenance on - ) - - const { - isPending: isMaintenancePending, - EventCountdownProgressBar: MaintenanceCountdownProgressBar, - } = useEventCountdownProgressBar( - 'Bridging on Optimism paused.', - MAINTENANCE_START_DATE, // Countdown Bar will automatically appear after start time - MAINTENANCE_END_DATE // Countdown Bar will automatically disappear when end time is reached - ) - - return { - isMaintenancePending, - isCurrentChainDisabled: isCurrentChain && isMaintenancePending, // Used to pause Bridge - MaintenanceCountdownProgressBar: isCurrentChain - ? MaintenanceCountdownProgressBar - : null, - } -} - -/** - * Checks if any of the chain IDs in `hasChains` are found within the `chainList` array. - * - * @param {number[]} chainList - The array of chain IDs to check against. - * @param {number[]} hasChains - The array of chain IDs to find within `checkChains`. - * @returns {boolean} - True if any chain ID from `hasChains` is found in `checkChains`, otherwise false. - */ -const isChainIncluded = (chainList: number[], hasChains: number[]) => { - return hasChains.some((chainId) => chainList.includes(chainId)) -} diff --git a/packages/synapse-interface/components/Maintenance/LinearAnimatedProgressBar.tsx b/packages/synapse-interface/components/Maintenance/LinearAnimatedProgressBar.tsx deleted file mode 100644 index 955b932afc..0000000000 --- a/packages/synapse-interface/components/Maintenance/LinearAnimatedProgressBar.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { memo } from 'react' -import { getCountdownTimeStatus } from './EventCountdownProgressBar' - -/** - * @param id unique identifier for progress bar instance - * @param startTime start time in unix seconds - * @param endTime end time in unix seconds - */ -export const LinearAnimatedProgressBar = memo( - ({ - id, - startDate, - endDate, - }: { - id: string - startDate: Date - endDate: Date - }) => { - const { - totalTimeInSeconds, - totalTimeElapsedInSeconds, - totalTimeRemainingInSeconds, - isComplete, - } = getCountdownTimeStatus(startDate, endDate) - - const percentElapsed = Math.floor( - (totalTimeElapsedInSeconds / totalTimeInSeconds) * 100 - ) - - let duration = isComplete ? 0.5 : totalTimeRemainingInSeconds - - const synapsePurple = 'hsl(265deg 100% 75%)' - const tailwindGreen400 = 'rgb(74 222 128)' - const height = 3 - const progressId = `progress-${id}` - const maskId = `mask-${id}` - - return ( - - - - - - - - - - - - - - - - - - {isComplete && ( - - )} - - {isComplete && ( - - )} - - ) - } -) diff --git a/packages/synapse-interface/components/Maintenance/Maintenance.tsx b/packages/synapse-interface/components/Maintenance/Maintenance.tsx new file mode 100644 index 0000000000..fbe90417fd --- /dev/null +++ b/packages/synapse-interface/components/Maintenance/Maintenance.tsx @@ -0,0 +1,189 @@ +import { MaintenanceBanner } from './components/MaintenanceBanner' +import { MaintenanceWarningMessage } from './components/MaintenanceWarningMessage' +import { useMaintenanceCountdownProgress } from './components/useMaintenanceCountdownProgress' +import { useBridgeState } from '@/slices/bridge/hooks' +import { useSwapState } from '@/slices/swap/hooks' +import pausedChains from '@/public/pauses/v1/paused-chains.json' +import pausedBridgeModules from '@/public/pauses/v1/paused-bridge-modules.json' + +/** Pause Chain Activity */ +interface ChainPause { + id: string + pausedFromChains: number[] + pausedToChains: number[] + pauseBridge: boolean + pauseSwap: boolean + startTimePauseChain: Date + endTimePauseChain: Date | null // If null, pause indefinitely + startTimeBanner: Date + endTimeBanner: Date | null // If null, pause indefinitely + inputWarningMessage: JSX.Element + bannerMessage: JSX.Element + progressBarMessage: JSX.Element + disableBanner: boolean + disableWarning: boolean + disableCountdown: boolean +} + +const PAUSED_CHAINS: ChainPause[] = pausedChains.map((pause) => { + return { + ...pause, + startTimePauseChain: new Date(pause.startTimePauseChain), + endTimePauseChain: pause.endTimePauseChain + ? new Date(pause.endTimePauseChain) + : null, + startTimeBanner: new Date(pause.startTimeBanner), + endTimeBanner: pause.endTimeBanner ? new Date(pause.endTimeBanner) : null, + inputWarningMessage:

{pause.inputWarningMessage}

, + bannerMessage:

{pause.bannerMessage}

, + progressBarMessage:

{pause.progressBarMessage}

, + } +}) + +export const MaintenanceBanners = () => { + return ( + <> + {PAUSED_CHAINS.map((event) => { + return ( + + ) + })} + + ) +} + +export const MaintenanceWarningMessages = ({ + type, +}: { + type: 'Bridge' | 'Swap' +}) => { + const { fromChainId: bridgeFromChainId, toChainId: bridgeToChainId } = + useBridgeState() + const { swapChainId } = useSwapState() + + if (type === 'Bridge') { + return ( + <> + {PAUSED_CHAINS.map((event) => { + return ( + + ) + })} + + ) + } else if (type === 'Swap') { + return ( + <> + {PAUSED_CHAINS.map((event) => { + return ( + + ) + })} + + ) + } else { + return null + } +} + +/** + * Hook that maps through PAUSED_CHAINS to apply the single event countdown progress logic to each. + * @returns A list of objects containing maintenance status and components for each paused chain. + */ +export const useMaintenanceCountdownProgresses = ({ + type, +}: { + type: 'Bridge' | 'Swap' +}) => { + const { fromChainId: bridgeFromChainId, toChainId: bridgeToChainId } = + useBridgeState() + const { swapChainId } = useSwapState() + + if (type === 'Bridge') { + return PAUSED_CHAINS.map((event) => { + return useMaintenanceCountdownProgress({ + fromChainId: bridgeFromChainId, + toChainId: bridgeToChainId, + startDate: event.startTimePauseChain, + endDate: event.endTimePauseChain, + pausedFromChains: event.pausedFromChains, + pausedToChains: event.pausedToChains, + progressBarMessage: event.progressBarMessage, + disabled: event.disableCountdown || !event.pauseBridge, + }) + }) + } else if (type === 'Swap') { + return PAUSED_CHAINS.map((event) => { + return useMaintenanceCountdownProgress({ + fromChainId: swapChainId, + toChainId: null, + startDate: event.startTimePauseChain, + endDate: event.endTimePauseChain, + pausedFromChains: event.pausedFromChains, + pausedToChains: event.pausedToChains, + progressBarMessage: event.progressBarMessage, + disabled: event.disableCountdown || !event.pauseSwap, + }) + }) + } +} + +/** Pause Bridge Modules */ +interface BridgeModulePause { + chainId?: number // Will pause for all chains if undefined + bridgeModuleName: 'SynapseBridge' | 'SynapseRFQ' | 'SynapseCCTP' | 'ALL' +} + +function isValidBridgeModule( + module: any +): module is 'SynapseBridge' | 'SynapseRFQ' | 'SynapseCCTP' | 'ALL' { + return ['SynapseBridge', 'SynapseRFQ', 'SynapseCCTP', 'ALL'].includes(module) +} + +export function getBridgeModuleNames(module) { + if (module.bridgeModuleName === 'ALL') { + return ['SynapseRFQ', 'SynapseCCTP', 'SynapseBridge'] + } + return [module.bridgeModuleName] +} + +export const PAUSED_MODULES: BridgeModulePause[] = pausedBridgeModules.map( + (route) => { + if (!isValidBridgeModule(route.bridgeModuleName)) { + throw new Error(`Invalid module type: ${route.bridgeModuleName}`) + } + + return { + ...route, + bridgeModuleName: route.bridgeModuleName as + | 'SynapseBridge' + | 'SynapseRFQ' + | 'SynapseCCTP' + | 'ALL', + } + } +) diff --git a/packages/synapse-interface/components/Maintenance/AnnouncementBanner.tsx b/packages/synapse-interface/components/Maintenance/components/AnnouncementBanner.tsx similarity index 63% rename from packages/synapse-interface/components/Maintenance/AnnouncementBanner.tsx rename to packages/synapse-interface/components/Maintenance/components/AnnouncementBanner.tsx index b9dcb30677..5433d115d1 100644 --- a/packages/synapse-interface/components/Maintenance/AnnouncementBanner.tsx +++ b/packages/synapse-interface/components/Maintenance/components/AnnouncementBanner.tsx @@ -1,14 +1,16 @@ import { useState, useEffect } from 'react' import { getCountdownTimeStatus } from './EventCountdownProgressBar' +import { isNull } from 'lodash' /** - * Reusable automated Announcement Banner with custom Start/End Time - * Will automatically appear after Start time - * Will automatically disappear after End time - * @param bannerId: store in $MMDDYYYY-$BANNER_NAME format (e.g 03132024-ETH-DENCUN) - * @param bannerContents: contents to display in banner - * @param startDate: start date to show banner - * @param endDate: end date to remove banner + * Generic Message Banner that appears between defined start and end time. + * If end date is null, banner will appear indefinitely until removed. + * + * @param bannerId Unique ID to prevent conflicts with other banner instances. + * Assign ID $MMDDYYYY-$BANNER_NAME format (e.g 03132024-ETH-DENCUN) + * @param bannerContents Message to display + * @param startDate Start time to display banner + * @param endDate End time to remove banner */ export const AnnouncementBanner = ({ bannerId, @@ -19,11 +21,12 @@ export const AnnouncementBanner = ({ bannerId: string bannerContents: any startDate: Date - endDate: Date + endDate: Date | null }) => { const { isStarted, isComplete } = getCountdownTimeStatus(startDate, endDate) + const [hasMounted, setHasMounted] = useState(false) - const [showBanner, setShowBanner] = useState(true) + const [showBanner, setShowBanner] = useState(false) useEffect(() => { setHasMounted(true) @@ -54,26 +57,38 @@ export const AnnouncementBanner = ({ if (!showBanner || !hasMounted || !isStarted || isComplete) return null return ( -
+