Răsfoiți Sursa

feat(frontend): Add a better auth flow and UI handling (#4603)

sp.wack 1 an în urmă
părinte
comite
87bc35d2c8

+ 2 - 2
frontend/src/components/modals/ModalBody.tsx

@@ -1,5 +1,5 @@
-import clsx from "clsx";
 import React from "react";
+import { cn } from "#/utils/utils";
 
 interface ModalBodyProps {
   testID?: string;
@@ -11,7 +11,7 @@ function ModalBody({ testID, children, className }: ModalBodyProps) {
   return (
     <div
       data-testid={testID}
-      className={clsx(
+      className={cn(
         "bg-root-primary flex flex-col gap-6 items-center w-[384px] p-6 rounded-xl",
         className,
       )}

+ 69 - 0
frontend/src/components/waitlist-modal.tsx

@@ -0,0 +1,69 @@
+import ModalButton from "./buttons/ModalButton";
+import { ModalBackdrop } from "./modals/modal-backdrop";
+import GitHubLogo from "#/assets/branding/github-logo.svg?react";
+import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
+import ModalBody from "./modals/ModalBody";
+
+interface WaitlistModalProps {
+  ghToken: string | null;
+  githubAuthUrl: string | null;
+}
+
+export function WaitlistModal({ ghToken, githubAuthUrl }: WaitlistModalProps) {
+  return (
+    <ModalBackdrop>
+      <ModalBody>
+        <AllHandsLogo width={68} height={46} />
+        <div className="flex flex-col gap-2 w-full items-center text-center">
+          <h1 className="text-2xl font-bold">
+            {ghToken ? "Just a little longer!" : "Sign in with GitHub"}
+          </h1>
+          {!ghToken && (
+            <p>
+              or{" "}
+              <a
+                href="https://www.all-hands.dev/join-waitlist"
+                target="_blank"
+                rel="noreferrer noopener"
+                className="text-blue-500 hover:underline underline-offset-2"
+              >
+                join the waitlist
+              </a>{" "}
+              if you haven&apos;t already
+            </p>
+          )}
+          {ghToken && (
+            <p className="text-sm">
+              Thanks for your patience! We&apos;re accepting new members
+              progressively. If you haven&apos;t joined the waitlist yet,
+              now&apos;s the time!
+            </p>
+          )}
+        </div>
+
+        {!ghToken && (
+          <ModalButton
+            text="Connect to GitHub"
+            icon={<GitHubLogo width={20} height={20} />}
+            className="bg-[#791B80] w-full"
+            onClick={() => {
+              if (githubAuthUrl) {
+                window.location.href = githubAuthUrl;
+              }
+            }}
+          />
+        )}
+        {ghToken && (
+          <a
+            href="https://www.all-hands.dev/join-waitlist"
+            target="_blank"
+            rel="noreferrer"
+            className="rounded bg-[#FFE165] text-black text-sm font-bold py-[10px] w-full text-center hover:opacity-80"
+          >
+            Join Waitlist
+          </a>
+        )}
+      </ModalBody>
+    </ModalBackdrop>
+  );
+}

+ 4 - 4
frontend/src/routes/_oh._index/route.tsx

@@ -23,6 +23,7 @@ import store from "#/store";
 import { setInitialQuery } from "#/state/initial-query-slice";
 import { clientLoader as rootClientLoader } from "#/routes/_oh";
 import OpenHands from "#/api/open-hands";
+import { generateGitHubAuthUrl } from "#/utils/generate-github-auth-url";
 
 interface GitHubAuthProps {
   onConnectToGitHub: () => void;
@@ -62,10 +63,10 @@ export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
     githubClientId = null;
   }
 
+  const ghToken = localStorage.getItem("ghToken");
   const token = localStorage.getItem("token");
   if (token) return redirect("/app");
 
-  const ghToken = localStorage.getItem("ghToken");
   let repositories: GitHubRepository[] = [];
   if (ghToken) {
     const data = await retrieveAllGitHubUserRepositories(ghToken);
@@ -75,10 +76,9 @@ export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
   }
 
   let githubAuthUrl: string | null = null;
-  if (isSaas) {
+  if (isSaas && githubClientId) {
     const requestUrl = new URL(request.url);
-    const redirectUri = `${requestUrl.origin}/oauth/github/callback`;
-    githubAuthUrl = `https://github.com/login/oauth/authorize?client_id=${githubClientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=repo,user,workflow`;
+    githubAuthUrl = generateGitHubAuthUrl(githubClientId, requestUrl);
   }
 
   return json({ repositories, githubAuthUrl });

+ 8 - 3
frontend/src/routes/_oh.app.tsx

@@ -73,10 +73,15 @@ const isAgentStateChange = (
 export const clientLoader = async () => {
   const ghToken = localStorage.getItem("ghToken");
 
-  const isAuthed = await userIsAuthenticated(ghToken);
-  if (!isAuthed) {
+  try {
+    const isAuthed = await userIsAuthenticated(ghToken);
+    if (!isAuthed) {
+      clearSession();
+      return redirect("/");
+    }
+  } catch (error) {
     clearSession();
-    return redirect("/waitlist");
+    return redirect("/");
   }
 
   const q = store.getState().initalQuery.initialQuery;

+ 91 - 58
frontend/src/routes/_oh.tsx

@@ -8,6 +8,7 @@ import {
   useLoaderData,
   useFetcher,
   Outlet,
+  ClientLoaderFunctionArgs,
 } from "@remix-run/react";
 import { retrieveGitHubUser, isGitHubErrorReponse } from "#/api/github";
 import OpenHands from "#/api/open-hands";
@@ -24,8 +25,11 @@ import { getSettings, settingsAreUpToDate } from "#/services/settings";
 import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
 import NewProjectIcon from "#/assets/new-project.svg?react";
 import DocsIcon from "#/assets/docs.svg?react";
+import { userIsAuthenticated } from "#/utils/user-is-authenticated";
+import { generateGitHubAuthUrl } from "#/utils/generate-github-auth-url";
+import { WaitlistModal } from "#/components/waitlist-modal";
 
-export const clientLoader = async () => {
+export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
   try {
     const config = await OpenHands.getConfig();
     window.__APP_MODE__ = config.APP_MODE;
@@ -38,6 +42,23 @@ export const clientLoader = async () => {
   let token = localStorage.getItem("token");
   const ghToken = localStorage.getItem("ghToken");
 
+  let isAuthed: boolean = false;
+  let githubAuthUrl: string | null = null;
+
+  try {
+    isAuthed = await userIsAuthenticated(ghToken);
+    if (!isAuthed && window.__GITHUB_CLIENT_ID__) {
+      const requestUrl = new URL(request.url);
+      githubAuthUrl = generateGitHubAuthUrl(
+        window.__GITHUB_CLIENT_ID__,
+        requestUrl,
+      );
+    }
+  } catch (error) {
+    isAuthed = false;
+    githubAuthUrl = null;
+  }
+
   let user: GitHubUser | GitHubErrorReponse | null = null;
   if (ghToken) user = await retrieveGitHubUser(ghToken);
 
@@ -53,6 +74,8 @@ export const clientLoader = async () => {
   return defer({
     token,
     ghToken,
+    isAuthed,
+    githubAuthUrl,
     user,
     settingsIsUpdated,
     settings,
@@ -101,8 +124,15 @@ export default function MainApp() {
   const { stop, isConnected } = useSocket();
   const navigation = useNavigation();
   const location = useLocation();
-  const { token, user, settingsIsUpdated, settings } =
-    useLoaderData<typeof clientLoader>();
+  const {
+    token,
+    ghToken,
+    user,
+    isAuthed,
+    githubAuthUrl,
+    settingsIsUpdated,
+    settings,
+  } = useLoaderData<typeof clientLoader>();
   const logoutFetcher = useFetcher({ key: "logout" });
   const endSessionFetcher = useFetcher({ key: "end-session" });
 
@@ -191,7 +221,7 @@ export default function MainApp() {
               type="button"
               aria-label="All Hands Logo"
               onClick={() => {
-                if (location.pathname !== "/")
+                if (location.pathname === "/app")
                   setStartNewProjectModalIsOpen(true);
               }}
             >
@@ -239,62 +269,65 @@ export default function MainApp() {
       </aside>
       <div className="h-full w-full relative">
         <Outlet />
-        {(!settingsIsUpdated || settingsModalIsOpen) && (
-          <ModalBackdrop onClose={() => setSettingsModalIsOpen(false)}>
-            <div className="bg-root-primary w-[384px] p-6 rounded-xl flex flex-col gap-2">
-              {settingsFormError && (
-                <p className="text-danger text-xs">{settingsFormError}</p>
-              )}
-              <span className="text-xl leading-6 font-semibold -tracking-[0.01em">
-                AI Provider Configuration
-              </span>
-              <p className="text-xs text-[#A3A3A3]">
-                To continue, connect an OpenAI, Anthropic, or other LLM account
+      </div>
+
+      {isAuthed && (!settingsIsUpdated || settingsModalIsOpen) && (
+        <ModalBackdrop onClose={() => setSettingsModalIsOpen(false)}>
+          <div className="bg-root-primary w-[384px] p-6 rounded-xl flex flex-col gap-2">
+            {settingsFormError && (
+              <p className="text-danger text-xs">{settingsFormError}</p>
+            )}
+            <span className="text-xl leading-6 font-semibold -tracking-[0.01em">
+              AI Provider Configuration
+            </span>
+            <p className="text-xs text-[#A3A3A3]">
+              To continue, connect an OpenAI, Anthropic, or other LLM account
+            </p>
+            {isConnected && (
+              <p className="text-xs text-danger">
+                Changing settings during an active session will end the session
               </p>
-              {isConnected && (
-                <p className="text-xs text-danger">
-                  Changing settings during an active session will end the
-                  session
-                </p>
-              )}
-              <SettingsForm
-                settings={settings}
-                models={settingsFormData.models}
-                agents={settingsFormData.agents}
-                securityAnalyzers={settingsFormData.securityAnalyzers}
-                onClose={() => setSettingsModalIsOpen(false)}
-              />
-            </div>
-          </ModalBackdrop>
-        )}
-        {accountSettingsModalOpen && (
-          <ModalBackdrop onClose={handleAccountSettingsModalClose}>
-            <AccountSettingsModal
-              onClose={handleAccountSettingsModalClose}
-              selectedLanguage={settings.LANGUAGE}
-              gitHubError={isGitHubErrorReponse(user)}
+            )}
+            <SettingsForm
+              settings={settings}
+              models={settingsFormData.models}
+              agents={settingsFormData.agents}
+              securityAnalyzers={settingsFormData.securityAnalyzers}
+              onClose={() => setSettingsModalIsOpen(false)}
             />
-          </ModalBackdrop>
-        )}
-        {startNewProjectModalIsOpen && (
-          <ModalBackdrop onClose={() => setStartNewProjectModalIsOpen(false)}>
-            <DangerModal
-              title="Are you sure you want to exit?"
-              description="You will lose any unsaved information."
-              buttons={{
-                danger: {
-                  text: "Exit Project",
-                  onClick: handleEndSession,
-                },
-                cancel: {
-                  text: "Cancel",
-                  onClick: () => setStartNewProjectModalIsOpen(false),
-                },
-              }}
-            />
-          </ModalBackdrop>
-        )}
-      </div>
+          </div>
+        </ModalBackdrop>
+      )}
+      {accountSettingsModalOpen && (
+        <ModalBackdrop onClose={handleAccountSettingsModalClose}>
+          <AccountSettingsModal
+            onClose={handleAccountSettingsModalClose}
+            selectedLanguage={settings.LANGUAGE}
+            gitHubError={isGitHubErrorReponse(user)}
+          />
+        </ModalBackdrop>
+      )}
+      {startNewProjectModalIsOpen && (
+        <ModalBackdrop onClose={() => setStartNewProjectModalIsOpen(false)}>
+          <DangerModal
+            title="Are you sure you want to exit?"
+            description="You will lose any unsaved information."
+            buttons={{
+              danger: {
+                text: "Exit Project",
+                onClick: handleEndSession,
+              },
+              cancel: {
+                text: "Cancel",
+                onClick: () => setStartNewProjectModalIsOpen(false),
+              },
+            }}
+          />
+        </ModalBackdrop>
+      )}
+      {!isAuthed && (
+        <WaitlistModal ghToken={ghToken} githubAuthUrl={githubAuthUrl} />
+      )}
     </div>
   );
 }

+ 0 - 39
frontend/src/routes/_oh.waitlist.tsx

@@ -1,39 +0,0 @@
-import { Link } from "@remix-run/react";
-import Clipboard from "#/assets/clipboard.svg?react";
-
-function Waitlist() {
-  return (
-    <div className="bg-neutral-800 h-full flex items-center justify-center rounded-xl">
-      <div className="w-[384px] flex flex-col gap-6 bg-neutral-900 rounded-xl p-6">
-        <Clipboard className="w-14 self-center" />
-
-        <div className="flex flex-col gap-2">
-          <h1 className="text-[20px] leading-6 -tracking-[0.01em] font-semibold">
-            You&apos;re not in the waitlist yet!
-          </h1>
-          <p className="text-neutral-400 text-xs">
-            Please click{" "}
-            <a
-              href="https://www.all-hands.dev/join-waitlist"
-              target="_blank"
-              rel="noreferrer noopener"
-              className="text-blue-500"
-            >
-              here
-            </a>{" "}
-            to join the waitlist.
-          </p>
-        </div>
-
-        <Link
-          to="/"
-          className="text-white text-sm py-[10px] bg-neutral-500 rounded text-center"
-        >
-          Go back to home
-        </Link>
-      </div>
-    </div>
-  );
-}
-
-export default Waitlist;

+ 10 - 0
frontend/src/utils/generate-github-auth-url.ts

@@ -0,0 +1,10 @@
+/**
+ * Generates a URL to redirect to for GitHub OAuth
+ * @param clientId The GitHub OAuth client ID
+ * @param requestUrl The URL of the request
+ * @returns The URL to redirect to for GitHub OAuth
+ */
+export const generateGitHubAuthUrl = (clientId: string, requestUrl: URL) => {
+  const redirectUri = `${requestUrl.origin}/oauth/github/callback`;
+  return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=repo,user,workflow`;
+};