From 12df3f9303d3f11415d29d88923ceae4259b6412 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Fri, 31 Oct 2025 11:12:28 -0400 Subject: [PATCH 01/20] working example of auto choose workspace with last used workspace --- apps/dashboard/app/auth/actions.ts | 19 +++ .../app/auth/sign-in/[[...sign-in]]/page.tsx | 19 ++- .../auth/sign-in/org-selector-improved.tsx | 161 ++++++++++++++++++ .../navigation/sidebar/team-switcher.tsx | 18 +- 4 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 apps/dashboard/app/auth/sign-in/org-selector-improved.tsx diff --git a/apps/dashboard/app/auth/actions.ts b/apps/dashboard/app/auth/actions.ts index 505fc4e0a9..33e9dcde9f 100644 --- a/apps/dashboard/app/auth/actions.ts +++ b/apps/dashboard/app/auth/actions.ts @@ -336,6 +336,15 @@ export async function completeOrgSelection( for (const cookie of result.cookies) { cookies().set(cookie.name, cookie.value, cookie.options); } + + // Store the last used organization ID in a cookie for auto-selection on next login + cookies().set("unkey_last_org_used", orgId, { + httpOnly: false, // Allow client-side access + secure: true, + sameSite: "lax", + path: "/", + maxAge: 60 * 60 * 24 * 30, // 30 Days + }); } return result; @@ -350,6 +359,16 @@ export async function switchOrg(orgId: string): Promise<{ success: boolean; erro throw new Error("Invalid session data returned from auth provider"); } await setSessionCookie({ token: newToken, expiresAt }); + + // Store the last used organization ID in a cookie for auto-selection on next login + cookies().set("unkey_last_org_used", orgId, { + httpOnly: false, // Allow client-side access + secure: true, + sameSite: "lax", + path: "/", + maxAge: 60 * 60 * 24 * 30, // 30 Days + }); + return { success: true }; } catch (error) { console.error("Organization switch failed:", { diff --git a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx index 26660edfd8..a4ef0669c9 100644 --- a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx +++ b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx @@ -13,7 +13,7 @@ import { EmailCode } from "../email-code"; import { EmailSignIn } from "../email-signin"; import { EmailVerify } from "../email-verify"; import { OAuthSignIn } from "../oauth-signin"; -import { OrgSelector } from "../org-selector"; +import { OrgSelectorImproved } from "../org-selector-improved"; function SignInContent() { const { @@ -94,12 +94,16 @@ function SignInContent() { return (
- {hasPendingAuth && } + {hasPendingAuth && ( + + )} {accountNotFound && (
-

Account not found, did you mean to sign up?

+

+ Account not found, did you mean to sign up? +

@@ -124,7 +128,10 @@ function SignInContent() {

Sign In

New to Unkey?{" "} - + Create new account

@@ -136,7 +143,9 @@ function SignInContent() {
- or continue using email + + or continue using email +
diff --git a/apps/dashboard/app/auth/sign-in/org-selector-improved.tsx b/apps/dashboard/app/auth/sign-in/org-selector-improved.tsx new file mode 100644 index 0000000000..f4ec737fc9 --- /dev/null +++ b/apps/dashboard/app/auth/sign-in/org-selector-improved.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { Combobox, type ComboboxOption } from "@/components/ui/combobox"; +import type { Organization } from "@/lib/auth/types"; +import { Button, DialogContainer, Loading } from "@unkey/ui"; +import type React from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { completeOrgSelection } from "../actions"; + +interface OrgSelectorProps { + organizations: Organization[]; + onError: (errorMessage: string) => void; +} + +export const OrgSelectorImproved: React.FC = ({ organizations, onError }) => { + const [isOpen, setIsOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [clientReady, setClientReady] = useState(false); + const [selectedOrgId, setSelectedOrgId] = useState(""); + const [hasAttemptedAutoSelection, setHasAttemptedAutoSelection] = useState(false); + // Set client ready after hydration + useEffect(() => { + setClientReady(true); + }, []); + + const orgOptions: ComboboxOption[] = useMemo(() => { + // Sort: recently created first (as proxy for recently used until we track that) + const sorted = [...organizations].sort((a, b) => { + const aDate = a.createdAt ? new Date(a.createdAt).getTime() : 0; + const bDate = b.createdAt ? new Date(b.createdAt).getTime() : 0; + return bDate - aDate; // Newest first + }); + + return sorted.map((org) => ({ + label: org.name, + value: org.id, + searchValue: org.name.toLowerCase(), + })); + }, [organizations]); + + const submit = useCallback( + async (orgId: string) => { + if (isLoading || !orgId) { + return; + } + + try { + setIsLoading(true); + const result = await completeOrgSelection(orgId); + + if (!result.success) { + onError(result.message); + setIsLoading(false); + } + // On success, the page will redirect, so we don't need to reset loading state + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : "Failed to complete organization selection. Please re-authenticate or contact support@unkey.dev"; + + onError(errorMessage); + setIsLoading(false); + } + }, + [isLoading, onError], + ); + + const handleSubmit = useCallback(() => { + submit(selectedOrgId); + }, [submit, selectedOrgId]); + + // Helper function to get cookie value on client side + const getCookie = (name: string): string | null => { + if (typeof document === "undefined") { + return null; + } + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) { + return parts.pop()?.split(";").shift() || null; + } + return null; + }; + + // Auto-select last used organization if available + useEffect(() => { + if (!clientReady || hasAttemptedAutoSelection) { + return; + } + + setHasAttemptedAutoSelection(true); + + // Get the last used organization ID from cookie + const lastUsedOrgId = getCookie("unkey_last_org_used"); + + if (lastUsedOrgId) { + // Check if the stored orgId exists in the current list of organizations + const orgExists = organizations.some((org) => org.id === lastUsedOrgId); + + if (orgExists) { + // Auto-submit this organization + submit(lastUsedOrgId); + return; + } + } + + // If no auto-selection, show the modal with first org pre-selected + if (organizations.length > 0) { + setSelectedOrgId(organizations[0].id); + } + setIsOpen(true); + }, [clientReady, organizations, hasAttemptedAutoSelection, submit]); + + return ( + { + setIsOpen(open); + }} + title="Select your workspace" + footer={ +
+ Select a workspace to sign in. +
+ } + > +
+ {/* Workspace selector */} +
+ + +
+ + {/* Submit button */} + +
+
+ ); +}; diff --git a/apps/dashboard/components/navigation/sidebar/team-switcher.tsx b/apps/dashboard/components/navigation/sidebar/team-switcher.tsx index 3b3566e2cc..7f37fe7eaa 100644 --- a/apps/dashboard/components/navigation/sidebar/team-switcher.tsx +++ b/apps/dashboard/components/navigation/sidebar/team-switcher.tsx @@ -11,7 +11,7 @@ import { } from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useSidebar } from "@/components/ui/sidebar"; -import { setSessionCookie } from "@/lib/auth/cookies"; +import { setCookie, setSessionCookie } from "@/lib/auth/cookies"; import { trpc } from "@/lib/trpc/client"; import { cn } from "@/lib/utils"; @@ -44,7 +44,7 @@ export const WorkspaceSwitcher: React.FC = (): JSX.Element => { ); const changeWorkspace = trpc.user.switchOrg.useMutation({ - async onSuccess(sessionData) { + async onSuccess(sessionData, orgId) { if (!sessionData.token || !sessionData.expiresAt) { toast.error("Failed to switch workspace. Invalid session data."); return; @@ -55,6 +55,20 @@ export const WorkspaceSwitcher: React.FC = (): JSX.Element => { token: sessionData.token, expiresAt: sessionData.expiresAt, }); + + // Store the last used organization ID in a cookie for auto-selection on next login + await setCookie({ + name: "unkey_last_org_used", + value: orgId, + options: { + httpOnly: false, // Allow client-side access + secure: true, + sameSite: "lax", + path: "/", + maxAge: 60 * 60 * 24 * 30, // 30 Days + }, + }); + // Instead of messing with the cache, we can simply redirect the user and we will refetch the user data. window.location.replace("/"); } catch (error) { From 00a152eb354c795fb4bc680514fa62052a5754b9 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Fri, 31 Oct 2025 12:22:18 -0400 Subject: [PATCH 02/20] cleanup --- .../app/auth/sign-in/[[...sign-in]]/page.tsx | 6 +- .../auth/sign-in/org-selector-improved.tsx | 161 ---------------- .../app/auth/sign-in/org-selector.tsx | 180 +++++++++++++----- 3 files changed, 137 insertions(+), 210 deletions(-) delete mode 100644 apps/dashboard/app/auth/sign-in/org-selector-improved.tsx diff --git a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx index a4ef0669c9..cdaed956c3 100644 --- a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx +++ b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx @@ -13,7 +13,7 @@ import { EmailCode } from "../email-code"; import { EmailSignIn } from "../email-signin"; import { EmailVerify } from "../email-verify"; import { OAuthSignIn } from "../oauth-signin"; -import { OrgSelectorImproved } from "../org-selector-improved"; +import { OrgSelector } from "../org-selector"; function SignInContent() { const { @@ -94,9 +94,7 @@ function SignInContent() { return (
- {hasPendingAuth && ( - - )} + {hasPendingAuth && } {accountNotFound && ( diff --git a/apps/dashboard/app/auth/sign-in/org-selector-improved.tsx b/apps/dashboard/app/auth/sign-in/org-selector-improved.tsx deleted file mode 100644 index f4ec737fc9..0000000000 --- a/apps/dashboard/app/auth/sign-in/org-selector-improved.tsx +++ /dev/null @@ -1,161 +0,0 @@ -"use client"; - -import { Combobox, type ComboboxOption } from "@/components/ui/combobox"; -import type { Organization } from "@/lib/auth/types"; -import { Button, DialogContainer, Loading } from "@unkey/ui"; -import type React from "react"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { completeOrgSelection } from "../actions"; - -interface OrgSelectorProps { - organizations: Organization[]; - onError: (errorMessage: string) => void; -} - -export const OrgSelectorImproved: React.FC = ({ organizations, onError }) => { - const [isOpen, setIsOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [clientReady, setClientReady] = useState(false); - const [selectedOrgId, setSelectedOrgId] = useState(""); - const [hasAttemptedAutoSelection, setHasAttemptedAutoSelection] = useState(false); - // Set client ready after hydration - useEffect(() => { - setClientReady(true); - }, []); - - const orgOptions: ComboboxOption[] = useMemo(() => { - // Sort: recently created first (as proxy for recently used until we track that) - const sorted = [...organizations].sort((a, b) => { - const aDate = a.createdAt ? new Date(a.createdAt).getTime() : 0; - const bDate = b.createdAt ? new Date(b.createdAt).getTime() : 0; - return bDate - aDate; // Newest first - }); - - return sorted.map((org) => ({ - label: org.name, - value: org.id, - searchValue: org.name.toLowerCase(), - })); - }, [organizations]); - - const submit = useCallback( - async (orgId: string) => { - if (isLoading || !orgId) { - return; - } - - try { - setIsLoading(true); - const result = await completeOrgSelection(orgId); - - if (!result.success) { - onError(result.message); - setIsLoading(false); - } - // On success, the page will redirect, so we don't need to reset loading state - } catch (error) { - const errorMessage = - error instanceof Error - ? error.message - : "Failed to complete organization selection. Please re-authenticate or contact support@unkey.dev"; - - onError(errorMessage); - setIsLoading(false); - } - }, - [isLoading, onError], - ); - - const handleSubmit = useCallback(() => { - submit(selectedOrgId); - }, [submit, selectedOrgId]); - - // Helper function to get cookie value on client side - const getCookie = (name: string): string | null => { - if (typeof document === "undefined") { - return null; - } - const value = `; ${document.cookie}`; - const parts = value.split(`; ${name}=`); - if (parts.length === 2) { - return parts.pop()?.split(";").shift() || null; - } - return null; - }; - - // Auto-select last used organization if available - useEffect(() => { - if (!clientReady || hasAttemptedAutoSelection) { - return; - } - - setHasAttemptedAutoSelection(true); - - // Get the last used organization ID from cookie - const lastUsedOrgId = getCookie("unkey_last_org_used"); - - if (lastUsedOrgId) { - // Check if the stored orgId exists in the current list of organizations - const orgExists = organizations.some((org) => org.id === lastUsedOrgId); - - if (orgExists) { - // Auto-submit this organization - submit(lastUsedOrgId); - return; - } - } - - // If no auto-selection, show the modal with first org pre-selected - if (organizations.length > 0) { - setSelectedOrgId(organizations[0].id); - } - setIsOpen(true); - }, [clientReady, organizations, hasAttemptedAutoSelection, submit]); - - return ( - { - setIsOpen(open); - }} - title="Select your workspace" - footer={ -
- Select a workspace to sign in. -
- } - > -
- {/* Workspace selector */} -
- - -
- - {/* Submit button */} - -
-
- ); -}; diff --git a/apps/dashboard/app/auth/sign-in/org-selector.tsx b/apps/dashboard/app/auth/sign-in/org-selector.tsx index a5a3b37af7..d625ac28a1 100644 --- a/apps/dashboard/app/auth/sign-in/org-selector.tsx +++ b/apps/dashboard/app/auth/sign-in/org-selector.tsx @@ -1,54 +1,122 @@ "use client"; +import { Combobox, type ComboboxOption } from "@/components/ui/combobox"; import type { Organization } from "@/lib/auth/types"; -import { Button, DialogContainer } from "@unkey/ui"; +import { Button, DialogContainer, Loading } from "@unkey/ui"; import type React from "react"; -import { useEffect, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { completeOrgSelection } from "../actions"; +import { SignInContext } from "../context/signin-context"; interface OrgSelectorProps { organizations: Organization[]; - onError: (errorMessage: string) => void; } -export const OrgSelector: React.FC = ({ organizations, onError }) => { +export const OrgSelector: React.FC = ({ organizations }) => { + const context = useContext(SignInContext); + if (!context) { + throw new Error("OrgSelector must be used within SignInProvider"); + } + const { setError } = context; const [isOpen, setIsOpen] = useState(false); - const [isLoading, setIsLoading] = useState(null); + const [isLoading, setIsLoading] = useState(false); const [clientReady, setClientReady] = useState(false); - + const [selectedOrgId, setSelectedOrgId] = useState(""); + const [hasAttemptedAutoSelection, setHasAttemptedAutoSelection] = + useState(false); // Set client ready after hydration useEffect(() => { setClientReady(true); - // Only open the dialog after hydration to prevent hydration mismatch - setIsOpen(true); }, []); - const submit = async (orgId: string) => { - if (isLoading) { + const orgOptions: ComboboxOption[] = useMemo(() => { + // Sort: recently created first (as proxy for recently used until we track that) + const sorted = [...organizations].sort((a, b) => { + const aDate = a.createdAt ? new Date(a.createdAt).getTime() : 0; + const bDate = b.createdAt ? new Date(b.createdAt).getTime() : 0; + return bDate - aDate; // Newest first + }); + + return sorted.map((org) => ({ + label: org.name, + value: org.id, + searchValue: org.name.toLowerCase(), + })); + }, [organizations]); + + const submit = useCallback( + async (orgId: string) => { + if (isLoading || !orgId) { + return; + } + + try { + setIsLoading(true); + const result = await completeOrgSelection(orgId); + + if (!result.success) { + setError(result.message); + setIsLoading(false); + } + // On success, the page will redirect, so we don't need to reset loading state + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : "Failed to complete organization selection. Please re-authenticate or contact support@unkey.dev"; + + setError(errorMessage); + setIsLoading(false); + } + }, + [isLoading, setError] + ); + + const handleSubmit = useCallback(() => { + submit(selectedOrgId); + }, [submit, selectedOrgId]); + + // Helper function to get cookie value on client side + const getCookie = (name: string): string | null => { + if (typeof document === "undefined") { + return null; + } + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) { + return parts.pop()?.split(";").shift() || null; + } + return null; + }; + + // Auto-select last used organization if available + useEffect(() => { + if (!clientReady || hasAttemptedAutoSelection) { return; } - try { - setIsLoading(orgId); - const result = await completeOrgSelection(orgId); + setHasAttemptedAutoSelection(true); + + // Get the last used organization ID from cookie + const lastUsedOrgId = getCookie("unkey_last_org_used"); + + if (lastUsedOrgId) { + // Check if the stored orgId exists in the current list of organizations + const orgExists = organizations.some((org) => org.id === lastUsedOrgId); - if (!result.success) { - onError(result.message); + if (orgExists) { + // Auto-submit this organization + submit(lastUsedOrgId); + return; } + } - return; - } catch (error) { - const errorMessage = - error instanceof Error - ? error.message - : "Failed to complete organization selection. Please re-authenticate or contact support@unkey.dev"; - - onError(errorMessage); - } finally { - setIsLoading(null); - setIsOpen(false); + // If no auto-selection, show the modal with first org pre-selected + if (organizations.length > 0) { + setSelectedOrgId(organizations[0].id); } - }; + setIsOpen(true); + }, [clientReady, organizations, hasAttemptedAutoSelection, submit]); return ( = ({ organizations, onError onOpenChange={(open) => { setIsOpen(open); }} - title="Select a workspace" + title="Select your workspace" footer={ -
+
Select a workspace to sign in.
} > -
    - {organizations - .sort((a, b) => a.name.localeCompare(b.name)) - .map((org) => ( - - ))} -
+
+ {/* Workspace selector */} +
+ + +
+ + {/* Submit button */} + +
); }; From ec6e4b22e257eedd3005a712836e9a58e14dab99 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Fri, 31 Oct 2025 12:59:25 -0400 Subject: [PATCH 03/20] rabbit --- .../app/auth/sign-in/org-selector.tsx | 30 ++++++++++++------- .../navigation/sidebar/team-switcher.tsx | 20 +++++++++---- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/apps/dashboard/app/auth/sign-in/org-selector.tsx b/apps/dashboard/app/auth/sign-in/org-selector.tsx index d625ac28a1..88b6781dfa 100644 --- a/apps/dashboard/app/auth/sign-in/org-selector.tsx +++ b/apps/dashboard/app/auth/sign-in/org-selector.tsx @@ -45,9 +45,9 @@ export const OrgSelector: React.FC = ({ organizations }) => { }, [organizations]); const submit = useCallback( - async (orgId: string) => { + async (orgId: string): Promise => { if (isLoading || !orgId) { - return; + return false; } try { @@ -57,8 +57,10 @@ export const OrgSelector: React.FC = ({ organizations }) => { if (!result.success) { setError(result.message); setIsLoading(false); + return false; } // On success, the page will redirect, so we don't need to reset loading state + return true; } catch (error) { const errorMessage = error instanceof Error @@ -67,9 +69,10 @@ export const OrgSelector: React.FC = ({ organizations }) => { setError(errorMessage); setIsLoading(false); + return false; } }, - [isLoading, setError] + [setError, completeOrgSelection] ); const handleSubmit = useCallback(() => { @@ -102,21 +105,28 @@ export const OrgSelector: React.FC = ({ organizations }) => { if (lastUsedOrgId) { // Check if the stored orgId exists in the current list of organizations - const orgExists = organizations.some((org) => org.id === lastUsedOrgId); + const orgExists = orgOptions.some((opt) => opt.value === lastUsedOrgId); if (orgExists) { - // Auto-submit this organization - submit(lastUsedOrgId); + // Auto-submit this organization and handle failure by reopening the dialog + submit(lastUsedOrgId).then((success) => { + if (!success) { + // If auto-submit fails, pre-select the last used org and open the dialog for manual selection + setSelectedOrgId(lastUsedOrgId); + setIsOpen(true); + } + }); return; } } - // If no auto-selection, show the modal with first org pre-selected - if (organizations.length > 0) { - setSelectedOrgId(organizations[0].id); + // If no auto-selection, show the modal with first org pre-selected (from sorted array) + // Use orgOptions[0].value to ensure the pre-selected value matches the displayed first option + if (orgOptions.length > 0 && orgOptions[0]?.value) { + setSelectedOrgId(orgOptions[0].value); } setIsOpen(true); - }, [clientReady, organizations, hasAttemptedAutoSelection, submit]); + }, [clientReady, orgOptions, hasAttemptedAutoSelection, submit]); return ( { } try { + // Critical: Set the session cookie to complete the workspace switch await setSessionCookie({ token: sessionData.token, expiresAt: sessionData.expiresAt, }); - // Store the last used organization ID in a cookie for auto-selection on next login + // Instead of messing with the cache, we can simply redirect the user and we will refetch the user data. + window.location.replace("/"); + } catch (error) { + console.error("Failed to set session cookie:", error); + toast.error("Failed to complete workspace switch. Please try again."); + return; + } + + // Non-critical: Store the last used organization ID in a cookie for auto-selection on next login + // This runs after the critical operations, and failures won't block the workspace switch + try { await setCookie({ name: "unkey_last_org_used", value: orgId, @@ -68,12 +79,9 @@ export const WorkspaceSwitcher: React.FC = (): JSX.Element => { maxAge: 60 * 60 * 24 * 30, // 30 Days }, }); - - // Instead of messing with the cache, we can simply redirect the user and we will refetch the user data. - window.location.replace("/"); } catch (error) { - console.error("Failed to set session cookie:", error); - toast.error("Failed to complete workspace switch. Please try again."); + // Swallow the error with a debug log - preference storage failure should not interrupt user flow + console.debug("Failed to store last used workspace preference:", error); } }, onError(error) { From 9ab816f8f193fe4c707e8e5be72d36456e30f493 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Mon, 3 Nov 2025 13:43:53 -0500 Subject: [PATCH 04/20] Broken Slow Things --- .../app/auth/sign-in/[[...sign-in]]/page.tsx | 50 ++++- .../app/auth/sign-in/org-selector.tsx | 179 +++++++++--------- .../navigation/sidebar/team-switcher.tsx | 6 +- 3 files changed, 135 insertions(+), 100 deletions(-) diff --git a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx index cdaed956c3..cb1dae2922 100644 --- a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx +++ b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx @@ -2,12 +2,13 @@ import { FadeIn } from "@/components/landing/fade-in"; import { ArrowRight } from "@unkey/icons"; -import { Loading } from "@unkey/ui"; +import { Loading, Empty } from "@unkey/ui"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { ErrorBanner, WarnBanner } from "../../banners"; import { SignInProvider } from "../../context/signin-context"; +import { completeOrgSelection } from "../../actions"; import { useSignIn } from "../../hooks"; import { EmailCode } from "../email-code"; import { EmailSignIn } from "../email-signin"; @@ -30,7 +31,6 @@ function SignInContent() { const verifyParam = searchParams?.get("verify"); const invitationToken = searchParams?.get("invitation_token"); const invitationEmail = searchParams?.get("email"); - // Initialize isLoading as false const [isLoading, setIsLoading] = useState(false); @@ -38,6 +38,34 @@ function SignInContent() { const [clientReady, setClientReady] = useState(false); const hasAttemptedSignIn = useRef(false); + // Helper function to get cookie value on client side + const getCookie = (name: string): string | null => { + if (typeof document === "undefined") { + return null; + } + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) { + return parts.pop()?.split(";").shift() || null; + } + return null; + }; + + const lastUsedOrgId = getCookie("unkey_last_org_used"); + if (hasPendingAuth && lastUsedOrgId) { + setIsLoading(true); + completeOrgSelection(lastUsedOrgId).then((result) => { + if (!result.success) { + setError(result.message); + setIsLoading(false); + return false; + } + // On success, redirect to the dashboard + window.location.href = result.redirectTo; + return true; + }); + } + // Set clientReady to true after hydration useEffect(() => { setClientReady(true); @@ -87,15 +115,19 @@ function SignInContent() { handleSignInViaEmail, ]); - // Show a loading indicator only when isLoading is true AND client has hydrated - if (clientReady && isLoading) { - return ; + // // Show a loading indicator only when isLoading is true AND client has hydrated + // if (clientReady && isLoading) { + // return ; + // } + if (isLoading && clientReady) { + + + ; } - - return ( + return hasPendingAuth ? ( + + ) : (
- {hasPendingAuth && } - {accountNotFound && (
diff --git a/apps/dashboard/app/auth/sign-in/org-selector.tsx b/apps/dashboard/app/auth/sign-in/org-selector.tsx index 88b6781dfa..679ef0aeb6 100644 --- a/apps/dashboard/app/auth/sign-in/org-selector.tsx +++ b/apps/dashboard/app/auth/sign-in/org-selector.tsx @@ -1,6 +1,13 @@ "use client"; -import { Combobox, type ComboboxOption } from "@/components/ui/combobox"; +import { + Empty, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@unkey/ui"; import type { Organization } from "@/lib/auth/types"; import { Button, DialogContainer, Loading } from "@unkey/ui"; import type React from "react"; @@ -10,9 +17,13 @@ import { SignInContext } from "../context/signin-context"; interface OrgSelectorProps { organizations: Organization[]; + lastOrgId?: string; } -export const OrgSelector: React.FC = ({ organizations }) => { +export const OrgSelector: React.FC = ({ + organizations, + lastOrgId, +}) => { const context = useContext(SignInContext); if (!context) { throw new Error("OrgSelector must be used within SignInProvider"); @@ -20,6 +31,7 @@ export const OrgSelector: React.FC = ({ organizations }) => { const { setError } = context; const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [isAttemptingAutoSignIn, setIsAttemptingAutoSignIn] = useState(false); const [clientReady, setClientReady] = useState(false); const [selectedOrgId, setSelectedOrgId] = useState(""); const [hasAttemptedAutoSelection, setHasAttemptedAutoSelection] = @@ -29,19 +41,13 @@ export const OrgSelector: React.FC = ({ organizations }) => { setClientReady(true); }, []); - const orgOptions: ComboboxOption[] = useMemo(() => { + const sortedOrgs = useMemo(() => { // Sort: recently created first (as proxy for recently used until we track that) - const sorted = [...organizations].sort((a, b) => { + return [...organizations].sort((a, b) => { const aDate = a.createdAt ? new Date(a.createdAt).getTime() : 0; const bDate = b.createdAt ? new Date(b.createdAt).getTime() : 0; return bDate - aDate; // Newest first }); - - return sorted.map((org) => ({ - label: org.name, - value: org.id, - searchValue: org.name.toLowerCase(), - })); }, [organizations]); const submit = useCallback( @@ -59,7 +65,8 @@ export const OrgSelector: React.FC = ({ organizations }) => { setIsLoading(false); return false; } - // On success, the page will redirect, so we don't need to reset loading state + // On success, redirect to the dashboard + window.location.href = result.redirectTo; return true; } catch (error) { const errorMessage = @@ -72,47 +79,33 @@ export const OrgSelector: React.FC = ({ organizations }) => { return false; } }, - [setError, completeOrgSelection] + [isLoading] ); const handleSubmit = useCallback(() => { submit(selectedOrgId); }, [submit, selectedOrgId]); - // Helper function to get cookie value on client side - const getCookie = (name: string): string | null => { - if (typeof document === "undefined") { - return null; - } - const value = `; ${document.cookie}`; - const parts = value.split(`; ${name}=`); - if (parts.length === 2) { - return parts.pop()?.split(";").shift() || null; - } - return null; - }; - // Auto-select last used organization if available useEffect(() => { - if (!clientReady || hasAttemptedAutoSelection) { + if (!clientReady) { return; } setHasAttemptedAutoSelection(true); - // Get the last used organization ID from cookie - const lastUsedOrgId = getCookie("unkey_last_org_used"); - - if (lastUsedOrgId) { + if (lastOrgId) { // Check if the stored orgId exists in the current list of organizations - const orgExists = orgOptions.some((opt) => opt.value === lastUsedOrgId); + const orgExists = sortedOrgs.some((org) => org.id === lastOrgId); if (orgExists) { + // Show loading state while attempting auto sign-in + setIsAttemptingAutoSignIn(true); // Auto-submit this organization and handle failure by reopening the dialog - submit(lastUsedOrgId).then((success) => { + submit(lastOrgId).then((success) => { if (!success) { // If auto-submit fails, pre-select the last used org and open the dialog for manual selection - setSelectedOrgId(lastUsedOrgId); + setSelectedOrgId(lastOrgId); setIsOpen(true); } }); @@ -121,65 +114,75 @@ export const OrgSelector: React.FC = ({ organizations }) => { } // If no auto-selection, show the modal with first org pre-selected (from sorted array) - // Use orgOptions[0].value to ensure the pre-selected value matches the displayed first option - if (orgOptions.length > 0 && orgOptions[0]?.value) { - setSelectedOrgId(orgOptions[0].value); + // Use sortedOrgs[0].id to ensure the pre-selected value matches the displayed first option + if (sortedOrgs.length > 0 && sortedOrgs[0]?.id) { + setSelectedOrgId(sortedOrgs[0].id); } - setIsOpen(true); - }, [clientReady, orgOptions, hasAttemptedAutoSelection, submit]); + }, [clientReady, sortedOrgs, hasAttemptedAutoSelection, submit, lastOrgId]); return ( - { - setIsOpen(open); - }} - title="Select your workspace" - footer={ -
- Select a workspace to sign in. -
- } - > -
- {/* Workspace selector */} -
-
- + + ) ); }; diff --git a/apps/dashboard/components/navigation/sidebar/team-switcher.tsx b/apps/dashboard/components/navigation/sidebar/team-switcher.tsx index 7b9eb895b1..3e40c06a8d 100644 --- a/apps/dashboard/components/navigation/sidebar/team-switcher.tsx +++ b/apps/dashboard/components/navigation/sidebar/team-switcher.tsx @@ -56,9 +56,6 @@ export const WorkspaceSwitcher: React.FC = (): JSX.Element => { token: sessionData.token, expiresAt: sessionData.expiresAt, }); - - // Instead of messing with the cache, we can simply redirect the user and we will refetch the user data. - window.location.replace("/"); } catch (error) { console.error("Failed to set session cookie:", error); toast.error("Failed to complete workspace switch. Please try again."); @@ -83,6 +80,9 @@ export const WorkspaceSwitcher: React.FC = (): JSX.Element => { // Swallow the error with a debug log - preference storage failure should not interrupt user flow console.debug("Failed to store last used workspace preference:", error); } + + // Instead of messing with the cache, we can simply redirect the user and we will refetch the user data. + window.location.replace("/"); }, onError(error) { console.error("Failed to switch workspace: ", error); From 1e6793ea0ecfef5f4f60459261f443444ef9fb84 Mon Sep 17 00:00:00 2001 From: James Perkins Date: Mon, 3 Nov 2025 14:35:38 -0500 Subject: [PATCH 05/20] Fix most of the loading. --- apps/dashboard/app/auth/actions.ts | 13 +- .../app/auth/sign-in/[[...sign-in]]/page.tsx | 81 +++---- .../app/auth/sign-in/org-selector.tsx | 202 +++++++++--------- 3 files changed, 149 insertions(+), 147 deletions(-) diff --git a/apps/dashboard/app/auth/actions.ts b/apps/dashboard/app/auth/actions.ts index 33e9dcde9f..87cc29bc85 100644 --- a/apps/dashboard/app/auth/actions.ts +++ b/apps/dashboard/app/auth/actions.ts @@ -68,7 +68,7 @@ export async function verifyAuthCode(params: { if (orgSelectionResult.success) { // Try to get organization name for better UX in success page - const redirectUrl = "/apis"; + let redirectUrl = "/apis"; try { const org = await auth.getOrg(invitation.organizationId); if (org?.name) { @@ -76,7 +76,7 @@ export async function verifyAuthCode(params: { from_invite: "true", org_name: org.name, }); - `/join/success?${params.toString()}`; + redirectUrl = `/join/success?${params.toString()}`; } } catch (error) { // Don't fail the redirect if we can't get org name @@ -126,7 +126,7 @@ export async function verifyAuthCode(params: { } // Try to get organization name for better UX - const redirectUrl = result.redirectTo; + let redirectUrl = result.redirectTo; try { const org = await auth.getOrg(invitation.organizationId); if (org?.name) { @@ -134,16 +134,16 @@ export async function verifyAuthCode(params: { from_invite: "true", org_name: org.name, }); - `/join/success?${params.toString()}`; + redirectUrl = `/join/success?${params.toString()}`; } else { const params = new URLSearchParams({ from_invite: "true" }); - `/join/success?${params.toString()}`; + redirectUrl = `/join/success?${params.toString()}`; } } catch (error) { // Don't fail if we can't get org name, just use join success without org name console.warn("Could not fetch organization name for new user success page:", error); const params = new URLSearchParams({ from_invite: "true" }); - `/join/success?${params.toString()}`; + redirectUrl = `/join/success?${params.toString()}`; } return { @@ -338,6 +338,7 @@ export async function completeOrgSelection( } // Store the last used organization ID in a cookie for auto-selection on next login + // This doesn't work on Safari. cookies().set("unkey_last_org_used", orgId, { httpOnly: false, // Allow client-side access secure: true, diff --git a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx index cb1dae2922..2fbb6e1ad8 100644 --- a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx +++ b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx @@ -2,13 +2,13 @@ import { FadeIn } from "@/components/landing/fade-in"; import { ArrowRight } from "@unkey/icons"; -import { Loading, Empty } from "@unkey/ui"; +import { Empty, Loading } from "@unkey/ui"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; +import { completeOrgSelection } from "../../actions"; import { ErrorBanner, WarnBanner } from "../../banners"; import { SignInProvider } from "../../context/signin-context"; -import { completeOrgSelection } from "../../actions"; import { useSignIn } from "../../hooks"; import { EmailCode } from "../email-code"; import { EmailSignIn } from "../email-signin"; @@ -31,12 +31,12 @@ function SignInContent() { const verifyParam = searchParams?.get("verify"); const invitationToken = searchParams?.get("invitation_token"); const invitationEmail = searchParams?.get("email"); - // Initialize isLoading as false - const [isLoading, setIsLoading] = useState(false); // Add clientReady state to handle hydration const [clientReady, setClientReady] = useState(false); const hasAttemptedSignIn = useRef(false); + const hasAttemptedAutoOrgSelection = useRef(false); + const [isLoading, setIsLoading] = useState(false); // Helper function to get cookie value on client side const getCookie = (name: string): string | null => { @@ -51,26 +51,40 @@ function SignInContent() { return null; }; - const lastUsedOrgId = getCookie("unkey_last_org_used"); - if (hasPendingAuth && lastUsedOrgId) { - setIsLoading(true); - completeOrgSelection(lastUsedOrgId).then((result) => { - if (!result.success) { - setError(result.message); - setIsLoading(false); - return false; - } - // On success, redirect to the dashboard - window.location.href = result.redirectTo; - return true; - }); - } - // Set clientReady to true after hydration useEffect(() => { setClientReady(true); }, []); + // Handle auto org selection when returning from OAuth + useEffect(() => { + if (!clientReady || !hasPendingAuth || hasAttemptedAutoOrgSelection.current) { + return; + } + + const lastUsedOrgId = getCookie("unkey_last_org_used"); + if (lastUsedOrgId) { + hasAttemptedAutoOrgSelection.current = true; + setIsLoading(true); + + completeOrgSelection(lastUsedOrgId) + .then((result) => { + if (!result.success) { + setError(result.message); + setIsLoading(false); + return; + } + // On success, redirect to the dashboard + window.location.href = result.redirectTo; + }) + .catch((err) => { + console.error("Auto org selection failed:", err); + setError("Failed to automatically sign in. Please select your workspace."); + setIsLoading(false); + }); + } + }, [clientReady, hasPendingAuth, setError]); + // Handle auto sign-in with invitation token and email useEffect(() => { // Only run this effect on the client side after hydration @@ -115,25 +129,23 @@ function SignInContent() { handleSignInViaEmail, ]); - // // Show a loading indicator only when isLoading is true AND client has hydrated - // if (clientReady && isLoading) { - // return ; - // } + // Show a loading indicator when auto-selecting org if (isLoading && clientReady) { - - - ; + return ( + + +

Signing you in...

+
+ ); } return hasPendingAuth ? ( - + ) : (
{accountNotFound && (
-

- Account not found, did you mean to sign up? -

+

Account not found, did you mean to sign up?

@@ -158,10 +170,7 @@ function SignInContent() {

Sign In

New to Unkey?{" "} - + Create new account

@@ -173,9 +182,7 @@ function SignInContent() {
- - or continue using email - + or continue using email
diff --git a/apps/dashboard/app/auth/sign-in/org-selector.tsx b/apps/dashboard/app/auth/sign-in/org-selector.tsx index 679ef0aeb6..4596be2198 100644 --- a/apps/dashboard/app/auth/sign-in/org-selector.tsx +++ b/apps/dashboard/app/auth/sign-in/org-selector.tsx @@ -1,14 +1,7 @@ "use client"; -import { - Empty, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@unkey/ui"; import type { Organization } from "@/lib/auth/types"; +import { Empty, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@unkey/ui"; import { Button, DialogContainer, Loading } from "@unkey/ui"; import type React from "react"; import { useCallback, useContext, useEffect, useMemo, useState } from "react"; @@ -20,10 +13,7 @@ interface OrgSelectorProps { lastOrgId?: string; } -export const OrgSelector: React.FC = ({ - organizations, - lastOrgId, -}) => { +export const OrgSelector: React.FC = ({ organizations, lastOrgId }) => { const context = useContext(SignInContext); if (!context) { throw new Error("OrgSelector must be used within SignInProvider"); @@ -31,11 +21,9 @@ export const OrgSelector: React.FC = ({ const { setError } = context; const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [isAttemptingAutoSignIn, setIsAttemptingAutoSignIn] = useState(false); const [clientReady, setClientReady] = useState(false); const [selectedOrgId, setSelectedOrgId] = useState(""); - const [hasAttemptedAutoSelection, setHasAttemptedAutoSelection] = - useState(false); + const [hasInitialized, setHasInitialized] = useState(false); // Set client ready after hydration useEffect(() => { setClientReady(true); @@ -79,110 +67,116 @@ export const OrgSelector: React.FC = ({ return false; } }, - [isLoading] + [isLoading], ); const handleSubmit = useCallback(() => { submit(selectedOrgId); }, [submit, selectedOrgId]); - // Auto-select last used organization if available + // Initialize org selector when client is ready useEffect(() => { - if (!clientReady) { + if (!clientReady || hasInitialized) { return; } - setHasAttemptedAutoSelection(true); + // Pre-select the last used org if it exists in the list, otherwise first org + const preselectedOrgId = + lastOrgId && sortedOrgs.some((org) => org.id === lastOrgId) + ? lastOrgId + : sortedOrgs[0]?.id || ""; - if (lastOrgId) { - // Check if the stored orgId exists in the current list of organizations - const orgExists = sortedOrgs.some((org) => org.id === lastOrgId); - - if (orgExists) { - // Show loading state while attempting auto sign-in - setIsAttemptingAutoSignIn(true); - // Auto-submit this organization and handle failure by reopening the dialog - submit(lastOrgId).then((success) => { - if (!success) { - // If auto-submit fails, pre-select the last used org and open the dialog for manual selection - setSelectedOrgId(lastOrgId); - setIsOpen(true); - } - }); - return; - } - } - - // If no auto-selection, show the modal with first org pre-selected (from sorted array) - // Use sortedOrgs[0].id to ensure the pre-selected value matches the displayed first option - if (sortedOrgs.length > 0 && sortedOrgs[0]?.id) { - setSelectedOrgId(sortedOrgs[0].id); - } - }, [clientReady, sortedOrgs, hasAttemptedAutoSelection, submit, lastOrgId]); + setSelectedOrgId(preselectedOrgId); + setIsOpen(true); // Always show the modal for manual selection + setHasInitialized(true); + }, [clientReady, sortedOrgs, lastOrgId, hasInitialized]); return ( - !isAttemptingAutoSignIn && ( - { - setIsOpen(open); - }} - title={!isAttemptingAutoSignIn ? "Select your workspace" : ""} - footer={ - !isAttemptingAutoSignIn && ( -
- Select a workspace to sign in. + { + setIsOpen(open); + }} + title="Select your workspace" + footer={ +
+ Select a workspace to sign in. +
+ } + > +
+ {/* Workspace selector */} + {sortedOrgs.length === 0 ? ( + +
+

No workspaces found

+

+ You don't have access to any workspaces. Please contact your administrator or create + a new workspace. +

+
+ + +
+
+
+ ) : ( + <> +
+ +
- ) - } - > -
- {/* Workspace selector */} -
- - -
- - {/* Submit button */} - -
- - ) + {isLoading ? ( +
+ + Signing in... +
+ ) : ( + "Continue" + )} + + + )} +
+
); }; From 13cab4afb9bd5ab40ee15150e649dff4e7fe959d Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Mon, 3 Nov 2025 17:09:58 -0500 Subject: [PATCH 06/20] Mostly good --- apps/dashboard/app/auth/actions.ts | 10 ++---- .../app/auth/sign-in/[[...sign-in]]/page.tsx | 34 ++++++++----------- .../app/auth/sign-in/org-selector.tsx | 22 +++++++++--- .../navigation/sidebar/team-switcher.tsx | 2 +- apps/dashboard/lib/auth/types.ts | 1 + 5 files changed, 36 insertions(+), 33 deletions(-) diff --git a/apps/dashboard/app/auth/actions.ts b/apps/dashboard/app/auth/actions.ts index 87cc29bc85..cdeef2c0fd 100644 --- a/apps/dashboard/app/auth/actions.ts +++ b/apps/dashboard/app/auth/actions.ts @@ -335,17 +335,11 @@ export async function completeOrgSelection( cookies().delete(PENDING_SESSION_COOKIE); for (const cookie of result.cookies) { cookies().set(cookie.name, cookie.value, cookie.options); + // Store the last used organization ID in a cookie for auto-selection on next login + cookies().set("unkey_last_org_used", orgId, cookie.options); } - // Store the last used organization ID in a cookie for auto-selection on next login // This doesn't work on Safari. - cookies().set("unkey_last_org_used", orgId, { - httpOnly: false, // Allow client-side access - secure: true, - sameSite: "lax", - path: "/", - maxAge: 60 * 60 * 24 * 30, // 30 Days - }); } return result; diff --git a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx index 2fbb6e1ad8..18d5a99619 100644 --- a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx +++ b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx @@ -1,6 +1,8 @@ "use client"; import { FadeIn } from "@/components/landing/fade-in"; +import { getCookie } from "@/lib/auth/cookies"; +import { UNKEY_LAST_ORG_COOKIE } from "@/lib/auth/types"; import { ArrowRight } from "@unkey/icons"; import { Empty, Loading } from "@unkey/ui"; import Link from "next/link"; @@ -31,29 +33,22 @@ function SignInContent() { const verifyParam = searchParams?.get("verify"); const invitationToken = searchParams?.get("invitation_token"); const invitationEmail = searchParams?.get("email"); - + const [lastUsedOrgId, setLastUsedOrgId] = useState(undefined); // Add clientReady state to handle hydration const [clientReady, setClientReady] = useState(false); const hasAttemptedSignIn = useRef(false); const hasAttemptedAutoOrgSelection = useRef(false); - const [isLoading, setIsLoading] = useState(false); - - // Helper function to get cookie value on client side - const getCookie = (name: string): string | null => { - if (typeof document === "undefined") { - return null; - } - const value = `; ${document.cookie}`; - const parts = value.split(`; ${name}=`); - if (parts.length === 2) { - return parts.pop()?.split(";").shift() || null; - } - return null; - }; + const [isLoading, setIsLoading] = useState(true); // Set clientReady to true after hydration useEffect(() => { - setClientReady(true); + getCookie(UNKEY_LAST_ORG_COOKIE).then((value) => { + if (value) { + setLastUsedOrgId(value); + } + setClientReady(true); + setIsLoading(false); + }); }, []); // Handle auto org selection when returning from OAuth @@ -62,7 +57,6 @@ function SignInContent() { return; } - const lastUsedOrgId = getCookie("unkey_last_org_used"); if (lastUsedOrgId) { hasAttemptedAutoOrgSelection.current = true; setIsLoading(true); @@ -83,7 +77,7 @@ function SignInContent() { setIsLoading(false); }); } - }, [clientReady, hasPendingAuth, setError]); + }, [clientReady, hasPendingAuth, setError, lastUsedOrgId]); // Handle auto sign-in with invitation token and email useEffect(() => { @@ -105,7 +99,7 @@ function SignInContent() { hasAttemptedSignIn.current = true; // Set loading state to true - setIsLoading(true); + // setIsLoading(true); try { // Attempt sign-in with the provided email @@ -139,7 +133,7 @@ function SignInContent() { ); } return hasPendingAuth ? ( - + ) : (
{accountNotFound && ( diff --git a/apps/dashboard/app/auth/sign-in/org-selector.tsx b/apps/dashboard/app/auth/sign-in/org-selector.tsx index 4596be2198..0125cfe23b 100644 --- a/apps/dashboard/app/auth/sign-in/org-selector.tsx +++ b/apps/dashboard/app/auth/sign-in/org-selector.tsx @@ -1,7 +1,15 @@ "use client"; import type { Organization } from "@/lib/auth/types"; -import { Empty, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@unkey/ui"; +import { + Empty, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + toast, +} from "@unkey/ui"; import { Button, DialogContainer, Loading } from "@unkey/ui"; import type React from "react"; import { useCallback, useContext, useEffect, useMemo, useState } from "react"; @@ -51,8 +59,10 @@ export const OrgSelector: React.FC = ({ organizations, lastOrg if (!result.success) { setError(result.message); setIsLoading(false); + toast.error(result.message); return false; } + // On success, redirect to the dashboard window.location.href = result.redirectTo; return true; @@ -61,13 +71,15 @@ export const OrgSelector: React.FC = ({ organizations, lastOrg error instanceof Error ? error.message : "Failed to complete organization selection. Please re-authenticate or contact support@unkey.dev"; - + toast.error( + "Failed to complete organization selection. Please re-authenticate or contact support@unkey.dev", + ); setError(errorMessage); setIsLoading(false); return false; } }, - [isLoading], + [isLoading, setError], ); const handleSubmit = useCallback(() => { @@ -96,7 +108,9 @@ export const OrgSelector: React.FC = ({ organizations, lastOrg className="dark bg-black" isOpen={clientReady && isOpen} onOpenChange={(open) => { - setIsOpen(open); + if (!isLoading) { + setIsOpen(open); + } }} title="Select your workspace" footer={ diff --git a/apps/dashboard/components/navigation/sidebar/team-switcher.tsx b/apps/dashboard/components/navigation/sidebar/team-switcher.tsx index 3e40c06a8d..74df3d5c94 100644 --- a/apps/dashboard/components/navigation/sidebar/team-switcher.tsx +++ b/apps/dashboard/components/navigation/sidebar/team-switcher.tsx @@ -71,7 +71,7 @@ export const WorkspaceSwitcher: React.FC = (): JSX.Element => { options: { httpOnly: false, // Allow client-side access secure: true, - sameSite: "lax", + sameSite: "strict", path: "/", maxAge: 60 * 60 * 24 * 30, // 30 Days }, diff --git a/apps/dashboard/lib/auth/types.ts b/apps/dashboard/lib/auth/types.ts index b896d09e05..c21043d56b 100644 --- a/apps/dashboard/lib/auth/types.ts +++ b/apps/dashboard/lib/auth/types.ts @@ -2,6 +2,7 @@ import type { Cookie } from "./cookies"; // Core Types export const UNKEY_SESSION_COOKIE = "unkey-session"; +export const UNKEY_LAST_ORG_COOKIE = "unkey_last_org_used"; export const PENDING_SESSION_COOKIE = "sess-temp"; export const SIGN_IN_URL = "/auth/sign-in"; export const SIGN_UP_URL = "/auth/sign-up"; From 44dc78e74fb80d070783fd9d81a5121d6684bc67 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Tue, 4 Nov 2025 08:55:43 -0500 Subject: [PATCH 07/20] cookie tweak --- apps/dashboard/app/auth/actions.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/dashboard/app/auth/actions.ts b/apps/dashboard/app/auth/actions.ts index cdeef2c0fd..3e92f3f27a 100644 --- a/apps/dashboard/app/auth/actions.ts +++ b/apps/dashboard/app/auth/actions.ts @@ -10,6 +10,7 @@ import { type OAuthResult, PENDING_SESSION_COOKIE, type SignInViaOAuthOptions, + UNKEY_LAST_ORG_COOKIE, type UserData, type VerificationResult, errorMessages, @@ -336,10 +337,14 @@ export async function completeOrgSelection( for (const cookie of result.cookies) { cookies().set(cookie.name, cookie.value, cookie.options); // Store the last used organization ID in a cookie for auto-selection on next login - cookies().set("unkey_last_org_used", orgId, cookie.options); + cookies().set(UNKEY_LAST_ORG_COOKIE, orgId, { + httpOnly: false, // Allow client-side access + secure: true, + sameSite: "lax", + path: "/", + maxAge: 60 * 60 * 24 * 30, // 30 Days + }); } - - // This doesn't work on Safari. } return result; @@ -356,7 +361,7 @@ export async function switchOrg(orgId: string): Promise<{ success: boolean; erro await setSessionCookie({ token: newToken, expiresAt }); // Store the last used organization ID in a cookie for auto-selection on next login - cookies().set("unkey_last_org_used", orgId, { + cookies().set(UNKEY_LAST_ORG_COOKIE, orgId, { httpOnly: false, // Allow client-side access secure: true, sameSite: "lax", From ae025a6b507181d98a8e4c93af56b82a0e8bcbe0 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Tue, 4 Nov 2025 16:26:55 -0500 Subject: [PATCH 08/20] cookie change and auto close when session expired --- .../app/auth/sign-in/[[...sign-in]]/page.tsx | 36 +++++++++++++++++-- .../app/auth/sign-in/org-selector.tsx | 13 +++++-- .../navigation/sidebar/team-switcher.tsx | 14 ++------ apps/dashboard/lib/auth/cookies.ts | 21 +++++++++++ 4 files changed, 66 insertions(+), 18 deletions(-) diff --git a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx index 18d5a99619..04f54252a8 100644 --- a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx +++ b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx @@ -2,11 +2,11 @@ import { FadeIn } from "@/components/landing/fade-in"; import { getCookie } from "@/lib/auth/cookies"; -import { UNKEY_LAST_ORG_COOKIE } from "@/lib/auth/types"; +import { PENDING_SESSION_COOKIE, UNKEY_LAST_ORG_COOKIE } from "@/lib/auth/types"; import { ArrowRight } from "@unkey/icons"; import { Empty, Loading } from "@unkey/ui"; import Link from "next/link"; -import { useSearchParams } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { completeOrgSelection } from "../../actions"; import { ErrorBanner, WarnBanner } from "../../banners"; @@ -29,6 +29,7 @@ function SignInContent() { handleSignInViaEmail, setError, } = useSignIn(); + const router = useRouter(); const searchParams = useSearchParams(); const verifyParam = searchParams?.get("verify"); const invitationToken = searchParams?.get("invitation_token"); @@ -123,6 +124,30 @@ function SignInContent() { handleSignInViaEmail, ]); + // Check for session expiration when org selector is shown + useEffect(() => { + if (!clientReady || !hasPendingAuth) { + return; + } + + const checkSessionValidity = async () => { + const pendingSession = await getCookie(PENDING_SESSION_COOKIE); + if (!pendingSession) { + setError("Your session has expired. Please sign in again."); + // Clear the orgs query parameter to reset to sign-in form + router.push("/auth/sign-in"); + } + }; + + // Check immediately when org selector is shown + checkSessionValidity(); + + // Then check periodically (every 30 seconds) + const interval = setInterval(checkSessionValidity, 30000); + + return () => clearInterval(interval); + }, [clientReady, hasPendingAuth, router, setError]); + // Show a loading indicator when auto-selecting org if (isLoading && clientReady) { return ( @@ -132,8 +157,13 @@ function SignInContent() { ); } + const handleOrgSelectorClose = () => { + // When user closes the org selector, navigate back to clean sign-in page + router.push("/auth/sign-in"); + }; + return hasPendingAuth ? ( - + ) : (
{accountNotFound && ( diff --git a/apps/dashboard/app/auth/sign-in/org-selector.tsx b/apps/dashboard/app/auth/sign-in/org-selector.tsx index 0125cfe23b..0c17c13b0b 100644 --- a/apps/dashboard/app/auth/sign-in/org-selector.tsx +++ b/apps/dashboard/app/auth/sign-in/org-selector.tsx @@ -2,7 +2,10 @@ import type { Organization } from "@/lib/auth/types"; import { + Button, + DialogContainer, Empty, + Loading, Select, SelectContent, SelectItem, @@ -10,7 +13,6 @@ import { SelectValue, toast, } from "@unkey/ui"; -import { Button, DialogContainer, Loading } from "@unkey/ui"; import type React from "react"; import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { completeOrgSelection } from "../actions"; @@ -19,9 +21,10 @@ import { SignInContext } from "../context/signin-context"; interface OrgSelectorProps { organizations: Organization[]; lastOrgId?: string; + onClose?: () => void; } -export const OrgSelector: React.FC = ({ organizations, lastOrgId }) => { +export const OrgSelector: React.FC = ({ organizations, lastOrgId, onClose }) => { const context = useContext(SignInContext); if (!context) { throw new Error("OrgSelector must be used within SignInProvider"); @@ -110,6 +113,10 @@ export const OrgSelector: React.FC = ({ organizations, lastOrg onOpenChange={(open) => { if (!isLoading) { setIsOpen(open); + // If dialog is being closed, notify parent + if (!open && onClose) { + onClose(); + } } }} title="Select your workspace" @@ -162,7 +169,7 @@ export const OrgSelector: React.FC = ({ organizations, lastOrg - + {sortedOrgs.map((org) => ( {org.name} diff --git a/apps/dashboard/components/navigation/sidebar/team-switcher.tsx b/apps/dashboard/components/navigation/sidebar/team-switcher.tsx index 74df3d5c94..198228bb62 100644 --- a/apps/dashboard/components/navigation/sidebar/team-switcher.tsx +++ b/apps/dashboard/components/navigation/sidebar/team-switcher.tsx @@ -11,7 +11,7 @@ import { } from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useSidebar } from "@/components/ui/sidebar"; -import { setCookie, setSessionCookie } from "@/lib/auth/cookies"; +import { setLastUsedOrgCookie, setSessionCookie } from "@/lib/auth/cookies"; import { trpc } from "@/lib/trpc/client"; import { cn } from "@/lib/utils"; @@ -65,17 +65,7 @@ export const WorkspaceSwitcher: React.FC = (): JSX.Element => { // Non-critical: Store the last used organization ID in a cookie for auto-selection on next login // This runs after the critical operations, and failures won't block the workspace switch try { - await setCookie({ - name: "unkey_last_org_used", - value: orgId, - options: { - httpOnly: false, // Allow client-side access - secure: true, - sameSite: "strict", - path: "/", - maxAge: 60 * 60 * 24 * 30, // 30 Days - }, - }); + await setLastUsedOrgCookie({ orgId }); } catch (error) { // Swallow the error with a debug log - preference storage failure should not interrupt user flow console.debug("Failed to store last used workspace preference:", error); diff --git a/apps/dashboard/lib/auth/cookies.ts b/apps/dashboard/lib/auth/cookies.ts index 5b61ac12a9..5844503e41 100644 --- a/apps/dashboard/lib/auth/cookies.ts +++ b/apps/dashboard/lib/auth/cookies.ts @@ -117,6 +117,27 @@ export async function setSessionCookie(params: { }); } +/** + * Encapsulates the logic for storing the last used organization ID in a cookie + * This cookie is used for auto-selection on next login + * @param params + */ +export async function setLastUsedOrgCookie(params: { orgId: string }): Promise { + const { orgId } = params; + + await setCookie({ + name: "unkey_last_org_used", + value: orgId, + options: { + httpOnly: false, // Allow client-side access + secure: true, + sameSite: "strict", + path: "/", + maxAge: 60 * 60 * 24 * 30, // 30 Days + }, + }); +} + export async function getCookieOptionsAsString( options: Partial = {}, ): Promise { From d9147254a5ceeab03e937185901e3a755488366c Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Thu, 6 Nov 2025 15:12:46 -0500 Subject: [PATCH 09/20] overflow --- apps/dashboard/app/auth/sign-in/org-selector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dashboard/app/auth/sign-in/org-selector.tsx b/apps/dashboard/app/auth/sign-in/org-selector.tsx index 0c17c13b0b..93aad93b12 100644 --- a/apps/dashboard/app/auth/sign-in/org-selector.tsx +++ b/apps/dashboard/app/auth/sign-in/org-selector.tsx @@ -169,7 +169,7 @@ export const OrgSelector: React.FC = ({ organizations, lastOrg - + {sortedOrgs.map((org) => ( {org.name} From 205966613c264fc3e21defb2f166a856712f84d1 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Fri, 7 Nov 2025 10:38:14 -0500 Subject: [PATCH 10/20] attempt to stop flicker --- apps/dashboard/app/auth/actions.ts | 25 ++++------- .../app/auth/sign-in/[[...sign-in]]/page.tsx | 44 ++++++++++++++----- .../app/auth/sign-in/org-selector.tsx | 4 +- apps/dashboard/lib/auth/cookies.ts | 4 +- 4 files changed, 45 insertions(+), 32 deletions(-) diff --git a/apps/dashboard/app/auth/actions.ts b/apps/dashboard/app/auth/actions.ts index 3e92f3f27a..55864d0110 100644 --- a/apps/dashboard/app/auth/actions.ts +++ b/apps/dashboard/app/auth/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { deleteCookie, getCookie, setCookies, setSessionCookie } from "@/lib/auth/cookies"; +import { deleteCookie, getCookie, setCookies, setSessionCookie, setLastUsedOrgCookie } from "@/lib/auth/cookies"; import { auth } from "@/lib/auth/server"; import { AuthErrorCode, @@ -333,18 +333,16 @@ export async function completeOrgSelection( }); if (result.success) { + cookies().delete(PENDING_SESSION_COOKIE); for (const cookie of result.cookies) { cookies().set(cookie.name, cookie.value, cookie.options); - // Store the last used organization ID in a cookie for auto-selection on next login - cookies().set(UNKEY_LAST_ORG_COOKIE, orgId, { - httpOnly: false, // Allow client-side access - secure: true, - sameSite: "lax", - path: "/", - maxAge: 60 * 60 * 24 * 30, // 30 Days - }); + + } + + // Store the last used organization ID in a cookie for auto-selection on next login + setLastUsedOrgCookie({ orgId }); } return result; @@ -361,13 +359,7 @@ export async function switchOrg(orgId: string): Promise<{ success: boolean; erro await setSessionCookie({ token: newToken, expiresAt }); // Store the last used organization ID in a cookie for auto-selection on next login - cookies().set(UNKEY_LAST_ORG_COOKIE, orgId, { - httpOnly: false, // Allow client-side access - secure: true, - sameSite: "lax", - path: "/", - maxAge: 60 * 60 * 24 * 30, // 30 Days - }); + await setLastUsedOrgCookie({orgId}); return { success: true }; } catch (error) { @@ -402,6 +394,7 @@ export async function acceptInvitationAndJoin( // Set the session cookie securely on the server side await setSessionCookie({ token: newToken, expiresAt }); + await setLastUsedOrgCookie({ orgId: organizationId }); return { success: true }; } catch (error) { diff --git a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx index 04f54252a8..323f3d4641 100644 --- a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx +++ b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx @@ -40,17 +40,29 @@ function SignInContent() { const hasAttemptedSignIn = useRef(false); const hasAttemptedAutoOrgSelection = useRef(false); const [isLoading, setIsLoading] = useState(true); + const [isAutoSelecting, setIsAutoSelecting] = useState(false); // Set clientReady to true after hydration useEffect(() => { - getCookie(UNKEY_LAST_ORG_COOKIE).then((value) => { - if (value) { - setLastUsedOrgId(value); - } - setClientReady(true); - setIsLoading(false); - }); - }, []); + getCookie(UNKEY_LAST_ORG_COOKIE) + .then((value) => { + if (value) { + setLastUsedOrgId(value); + } + }) + .catch((error) => { + console.error("Failed to read last used org cookie:", error); + }) + .finally(() => { + setClientReady(true); + // Only set isLoading to false if we don't have pending auth + // (if we do, the auto-selection effect will handle loading state) + if (!hasPendingAuth) { + setIsLoading(false); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [!hasPendingAuth]); // Handle auto org selection when returning from OAuth useEffect(() => { @@ -61,24 +73,31 @@ function SignInContent() { if (lastUsedOrgId) { hasAttemptedAutoOrgSelection.current = true; setIsLoading(true); + setIsAutoSelecting(true); completeOrgSelection(lastUsedOrgId) .then((result) => { if (!result.success) { setError(result.message); setIsLoading(false); + setIsAutoSelecting(false); return; } // On success, redirect to the dashboard - window.location.href = result.redirectTo; + router.push(result.redirectTo); }) .catch((err) => { console.error("Auto org selection failed:", err); setError("Failed to automatically sign in. Please select your workspace."); setIsLoading(false); + setIsAutoSelecting(false); }); + } else { + // No lastUsedOrgId, so we need to show the org selector manually + hasAttemptedAutoOrgSelection.current = true; + setIsLoading(false); } - }, [clientReady, hasPendingAuth, setError, lastUsedOrgId]); + }, [clientReady, hasPendingAuth, setError, lastUsedOrgId, router]); // Handle auto sign-in with invitation token and email useEffect(() => { @@ -100,7 +119,7 @@ function SignInContent() { hasAttemptedSignIn.current = true; // Set loading state to true - // setIsLoading(true); + setIsLoading(true); try { // Attempt sign-in with the provided email @@ -162,7 +181,8 @@ function SignInContent() { router.push("/auth/sign-in"); }; - return hasPendingAuth ? ( + // Only show org selector if we have pending auth and we're not actively auto-selecting + return hasPendingAuth && !isAutoSelecting ? ( ) : (
diff --git a/apps/dashboard/app/auth/sign-in/org-selector.tsx b/apps/dashboard/app/auth/sign-in/org-selector.tsx index 93aad93b12..5ef5f2ad53 100644 --- a/apps/dashboard/app/auth/sign-in/org-selector.tsx +++ b/apps/dashboard/app/auth/sign-in/org-selector.tsx @@ -85,8 +85,8 @@ export const OrgSelector: React.FC = ({ organizations, lastOrg [isLoading, setError], ); - const handleSubmit = useCallback(() => { - submit(selectedOrgId); + const handleSubmit = useCallback(async () => { + await submit(selectedOrgId); }, [submit, selectedOrgId]); // Initialize org selector when client is ready diff --git a/apps/dashboard/lib/auth/cookies.ts b/apps/dashboard/lib/auth/cookies.ts index 5844503e41..8b86238bb7 100644 --- a/apps/dashboard/lib/auth/cookies.ts +++ b/apps/dashboard/lib/auth/cookies.ts @@ -3,7 +3,7 @@ import { cookies } from "next/headers"; import type { NextRequest, NextResponse } from "next/server"; import { getDefaultCookieOptions } from "./cookie-security"; -import { UNKEY_SESSION_COOKIE } from "./types"; +import { UNKEY_LAST_ORG_COOKIE, UNKEY_SESSION_COOKIE } from "./types"; export interface CookieOptions { httpOnly: boolean; @@ -126,7 +126,7 @@ export async function setLastUsedOrgCookie(params: { orgId: string }): Promise Date: Fri, 7 Nov 2025 13:21:43 -0500 Subject: [PATCH 11/20] stop flicker and show loading more consistently. --- .../app/(app)/[workspaceSlug]/page.tsx | 7 ++++- apps/dashboard/app/auth/actions.ts | 28 +++++++++++++------ .../app/auth/sign-in/[[...sign-in]]/page.tsx | 14 ++++++---- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/page.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/page.tsx index a42d0d51e3..fb8611cbf9 100644 --- a/apps/dashboard/app/(app)/[workspaceSlug]/page.tsx +++ b/apps/dashboard/app/(app)/[workspaceSlug]/page.tsx @@ -1,10 +1,15 @@ "use client"; import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation"; import { useRouter } from "next/navigation"; +import { useEffect } from "react"; export default function WorkspacePage() { const router = useRouter(); const workspace = useWorkspaceNavigation(); - router.replace(`/${workspace.slug}/apis`); + useEffect(() => { + router.replace(`/${workspace.slug}/apis`); + }, [router, workspace.slug]); + + return null; } diff --git a/apps/dashboard/app/auth/actions.ts b/apps/dashboard/app/auth/actions.ts index 55864d0110..57668553a3 100644 --- a/apps/dashboard/app/auth/actions.ts +++ b/apps/dashboard/app/auth/actions.ts @@ -1,6 +1,12 @@ "use server"; -import { deleteCookie, getCookie, setCookies, setSessionCookie, setLastUsedOrgCookie } from "@/lib/auth/cookies"; +import { + deleteCookie, + getCookie, + setCookies, + setLastUsedOrgCookie, + setSessionCookie, +} from "@/lib/auth/cookies"; import { auth } from "@/lib/auth/server"; import { AuthErrorCode, @@ -10,7 +16,6 @@ import { type OAuthResult, PENDING_SESSION_COOKIE, type SignInViaOAuthOptions, - UNKEY_LAST_ORG_COOKIE, type UserData, type VerificationResult, errorMessages, @@ -257,7 +262,7 @@ export async function resendAuthCode(email: string): Promise { } export async function signIntoWorkspace(orgId: string): Promise { - const pendingToken = cookies().get("sess-temp")?.value; + const pendingToken = cookies().get(PENDING_SESSION_COOKIE)?.value; if (!pendingToken) { return { @@ -275,7 +280,7 @@ export async function signIntoWorkspace(orgId: string): Promise { + const pendingToken = cookies().get(PENDING_SESSION_COOKIE); + return !!pendingToken?.value; +} diff --git a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx index 323f3d4641..51b3ca1b5b 100644 --- a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx +++ b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx @@ -62,7 +62,7 @@ function SignInContent() { } }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [!hasPendingAuth]); + }, [hasPendingAuth]); // Handle auto org selection when returning from OAuth useEffect(() => { @@ -167,15 +167,19 @@ function SignInContent() { return () => clearInterval(interval); }, [clientReady, hasPendingAuth, router, setError]); - // Show a loading indicator when auto-selecting org - if (isLoading && clientReady) { + if (isAutoSelecting || isLoading) { + let message = "Loading last workspace..."; + if (isLoading) { + message = "Loading..."; + } return ( - -

Signing you in...

+ +

{message}

); } + const handleOrgSelectorClose = () => { // When user closes the org selector, navigate back to clean sign-in page router.push("/auth/sign-in"); From 300ef7d8fd67b2cbf57b1238c097cc6aa1f6ffc1 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Fri, 7 Nov 2025 13:37:47 -0500 Subject: [PATCH 12/20] minor rabbit changes --- apps/dashboard/app/auth/actions.ts | 2 +- apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/dashboard/app/auth/actions.ts b/apps/dashboard/app/auth/actions.ts index 57668553a3..a0827a3246 100644 --- a/apps/dashboard/app/auth/actions.ts +++ b/apps/dashboard/app/auth/actions.ts @@ -343,7 +343,7 @@ export async function completeOrgSelection( cookies().set(cookie.name, cookie.value, cookie.options); } // Store the last used organization ID in a cookie for auto-selection on next login - setLastUsedOrgCookie({ orgId }); + await setLastUsedOrgCookie({ orgId }); } return result; diff --git a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx index 51b3ca1b5b..37c31dc749 100644 --- a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx +++ b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx @@ -168,9 +168,9 @@ function SignInContent() { }, [clientReady, hasPendingAuth, router, setError]); if (isAutoSelecting || isLoading) { - let message = "Loading last workspace..."; + let message = "Loading..."; if (isLoading) { - message = "Loading..."; + message = "Loading last workspace..."; } return ( From 82d3ea3e11f3fbe8ff60b2d6b865fe56efae8328 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Fri, 7 Nov 2025 13:45:57 -0500 Subject: [PATCH 13/20] better error handling for setting last used --- apps/dashboard/app/auth/actions.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/apps/dashboard/app/auth/actions.ts b/apps/dashboard/app/auth/actions.ts index a0827a3246..795e07f0f4 100644 --- a/apps/dashboard/app/auth/actions.ts +++ b/apps/dashboard/app/auth/actions.ts @@ -343,7 +343,14 @@ export async function completeOrgSelection( cookies().set(cookie.name, cookie.value, cookie.options); } // Store the last used organization ID in a cookie for auto-selection on next login - await setLastUsedOrgCookie({ orgId }); + try { + await setLastUsedOrgCookie({ orgId }); + } catch (error) { + console.error("Failed to set last used org cookie in completeOrgSelection:", { + orgId, + error: error instanceof Error ? error.message : "Unknown error", + }); + } } return result; @@ -360,7 +367,14 @@ export async function switchOrg(orgId: string): Promise<{ success: boolean; erro await setSessionCookie({ token: newToken, expiresAt }); // Store the last used organization ID in a cookie for auto-selection on next login - await setLastUsedOrgCookie({ orgId }); + try { + await setLastUsedOrgCookie({ orgId }); + } catch (error) { + console.error("Failed to set last used org cookie in switchOrg:", { + orgId, + error: error instanceof Error ? error.message : "Unknown error", + }); + } return { success: true }; } catch (error) { @@ -395,7 +409,14 @@ export async function acceptInvitationAndJoin( // Set the session cookie securely on the server side await setSessionCookie({ token: newToken, expiresAt }); - await setLastUsedOrgCookie({ orgId: organizationId }); + try { + await setLastUsedOrgCookie({ orgId: organizationId }); + } catch (error) { + console.error("Failed to set last used org cookie in acceptInvitationAndJoin:", { + orgId: organizationId, + error: error instanceof Error ? error.message : "Unknown error", + }); + } return { success: true }; } catch (error) { From 6f09b4aef3f232123b484f89cc2a5061fb8c170d Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Mon, 10 Nov 2025 08:50:55 -0500 Subject: [PATCH 14/20] set cookie on workspace create again --- apps/dashboard/app/new/hooks/use-workspace-step.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/dashboard/app/new/hooks/use-workspace-step.tsx b/apps/dashboard/app/new/hooks/use-workspace-step.tsx index 3aa41e51ea..b91448538c 100644 --- a/apps/dashboard/app/new/hooks/use-workspace-step.tsx +++ b/apps/dashboard/app/new/hooks/use-workspace-step.tsx @@ -1,4 +1,4 @@ -import { setSessionCookie } from "@/lib/auth/cookies"; +import { setSessionCookie, setLastUsedOrgCookie } from "@/lib/auth/cookies"; import { trpc } from "@/lib/trpc/client"; import { zodResolver } from "@hookform/resolvers/zod"; import { StackPerspective2 } from "@unkey/icons"; @@ -66,11 +66,13 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { token: sessionData.token, expiresAt: sessionData.expiresAt, }); + // invalidate the user cache and workspace cache. await utils.user.getCurrentUser.invalidate(); await utils.workspace.getCurrent.invalidate(); await utils.api.invalidate(); await utils.ratelimit.invalidate(); + await utils.billing.invalidate(); // Force a router refresh to ensure the server-side layout // re-renders with the new session context and fresh workspace data router.refresh(); @@ -84,6 +86,7 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { onSuccess: async ({ orgId }) => { setWorkspaceCreated(true); await switchOrgMutation.mutateAsync(orgId); + await setLastUsedOrgCookie({orgId}); props.advance(); }, onError: (error) => { From ac0339240d5686f60d590ec6f73f155a9e0d7f19 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Mon, 10 Nov 2025 08:51:14 -0500 Subject: [PATCH 15/20] fmt --- apps/dashboard/app/new/hooks/use-workspace-step.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/app/new/hooks/use-workspace-step.tsx b/apps/dashboard/app/new/hooks/use-workspace-step.tsx index b91448538c..9b2eab1c4b 100644 --- a/apps/dashboard/app/new/hooks/use-workspace-step.tsx +++ b/apps/dashboard/app/new/hooks/use-workspace-step.tsx @@ -1,4 +1,4 @@ -import { setSessionCookie, setLastUsedOrgCookie } from "@/lib/auth/cookies"; +import { setLastUsedOrgCookie, setSessionCookie } from "@/lib/auth/cookies"; import { trpc } from "@/lib/trpc/client"; import { zodResolver } from "@hookform/resolvers/zod"; import { StackPerspective2 } from "@unkey/icons"; @@ -86,7 +86,7 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { onSuccess: async ({ orgId }) => { setWorkspaceCreated(true); await switchOrgMutation.mutateAsync(orgId); - await setLastUsedOrgCookie({orgId}); + await setLastUsedOrgCookie({ orgId }); props.advance(); }, onError: (error) => { From 723c72100b33b9ba8719cc1a866192356e41e3b5 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Mon, 10 Nov 2025 09:08:17 -0500 Subject: [PATCH 16/20] fmt from main issues --- tools/artillery/create-keys.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/artillery/create-keys.ts b/tools/artillery/create-keys.ts index a17c90904b..9a63f60627 100644 --- a/tools/artillery/create-keys.ts +++ b/tools/artillery/create-keys.ts @@ -40,12 +40,12 @@ interface CreateKeyResult { keyId: string; } -interface CreateKeyError { - index: number; - error: string; - statusCode: number; - response?: any; -} +// interface CreateKeyError { +// index: number; +// error: string; +// statusCode: number; +// response?: any; +// } async function createKey( rootKey: string, @@ -88,7 +88,7 @@ async function createKey( console.error( `Failed to create key at index ${index}: ${response.status} ${response.statusText}`, ); - console.error(`Error response body:`, errorBody); + console.error("Error response body:", errorBody); return null; } From 95d342aee0894ac12a15f269ee73a71ff78aa516 Mon Sep 17 00:00:00 2001 From: CodeReaper <148160799+MichaelUnkey@users.noreply.github.com> Date: Mon, 10 Nov 2025 09:11:43 -0500 Subject: [PATCH 17/20] Update apps/dashboard/app/new/hooks/use-workspace-step.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/dashboard/app/new/hooks/use-workspace-step.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/dashboard/app/new/hooks/use-workspace-step.tsx b/apps/dashboard/app/new/hooks/use-workspace-step.tsx index 9b2eab1c4b..e8c1253122 100644 --- a/apps/dashboard/app/new/hooks/use-workspace-step.tsx +++ b/apps/dashboard/app/new/hooks/use-workspace-step.tsx @@ -86,7 +86,14 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { onSuccess: async ({ orgId }) => { setWorkspaceCreated(true); await switchOrgMutation.mutateAsync(orgId); - await setLastUsedOrgCookie({ orgId }); + await switchOrgMutation.mutateAsync(orgId); + try { + await setLastUsedOrgCookie({ orgId }); + } catch (error) { + console.error("Failed to persist last-used workspace:", error); + // Continue anyway - cookie is a UX enhancement, not critical + } + props.advance(); props.advance(); }, onError: (error) => { From 9fdd2cd5333c893a54cc59636c936acd090f83bd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:27:31 +0000 Subject: [PATCH 18/20] [autofix.ci] apply automated fixes --- tools/artillery/create-keys.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/artillery/create-keys.ts b/tools/artillery/create-keys.ts index ed4e9fde17..99145a5f76 100644 --- a/tools/artillery/create-keys.ts +++ b/tools/artillery/create-keys.ts @@ -171,4 +171,4 @@ async function main() { main().catch((error) => { console.error("Script failed:", error); process.exit(1); -}); \ No newline at end of file +}); From fabcf97aeacd1e4b9c49dc59dadaa0d88ef69138 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Mon, 10 Nov 2025 15:11:01 -0500 Subject: [PATCH 19/20] remove comment about ts --- apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx index 37c31dc749..ebce8f2d56 100644 --- a/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx +++ b/apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx @@ -61,7 +61,6 @@ function SignInContent() { setIsLoading(false); } }); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasPendingAuth]); // Handle auto org selection when returning from OAuth From e60fe9a11c8d9786d1de6fea00bce811745baf0d Mon Sep 17 00:00:00 2001 From: CodeReaper <148160799+MichaelUnkey@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:44:44 -0500 Subject: [PATCH 20/20] Update use-workspace-step.tsx remove doubles from merge --- apps/dashboard/app/new/hooks/use-workspace-step.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/app/new/hooks/use-workspace-step.tsx b/apps/dashboard/app/new/hooks/use-workspace-step.tsx index d3907cca7b..c543d777a9 100644 --- a/apps/dashboard/app/new/hooks/use-workspace-step.tsx +++ b/apps/dashboard/app/new/hooks/use-workspace-step.tsx @@ -85,7 +85,7 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { const createWorkspace = trpc.workspace.create.useMutation({ onSuccess: async ({ orgId }) => { setWorkspaceCreated(true); - await switchOrgMutation.mutateAsync(orgId); + await switchOrgMutation.mutateAsync(orgId); try { await setLastUsedOrgCookie({ orgId }); @@ -93,7 +93,7 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { console.error("Failed to persist last-used workspace:", error); // Continue anyway - cookie is a UX enhancement, not critical } - props.advance(); + props.advance(); }, onError: (error) => {