fix: filemanager state handling (#232)

* fix: filemanager state handling
* refactor: fm provider and fm page
* fix: detect bee warmup and wait more for syncing
* refactor: optimize bee provider to avoid rerenders
---------

Co-authored-by: Roland Seres <roland.seres90@gmail.com>
This commit is contained in:
Bálint Ujvári
2026-03-19 16:03:25 +01:00
parent 55e7879849
commit 855a017033
5 changed files with 334 additions and 290 deletions
@@ -1,4 +1,4 @@
import React, { ReactElement } from 'react'
import React, { ReactElement, useLayoutEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { Button } from '../Button/Button'
@@ -35,7 +35,11 @@ export function ConfirmModal({
onMinimize,
background = true,
}: ConfirmModalProps): ReactElement {
const modalRoot = document.querySelector('.fm-main') || document.body
const [modalRoot, setModalRoot] = useState<Element>(() => document.querySelector('.fm-main') || document.body)
useLayoutEffect(() => {
setModalRoot(document.querySelector('.fm-main') || document.body)
}, [])
return createPortal(
<div className={`fm-modal-container fm-confirm-modal ${background ? '' : 'fm-modal-no-background'}`}>
@@ -20,7 +20,6 @@ import './InitialModal.scss'
interface InitialModalProps {
resetState: boolean
handleVisibility: (isVisible: boolean) => void
handleShowError: (flag: boolean, errorMessage?: string) => void
setIsCreationInProgress: (isCreating: boolean) => void
}
@@ -65,7 +64,6 @@ const setSecurityLevel = (setter: (value: RedundancyLevel) => void) => {
export function InitialModal({
resetState,
setIsCreationInProgress,
handleVisibility,
handleShowError,
}: InitialModalProps): ReactElement {
const [isCreateEnabled, setIsCreateEnabled] = useState(false)
@@ -133,7 +131,6 @@ export function InitialModal({
const createAdminDrive = useCallback(async () => {
setIsCreationInProgress?.(true)
handleVisibility(false)
await handleCreateDrive({
beeApi,
@@ -148,7 +145,6 @@ export function InitialModal({
resetState,
existingBatch: selectedBatch,
onSuccess: () => {
handleVisibility(false)
setIsCreationInProgress(false)
},
onError: err => {
@@ -164,7 +160,6 @@ export function InitialModal({
validityEndDate,
erasureCodeLevel,
selectedBatch,
handleVisibility,
handleShowError,
setIsCreationInProgress,
resetState,
+69 -86
View File
@@ -20,7 +20,6 @@ import { getSignerPk, removeSignerPk } from '@/modules/filemanager/utils/common'
import { CheckState, Context as BeeContext } from '@/providers/Bee'
import { Context as FMContext } from '@/providers/FileManager'
import { BrowserPlatform, cacheClearUrls, detectBrowser } from '@/providers/Platform'
import { Context as SettingsContext } from '@/providers/Settings'
function PrivateKeyModalBlock({ onSaved }: { onSaved: () => void }) {
return (
@@ -74,7 +73,6 @@ function ResetModalBlock({ cacheHelpUrl, onConfirm }: { cacheHelpUrl: string; on
function InitialModalBlock(props: {
resetState: boolean
handleVisibility: (isVisible: boolean) => void
handleShowError: (flag: boolean, error?: string) => void
setIsCreationInProgress: (isCreating: boolean) => void
}) {
@@ -153,22 +151,29 @@ function FileManagerMainContent(props: {
)
}
enum PageState {
Connecting = 'connecting', // still warming up — show nothing / loader
NoPrivateKey = 'no-pk', // private key not set
Loading = 'loading', // bee ready, pk present, FM init in progress
Reset = 'reset', // STATE_INVALID emitted and user has not yet acknowledged
InitError = 'init-error', // FM init completed with an error (non-reset case)
Initial = 'initial', // FM ready but no admin stamp/drive → show InitialModal
AdminError = 'admin-error', // drive creation failed
Ready = 'ready', // fully operational
}
export function FileManagerPage(): ReactElement {
const isMountedRef = useRef(true)
const [showInitialModal, setShowInitialModal] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [hasAdminDrive, setHasAdminDrive] = useState(false)
const [hasPk, setHasPk] = useState<boolean>(getSignerPk() !== undefined)
const [showErrorModal, setShowErrorModal] = useState<boolean>(false)
const [showAdminErrorModal, setAdminShowErrorModal] = useState<boolean>(false)
const [errorMessage, setErrorMessage] = useState<string>('')
const [showResetModal, setShowResetModal] = useState<boolean>(false)
const [resetAcknowledged, setResetAcknowledged] = useState<boolean>(false)
const [isCreationInProgress, setIsCreationInProgress] = useState<boolean>(false)
const [showConnectionError, setShowConnectionError] = useState<boolean>(false)
const [connectionErrorDismissed, setConnectionErrorDismissed] = useState<boolean>(false)
const [cacheHelpUrl, setCacheHelpUrl] = useState<string>(cacheClearUrls[BrowserPlatform.Chrome])
const { status } = useContext(BeeContext)
const { beeApi } = useContext(SettingsContext)
const { fm, shallReset, adminDrive, initializationError, init } = useContext(FMContext)
const { fm, initDone, shallReset, adminDrive, initializationError, notifyPkSaved } = useContext(FMContext)
useEffect(() => {
isMountedRef.current = true
@@ -185,93 +190,76 @@ export function FileManagerPage(): ReactElement {
}
}, [])
useEffect(() => {
const isApiError = status.apiConnection.checkState !== CheckState.OK || !status.apiConnection.isEnabled
setShowConnectionError(isApiError)
}, [status.apiConnection])
const { isBeeReady, isConnectionError } = useMemo(() => {
const isConnecting = status.all === CheckState.CONNECTING
const isApiOk = status.apiConnection.isEnabled && status.apiConnection.checkState === CheckState.OK
return {
isBeeReady: !isConnecting && isApiOk,
isConnectionError: !isConnecting && !isApiOk && Boolean(fm),
}
}, [status, fm])
useEffect(() => {
if (!beeApi) {
return
if (!isConnectionError) {
setConnectionErrorDismissed(false)
}
}, [isConnectionError])
if (!hasPk) {
setIsLoading(false)
const pageState = useMemo((): PageState => {
if (!isBeeReady && !initDone) return PageState.Connecting
return
}
if (!hasPk) return PageState.NoPrivateKey
setShowResetModal(shallReset)
if (!initDone) return PageState.Loading
if (shallReset) {
setShowInitialModal(true)
if (shallReset && !resetAcknowledged) return PageState.Reset
return
}
if (initializationError && !shallReset) return PageState.InitError
if (initializationError) {
setIsLoading(false)
if (showAdminErrorModal) return PageState.AdminError
return
}
const hasAdminStamp = Boolean(fm?.adminStamp)
const hasAdminDrive = Boolean(adminDrive)
if (fm) {
const hasAdminStamp = Boolean(fm.adminStamp)
const tmpHasAdminDrive = Boolean(adminDrive)
setHasAdminDrive(hasAdminStamp || tmpHasAdminDrive)
setIsLoading(false)
if (!hasAdminStamp && !hasAdminDrive && !isCreationInProgress) return PageState.Initial
setShowInitialModal(!(hasAdminStamp || tmpHasAdminDrive))
return PageState.Ready
}, [
isBeeReady,
hasPk,
initDone,
shallReset,
resetAcknowledged,
initializationError,
showAdminErrorModal,
fm,
adminDrive,
isCreationInProgress,
])
return
}
setIsLoading(true)
}, [fm, beeApi, hasPk, initializationError, adminDrive, shallReset])
const handlePrivateKeySaved = useCallback(async () => {
const handlePrivateKeySaved = useCallback(() => {
if (!isMountedRef.current) return
setHasPk(true)
if (fm) {
if (!isMountedRef.current) return
if (fm) return
setIsLoading(false)
return
}
setIsLoading(true)
const manager = await init()
if (!isMountedRef.current) return
setIsLoading(false)
const hasAdminStamp = Boolean(manager?.adminStamp)
const tmpHasAdminDrive = Boolean(adminDrive)
setShowInitialModal(!(hasAdminStamp || tmpHasAdminDrive))
}, [fm, adminDrive, init])
const isEmptyState = useMemo(() => {
return showInitialModal && !isLoading && !hasAdminDrive && !isCreationInProgress
}, [showInitialModal, isLoading, hasAdminDrive, isCreationInProgress])
const isInvalidState = useMemo(
() => shallReset && fm && !isCreationInProgress,
[shallReset, fm, isCreationInProgress],
)
notifyPkSaved()
}, [fm, notifyPkSaved])
const loading = !fm?.adminStamp || !adminDrive
const isFormbricksActive = Boolean(fm && fm.adminStamp && adminDrive && !loading)
const isFormbricksActive = Boolean(fm && fm.adminStamp && adminDrive && !showInitialModal && !loading)
if (pageState === PageState.Connecting || pageState === PageState.Loading) {
return <LoadingBlock />
}
if (!hasPk) {
if (pageState === PageState.NoPrivateKey) {
return <PrivateKeyModalBlock onSaved={handlePrivateKeySaved} />
}
if (initializationError && !isLoading && !shallReset) {
if (pageState === PageState.InitError) {
return (
<InitializationErrorBlock
onOk={() => {
@@ -282,17 +270,16 @@ export function FileManagerPage(): ReactElement {
)
}
if (showResetModal) {
return <ResetModalBlock cacheHelpUrl={cacheHelpUrl} onConfirm={() => setShowResetModal(false)} />
if (pageState === PageState.Reset) {
return <ResetModalBlock cacheHelpUrl={cacheHelpUrl} onConfirm={() => setResetAcknowledged(true)} />
}
if (!showErrorModal && (isEmptyState || isInvalidState)) {
if (pageState === PageState.Initial) {
return (
<InitialModalBlock
resetState={shallReset}
handleVisibility={(isVisible: boolean) => setShowInitialModal(isVisible)}
handleShowError={(flag: boolean, error?: string) => {
setShowErrorModal(flag)
setAdminShowErrorModal(flag)
if (error) setErrorMessage(error)
}}
@@ -301,19 +288,15 @@ export function FileManagerPage(): ReactElement {
)
}
if (!fm) {
return <LoadingBlock />
}
if (showErrorModal) {
if (pageState === PageState.AdminError) {
return (
<ErrorModalBlock
label={
errorMessage ||
'Error creating Admin Drive. Please try again. Possible causes include insufficient xDAI balance or a lost connection to the RPC.'
}
onClick={() => {
setShowErrorModal(false)
setShowInitialModal(true)
setAdminShowErrorModal(false)
setErrorMessage('')
}}
/>
@@ -323,8 +306,8 @@ export function FileManagerPage(): ReactElement {
return (
<FileManagerMainContent
fm={fm}
showConnectionError={showConnectionError}
setShowConnectionError={() => setShowConnectionError(false)}
showConnectionError={isConnectionError && !connectionErrorDismissed}
setShowConnectionError={(show: boolean) => setConnectionErrorDismissed(!show)}
isFormbricksActive={isFormbricksActive}
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
+112 -113
View File
@@ -29,7 +29,7 @@ import { useLatestBeeRelease } from '../hooks/apiHooks'
import { Context as SettingsContext } from './Settings'
const LAUNCH_GRACE_PERIOD = 15_000
const LAUNCH_GRACE_PERIOD = 35_000
const REFRESH_WHEN_OK = 30_000
const REFRESH_WHEN_ERROR = 5_000
const TIMEOUT = 3_000
@@ -116,15 +116,22 @@ interface Props {
children: ReactNode
}
function getStatus(
nodeInfo: NodeInfo | null,
apiHealth: boolean,
topology: Topology | null,
chequebookAddress: ChequebookAddressResponse | null,
chequebookBalance: ChequebookBalanceResponse | null,
error: Error | null,
startedAt: number,
): Status {
interface StatusProps {
nodeInfo: NodeInfo | null
apiHealth: boolean
topology: Topology | null
isWarmingUp: boolean
chequebookAddress: ChequebookAddressResponse | null
chequebookBalance: ChequebookBalanceResponse | null
error: Error | null
startedAt: number
}
function getStatus(props: StatusProps): Status {
const { nodeInfo, apiHealth, topology, isWarmingUp, chequebookAddress, chequebookBalance, error, startedAt } = {
...props,
}
const status: Status = { ...initialValues.status }
// API connection check
@@ -143,15 +150,17 @@ function getStatus(
if (chequebookAddress?.chequebookAddress && chequebookBalance !== null) {
status.chequebook.checkState = CheckState.OK
} else status.chequebook.checkState = CheckState.OK
} else {
status.chequebook.checkState = CheckState.WARNING
}
}
status.all = determineOverallStatus(status, startedAt)
status.all = determineOverallStatus(status, isWarmingUp, startedAt)
return status
}
function determineOverallStatus(status: Status, startedAt: number): CheckState {
function determineOverallStatus(status: Status, isWarmingUp: boolean, startedAt: number): CheckState {
const hasErrors = Object.values(status).some(
({ isEnabled, checkState }) => isEnabled && checkState === CheckState.ERROR,
)
@@ -160,15 +169,23 @@ function determineOverallStatus(status: Status, startedAt: number): CheckState {
)
const isInGracePeriod = Date.now() - startedAt < LAUNCH_GRACE_PERIOD
if (hasErrors && isInGracePeriod) {
if (isWarmingUp || isInGracePeriod) {
return CheckState.CONNECTING
} else if (hasErrors) {
return CheckState.ERROR
} else if (hasWarnings) {
return CheckState.WARNING
} else {
return CheckState.OK
}
if (hasErrors) {
return CheckState.ERROR
}
if (hasWarnings) {
return CheckState.WARNING
}
return CheckState.OK
}
function getFulfilledValue<T>(result: PromiseSettledResult<T>): T | null {
return result.status === 'fulfilled' ? result.value : null
}
// This does not need to be exposed and works much better as variable than state variable which may trigger some unnecessary re-renders
@@ -179,6 +196,7 @@ export function Provider({ children }: Props): ReactElement {
const [beeVersion, setBeeVersion] = useState<string | null>(null)
const [apiHealth, setApiHealth] = useState<boolean>(false)
const [isWarmingUp, setIsWarmingUp] = useState<boolean>(true)
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
const [nodeInfo, setNodeInfo] = useState<NodeInfo | null>(null)
const [topology, setNodeTopology] = useState<Topology | null>(null)
@@ -191,7 +209,7 @@ export function Provider({ children }: Props): ReactElement {
const [settlements, setSettlements] = useState<AllSettlements | null>(null)
const [chainState, setChainState] = useState<ChainState | null>(null)
const [walletBalance, setWalletBalance] = useState<WalletBalance | null>(null)
const [startedAt] = useState(() => Date.now())
const [startedAt, setStartedAt] = useState(() => Date.now())
const { latestBeeRelease } = useLatestBeeRelease()
@@ -202,6 +220,15 @@ export function Provider({ children }: Props): ReactElement {
const frequencyRef = useRef<number | null>(frequency)
useEffect(() => {
if (isWarmingUp) return
setStartedAt(Date.now())
const timer = setTimeout(() => setStartedAt(0), LAUNCH_GRACE_PERIOD)
return () => clearTimeout(timer)
}, [isWarmingUp])
const refresh = useCallback(async () => {
// Don't want to refresh when already refreshing
if (isRefreshing) {
@@ -215,101 +242,63 @@ export function Provider({ children }: Props): ReactElement {
return
}
try {
isRefreshing = true
setError(null)
isRefreshing = true
const promises = [
// API health
beeApi
.getHealth({ timeout: TIMEOUT })
.then(response => setBeeVersion(response.version))
.then(() => setApiHealth(true))
.catch(() => {
setBeeVersion(null)
setApiHealth(false)
}),
const [
healthResult,
statusResult,
nodeAddressesResult,
nodeInfoResult,
topologyResult,
peersResult,
chequebookAddressResult,
peerChequesResult,
chainStateResult,
walletResult,
chequebookBalanceResult,
stakeResult,
peerBalancesResult,
settlementsResult,
] = await Promise.allSettled([
beeApi.getHealth({ timeout: TIMEOUT }),
beeApi.getStatus({ timeout: TIMEOUT }),
beeApi.getNodeAddresses({ timeout: TIMEOUT }),
beeApi.getNodeInfo({ timeout: TIMEOUT }),
beeApi.getTopology({ timeout: TIMEOUT }),
beeApi.getPeers({ timeout: TIMEOUT }),
beeApi.getChequebookAddress({ timeout: TIMEOUT }),
beeApi.getLastCheques({ timeout: TIMEOUT }),
beeApi.getChainState({ timeout: TIMEOUT }),
beeApi.getWalletBalance({ timeout: TIMEOUT }),
beeApi.getChequebookBalance({ timeout: TIMEOUT }),
beeApi.getStake({ timeout: TIMEOUT }),
beeApi.getAllBalances({ timeout: TIMEOUT }),
beeApi.getAllSettlements(),
])
// Node Addresses
beeApi
.getNodeAddresses({ timeout: TIMEOUT })
.then(setNodeAddresses)
.catch(() => setNodeAddresses(null)),
// NodeInfo
beeApi
.getNodeInfo({ timeout: TIMEOUT })
.then(setNodeInfo)
.catch(() => setNodeInfo(null)),
// Network Topology
beeApi
.getTopology({ timeout: TIMEOUT })
.then(setNodeTopology)
.catch(() => setNodeTopology(null)),
// Peers
beeApi
.getPeers({ timeout: TIMEOUT })
.then(setPeers)
.catch(() => setPeers(null)),
// Chequebook address
beeApi
.getChequebookAddress({ timeout: TIMEOUT })
.then(setChequebookAddress)
.catch(() => setChequebookAddress(null)),
// Cheques
beeApi
.getLastCheques({ timeout: TIMEOUT })
.then(setPeerCheques)
.catch(() => setPeerCheques(null)),
// Chain state
beeApi
.getChainState({ timeout: TIMEOUT })
.then(setChainState)
.catch(() => setChainState(null)),
// Wallet
beeApi
.getWalletBalance({ timeout: TIMEOUT })
.then(setWalletBalance)
.catch(() => setWalletBalance(null)),
// Chequebook balance
beeApi
.getChequebookBalance({ timeout: TIMEOUT })
.then(setChequebookBalance)
.catch(() => setChequebookBalance(null)),
beeApi
.getStake({ timeout: TIMEOUT })
.then(stake => setStake(stake))
.catch(() => setStake(null)),
// Peer balances
beeApi
.getAllBalances({ timeout: TIMEOUT })
.then(x => setPeerBalances(x.balances))
.catch(() => setPeerBalances(null)),
// Settlements
beeApi
.getAllSettlements()
.then(setSettlements)
.catch(() => setSettlements(null)),
]
await Promise.allSettled(promises)
} catch (e) {
setError(e as Error)
}
// All setters called synchronously — React 18 batches them into one render.
const health = getFulfilledValue(healthResult)
setBeeVersion(health?.version ?? null)
setApiHealth(Boolean(health))
setIsWarmingUp(getFulfilledValue(statusResult)?.isWarmingUp ?? false)
setNodeAddresses(getFulfilledValue(nodeAddressesResult))
setNodeInfo(getFulfilledValue(nodeInfoResult))
setNodeTopology(getFulfilledValue(topologyResult))
setPeers(getFulfilledValue(peersResult))
setChequebookAddress(getFulfilledValue(chequebookAddressResult))
setPeerCheques(getFulfilledValue(peerChequesResult))
setChainState(getFulfilledValue(chainStateResult))
setWalletBalance(getFulfilledValue(walletResult))
setChequebookBalance(getFulfilledValue(chequebookBalanceResult))
setStake(getFulfilledValue(stakeResult))
setPeerBalances(getFulfilledValue(peerBalancesResult)?.balances ?? null)
setSettlements(getFulfilledValue(settlementsResult))
setError(null)
setIsLoading(false)
isRefreshing = false
setLastUpdate(Date.now())
isRefreshing = false
}, [beeApi])
const start = useCallback(
@@ -322,8 +311,18 @@ export function Provider({ children }: Props): ReactElement {
const stop = useCallback(() => setFrequency(null), [])
const status = useMemo(
() => getStatus(nodeInfo, apiHealth, topology, chequebookAddress, chequebookBalance, error, startedAt),
[nodeInfo, apiHealth, topology, chequebookAddress, chequebookBalance, error, startedAt],
() =>
getStatus({
nodeInfo,
apiHealth,
topology,
isWarmingUp,
chequebookAddress,
chequebookBalance,
error,
startedAt,
}),
[nodeInfo, apiHealth, topology, chequebookAddress, chequebookBalance, error, startedAt, isWarmingUp],
)
useEffect(() => {
+147 -84
View File
@@ -7,10 +7,12 @@ import { FILE_MANAGER_EVENTS } from '../modules/filemanager/constants/common'
import { getUsableStamps } from '../modules/filemanager/utils/bee'
import { getSignerPk } from '../modules/filemanager/utils/common'
import { CheckState, Context as BeeContext } from './Bee'
import { Context as SettingsContext } from './Settings'
interface ContextInterface {
fm: FileManagerBase | null
initDone: boolean
files: FileInfo[]
currentDrive?: DriveInfo
currentStamp?: PostageBatch
@@ -24,6 +26,7 @@ interface ContextInterface {
setCurrentStamp: (s: PostageBatch | undefined) => void
resync: () => Promise<void>
init: () => Promise<FileManagerBase | null>
notifyPkSaved: () => void
setShowError: (show: boolean) => void
syncDrives: () => Promise<void>
refreshStamp: (batchId: string) => Promise<PostageBatch | undefined>
@@ -31,6 +34,7 @@ interface ContextInterface {
const initialValues: ContextInterface = {
fm: null,
initDone: false,
files: [],
currentDrive: undefined,
currentStamp: undefined,
@@ -45,6 +49,7 @@ const initialValues: ContextInterface = {
resync: async () => {},
// eslint-disable-next-line require-await
init: async () => null,
notifyPkSaved: () => {},
setShowError: () => {},
syncDrives: async () => {},
// eslint-disable-next-line require-await
@@ -85,12 +90,18 @@ const findDrives = (
}
export function Provider({ children }: Props) {
const initInProgressRef = useRef(false)
const beeInstanceRef = useRef<Bee | null>(null)
const initInProgressRef = useRef<boolean>(false)
const isBeeApiInitialized = useRef<boolean>(false)
const { status } = useContext(BeeContext)
const { apiUrl } = useContext(SettingsContext)
const apiUrlRef = useRef<string>(apiUrl)
const [pkSaved, setPkSaved] = useState<boolean>(false)
const [beeInstance, setBeeInstance] = useState<Bee | null>(null)
const [fm, setFm] = useState<FileManagerBase | null>(null)
const [initDone, setInitDone] = useState<boolean>(false)
const [shallReset, setShallReset] = useState<boolean>(false)
const [files, setFiles] = useState<FileInfo[]>([])
const [drives, setDrives] = useState<DriveInfo[]>([])
@@ -102,6 +113,8 @@ export function Provider({ children }: Props) {
const [initializationError, setInitializationError] = useState<boolean>(false)
const [showError, setShowError] = useState<boolean>(false)
const notifyPkSaved = useCallback(() => setPkSaved(v => !v), [])
const syncFiles = useCallback((manager: FileManagerBase, fi?: FileInfo, remove?: boolean): void => {
if (fi) {
if (remove) {
@@ -129,70 +142,73 @@ export function Provider({ children }: Props) {
setFiles([...manager.fileInfoList])
}, [])
const syncDrives = useCallback(async (manager: FileManagerBase, di?: DriveInfo, remove?: boolean): Promise<void> => {
if (!beeInstanceRef.current) {
return
}
const syncDrives = useCallback(
async (manager: FileManagerBase, di?: DriveInfo, remove?: boolean): Promise<void> => {
if (!beeInstance) {
return
}
const usableStamps = await getUsableStamps(beeInstanceRef.current)
const usableStamps = await getUsableStamps(beeInstance)
if (di) {
const isNotExpired = usableStamps.some(s => s.batchID.toString() === di.batchId.toString())
if (di) {
const isNotExpired = usableStamps.some(s => s.batchID.toString() === di.batchId.toString())
if (isNotExpired) {
if (remove) {
setDrives(prev => prev.filter(d => d.id.toString() !== di.id.toString()))
if (isNotExpired) {
if (remove) {
setDrives(prev => prev.filter(d => d.id.toString() !== di.id.toString()))
return
}
if (di.isAdmin) {
setAdminDrive(di)
return
}
setDrives(prev => {
const existingIndex = prev.findIndex(d => d.id.toString() === di.id.toString())
if (existingIndex >= 0) {
const updated = [...prev]
updated[existingIndex] = di
return updated
return
}
return [...prev, di]
})
if (di.isAdmin) {
setAdminDrive(di)
return
}
setDrives(prev => {
const existingIndex = prev.findIndex(d => d.id.toString() === di.id.toString())
if (existingIndex >= 0) {
const updated = [...prev]
updated[existingIndex] = di
return updated
}
return [...prev, di]
})
return
}
if (remove) {
setExpiredDrives(prev => prev.filter(d => d.id.toString() !== di.id.toString()))
return
}
if (!di.isAdmin) {
setExpiredDrives(prev => {
const exists = prev.some(d => d.id.toString() === di.id.toString())
return exists ? prev : [...prev, di]
})
return
}
// TODO: handle admin drive expiration!
return
}
if (remove) {
setExpiredDrives(prev => prev.filter(d => d.id.toString() !== di.id.toString()))
return
}
if (!di.isAdmin) {
setExpiredDrives(prev => {
const exists = prev.some(d => d.id.toString() === di.id.toString())
return exists ? prev : [...prev, di]
})
return
}
// TODO: handle admin drive expiration!
return
}
const { adminDrive: tmpAdminDrive, userDrives, expiredDrives } = findDrives(manager.driveList, usableStamps)
setAdminDrive(tmpAdminDrive)
setDrives(userDrives)
setExpiredDrives(expiredDrives)
}, [])
const { adminDrive: tmpAdminDrive, userDrives, expiredDrives } = findDrives(manager.driveList, usableStamps)
setAdminDrive(tmpAdminDrive)
setDrives(userDrives)
setExpiredDrives(expiredDrives)
},
[beeInstance],
)
const syncDrivesPublic = useCallback(async () => {
if (fm) {
@@ -200,55 +216,56 @@ export function Provider({ children }: Props) {
}
}, [fm, syncDrives])
const refreshStamp = useCallback(async (batchId: string): Promise<PostageBatch | undefined> => {
if (!beeInstanceRef.current) {
return
}
const usableStamps = await getUsableStamps(beeInstanceRef.current)
const refreshedStamp = usableStamps.find(s => s.batchID.toString() === batchId)
setCurrentStamp(prev => {
if (prev && prev.batchID.toString() === batchId && refreshedStamp) {
return refreshedStamp
const refreshStamp = useCallback(
async (batchId: string): Promise<PostageBatch | undefined> => {
if (!beeInstance) {
return
}
return prev
})
const usableStamps = await getUsableStamps(beeInstance)
const refreshedStamp = usableStamps.find(s => s.batchID.toString() === batchId)
return refreshedStamp
}, [])
setCurrentStamp(prev => {
if (prev && prev.batchID.toString() === batchId && refreshedStamp) {
return refreshedStamp
}
return prev
})
return refreshedStamp
},
[beeInstance],
)
const init = useCallback(async (): Promise<FileManagerBase | null> => {
const pk = getSignerPk()
if (!apiUrl || !pk || initInProgressRef.current) return null
if (!beeInstance || !pk || initInProgressRef.current) return null
initInProgressRef.current = true
setFm(null)
setInitDone(false)
setFiles([])
setDrives([])
setAdminDrive(null)
setInitializationError(false)
setCurrentDrive(undefined)
setCurrentStamp(undefined)
setShallReset(false)
if (!beeInstanceRef.current) {
beeInstanceRef.current = new Bee(apiUrl, { signer: pk })
}
const manager = new FileManagerBase(beeInstanceRef.current)
const manager = new FileManagerBase(beeInstance)
const handleInitialized = (success: boolean) => {
setInitializationError(!success)
setInitDone(true)
if (success) {
if (manager.adminStamp && !manager.adminStamp.usable) {
// eslint-disable-next-line no-console
console.warn('Admin stamp exists but is not usable')
setShallReset(true)
setInitializationError(true)
return
}
@@ -310,11 +327,13 @@ export function Provider({ children }: Props) {
return manager
} catch {
setInitDone(true)
return null
} finally {
initInProgressRef.current = false
}
}, [apiUrl, syncDrives, syncFiles])
}, [beeInstance, syncDrives, syncFiles])
const resync = useCallback(async (): Promise<void> => {
const prevDriveId = currentDrive?.id.toString()
@@ -322,19 +341,59 @@ export function Provider({ children }: Props) {
const manager = await init()
if (prevDriveId && manager && beeInstanceRef.current) {
if (prevDriveId && manager && beeInstance) {
const refreshedDrive = manager.driveList.find(d => d.id.toString() === prevDriveId)
setCurrentDrive(refreshedDrive)
const uStamps: PostageBatch[] = await getUsableStamps(beeInstanceRef.current)
const uStamps: PostageBatch[] = await getUsableStamps(beeInstance)
const isValidCurrentStamp = uStamps.find(s => s.batchID.toString() === prevStamp?.batchID.toString())
setCurrentStamp(isValidCurrentStamp)
}
}, [currentDrive?.id, currentStamp, init])
}, [beeInstance, currentDrive?.id, currentStamp, init])
useEffect(() => {
if (!apiUrl || initInProgressRef.current) {
apiUrlRef.current = apiUrl
}, [apiUrl])
useEffect(() => {
const isConnecting = status.all === CheckState.CONNECTING
const isApiOk = status.apiConnection.isEnabled && status.apiConnection.checkState === CheckState.OK
const currentApiUrl = apiUrlRef.current
const pk = getSignerPk()
if (!currentApiUrl || !pk) {
isBeeApiInitialized.current = false
setBeeInstance(null)
return
}
if (isConnecting) {
return
}
if (isBeeApiInitialized.current) {
return
}
if (!isApiOk) {
return
}
isBeeApiInitialized.current = true
setBeeInstance(new Bee(currentApiUrl, { signer: pk }))
}, [status.all, status.apiConnection, pkSaved])
useEffect(() => {
isBeeApiInitialized.current = false
setBeeInstance(null)
setInitDone(false)
initInProgressRef.current = false
}, [apiUrl])
useEffect(() => {
if (!beeInstance || initInProgressRef.current) {
return
}
@@ -343,7 +402,7 @@ export function Provider({ children }: Props) {
}
initFromLocalState()
}, [apiUrl, init])
}, [beeInstance, init])
useEffect(() => {
if (fm && drives.length === 0 && !adminDrive) {
@@ -354,6 +413,7 @@ export function Provider({ children }: Props) {
const contextValue = useMemo(
() => ({
fm,
initDone,
files,
currentDrive,
currentStamp,
@@ -367,12 +427,14 @@ export function Provider({ children }: Props) {
setCurrentStamp,
resync,
init,
notifyPkSaved,
setShowError,
syncDrives: syncDrivesPublic,
refreshStamp,
}),
[
fm,
initDone,
files,
currentDrive,
currentStamp,
@@ -386,6 +448,7 @@ export function Provider({ children }: Props) {
setCurrentStamp,
resync,
init,
notifyPkSaved,
setShowError,
syncDrivesPublic,
refreshStamp,