import type { LiFiStepExtended } from '@lifi/sdk'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useTimer } from '../../hooks/timer/useTimer.js'
import { formatTimer } from '../../utils/timer.js'
import { TimerContent } from './TimerContent.js'
/**
* Finds the most recent process that is either a SWAP, CROSS_CHAIN, or RECEIVING_CHAIN.
* Includes RECEIVING_CHAIN to track the complete transaction lifecycle for UI updates.
*/
const getProgressProcess = (step: LiFiStepExtended) =>
step.execution?.process.findLast(
(process) =>
process.type === 'SWAP' ||
process.type === 'CROSS_CHAIN' ||
process.type === 'RECEIVING_CHAIN'
)
/**
* Finds the most recent SWAP or CROSS_CHAIN process, excluding RECEIVING_CHAIN.
* Expiry time is based on when the active transaction started, not the receiving phase.
*/
const getExpiryProcess = (step: LiFiStepExtended) =>
step.execution?.process.findLast(
(process) => process.type === 'SWAP' || process.type === 'CROSS_CHAIN'
)
/**
* Calculates expiry timestamp based on process start time, estimated duration, and pause time.
* Pause time is added when action is required (usually for signature requests).
*/
const getExpiryTimestamp = (step: LiFiStepExtended) => {
const lastProcess = getExpiryProcess(step)
let timeInPause = 0
if (lastProcess?.actionRequiredAt) {
const actionDoneAt =
lastProcess.pendingAt ?? lastProcess.doneAt ?? Date.now()
timeInPause = new Date(
actionDoneAt - lastProcess.actionRequiredAt
).getTime()
}
const expiry = new Date(
(lastProcess?.startedAt ?? Date.now()) +
step.estimate.executionDuration * 1000 +
timeInPause
)
return expiry
}
export const StepTimer: React.FC<{
step: LiFiStepExtended
hideInProgress?: boolean
}> = ({ step, hideInProgress }) => {
const { t, i18n } = useTranslation()
const [isExpired, setExpired] = useState(false)
const [isExecutionStarted, setExecutionStarted] = useState(
() => !!getProgressProcess(step)
)
const [expiryTimestamp, setExpiryTimestamp] = useState(() =>
getExpiryTimestamp(step)
)
const { days, hours, minutes, seconds, isRunning, pause, resume, restart } =
useTimer({
autoStart: false,
expiryTimestamp,
onExpire: () => setExpired(true),
})
useEffect(() => {
const executionProcess = getProgressProcess(step)
if (!executionProcess) {
return
}
if (isExecutionStarted && isExpired) {
return
}
const isProcessStarted =
executionProcess.status === 'STARTED' ||
executionProcess.status === 'PENDING'
const shouldRestart = !isExecutionStarted && isProcessStarted && !isRunning
const shouldPause =
isExecutionStarted &&
(executionProcess.status === 'ACTION_REQUIRED' ||
executionProcess.status === 'MESSAGE_REQUIRED' ||
executionProcess.status === 'RESET_REQUIRED') &&
isRunning
const shouldStop =
isExecutionStarted && executionProcess.status === 'FAILED'
const shouldResume = isExecutionStarted && isProcessStarted && !isRunning
if (shouldRestart) {
const newExpiryTimestamp = getExpiryTimestamp(step)
setExecutionStarted(true)
setExpiryTimestamp(newExpiryTimestamp)
return restart(newExpiryTimestamp, true)
}
if (shouldPause) {
return pause()
}
if (shouldResume) {
return resume()
}
if (shouldStop) {
setExecutionStarted(false)
setExpired(false)
}
}, [isExecutionStarted, isExpired, isRunning, pause, restart, resume, step])
const isTimerExpired = isExpired || (!minutes && !seconds)
if (
step.execution?.status === 'DONE' ||
step.execution?.status === 'FAILED' ||
(isTimerExpired && hideInProgress)
) {
return null
}
if (!isExecutionStarted) {
const showSeconds = step.estimate.executionDuration < 60
const duration = showSeconds
? Math.floor(step.estimate.executionDuration)
: Math.floor(step.estimate.executionDuration / 60)
return (
{duration.toLocaleString(i18n.language, {
style: 'unit',
unit: showSeconds ? 'second' : 'minute',
unitDisplay: 'narrow',
})}
)
}
return isTimerExpired ? (
t('main.inProgress')
) : (
{formatTimer({
locale: i18n.language,
days,
hours,
minutes,
seconds,
})}
)
}