From 855a0170334fb36d8ff119ce96f8c9bdb4294d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20Ujv=C3=A1ri?= <58116288+bosi95@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:03:25 +0100 Subject: [PATCH] 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 --- .../components/ConfirmModal/ConfirmModal.tsx | 8 +- .../components/InitialModal/InitialModal.tsx | 5 - src/pages/filemanager/index.tsx | 155 ++++++------ src/providers/Bee.tsx | 225 +++++++++-------- src/providers/FileManager.tsx | 231 +++++++++++------- 5 files changed, 334 insertions(+), 290 deletions(-) diff --git a/src/modules/filemanager/components/ConfirmModal/ConfirmModal.tsx b/src/modules/filemanager/components/ConfirmModal/ConfirmModal.tsx index 9c86632..f849cf6 100644 --- a/src/modules/filemanager/components/ConfirmModal/ConfirmModal.tsx +++ b/src/modules/filemanager/components/ConfirmModal/ConfirmModal.tsx @@ -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(() => document.querySelector('.fm-main') || document.body) + + useLayoutEffect(() => { + setModalRoot(document.querySelector('.fm-main') || document.body) + }, []) return createPortal(
diff --git a/src/modules/filemanager/components/InitialModal/InitialModal.tsx b/src/modules/filemanager/components/InitialModal/InitialModal.tsx index f95ab89..b4e51b7 100644 --- a/src/modules/filemanager/components/InitialModal/InitialModal.tsx +++ b/src/modules/filemanager/components/InitialModal/InitialModal.tsx @@ -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, diff --git a/src/pages/filemanager/index.tsx b/src/pages/filemanager/index.tsx index a047bb6..699ec03 100644 --- a/src/pages/filemanager/index.tsx +++ b/src/pages/filemanager/index.tsx @@ -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(getSignerPk() !== undefined) - const [showErrorModal, setShowErrorModal] = useState(false) + const [showAdminErrorModal, setAdminShowErrorModal] = useState(false) const [errorMessage, setErrorMessage] = useState('') - const [showResetModal, setShowResetModal] = useState(false) + const [resetAcknowledged, setResetAcknowledged] = useState(false) const [isCreationInProgress, setIsCreationInProgress] = useState(false) - const [showConnectionError, setShowConnectionError] = useState(false) + const [connectionErrorDismissed, setConnectionErrorDismissed] = useState(false) const [cacheHelpUrl, setCacheHelpUrl] = useState(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 + } - if (!hasPk) { + if (pageState === PageState.NoPrivateKey) { return } - if (initializationError && !isLoading && !shallReset) { + if (pageState === PageState.InitError) { return ( { @@ -282,17 +270,16 @@ export function FileManagerPage(): ReactElement { ) } - if (showResetModal) { - return setShowResetModal(false)} /> + if (pageState === PageState.Reset) { + return setResetAcknowledged(true)} /> } - if (!showErrorModal && (isEmptyState || isInvalidState)) { + if (pageState === PageState.Initial) { return ( 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 - } - - if (showErrorModal) { + if (pageState === PageState.AdminError) { return ( { - setShowErrorModal(false) - setShowInitialModal(true) + setAdminShowErrorModal(false) setErrorMessage('') }} /> @@ -323,8 +306,8 @@ export function FileManagerPage(): ReactElement { return ( setShowConnectionError(false)} + showConnectionError={isConnectionError && !connectionErrorDismissed} + setShowConnectionError={(show: boolean) => setConnectionErrorDismissed(!show)} isFormbricksActive={isFormbricksActive} errorMessage={errorMessage} setErrorMessage={setErrorMessage} diff --git a/src/providers/Bee.tsx b/src/providers/Bee.tsx index 0613f46..9fbd5db 100644 --- a/src/providers/Bee.tsx +++ b/src/providers/Bee.tsx @@ -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(result: PromiseSettledResult): 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(null) const [apiHealth, setApiHealth] = useState(false) + const [isWarmingUp, setIsWarmingUp] = useState(true) const [nodeAddresses, setNodeAddresses] = useState(null) const [nodeInfo, setNodeInfo] = useState(null) const [topology, setNodeTopology] = useState(null) @@ -191,7 +209,7 @@ export function Provider({ children }: Props): ReactElement { const [settlements, setSettlements] = useState(null) const [chainState, setChainState] = useState(null) const [walletBalance, setWalletBalance] = useState(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(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(() => { diff --git a/src/providers/FileManager.tsx b/src/providers/FileManager.tsx index 9448a9b..076e8c2 100644 --- a/src/providers/FileManager.tsx +++ b/src/providers/FileManager.tsx @@ -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 init: () => Promise + notifyPkSaved: () => void setShowError: (show: boolean) => void syncDrives: () => Promise refreshStamp: (batchId: string) => Promise @@ -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(null) + const initInProgressRef = useRef(false) + const isBeeApiInitialized = useRef(false) + const { status } = useContext(BeeContext) const { apiUrl } = useContext(SettingsContext) + const apiUrlRef = useRef(apiUrl) + + const [pkSaved, setPkSaved] = useState(false) + const [beeInstance, setBeeInstance] = useState(null) const [fm, setFm] = useState(null) + const [initDone, setInitDone] = useState(false) const [shallReset, setShallReset] = useState(false) const [files, setFiles] = useState([]) const [drives, setDrives] = useState([]) @@ -102,6 +113,8 @@ export function Provider({ children }: Props) { const [initializationError, setInitializationError] = useState(false) const [showError, setShowError] = useState(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 => { - if (!beeInstanceRef.current) { - return - } + const syncDrives = useCallback( + async (manager: FileManagerBase, di?: DriveInfo, remove?: boolean): Promise => { + 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 => { - 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 => { + 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 => { 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 => { 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,