SetupFinish.tsx raw

   1  import React, { useEffect } from "react";
   2  import { useNavigate } from "react-router";
   3  import { toast } from "sonner";
   4  import Container from "src/components/Container";
   5  import LottieLoading from "src/components/LottieLoading";
   6  import { Button } from "src/components/ui/button";
   7  import { useInfo } from "src/hooks/useInfo";
   8  import { saveAuthToken } from "src/lib/auth";
   9  import useSetupStore from "src/state/SetupStore";
  10  import { AuthTokenResponse, SetupNodeInfo } from "src/types";
  11  import { handleRequestError } from "src/utils/handleRequestError";
  12  import { request } from "src/utils/request";
  13  
  14  let lastStartupErrorTime: string;
  15  export function SetupFinish() {
  16    const navigate = useNavigate();
  17    const { data: info } = useInfo(true); // poll the info endpoint to auto-redirect when app is running
  18  
  19    const [loading, setLoading] = React.useState(false);
  20    const [connectionError, setConnectionError] = React.useState(false);
  21    const hasFetchedRef = React.useRef(false);
  22  
  23    const startupError = info?.startupError;
  24    const startupErrorTime = info?.startupErrorTime;
  25  
  26    React.useEffect(() => {
  27      // lastStartupErrorTime check is required because user may leave page and come back
  28      // after re-configuring settings
  29      if (
  30        startupError &&
  31        startupErrorTime &&
  32        startupErrorTime !== lastStartupErrorTime
  33      ) {
  34        lastStartupErrorTime = startupErrorTime;
  35        toast.error("Failed to start", {
  36          description: startupError,
  37        });
  38        setLoading(false);
  39        setConnectionError(true);
  40      }
  41    }, [startupError, startupErrorTime]);
  42  
  43    useEffect(() => {
  44      if (!loading) {
  45        return;
  46      }
  47      const timer = setTimeout(() => {
  48        // SetupRedirect takes care of redirection once info.running is true
  49        // if it still didn't redirect after 30 seconds, we show an error
  50        // Typically initial startup should complete in less than 10 seconds.
  51        setLoading(false);
  52        setConnectionError(true);
  53      }, 30000);
  54  
  55      return () => {
  56        clearTimeout(timer);
  57      };
  58    }, [loading]);
  59  
  60    useEffect(() => {
  61      if (!info) {
  62        return;
  63      }
  64      // ensure setup call is only called once
  65      if (hasFetchedRef.current) {
  66        return;
  67      }
  68      hasFetchedRef.current = true;
  69  
  70      (async () => {
  71        setLoading(true);
  72        const succeeded = await finishSetup(
  73          useSetupStore.getState().nodeInfo,
  74          useSetupStore.getState().unlockPassword
  75        );
  76        // only setup call is successful as start is async
  77        if (!succeeded) {
  78          setLoading(false);
  79          setConnectionError(true);
  80        }
  81      })();
  82    }, [navigate, info]);
  83  
  84    if (connectionError) {
  85      return (
  86        <>
  87          <title>Connection Failed · Alby Hub</title>
  88          <Container>
  89            <div className="flex flex-col gap-5 text-center items-center">
  90              <div className="grid gap-2">
  91                <h1 className="font-semibold text-lg">Connection Failed</h1>
  92                <p>Please check your node configuration and try again.</p>
  93              </div>
  94              <Button
  95                onClick={() => {
  96                  navigate(-1);
  97                }}
  98              >
  99                Try again
 100              </Button>
 101            </div>
 102          </Container>
 103        </>
 104      );
 105    }
 106  
 107    return (
 108      <>
 109        <title>Setting up... · Alby Hub</title>
 110        <Container>
 111          <div className="flex flex-col gap-5 justify-center text-center">
 112            <LottieLoading size={400} />
 113            <h1 className="font-semibold text-lg font-headline">
 114              Setting up your Hub...
 115            </h1>
 116          </div>
 117        </Container>
 118      </>
 119    );
 120  }
 121  
 122  const finishSetup = async (
 123    nodeInfo: SetupNodeInfo,
 124    unlockPassword: string
 125  ): Promise<boolean> => {
 126    try {
 127      await request("/api/setup", {
 128        method: "POST",
 129        headers: {
 130          "Content-Type": "application/json",
 131        },
 132        body: JSON.stringify({
 133          ...nodeInfo,
 134          unlockPassword,
 135        }),
 136      });
 137  
 138      const authTokenResponse = await request<AuthTokenResponse>("/api/start", {
 139        method: "POST",
 140        headers: {
 141          "Content-Type": "application/json",
 142        },
 143        body: JSON.stringify({
 144          unlockPassword,
 145        }),
 146      });
 147      if (authTokenResponse) {
 148        saveAuthToken(authTokenResponse.token);
 149      }
 150      return true;
 151    } catch (error) {
 152      handleRequestError("Failed to connect", error);
 153      return false;
 154    }
 155  };
 156