Skip to content
Snippets Groups Projects
App.tsx 5.18 KiB
import React, { useEffect, useState } from 'react';
import {
  ErrorBoundary,
  useErrorBoundary,
  type FallbackProps,
} from 'react-error-boundary';
import { useSelector } from 'react-redux';

import * as Platform from 'loot-core/src/client/platform';
import {
  init as initConnection,
  send,
} from 'loot-core/src/platform/client/fetch';
import { type GlobalPrefs } from 'loot-core/src/types/prefs';

import { useActions } from '../hooks/useActions';
import installPolyfills from '../polyfills';
import { ResponsiveProvider } from '../ResponsiveProvider';
import { styles, hasHiddenScrollbars, ThemeStyle } from '../style';

import AppBackground from './AppBackground';
import View from './common/View';
import DevelopmentTopBar from './DevelopmentTopBar';
import FatalError from './FatalError';
import FinancesApp from './FinancesApp';
import ManagementApp from './manager/ManagementApp';
import MobileWebMessage from './MobileWebMessage';
import UpdateNotification from './UpdateNotification';

type AppProps = {
  budgetId: string;
  cloudFileId: string;
  loadingText: string;
  loadBudget: (
    id: string,
    loadingText?: string,
    options?: object,
  ) => Promise<void>;
  closeBudget: () => Promise<void>;
  loadGlobalPrefs: () => Promise<GlobalPrefs>;
};

function App({
  budgetId,
  cloudFileId,
  loadingText,
  loadBudget,
  closeBudget,
  loadGlobalPrefs,
}: AppProps) {
  const [initializing, setInitializing] = useState(true);
  const { showBoundary: showErrorBoundary } = useErrorBoundary();

  async function init() {
    const socketName = await global.Actual.getServerSocket();

    await initConnection(socketName);

    // Load any global prefs
    await loadGlobalPrefs();

    // Open the last opened budget, if any
    const budgetId = await send('get-last-opened-backup');
    if (budgetId) {
      await loadBudget(budgetId);

      // Check to see if this file has been remotely deleted (but
      // don't block on this in case they are offline or something)
      send('get-remote-files').then(files => {
        if (files) {
          let remoteFile = files.find(f => f.fileId === cloudFileId);
          if (remoteFile && remoteFile.deleted) {
            closeBudget();
          }
        }
      });
    }
  }

  useEffect(() => {
    async function initAll() {
      await Promise.all([installPolyfills(), init()]);
      setInitializing(false);
    }

    initAll().catch(showErrorBoundary);
  }, []);

  useEffect(() => {
    global.Actual.updateAppMenu(!!budgetId);
  }, [budgetId]);

  return (
    <>
      {initializing ? (
        <AppBackground initializing={initializing} loadingText={loadingText} />
      ) : budgetId ? (
        <FinancesApp />
      ) : (
        <>
          <AppBackground
            initializing={initializing}
            loadingText={loadingText}
          />
          <ManagementApp isLoading={loadingText != null} />
        </>
      )}

      <UpdateNotification />
      <MobileWebMessage />
    </>
  );
}

function ErrorFallback({ error }: FallbackProps) {
  return (
    <>
      <AppBackground />
      <FatalError error={error} buttonText="Restart app" />
    </>
  );
}

function AppWrapper() {
  let budgetId = useSelector(
    state => state.prefs.local && state.prefs.local.id,
  );
  let cloudFileId = useSelector(
    state => state.prefs.local && state.prefs.local.cloudFileId,
  );
  let loadingText = useSelector(state => state.app.loadingText);
  let { loadBudget, closeBudget, loadGlobalPrefs, sync } = useActions();
  const [hiddenScrollbars, setHiddenScrollbars] = useState(
    hasHiddenScrollbars(),
  );

  useEffect(() => {
    function checkScrollbars() {
      if (hiddenScrollbars !== hasHiddenScrollbars()) {
        setHiddenScrollbars(hasHiddenScrollbars());
      }
    }

    let isSyncing = false;

    async function onVisibilityChange() {
      if (!isSyncing) {
        console.debug('triggering sync because of visibility change');
        isSyncing = true;
        await sync();
        isSyncing = false;
      }
    }

    window.addEventListener('focus', checkScrollbars);
    window.addEventListener('visibilitychange', onVisibilityChange);

    return () => {
      window.removeEventListener('focus', checkScrollbars);
      window.removeEventListener('visibilitychange', onVisibilityChange);
    };
  }, [sync]);

  return (
    <ResponsiveProvider>
      <View
        style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
      >
        <View
          key={hiddenScrollbars ? 'hidden-scrollbars' : 'scrollbars'}
          style={{
            flexGrow: 1,
            overflow: 'hidden',
            ...styles.lightScrollbar,
          }}
        >
          <ErrorBoundary FallbackComponent={ErrorFallback}>
            {process.env.REACT_APP_REVIEW_ID && !Platform.isPlaywright && (
              <DevelopmentTopBar />
            )}
            <App
              budgetId={budgetId}
              cloudFileId={cloudFileId}
              loadingText={loadingText}
              loadBudget={loadBudget}
              closeBudget={closeBudget}
              loadGlobalPrefs={loadGlobalPrefs}
            />
          </ErrorBoundary>
          <ThemeStyle />
        </View>
      </View>
    </ResponsiveProvider>
  );
}

export default AppWrapper;