Skip to content

Commit

Permalink
feat(widget): maintenance (#2616)
Browse files Browse the repository at this point in the history
* Fetch pause data and store JSON object in client browser

* Refetch only if last fetch was more than 24 hours ago

* Read chain + module pause from local storage

* Maintenance components rendering based off of fetched pause data

* Pause Bridge button based on Maintenance status

* Filter quotes based on paused modules

* Use user defined styling or defaults

* Style Progress Bar

* Refactor getSynapsePauseData

* Clean

* Fix bridge quote filter

* Adjust text size for maintenance

* Add comments + clean

* Update comment

* Refresh data every hour

* Clean

* Add key to warning messages

* Fix render issues, start move event countdown component directly to Widget to resolve hooks issue

* Resolve hooks render issue with localized component

* Progress bar renders when not isabled

* Clean and simplify Maintenance components

* getMaintenanceData

* Organize back into useMaintenance hook

* Clean / organize

* Use prod urls

* Organizational updates

* Fetch pause data every render, set fetching status flag

* Rm timestamp key

---------

Co-authored-by: abtestingalpha <abtestingalpha@gmail.com>
  • Loading branch information
bigboydiamonds and abtestingalpha authored May 17, 2024
1 parent 9b30673 commit 96f3e77
Show file tree
Hide file tree
Showing 19 changed files with 723 additions and 11 deletions.
10 changes: 10 additions & 0 deletions packages/widget/src/components/BridgeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface BridgeButtonProps {
handleBridge: () => any
isApprovalPending: boolean
isBridgePending: boolean
isBridgePaused: boolean
}

export const BridgeButton = ({
Expand All @@ -23,6 +24,7 @@ export const BridgeButton = ({
handleBridge,
isApprovalPending,
isBridgePending,
isBridgePaused,
}: BridgeButtonProps) => {
const web3Context = useContext(Web3Context)

Expand Down Expand Up @@ -52,6 +54,14 @@ export const BridgeButton = ({

const tooltipPositionStyle = '-top-8'

if (isBridgePaused) {
return (
<button className={buttonClassName} style={buttonStyle} disabled>
Bridge paused
</button>
)
}

if (!provider || !connectedAddress) {
return (
<Tooltip hoverText="Connect Wallet" positionStyles={tooltipPositionStyle}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { isNull } from 'lodash'

import { LinearAnimatedProgressBar } from './LinearAnimatedProgressBar'

export const EventCountdownProgressBar = ({
eventLabel,
startDate,
endDate,
timeRemaining,
status,
}: {
eventLabel: string
startDate: Date
endDate: Date | null
timeRemaining: string
status: 'idle' | 'pending' | 'complete'
}) => {
const isIndefinite = isNull(endDate)

if (status === 'pending') {
return (
<div
className={`
flex flex-col bg-[--synapse-surface]
border border-[--synapse-border] rounded-md
text-[--synapse-text] text-xs md:text-base
`}
>
<div className="flex justify-between px-3 py-2">
<div>{eventLabel}</div>
{isIndefinite ? null : <div>{timeRemaining} remaining</div>}
</div>
<div className="flex px-1">
<LinearAnimatedProgressBar
id="event-countdown-progress-bar"
startDate={startDate}
endDate={endDate}
/>
</div>
</div>
)
} else {
return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { memo } from 'react'
import { isNull } from 'lodash'

import { getCountdownTimeStatus } from '@/utils/getCountdownTimeStatus'

/**
* Constructs animated progress bar visualizing time progress of an event.
* If end date is not provided, progress bar animates indefinitely.
*
* @param id - A unique ID to distinguish instances.
* @param startDate - The start date of the tracked event.
* @param endDate - The end date of the tracked event.
*/
export const LinearAnimatedProgressBar = memo(
({
id,
startDate,
endDate,
}: {
id: string
startDate: Date
endDate: Date | null
}) => {
const height = 3
const maskId = `mask-${id}`
const progressId = `progress-${id}`
const greenColor = 'rgb(74 222 128)'
const purpleColor = 'hsl(265deg 100% 75%)'

let duration: string | number

const isIndefinite = isNull(endDate)

if (isIndefinite) {
duration = 'infinite'
return (
<svg
id="linear-animated-progress-bar"
key={Date.now()}
width="100%"
height={height}
xmlns="http://www.w3.org/2000/svg"
className="rounded-sm"
>
<defs>
<linearGradient
id={progressId}
spreadMethod="reflect"
x1="0"
x2="1"
>
<stop stopColor={purpleColor} />
<stop stopColor={purpleColor} offset=".25" />
<stop stopColor={purpleColor} stopOpacity=".67" offset=".75" />
<stop stopColor={purpleColor} stopOpacity=".67" offset="1" />
<animate
attributeName="x1"
values="0%; -6%"
dur=".67s"
repeatCount="indefinite"
/>
<animate
attributeName="x2"
values="3%; -3%"
dur=".67s"
repeatCount="indefinite"
/>
</linearGradient>
<clipPath id={maskId}>
<rect height="100%">
<animate
attributeName="width"
values={`100%; 100%`}
dur="infinite"
fill="freeze"
calcMode={'linear'}
/>
</rect>
</clipPath>
</defs>
<rect
width="100%"
height={height}
fill={`url(#${progressId})`}
clipPath={`url(#${maskId})`}
></rect>
</svg>
)
} else {
const {
totalTimeInSeconds,
totalTimeElapsedInSeconds,
totalTimeRemainingInSeconds,
isComplete,
} = getCountdownTimeStatus(startDate, endDate)

const percentElapsed = Math.floor(
(totalTimeElapsedInSeconds / totalTimeInSeconds) * 100
)

duration = isComplete ? 0.5 : totalTimeRemainingInSeconds

return (
<svg
id="linear-animated-progress-bar"
key={Date.now()}
width="100%"
height={height}
xmlns="http://www.w3.org/2000/svg"
className="rounded-sm"
>
<defs>
<linearGradient
id={progressId}
spreadMethod="reflect"
x1="0"
x2="1"
>
<stop stopColor={purpleColor} />
<stop stopColor={purpleColor} offset=".25" />
<stop stopColor={purpleColor} stopOpacity=".67" offset=".75" />
<stop stopColor={purpleColor} stopOpacity=".67" offset="1" />
<animate
attributeName="x1"
values="0%; -6%"
dur=".67s"
repeatCount="indefinite"
/>
<animate
attributeName="x2"
values="3%; -3%"
dur=".67s"
repeatCount="indefinite"
/>
</linearGradient>
<clipPath id={maskId}>
<rect height="100%">
<animate
attributeName="width"
values={`${isComplete ? 100 : percentElapsed}%; 100%`}
dur={totalTimeInSeconds}
fill="freeze"
calcMode={'linear'}
/>
</rect>
</clipPath>
</defs>
<rect
width="100%"
height={height}
fill={`url(#${progressId})`}
clipPath={`url(#${maskId})`}
>
{isComplete && (
<animate
attributeName="fill"
values={`${purpleColor}; hsl(185deg 100% 40%); ${greenColor}`}
keyTimes="0; .5; 1"
dur={duration}
fill="freeze"
/>
)}
</rect>
{isComplete && (
<animate
attributeName="height"
values={`${height}; ${height}; 0`}
keyTimes="0; .5; 1"
calcMode="spline"
keySplines="0 0 1 1; .8 0 .2 1"
dur={duration * 1.5}
fill="freeze"
/>
)}
</svg>
)
}
}
)
106 changes: 106 additions & 0 deletions packages/widget/src/components/Maintenance/Maintenance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useBridgeState } from '@/state/slices/bridge/hooks'
import { isChainIncluded } from '@/utils/isChainIncluded'
import { useEventCountdownProgressBar } from '@/hooks/useEventCountdownProgressBar'
import { MaintenanceWarningMessage } from './MaintenanceWarningMessage'
import { getSynapsePauseData } from '@/utils/getSynapsePauseData'
import { isValidBridgeModule } from '@/utils/isValidBridgeModule'

interface ChainPause {
id: string
pausedFromChains: number[]
pausedToChains: number[]
pauseBridge: boolean
startTimePauseChain: Date
endTimePauseChain: Date | null
inputWarningMessage: string
progressBarMessage: string
disableWarning: boolean
disableCountdown: boolean
}

interface BridgeModulePause {
chainId: number | undefined
bridgeModuleName: 'SynapseBridge' | 'SynapseRFQ' | 'SynapseCCTP' | 'ALL'
}

export const getMaintenanceData = () => {
const { pausedChainsData, pausedModulesData } = getSynapsePauseData()

const pausedChainsList: ChainPause[] = pausedChainsData
? pausedChainsData?.map((pause: ChainPause) => {
return {
...pause,
startTimePauseChain: new Date(pause.startTimePauseChain),
endTimePauseChain: pause.endTimePauseChain
? new Date(pause.endTimePauseChain)
: null,
inputWarningMessage: pause.inputWarningMessage,
progressBarMessage: pause.progressBarMessage,
}
})
: []

const pausedModulesList: BridgeModulePause[] = pausedModulesData
? pausedModulesData?.map((route: BridgeModulePause) => {
if (!isValidBridgeModule(route.bridgeModuleName)) {
throw new Error(`Invalid module type: ${route.bridgeModuleName}`)
}

return {
...route,
bridgeModuleName: route.bridgeModuleName as
| 'SynapseBridge'
| 'SynapseRFQ'
| 'SynapseCCTP'
| 'ALL',
}
})
: []

return {
pausedChainsList,
pausedModulesList,
}
}

export const useMaintenance = () => {
const { originChainId, destinationChainId } = useBridgeState()
const { pausedChainsList, pausedModulesList } = getMaintenanceData()

const activePause = pausedChainsList.find(
(pauseData) =>
isChainIncluded(pauseData?.pausedFromChains, [originChainId]) ||
isChainIncluded(pauseData?.pausedToChains, [destinationChainId])
)

const { isPending: isBridgePaused, EventCountdownProgressBar } =
useEventCountdownProgressBar(
activePause?.progressBarMessage,
activePause?.startTimePauseChain,
activePause?.endTimePauseChain,
activePause?.disableCountdown
)

const BridgeMaintenanceProgressBar = () => EventCountdownProgressBar

const BridgeMaintenanceWarningMessage = () => (
<MaintenanceWarningMessage
originChainId={originChainId}
destinationChainId={destinationChainId}
startDate={activePause?.startTimePauseChain}
endDate={activePause?.endTimePauseChain}
pausedOriginChains={activePause?.pausedFromChains}
pausedDestinationChains={activePause?.pausedToChains}
warningMessage={activePause?.inputWarningMessage}
disabled={activePause?.disableWarning}
/>
)

return {
isBridgePaused,
pausedChainsList,
pausedModulesList,
BridgeMaintenanceProgressBar,
BridgeMaintenanceWarningMessage,
}
}
Loading

0 comments on commit 96f3e77

Please sign in to comment.