Repository: LIT-Protocol/Vincent
Pull request #96: Better Route Organization, Withdraw Page Simplification
Author: login: awisniew207
Created: 2025-05-01T23:20:11Z | Merged: 2025-05-02T20:48:08Z

- Better organization of routes, removal of the `wrap function`
- Simplified the Withdraw Page to consume existing authentication, not create a new `ConsentView`
- Better auth handling for User Dashboard Routes. Any unauthenticated users will be redirected to `/user` to authenticate again


Diff:
diff --git a/packages/app-dashboard/src/components/consent/pages/consent.tsx b/packages/app-dashboard/src/components/consent/pages/consent.tsx
index 31a51504d..f0b17d145 100644
--- a/packages/app-dashboard/src/components/consent/pages/consent.tsx
+++ b/packages/app-dashboard/src/components/consent/pages/consent.tsx
@@ -262,7 +262,7 @@ export default function ConsentView({ isUserDashboardFlow = false }: ConsentView
         // User flow: navigate to apps page
         if (isUserDashboardFlow) {
           navigate('/user/apps');
-          return null;
+          return <></>;
         }
 
         // Consent flow: show the consent form
@@ -302,7 +302,7 @@ export default function ConsentView({ isUserDashboardFlow = false }: ConsentView
     if (isUserDashboardFlow && validatedSessionSigs && authInfo?.userPKP) {
       // Redirect to /apps page
       navigate('/user/apps');
-      return null;
+      return <></>;
     }
 
     // If authenticated but no accounts found
diff --git a/packages/app-dashboard/src/components/layout/UserHeader.tsx b/packages/app-dashboard/src/components/layout/UserHeader.tsx
index 7dc1a1729..0c00d588f 100644
--- a/packages/app-dashboard/src/components/layout/UserHeader.tsx
+++ b/packages/app-dashboard/src/components/layout/UserHeader.tsx
@@ -1,4 +1,4 @@
-import { ArrowLeft, LogOut } from 'lucide-react';
+import { ArrowLeft, BanknoteArrowDown, LogOut } from 'lucide-react';
 import { useNavigate, useLocation } from 'react-router-dom';
 import { Button } from '@/components/ui/button';
 import { useClearAuthInfo } from '@/components/consent/hooks/useAuthInfo';
@@ -49,10 +49,14 @@ export default function UserHeader({ backButton, title, onSignOut }: UserHeaderP
           )}
 
           {!isRootPath && (
-            <Button onClick={handleSignOut} variant="outline" size="sm">
-              <LogOut className="mr-2 h-4 w-4" /> Sign Out
+            <Button onClick={() => navigate('/user/withdraw')} variant="outline" size="sm">
+              <BanknoteArrowDown className="mr-2 h-4 w-4" /> Withdraw
             </Button>
           )}
+
+          <Button onClick={handleSignOut} variant="outline" size="sm">
+            <LogOut className="mr-2 h-4 w-4" /> Sign Out
+          </Button>
         </div>
       </div>
     </div>
diff --git a/packages/app-dashboard/src/components/user/AuthGuard.tsx b/packages/app-dashboard/src/components/user/AuthGuard.tsx
new file mode 100644
index 000000000..b8f46adca
--- /dev/null
+++ b/packages/app-dashboard/src/components/user/AuthGuard.tsx
@@ -0,0 +1,17 @@
+import { useNavigate } from 'react-router-dom';
+import { useReadAuthInfo } from '@/components/consent/hooks/useAuthInfo';
+
+/**
+ * Utility function for checking authentication status in protected routes
+ * Returns a loading spinner, redirect component, or null if authenticated
+ */
+export function useAuthGuard() {
+  const { authInfo, sessionSigs, isProcessing } = useReadAuthInfo();
+  const navigate = useNavigate();
+  const userIsAuthed = authInfo?.userPKP && authInfo?.agentPKP && sessionSigs;
+  if (!isProcessing && !userIsAuthed) {
+    navigate(`/user`, { replace: true });
+  }
+
+  return isProcessing;
+}
diff --git a/packages/app-dashboard/src/components/user/UserAppsView.tsx b/packages/app-dashboard/src/components/user/UserAppsView.tsx
index bfd1af33f..52240fb47 100644
--- a/packages/app-dashboard/src/components/user/UserAppsView.tsx
+++ b/packages/app-dashboard/src/components/user/UserAppsView.tsx
@@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
 import { UserAppsViewProps, AppDetails } from './types';
 import { AppCard } from './AppCard';
 import { fetchUserApps } from '@/components/user/userAppsUtils';
+import Loading from '../consent/components/Loading';
 
 export default function UserAppsView({ userPKP, sessionSigs, agentPKP }: UserAppsViewProps) {
   const [statusMessage, setStatusMessage] = useState<string>('');
@@ -78,14 +79,7 @@ export default function UserAppsView({ userPKP, sessionSigs, agentPKP }: UserApp
   };
 
   if (isLoading) {
-    return (
-      <div className="flex items-center justify-center h-[50vh]">
-        <div className="space-y-4 text-center">
-          <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto"></div>
-          <p className="text-sm text-gray-600">Loading your applications...</p>
-        </div>
-      </div>
-    );
+    return <Loading copy="Loading your applications..." />;
   }
 
   return (
diff --git a/packages/app-dashboard/src/pages/appId/[appId]/advanced-functions.tsx b/packages/app-dashboard/src/pages/app-dashboard/advanced-functions.tsx
similarity index 100%
rename from packages/app-dashboard/src/pages/appId/[appId]/advanced-functions.tsx
rename to packages/app-dashboard/src/pages/app-dashboard/advanced-functions.tsx
diff --git a/packages/app-dashboard/src/pages/appId/[appId]/index.tsx b/packages/app-dashboard/src/pages/app-dashboard/app-details.tsx
similarity index 100%
rename from packages/app-dashboard/src/pages/appId/[appId]/index.tsx
rename to packages/app-dashboard/src/pages/app-dashboard/app-details.tsx
diff --git a/packages/app-dashboard/src/pages/appId/[appId]/consent.tsx b/packages/app-dashboard/src/pages/app-dashboard/consent.tsx
similarity index 100%
rename from packages/app-dashboard/src/pages/appId/[appId]/consent.tsx
rename to packages/app-dashboard/src/pages/app-dashboard/consent.tsx
diff --git a/packages/app-dashboard/src/pages/create-app.tsx b/packages/app-dashboard/src/pages/app-dashboard/create-app.tsx
similarity index 100%
rename from packages/app-dashboard/src/pages/create-app.tsx
rename to packages/app-dashboard/src/pages/app-dashboard/create-app.tsx
diff --git a/packages/app-dashboard/src/pages/index.tsx b/packages/app-dashboard/src/pages/app-dashboard/dashboard.tsx
similarity index 100%
rename from packages/app-dashboard/src/pages/index.tsx
rename to packages/app-dashboard/src/pages/app-dashboard/dashboard.tsx
diff --git a/packages/app-dashboard/src/pages/appId/[appId]/delegatee.tsx b/packages/app-dashboard/src/pages/app-dashboard/delegatee.tsx
similarity index 100%
rename from packages/app-dashboard/src/pages/appId/[appId]/delegatee.tsx
rename to packages/app-dashboard/src/pages/app-dashboard/delegatee.tsx
diff --git a/packages/app-dashboard/src/pages/app-dashboard/index.ts b/packages/app-dashboard/src/pages/app-dashboard/index.ts
new file mode 100644
index 000000000..4287bec13
--- /dev/null
+++ b/packages/app-dashboard/src/pages/app-dashboard/index.ts
@@ -0,0 +1,6 @@
+export { default as Dashboard } from './dashboard';
+export { default as CreateApp } from './create-app';
+export { default as AppDetails } from './app-details';
+export { default as AdvancedFunctions } from './advanced-functions';
+export { default as Delegatee } from './delegatee';
+export { default as ToolPolicies } from './tool-policies';
diff --git a/packages/app-dashboard/src/pages/appId/[appId]/tool-policies.tsx b/packages/app-dashboard/src/pages/app-dashboard/tool-policies.tsx
similarity index 100%
rename from packages/app-dashboard/src/pages/appId/[appId]/tool-policies.tsx
rename to packages/app-dashboard/src/pages/app-dashboard/tool-policies.tsx
diff --git a/packages/app-dashboard/src/pages/user/appId/[appId]/index.tsx b/packages/app-dashboard/src/pages/user-dashboard/app-details.tsx
similarity index 67%
rename from packages/app-dashboard/src/pages/user/appId/[appId]/index.tsx
rename to packages/app-dashboard/src/pages/user-dashboard/app-details.tsx
index 354bb55aa..b5307d928 100644
--- a/packages/app-dashboard/src/pages/user/appId/[appId]/index.tsx
+++ b/packages/app-dashboard/src/pages/user-dashboard/app-details.tsx
@@ -2,9 +2,12 @@ import { Helmet } from 'react-helmet';
 import { useReadAuthInfo } from '@/components/consent/hooks/useAuthInfo';
 import UserAuthenticatedConsentForm from '@/components/consent/components/UserAuthenticatedConsentForm';
 import UserHeader from '@/components/layout/UserHeader';
+import { useAuthGuard } from '@/components/user/AuthGuard';
+import Loading from '@/components/consent/components/Loading';
 
 export default function AppDetailsPage() {
   const { authInfo, sessionSigs } = useReadAuthInfo();
+  const authGuardElement = useAuthGuard();
 
   return (
     <>
@@ -20,11 +23,13 @@ export default function AppDetailsPage() {
         }}
       />
 
-      {authInfo?.userPKP && authInfo?.agentPKP && sessionSigs && (
+      {authGuardElement ? (
+        <Loading copy="Loading..." />
+      ) : (
         <UserAuthenticatedConsentForm
-          userPKP={authInfo.userPKP}
-          sessionSigs={sessionSigs}
-          agentPKP={authInfo.agentPKP}
+          userPKP={authInfo!.userPKP!}
+          sessionSigs={sessionSigs!}
+          agentPKP={authInfo!.agentPKP!}
         />
       )}
     </>
diff --git a/packages/app-dashboard/src/pages/user/apps.tsx b/packages/app-dashboard/src/pages/user-dashboard/apps.tsx
similarity index 62%
rename from packages/app-dashboard/src/pages/user/apps.tsx
rename to packages/app-dashboard/src/pages/user-dashboard/apps.tsx
index efd250e00..02844cd9f 100644
--- a/packages/app-dashboard/src/pages/user/apps.tsx
+++ b/packages/app-dashboard/src/pages/user-dashboard/apps.tsx
@@ -2,10 +2,12 @@ import { Helmet } from 'react-helmet';
 import UserAppsView from '@/components/user/UserAppsView';
 import { useReadAuthInfo } from '@/components/consent/hooks/useAuthInfo';
 import UserHeader from '@/components/layout/UserHeader';
+import { useAuthGuard } from '@/components/user/AuthGuard';
+import Loading from '@/components/consent/components/Loading';
 
 export default function AppsPage() {
   const { authInfo, sessionSigs } = useReadAuthInfo();
-
+  const authGuardElement = useAuthGuard();
   return (
     <>
       <Helmet>
@@ -15,11 +17,13 @@ export default function AppsPage() {
 
       <UserHeader title="My Applications" />
 
-      {authInfo?.userPKP && authInfo?.agentPKP && sessionSigs && (
+      {authGuardElement ? (
+        <Loading copy="Loading..." />
+      ) : (
         <UserAppsView
-          userPKP={authInfo.userPKP}
-          sessionSigs={sessionSigs}
-          agentPKP={authInfo.agentPKP}
+          userPKP={authInfo!.userPKP!}
+          sessionSigs={sessionSigs!}
+          agentPKP={authInfo!.agentPKP!}
         />
       )}
     </>
diff --git a/packages/app-dashboard/src/pages/user/index.tsx b/packages/app-dashboard/src/pages/user-dashboard/dashboard.tsx
similarity index 100%
rename from packages/app-dashboard/src/pages/user/index.tsx
rename to packages/app-dashboard/src/pages/user-dashboard/dashboard.tsx
diff --git a/packages/app-dashboard/src/pages/user-dashboard/index.ts b/packages/app-dashboard/src/pages/user-dashboard/index.ts
new file mode 100644
index 000000000..36d8a368e
--- /dev/null
+++ b/packages/app-dashboard/src/pages/user-dashboard/index.ts
@@ -0,0 +1,5 @@
+export { default as Dashboard } from './dashboard';
+export { default as Withdraw } from './withdraw';
+export { default as Apps } from './apps';
+export { default as AppDetails } from './app-details';
+export { default as Consent } from '../app-dashboard/consent';
diff --git a/packages/app-dashboard/src/pages/user-dashboard/withdraw.tsx b/packages/app-dashboard/src/pages/user-dashboard/withdraw.tsx
new file mode 100644
index 000000000..06033268e
--- /dev/null
+++ b/packages/app-dashboard/src/pages/user-dashboard/withdraw.tsx
@@ -0,0 +1,40 @@
+import { Helmet } from 'react-helmet';
+import WithdrawForm from '@/components/withdraw/WithdrawForm';
+import UserHeader from '@/components/layout/UserHeader';
+import useReadAuthInfo from '@/components/consent/hooks/useAuthInfo';
+import { useAuthGuard } from '@/components/user/AuthGuard';
+import Loading from '@/components/consent/components/Loading';
+
+export function Withdraw() {
+  const { authInfo, sessionSigs } = useReadAuthInfo();
+  const authGuardElement = useAuthGuard();
+
+  return (
+    <>
+      <Helmet>
+        <title>Vincent | Withdraw</title>
+        <meta name="description" content="Withdraw your funds" />
+      </Helmet>
+
+      <UserHeader
+        title="Withdraw"
+        backButton={{
+          to: '/user/apps',
+          label: 'Back to Apps',
+        }}
+      />
+
+      {authGuardElement ? (
+        <Loading copy="Loading..." />
+      ) : (
+        <WithdrawForm
+          sessionSigs={sessionSigs!}
+          agentPKP={authInfo!.agentPKP!}
+          userPKP={authInfo!.userPKP!}
+        />
+      )}
+    </>
+  );
+}
+
+export default Withdraw;
diff --git a/packages/app-dashboard/src/pages/withdraw.tsx b/packages/app-dashboard/src/pages/withdraw.tsx
deleted file mode 100644
index a3ca204b2..000000000
--- a/packages/app-dashboard/src/pages/withdraw.tsx
+++ /dev/null
@@ -1,296 +0,0 @@
-import { useEffect, useState, useCallback } from 'react';
-import { AUTH_METHOD_TYPE } from '@lit-protocol/constants';
-import { SessionSigs, IRelayPKP } from '@lit-protocol/types';
-
-import useAuthenticate from '@/components/consent/hooks/useAuthenticate';
-import useAccounts from '@/components/consent/hooks/useAccounts';
-import { registerWebAuthn, getSessionSigs } from '@/components/consent/utils/lit';
-import LoginMethods from '@/components/consent/components/LoginMethods';
-import { getAgentPKP } from '@/components/consent/utils/getAgentPKP';
-import { useErrorPopup } from '@/providers/ErrorPopup';
-import {
-  useSetAuthInfo,
-  useReadAuthInfo,
-  useClearAuthInfo,
-} from '@/components/consent/hooks/useAuthInfo';
-
-import Loading from '@/components/consent/components/Loading';
-import WithdrawForm from '@/components/withdraw/WithdrawForm';
-import ExistingAccountView from '@/components/consent/views/ExistingAccountView';
-import SignUpView from '@/components/consent/views/SignUpView';
-export function Withdraw() {
-  // ------ STATE AND HOOKS ------
-  const { showError } = useErrorPopup();
-  const { updateAuthInfo } = useSetAuthInfo();
-  const { clearAuthInfo } = useClearAuthInfo();
-
-  // Shared state for session sigs and agent PKP
-  const [sessionSigs, setSessionSigs] = useState<SessionSigs>();
-  const [agentPKP, setAgentPKP] = useState<IRelayPKP>();
-  const [sessionError, setSessionError] = useState<Error>();
-
-  // State for loading messages
-  const [loadingMessage, setLoadingMessage] = useState<string>('');
-
-  // ------ EXISTING SESSION HANDLING ------
-
-  // Check for existing auth info
-  const {
-    authInfo,
-    sessionSigs: validatedSessionSigs,
-    isProcessing,
-    error: readError,
-  } = useReadAuthInfo();
-
-  // State to show existing account option
-  const [showExistingAccount, setShowExistingAccount] = useState(false);
-
-  // Check for existing auth info once the validation process is complete
-  useEffect(() => {
-    if (isProcessing) return;
-    if (validatedSessionSigs) {
-      setShowExistingAccount(true);
-    }
-  }, [validatedSessionSigs, isProcessing]);
-
-  // Handle using existing account
-  const handleUseExistingAccount = () => {
-    setShowExistingAccount(false);
-  };
-
-  // ------ NEW AUTHENTICATION FLOW ------
-
-  // Authentication state
-  const [sessionLoading, setSessionLoading] = useState<boolean>(false);
-
-  // Authentication methods
-  const {
-    authMethod,
-    authWithEthWallet,
-    authWithWebAuthn,
-    authWithStytch,
-    loading: authLoading,
-    error: authError,
-  } = useAuthenticate();
-
-  // Account handling
-  const {
-    fetchAccounts,
-    setuserPKP,
-    userPKP,
-    accounts,
-    loading: accountsLoading,
-    error: accountsError,
-  } = useAccounts();
-
-  // Combine errors
-  const error = authError || accountsError || sessionError || readError;
-
-  // Show errors in the popup when they occur
-  useEffect(() => {
-    if (error) {
-      showError(error, 'Authentication Error');
-    }
-  }, [error, showError]);
-
-  // Register with WebAuthn
-  async function handleRegisterWithWebAuthn() {
-    const newPKP = await registerWebAuthn();
-    if (newPKP) {
-      setuserPKP(newPKP);
-    }
-  }
-
-  // Generate session signatures on-demand
-  const generateSessionSigs = useCallback(async () => {
-    if (!authMethod || !userPKP) return;
-
-    setSessionLoading(true);
-    setSessionError(undefined);
-    try {
-      // Generate session signatures for the user PKP
-      const sigs = await getSessionSigs({
-        pkpPublicKey: userPKP.publicKey,
-        authMethod,
-      });
-      setSessionSigs(sigs);
-
-      // After getting user PKP session sigs, try to get the agent PKP
-      try {
-        const agentPkpInfo = await getAgentPKP(userPKP.ethAddress);
-        setAgentPKP(agentPkpInfo);
-      } catch (agentError) {
-        console.error('Error handling Agent PKP:', agentError);
-        showError(agentError as Error, 'Agent PKP Error');
-      }
-    } catch (err) {
-      setSessionError(err as Error);
-    } finally {
-      setSessionLoading(false);
-    }
-  }, [
-    authMethod,
-    userPKP,
-    setSessionSigs,
-    setAgentPKP,
-    setSessionError,
-    setSessionLoading,
-    showError,
-  ]);
-
-  // If user is authenticated, fetch accounts
-  useEffect(() => {
-    if (authMethod) {
-      fetchAccounts(authMethod);
-    }
-  }, [authMethod, fetchAccounts]);
-
-  // If user is authenticated and has accounts, select the first one
-  useEffect(() => {
-    if (authMethod && accounts.length > 0 && !userPKP) {
-      setuserPKP(accounts[0]);
-    }
-  }, [authMethod, accounts, userPKP, setuserPKP]);
-
-  // If user is authenticated and has selected an account, generate session sigs
-  useEffect(() => {
-    if (authMethod && userPKP) {
-      generateSessionSigs();
-    }
-  }, [authMethod, userPKP, generateSessionSigs]);
-
-  // ------ LOADING STATES ------
-
-  // Update loading message based on current state with smooth transitions
-  useEffect(() => {
-    // Determine the appropriate message based on current loading state
-    let newMessage = '';
-    if (authLoading) {
-      newMessage = 'Authenticating your credentials...';
-    } else if (accountsLoading) {
-      newMessage = 'Fetching your Agent Wallet...';
-    } else if (sessionLoading) {
-      newMessage = 'Securing your session...';
-    } else if (isProcessing) {
-      newMessage = 'Checking existing session...';
-    }
-
-    // Only transition if the message is actually changing and not empty
-    if (newMessage && newMessage !== loadingMessage) {
-      // Wait briefly before changing the message
-      const timeout = setTimeout(() => {
-        setLoadingMessage(newMessage);
-      }, 150);
-
-      return () => clearTimeout(timeout);
-    }
-  }, [authLoading, accountsLoading, sessionLoading, isProcessing, loadingMessage]);
-
-  // ------ CLEANUP ------
-
-  const handleSignOut = async () => {
-    clearAuthInfo();
-    window.location.reload();
-  };
-
-  useEffect(() => {
-    return () => {
-      // Cleanup web3 connection when component unmounts
-      if (sessionSigs) {
-        clearAuthInfo();
-      }
-    };
-  }, [sessionSigs, clearAuthInfo]);
-
-  // ------ RENDER CONTENT ------
-
-  const renderContent = () => {
-    // Handle loading states first
-    if (authLoading || accountsLoading || sessionLoading || isProcessing) {
-      return <Loading copy={loadingMessage} />;
-    }
-
-    // If we have existing auth info, show the option to use it
-    if (showExistingAccount) {
-      return (
-        <ExistingAccountView
-          authInfo={authInfo}
-          handleUseExistingAccount={handleUseExistingAccount}
-          handleSignOut={handleSignOut}
-        />
-      );
-    }
-
-    // If authenticated with a new PKP and session sigs
-    if (userPKP && sessionSigs) {
-      // Save the PKP info in localStorage for SessionValidator to use
-      try {
-        updateAuthInfo({
-          agentPKP,
-          userPKP,
-        });
-      } catch (error) {
-        console.error('Error saving PKP info to localStorage:', error);
-        showError(error as Error, 'Authentication Error');
-        return (
-          <LoginMethods
-            authWithEthWallet={authWithEthWallet}
-            authWithWebAuthn={authWithWebAuthn}
-            authWithStytch={authWithStytch}
-            registerWithWebAuthn={handleRegisterWithWebAuthn}
-          />
-        );
-      }
-
-      return (
-        <div className="flex justify-center items-start w-full">
-          <div className="w-[550px] position-relative mt-0">
-            <WithdrawForm sessionSigs={sessionSigs} agentPKP={agentPKP} userPKP={userPKP} />
-          </div>
-        </div>
-      );
-    }
-
-    // If we're not showing the existing account and have validated session sigs
-    if (!showExistingAccount && validatedSessionSigs && authInfo?.userPKP) {
-      return (
-        <div className="flex justify-center items-start w-full">
-          <div className="w-[550px] position-relative mt-0">
-            <WithdrawForm
-              sessionSigs={validatedSessionSigs}
-              agentPKP={authInfo.agentPKP}
-              userPKP={authInfo.userPKP}
-            />
-          </div>
-        </div>
-      );
-    }
-
-    // If authenticated but no accounts found
-    if (authMethod && accounts.length === 0) {
-      return (
-        <SignUpView
-          authMethodType={
-            authMethod.authMethodType as (typeof AUTH_METHOD_TYPE)[keyof typeof AUTH_METHOD_TYPE]
-          }
-          handleRegisterWithWebAuthn={handleRegisterWithWebAuthn}
-          authWithWebAuthn={authWithWebAuthn}
-        />
-      );
-    }
-
-    // Initial authentication state - show login methods
-    return (
-      <LoginMethods
-        authWithEthWallet={authWithEthWallet}
-        authWithWebAuthn={authWithWebAuthn}
-        authWithStytch={authWithStytch}
-        registerWithWebAuthn={handleRegisterWithWebAuthn}
-      />
-    );
-  };
-
-  return <div className="grow flex items-center justify-center">{renderContent()}</div>;
-}
-
-export default Withdraw;
diff --git a/packages/app-dashboard/src/routes.tsx b/packages/app-dashboard/src/routes.tsx
index b6eab6ef8..c4733df0d 100644
--- a/packages/app-dashboard/src/routes.tsx
+++ b/packages/app-dashboard/src/routes.tsx
@@ -2,78 +2,88 @@ import { Outlet, RouteObject } from 'react-router-dom';
 import AppLayout from './components/layout/AppLayout';
 import UserLayout from './components/layout/UserLayout';
 import { AppProviders, UserProviders } from './providers';
-import { wrap } from './utils/components';
 
-import Home from './pages/index';
-import Withdraw from './pages/withdraw';
-import CreateApp from './pages/create-app';
-import AppDetails from './pages/appId/[appId]/index';
-import AdvancedFunctions from './pages/appId/[appId]/advanced-functions';
-import Consent from './pages/appId/[appId]/consent';
-import Delegatee from './pages/appId/[appId]/delegatee';
-import ToolPolicies from './pages/appId/[appId]/tool-policies';
-import UserDashboard from './pages/user/index';
-import UserApps from './pages/user/apps';
-import UserAppDetails from './pages/user/appId/[appId]/index';
+import * as AppPages from './pages/app-dashboard';
+import * as UserPages from './pages/user-dashboard';
 
-const AppLayoutWithProviders = wrap(() => <Outlet />, [...AppProviders, AppLayout]);
-const UserLayoutWithProviders = wrap(() => <Outlet />, [...UserProviders, UserLayout]);
+const appRoutes = {
+  element: (
+    <>
+      {AppProviders.reduceRight(
+        (children, Provider) => (
+          <Provider>{children}</Provider>
+        ),
+        <AppLayout>
+          <Outlet />
+        </AppLayout>,
+      )}
+    </>
+  ),
+  children: [
+    {
+      path: '/',
+      element: <AppPages.Dashboard />,
+    },
+    {
+      path: '/create-app',
+      element: <AppPages.CreateApp />,
+    },
+    {
+      path: '/appId/:appId',
+      element: <AppPages.AppDetails />,
+    },
+    {
+      path: '/appId/:appId/advanced-functions',
+      element: <AppPages.AdvancedFunctions />,
+    },
+    {
+      path: '/appId/:appId/delegatee',
+      element: <AppPages.Delegatee />,
+    },
+    {
+      path: '/appId/:appId/tool-policies',
+      element: <AppPages.ToolPolicies />,
+    },
+  ],
+};
 
-const routes: RouteObject[] = [
-  {
-    element: <AppLayoutWithProviders />,
-    children: [
-      {
-        path: '/',
-        element: <Home />,
-      },
-      {
-        path: '/create-app',
-        element: <CreateApp />,
-      },
-      {
-        path: '/appId/:appId',
-        element: <AppDetails />,
-      },
-      {
-        path: '/appId/:appId/advanced-functions',
-        element: <AdvancedFunctions />,
-      },
-      {
-        path: '/appId/:appId/delegatee',
-        element: <Delegatee />,
-      },
-      {
-        path: '/appId/:appId/tool-policies',
-        element: <ToolPolicies />,
-      },
-    ],
-  },
-  {
-    element: <UserLayoutWithProviders />,
-    children: [
-      {
-        path: '/withdraw',
-        element: <Withdraw />,
-      },
-      {
-        path: '/appId/:appId/consent',
-        element: <Consent />,
-      },
-      {
-        path: '/user',
-        element: <UserDashboard />,
-      },
-      {
-        path: '/user/apps',
-        element: <UserApps />,
-      },
-      {
-        path: '/user/appId/:appId',
-        element: <UserAppDetails />,
-      },
-    ],
-  },
-];
+const userRoutes = {
+  element: (
+    <>
+      {UserProviders.reduceRight(
+        (children, Provider) => (
+          <Provider>{children}</Provider>
+        ),
+        <UserLayout>
+          <Outlet />
+        </UserLayout>,
+      )}
+    </>
+  ),
+  children: [
+    {
+      path: '/user/withdraw',
+      element: <UserPages.Withdraw />,
+    },
+    {
+      path: '/appId/:appId/consent',
+      element: <UserPages.Consent />,
+    },
+    {
+      path: '/user',
+      element: <UserPages.Dashboard />,
+    },
+    {
+      path: '/user/apps',
+      element: <UserPages.Apps />,
+    },
+    {
+      path: '/user/appId/:appId',
+      element: <UserPages.AppDetails />,
+    },
+  ],
+};
+
+const routes: RouteObject[] = [appRoutes, userRoutes];
 
 export default routes;
diff --git a/packages/app-dashboard/src/utils/components.tsx b/packages/app-dashboard/src/utils/components.tsx
deleted file mode 100644
index 4e1f4ca96..000000000
--- a/packages/app-dashboard/src/utils/components.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { ComponentType, ReactNode } from 'react';
-
-export type ParentComponent = ComponentType<{ children: ReactNode }>;
-
-/**
- * Compose multiple components into a single one respecting the order they are passed
- * @example
- * compose([A, B, C]) => ({ children }) => <A><B><C>{children}</C></B></A>
- *
- * @param components
- */
-export const compose = (components: ParentComponent[]) => {
-  return ({ children }: { children: ReactNode }) => {
-    return components.reduceRight((acc, Component) => {
-      return <Component>{acc}</Component>;
-    }, children);
-  };
-}
-
-/**
- * Wraps a component with the given array of components
- * @example
- * wrap(Component, [A, B, C]) => () => <A><B><C><Component /></C></B></A>
- *
- * @param Wrapped - The component to wrap
- * @param Wrappers - The components to wrap the given component with
- */
-export const wrap = <P extends object>(Wrapped: ComponentType<P>, Wrappers: ParentComponent[]) => {
-  const ComposedProviders = compose(Wrappers);
-
-  return function WithProvidersWrapper(props: P) {
-    return (
-      <ComposedProviders>
-        <Wrapped {...props} />
-      </ComposedProviders>
-    );
-  };
-}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR reorganizes routes and simplifies the Withdraw page flow by removing deprecated components and consolidating authentication flows. Key changes include:
- Removal of the wrap/compose functions and their replacement with inline provider composition in the routing configuration.
- Transition of Withdraw and Consent flows to user-dashboard-specific pages using updated components and routes.
- Enhancement of the user dashboard with consistent auth handling and a new withdraw navigation button.

### Reviewed Changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| packages/app-dashboard/src/utils/components.tsx | Removed unused higher-order components (wrap and compose) in favor of inline provider composition. |
| packages/app-dashboard/src/routes.tsx | Refactored routing to distinguish between app and user routes while updating component imports. |
| packages/app-dashboard/src/pages/withdraw.tsx | Removed the standalone withdraw page in favor of the new user-dashboard implementation. |
| packages/app-dashboard/src/pages/user-dashboard/* | Introduced new pages (Withdraw, Apps, AppDetails) incorporating updated auth and UI flows. |
| packages/app-dashboard/src/components/user/AuthGuard.tsx | Added an authentication guard that handles both loading and redirection behaviors. |
| packages/app-dashboard/src/components/layout/UserHeader.tsx | Added a withdraw button to the header with updated navigation using a new icon. |
| packages/app-dashboard/src/components/consent/pages/consent.tsx | Removed the redundant “return null” after navigation in the Consent flow. |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**packages/app-dashboard/src/components/consent/pages/consent.tsx:304**
* After calling navigate in the consent flow, the 'return null' statement was removed, which might lead to unintended content rendering. Consider reinstating 'return null' (or a suitable fallback) immediately after navigate to prevent further render.
```
navigate('/user/apps');
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T14:26:03Z
user: login: FedericoAmura
body: I would prefer we merge this and check everything together in #95 . Comments still apply but checking things there that might be changed here makes the review more complicated
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:57:14Z
user: login: awisniew207
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T19:07:03Z
user: login: awisniew207
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T19:08:27Z
user: login: awisniew207
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T19:09:18Z
user: login: awisniew207
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T19:13:38Z

Review comments:
diff_hunk: @@ -302,7 +302,6 @@ export default function ConsentView({ isUserDashboardFlow = false }: ConsentView
     if (isUserDashboardFlow && validatedSessionSigs && authInfo?.userPKP) {
       // Redirect to /apps page
       navigate('/user/apps');
-      return null;
path: packages/app-dashboard/src/components/consent/pages/consent.tsx
user: login: FedericoAmura
body: This function is expected to return page content. On this case (which should be really a hook, let's fix that on the parent PR. I'll add the comment there for this) we should still return something. For example `<></>`

The concept is that this function takes some values and returns the needed view of it, even if it is an "empty" view
created_at: 2025-05-02T15:03:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,10 +49,14 @@ export default function UserHeader({ backButton, title, onSignOut }: UserHeaderP
           )}
 
           {!isRootPath && (
-            <Button onClick={handleSignOut} variant="outline" size="sm">
-              <LogOut className="mr-2 h-4 w-4" /> Sign Out
+            <Button onClick={() => navigate('/user/withdraw')} variant="outline" size="sm">
+              <CreditCard className="mr-2 h-4 w-4" /> Withdraw
path: packages/app-dashboard/src/components/layout/UserHeader.tsx
user: login: FedericoAmura
body: Let's use https://lucide.dev/icons/banknote-arrow-down

Credit card could be interpreted as deposit, buy, sell, anything
created_at: 2025-05-02T15:05:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+import { Navigate } from 'react-router-dom';
+import { useReadAuthInfo } from '@/components/consent/hooks/useAuthInfo';
+
+/**
+ * Utility function for checking authentication status in protected routes
+ * Returns a loading spinner, redirect component, or null if authenticated
+ */
+export function useAuthGuard() {
+  const { authInfo, sessionSigs, isProcessing } = useReadAuthInfo();
+
+  if (isProcessing) {
+    return (
+      <div className="flex items-center justify-center h-[50vh]">
+        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
+      </div>
+    );
+  }
+
+  if (!authInfo?.userPKP || !authInfo?.agentPKP || !sessionSigs) {
+    return <Navigate to="/user" replace />;
+  }
+
+  // Return null if authenticated, allowing the protected content to render
+  return null;
+}
path: packages/app-dashboard/src/components/user/AuthGuard.tsx
user: login: FedericoAmura
body: Some things with this hook

- The response should be as consistent as possible. In functions, not only in hooks really. In this case we are returning 3 different things: a loading component, a navigation component that will change app state or a `null`
- Caller will have a very hard time trying to understand if the returned component is a loading (which they might want to overload) or a navigate (which they cannot overload without breaking the goal of calling this)
- Caller has to be aware that what it receives might be, or not, something it CAN+MUST render (when you want to return no component return `<></>` (it is kind of the component equivalent of `null`))
- React router discourages `Navigate` over `useNavigate`. Navigate is a workaround where you can't use the hook

A much better implementation
```suggestion
export function useAuthGuard() {
  const { authInfo, sessionSigs, isProcessing } = useReadAuthInfo();
  const navigate = useNavigate();

  const userIsAuthed = authInfo?.userPKP && authInfo?.agentPKP && sessionSigs;

  if (!isProcessing && !userIsAuthed) {
    navigate(`/user`, { replace: true });
  }
  
  return isProcessing;
}
```

This implementation does just 1 thing: redirect if needed. And lets caller know it is in process of doing that. It doesn't force the caller to use a loading component nor returns different type of things on the same variable

And then in components that need this just add a `useAuthGuard()` at the top, If components wants to show a loading, skeleton or whatever, then they can decide it and customize it any way they want (there is a loading component already btw, let's stop copying this code)
created_at: 2025-05-02T15:51:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,9 +2,15 @@ import { Helmet } from 'react-helmet';
 import { useReadAuthInfo } from '@/components/consent/hooks/useAuthInfo';
 import UserAuthenticatedConsentForm from '@/components/consent/components/UserAuthenticatedConsentForm';
 import UserHeader from '@/components/layout/UserHeader';
+import { useAuthGuard } from '@/components/user/AuthGuard';
 
 export default function AppDetailsPage() {
   const { authInfo, sessionSigs } = useReadAuthInfo();
+  const authGuardElement = useAuthGuard();
+
+  if (authGuardElement) {
+    return authGuardElement;
path: packages/app-dashboard/src/pages/user-dashboard/app-details.tsx
user: login: FedericoAmura
body: Following the comment above. If the guard is pending, then this page should decide what to do. Almost always showing a `<Loading />` until the guard resolves and redirects or says it finished without issue

This way the guard has also the responsibility of telling the caller what to BLINDLY render.
created_at: 2025-05-02T15:54:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,9 +2,15 @@ import { Helmet } from 'react-helmet';
 import { useReadAuthInfo } from '@/components/consent/hooks/useAuthInfo';
 import UserAuthenticatedConsentForm from '@/components/consent/components/UserAuthenticatedConsentForm';
 import UserHeader from '@/components/layout/UserHeader';
+import { useAuthGuard } from '@/components/user/AuthGuard';
 
 export default function AppDetailsPage() {
   const { authInfo, sessionSigs } = useReadAuthInfo();
+  const authGuardElement = useAuthGuard();
+
+  if (authGuardElement) {
+    return authGuardElement;
path: packages/app-dashboard/src/pages/user-dashboard/app-details.tsx
user: login: awisniew207
body: Changed this return to:

```
return (
    <>
      <Helmet>
        <title>Vincent | App Details</title>
        <meta name="description" content="View and manage your app parameters" />
      </Helmet>

      <UserHeader
        backButton={{
          to: '/user/apps',
          label: 'Back to Apps',
        }}
      />

      {authGuardElement ? (
        <Loading copy="Loading..." />
      ) : (
        <UserAuthenticatedConsentForm
          userPKP={authInfo!.userPKP!}
          sessionSigs={sessionSigs!}
          agentPKP={authInfo!.agentPKP!}
        />
      )}
    </>
  );
  ```
created_at: 2025-05-02T19:07:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -302,7 +302,6 @@ export default function ConsentView({ isUserDashboardFlow = false }: ConsentView
     if (isUserDashboardFlow && validatedSessionSigs && authInfo?.userPKP) {
       // Redirect to /apps page
       navigate('/user/apps');
-      return null;
path: packages/app-dashboard/src/components/consent/pages/consent.tsx
user: login: awisniew207
body: Temporarily changed to:
```
return <></>;
```
created_at: 2025-05-02T19:08:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,10 +49,14 @@ export default function UserHeader({ backButton, title, onSignOut }: UserHeaderP
           )}
 
           {!isRootPath && (
-            <Button onClick={handleSignOut} variant="outline" size="sm">
-              <LogOut className="mr-2 h-4 w-4" /> Sign Out
+            <Button onClick={() => navigate('/user/withdraw')} variant="outline" size="sm">
+              <CreditCard className="mr-2 h-4 w-4" /> Withdraw
path: packages/app-dashboard/src/components/layout/UserHeader.tsx
user: login: awisniew207
body: Changed to:
```
<Button onClick={() => navigate('/user/withdraw')} variant="outline" size="sm">
  <BanknoteArrowDown className="mr-2 h-4 w-4" /> Withdraw
</Button>
```
created_at: 2025-05-02T19:09:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+import { Navigate } from 'react-router-dom';
+import { useReadAuthInfo } from '@/components/consent/hooks/useAuthInfo';
+
+/**
+ * Utility function for checking authentication status in protected routes
+ * Returns a loading spinner, redirect component, or null if authenticated
+ */
+export function useAuthGuard() {
+  const { authInfo, sessionSigs, isProcessing } = useReadAuthInfo();
+
+  if (isProcessing) {
+    return (
+      <div className="flex items-center justify-center h-[50vh]">
+        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
+      </div>
+    );
+  }
+
+  if (!authInfo?.userPKP || !authInfo?.agentPKP || !sessionSigs) {
+    return <Navigate to="/user" replace />;
+  }
+
+  // Return null if authenticated, allowing the protected content to render
+  return null;
+}
path: packages/app-dashboard/src/components/user/AuthGuard.tsx
user: login: awisniew207
body: - Updated this code to render the loading component in the consumers of this hook
- Took your function to either return a bool or navigation
- Calls this to now render a more unified loading screen:
![image](https://github.com/user-attachments/assets/ff0d2963-4b73-4fb1-96fc-90efd117a728)

created_at: 2025-05-02T19:13:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-01T23:20:17Z
body: [vc]: #drd2x+vBEwOLK0DHs6/L9mGMB3hz5tHivnQvLkYg9uc=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ2aW5jZW50Iiwicm9vdERpcmVjdG9yeSI6InBhY2thZ2VzL2Rhc2hib2FyZCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2xpdC1wcm90b2NvbC92aW5jZW50LzJMWUQ0dFp0NG5LWFpuSFhyTlZoMktGV0pCMnciLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IkZBSUxFRCJ9LHsibmFtZSI6InZpbmNlbnQtYXBwLWRhc2hib2FyZCIsInJvb3REaXJlY3RvcnkiOiJwYWNrYWdlcy9hcHAtZGFzaGJvYXJkIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InZpbmNlbnQtYXBwLWRhc2hib2FyZC1naXQtZml4LXJvdXRlcy1saXQtcHJvdG9jb2wudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vbGl0LXByb3RvY29sL3ZpbmNlbnQtYXBwLWRhc2hib2FyZC9BZ0h2b1RiZWZoUlZxdHl3VXZXczlHU2FrUjZ0IiwicHJldmlld1VybCI6InZpbmNlbnQtYXBwLWRhc2hib2FyZC1naXQtZml4LXJvdXRlcy1saXQtcHJvdG9jb2wudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9LHsibmFtZSI6InZpbmNlbnQtdXNlci1kYXNoYm9hcmQiLCJyb290RGlyZWN0b3J5IjoicGFja2FnZXMvdXNlci1kYXNoYm9hcmQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9saXQtcHJvdG9jb2wvdmluY2VudC11c2VyLWRhc2hib2FyZC81bk1aamczWjJzaDU0WkE4MUNvWjE4OUZkNjZqIiwicHJldmlld1VybCI6IiIsIm5leHRDb21taXRTdGF0dXMiOiJGQUlMRUQifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **vincent** | ❌ Failed ([Inspect](https://vercel.com/lit-protocol/vincent/2LYD4tZt4nKXZnHXrNVh2KFWJB2w)) |  |  | May 2, 2025 7:09pm |
| **vincent-app-dashboard** | ✅ Ready ([Inspect](https://vercel.com/lit-protocol/vincent-app-dashboard/AgHvoTbefhRVqtywUvWs9GSakR6t)) | [Visit Preview](https://vincent-app-dashboard-git-fix-routes-lit-protocol.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/vincent-app-dashboard-git-fix-routes-lit-protocol.vercel.app?via=pr-comment-feedback-link) | May 2, 2025 7:09pm |
| **vincent-user-dashboard** | ❌ Failed ([Inspect](https://vercel.com/lit-protocol/vincent-user-dashboard/5nMZjg3Z2sh54ZA81CoZ189Fd66j)) |  |  | May 2, 2025 7:09pm |




author_association: NONE
user: login: nx-cloud[bot]
created_at: 2025-05-01T23:21:28Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/681517d248cd964236d4b73c?utm_source=pull-request&utm_medium=comment) for commit 3fa700b662b46e0c73b78bc0df67cea314cf8740.

| Command | Status | Duration | Result |
|---------|--------|----------:|--------|
| `nx affected -t lint test build` | ❌ Failed | 38s | [View ↗](https://cloud.nx.app/runs/4oFt7cmTaD?utm_source=pull-request&utm_medium=comment) |

---

☁️ [Nx Cloud](https://cloud.nx.app?utm_source=pull-request&utm_medium=comment) last updated this comment at `2025-05-02 19:07:38` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: NONE