From fd0f2dd459b8fd2febe0404477e7445901e9aecb Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 18 Jun 2026 17:42:18 +0800 Subject: [PATCH] Scope last visited organization local storage to profile (#47071) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context If a user switches account without an explicit log out via the dashboard, landing back on `/org` will redirect users to the last visited organization as stored in local storage, in which it can result in the following state if the last visited organization does not exist in the current account image ## Changes involved Am opting to scope the last visited organization to the user profile instead - this would be a bit more cleaner than trying to actively clear the last visited org slug from local storage with implicit account changes as there's no deterministic way to track that (afaik) from FE side of things ## To test Can reproduce the problem as such - Ensure that you have 2 accounts to log in with, and one account has an org that the other is not a part of - For the organization that has the "extra" org, ensure that you click into it so that the last visited org slug is saved in local storage - Mimic changing accounts by visiting `/auth/v1/authorize?provider=github` (using the domain for the env that you're testing on - e.g localhost:8000 for local, or green for staging preview) ## Summary by CodeRabbit * **New Features** * Unified “last visited organization” handling across the Studio UI with a shared hook, improving consistency for home/dashboard return, cancel/back navigation, and account routing. * **Bug Fixes** * Updated redirects to only route to an organization when a valid last-visited value is available; otherwise users go to the general organizations page. * Kept MFA enrollment and factor delete/leave flows aligned to the unified last-visited organization value. * **Tests** * Updated onboarding and layout tests to match the new last-visited organization storage key format and hook/query success behavior. --- .../Account/TOTPFactors/AddNewFactorModal.tsx | 8 ++----- .../Account/TOTPFactors/DeleteFactorModal.tsx | 8 ++----- .../interfaces/App/RouteValidationWrapper.tsx | 9 +++----- .../Connect/OrganizationSelector.tsx | 8 ++----- .../DeleteOrganizationButton.tsx | 8 ++----- .../Organization/NewOrg/NewOrgForm.tsx | 9 ++------ .../TeamSettings/LeaveTeamButton.tsx | 9 +++----- .../ProjectCreation/ProjectCreationFooter.tsx | 10 +++----- .../DeleteProjectPanel/DeleteProjectModal.tsx | 8 ++----- .../AccountLayout.selfhosted.test.tsx | 2 +- .../layouts/AccountLayout/AccountLayout.tsx | 9 ++------ .../components/layouts/DefaultLayout.tsx | 9 +++----- .../Navigation/LayoutHeader/HomeIcon.tsx | 8 ++----- .../hooks/misc/useLastVisitedOrganization.ts | 23 +++++++++++++++++++ apps/studio/pages/500.tsx | 8 ++----- apps/studio/pages/new/[slug].tsx | 9 +++----- apps/studio/pages/org/index.tsx | 15 ++++++------ .../pages/project/_/[[...routeSlug]].tsx | 17 ++++++-------- .../pages/aws-marketplace-onboarding.test.tsx | 2 +- packages/common/constants/local-storage.ts | 2 +- 20 files changed, 73 insertions(+), 108 deletions(-) create mode 100644 apps/studio/hooks/misc/useLastVisitedOrganization.ts diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx index b7ab33bf87e..63c89019637 100644 --- a/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx +++ b/apps/studio/components/interfaces/Account/TOTPFactors/AddNewFactorModal.tsx @@ -1,6 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useQueryClient } from '@tanstack/react-query' -import { LOCAL_STORAGE_KEYS } from 'common' import { useEffect, useState } from 'react' import { useForm, type SubmitHandler } from 'react-hook-form' import { toast } from 'sonner' @@ -16,7 +15,7 @@ import { organizationKeys } from '@/data/organizations/keys' import { useMfaChallengeAndVerifyMutation } from '@/data/profile/mfa-challenge-and-verify-mutation' import { useMfaEnrollMutation } from '@/data/profile/mfa-enroll-mutation' import { useMfaUnenrollMutation } from '@/data/profile/mfa-unenroll-mutation' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' type TOTP = { qr_code: string; secret: string; uri: string } @@ -140,10 +139,7 @@ const SecondStep = ({ onClose, }: SecondStepProps) => { const queryClient = useQueryClient() - const [lastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) + const { lastVisitedOrganization } = useLastVisitedOrganization() const FormSchema = z.object({ code: z.string().min(1, 'Please provide a code from your authenticator app'), diff --git a/apps/studio/components/interfaces/Account/TOTPFactors/DeleteFactorModal.tsx b/apps/studio/components/interfaces/Account/TOTPFactors/DeleteFactorModal.tsx index 41f1667081c..06f887d3af9 100644 --- a/apps/studio/components/interfaces/Account/TOTPFactors/DeleteFactorModal.tsx +++ b/apps/studio/components/interfaces/Account/TOTPFactors/DeleteFactorModal.tsx @@ -1,11 +1,10 @@ import { useQueryClient } from '@tanstack/react-query' -import { LOCAL_STORAGE_KEYS } from 'common' import { toast } from 'sonner' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { organizationKeys } from '@/data/organizations/keys' import { useMfaUnenrollMutation } from '@/data/profile/mfa-unenroll-mutation' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' interface DeleteFactorModalProps { visible: boolean @@ -21,10 +20,7 @@ const DeleteFactorModal = ({ onClose, }: DeleteFactorModalProps) => { const queryClient = useQueryClient() - const [lastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) + const { lastVisitedOrganization } = useLastVisitedOrganization() const { mutate: unenroll, isPending } = useMfaUnenrollMutation({ onSuccess: async () => { diff --git a/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx b/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx index e69aa8c9392..bb416823fcd 100644 --- a/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx +++ b/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx @@ -1,4 +1,4 @@ -import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsMFAEnabled, useParams } from 'common' +import { useIsLoggedIn, useIsMFAEnabled, useParams } from 'common' import { useRouter } from 'next/router' import { PropsWithChildren, useEffect } from 'react' import { toast } from 'sonner' @@ -6,8 +6,8 @@ import { toast } from 'sonner' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import { useProjectDetailQuery } from '@/data/projects/project-detail-query' import { useDashboardHistory } from '@/hooks/misc/useDashboardHistory' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import useLatest from '@/hooks/misc/useLatest' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from '@/lib/constants' @@ -21,10 +21,7 @@ export const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => { const isUserMFAEnabled = useIsMFAEnabled() const { setLastVisitedSnippet, setLastVisitedTable } = useDashboardHistory() - const [lastVisitedOrganization, setLastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) + const { lastVisitedOrganization, setLastVisitedOrganization } = useLastVisitedOrganization() const DEFAULT_HOME = IS_PLATFORM ? !!lastVisitedOrganization diff --git a/apps/studio/components/interfaces/Connect/OrganizationSelector.tsx b/apps/studio/components/interfaces/Connect/OrganizationSelector.tsx index 8e989dfaa92..feac48d13a2 100644 --- a/apps/studio/components/interfaces/Connect/OrganizationSelector.tsx +++ b/apps/studio/components/interfaces/Connect/OrganizationSelector.tsx @@ -1,4 +1,3 @@ -import { LOCAL_STORAGE_KEYS } from 'common' import { Check, ChevronDown } from 'lucide-react' import { useMemo, useState, type ReactNode } from 'react' import { cn, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui' @@ -7,7 +6,7 @@ import { CreateOrganizationCard, OrganizationCard, } from '@/components/interfaces/Organization/OrganizationCard' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import type { Organization } from '@/types' const VISIBLE_ORGANIZATIONS_LIMIT = 3 @@ -46,10 +45,7 @@ export const OrganizationSelector = ({ unavailableReason?: ReactNode }) => { const [showMore, setShowMore] = useState(false) - const [lastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) + const { lastVisitedOrganization } = useLastVisitedOrganization() const { visibleOrganizations, overflowOrganizations } = useMemo(() => { const lastVisitedOrg = organizations.find(({ slug }) => slug === lastVisitedOrganization) diff --git a/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx b/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx index 359d0868497..74baf2ba06e 100644 --- a/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx +++ b/apps/studio/components/interfaces/Organization/GeneralSettings/DeleteOrganizationButton.tsx @@ -1,5 +1,4 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { LOCAL_STORAGE_KEYS } from 'common' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { toast } from 'sonner' @@ -11,7 +10,7 @@ import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper' import { useOrganizationDeleteMutation } from '@/data/organizations/organization-delete-mutation' import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' const MAX_PROJECT_ACKNOWLEDGEMENTS = 10 @@ -90,10 +89,7 @@ export const DeleteOrganizationButton = () => { const allChecked = isDeletionConfirmed() - const [_, setLastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) + const { setLastVisitedOrganization } = useLastVisitedOrganization() const { can: canDeleteOrganization } = useAsyncCheckPermissions( PermissionAction.UPDATE, diff --git a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx index 7197b5679d0..a7c147df6d1 100644 --- a/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx +++ b/apps/studio/components/interfaces/Organization/NewOrg/NewOrgForm.tsx @@ -3,7 +3,6 @@ import { Elements } from '@stripe/react-stripe-js' import type { PaymentIntentResult, PaymentMethod, StripeElementsOptions } from '@stripe/stripe-js' import { loadStripe } from '@stripe/stripe-js' import { useDebounce } from '@uidotdev/usehooks' -import { LOCAL_STORAGE_KEYS } from 'common' import { groupBy } from 'lodash' import { HelpCircle } from 'lucide-react' import { useTheme } from 'next-themes' @@ -55,7 +54,7 @@ import { useProjectsInfiniteQuery } from '@/data/projects/projects-infinite-quer import { SetupIntentResponse } from '@/data/stripe/setup-intent-mutation' import { useConfirmPendingSubscriptionCreateMutation } from '@/data/subscriptions/org-subscription-confirm-pending-create' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import { PRICING_TIER_LABELS_ORG, STRIPE_PUBLIC_KEY } from '@/lib/constants' import { validateReturnTo } from '@/lib/gotrue' import { useProfile } from '@/lib/profile' @@ -94,6 +93,7 @@ export const NewOrgForm = ({ const router = useRouter() const user = useProfile() const { resolvedTheme } = useTheme() + const { lastVisitedOrganization } = useLastVisitedOrganization() const isBillingEnabled = useIsFeatureEnabled('billing:all') @@ -101,11 +101,6 @@ export const NewOrgForm = ({ const { data } = useProjectsInfiniteQuery({}) const projects = useMemo(() => data?.pages.flatMap((page) => page.projects) ?? [], [data?.pages]) - const [lastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) - const freeOrgs = (organizations || []).filter((it) => it.plan.id === 'free') // [Joshen] JFYI because we're now using a paginated endpoint, there's a chance that not all projects will be diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/LeaveTeamButton.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/LeaveTeamButton.tsx index 556958d6ce0..336b89a4ddc 100644 --- a/apps/studio/components/interfaces/Organization/TeamSettings/LeaveTeamButton.tsx +++ b/apps/studio/components/interfaces/Organization/TeamSettings/LeaveTeamButton.tsx @@ -1,4 +1,4 @@ -import { LOCAL_STORAGE_KEYS, useParams } from 'common' +import { useParams } from 'common' import { useRouter } from 'next/router' import { useState } from 'react' import { toast } from 'sonner' @@ -11,7 +11,7 @@ import { useOrganizationMemberDeleteMutation } from '@/data/organizations/organi import { useOrganizationMembersQuery } from '@/data/organizations/organization-members-query' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useProfile } from '@/lib/profile' @@ -20,6 +20,7 @@ export const LeaveTeamButton = () => { const { slug } = useParams() const { profile } = useProfile() const { data: selectedOrganization } = useSelectedOrganizationQuery() + const { setLastVisitedOrganization } = useLastVisitedOrganization() // if organizationMembersDeletionEnabled is false, you also can't delete yourself const { organizationMembersDelete: organizationMembersDeletionEnabled } = useIsFeatureEnabled([ @@ -28,10 +29,6 @@ export const LeaveTeamButton = () => { const [isLeaving, setIsLeaving] = useState(false) const [isLeaveTeamModalOpen, setIsLeaveTeamModalOpen] = useState(false) - const [_, setLastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) const { refetch: refetchOrganizations } = useOrganizationsQuery() const { data: members } = useOrganizationMembersQuery({ slug }) diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreationFooter.tsx b/apps/studio/components/interfaces/ProjectCreation/ProjectCreationFooter.tsx index ae6d34c9173..8d237ed1525 100644 --- a/apps/studio/components/interfaces/ProjectCreation/ProjectCreationFooter.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreationFooter.tsx @@ -1,4 +1,4 @@ -import { LOCAL_STORAGE_KEYS, useFlag } from 'common' +import { useFlag } from 'common' import { useRouter } from 'next/router' import { UseFormReturn } from 'react-hook-form' import { @@ -18,7 +18,7 @@ import { CreateProjectForm } from './ProjectCreation.schema' import { instanceLabel, monthlyInstancePrice } from './ProjectCreation.utils' import { InlineLink } from '@/components/ui/InlineLink' import { OrgProject } from '@/data/projects/org-projects-infinite-query' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { DOCS_URL } from '@/lib/constants' @@ -42,14 +42,10 @@ export const ProjectCreationFooter = ({ const router = useRouter() const { data: currentOrg } = useSelectedOrganizationQuery() const isFreePlan = currentOrg?.plan?.id === 'free' + const { lastVisitedOrganization } = useLastVisitedOrganization() const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate') - const [lastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) - const availableComputeCredits = organizationProjects.length === 0 ? 10 : 0 const additionalMonthlySpend = isFreePlan ? 0 diff --git a/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal.tsx b/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal.tsx index 1f6c291d868..c3af8e4f169 100644 --- a/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal.tsx +++ b/apps/studio/components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal.tsx @@ -1,4 +1,3 @@ -import { LOCAL_STORAGE_KEYS } from 'common' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { toast } from 'sonner' @@ -12,7 +11,7 @@ import { useSendDowngradeFeedbackMutation } from '@/data/feedback/exit-survey-se import type { OrgProject } from '@/data/projects/org-projects-infinite-query' import { useProjectDeleteMutation } from '@/data/projects/project-delete-mutation' import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import type { Organization } from '@/types' @@ -36,10 +35,7 @@ export const DeleteProjectModal = ({ const project = projectProp || projectFromQuery const organization = organizationProp || organizationFromQuery - const [lastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) + const { lastVisitedOrganization } = useLastVisitedOrganization() const projectRef = project?.ref const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) diff --git a/apps/studio/components/layouts/AccountLayout/AccountLayout.selfhosted.test.tsx b/apps/studio/components/layouts/AccountLayout/AccountLayout.selfhosted.test.tsx index 34b25e4a231..0c581abb557 100644 --- a/apps/studio/components/layouts/AccountLayout/AccountLayout.selfhosted.test.tsx +++ b/apps/studio/components/layouts/AccountLayout/AccountLayout.selfhosted.test.tsx @@ -55,7 +55,7 @@ vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({ })) vi.mock('@/hooks/misc/useLocalStorage', () => ({ - useLocalStorageQuery: () => [''], + useLocalStorageQuery: () => ['', vi.fn(), { isLoading: false, isSuccess: true }], })) vi.mock('@/hooks/misc/withAuth', () => ({ diff --git a/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx b/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx index 2774fbbff19..6a7b39be1cd 100644 --- a/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx +++ b/apps/studio/components/layouts/AccountLayout/AccountLayout.tsx @@ -1,4 +1,3 @@ -import { LOCAL_STORAGE_KEYS } from 'common' import Head from 'next/head' import { useRouter } from 'next/router' import type { PropsWithChildren } from 'react' @@ -10,7 +9,7 @@ import { AccountMenuContent } from './AccountMenuContent' import { WithSidebar } from './WithSidebar' import { useCustomContent } from '@/hooks/custom-content/useCustomContent' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import { withAuth } from '@/hooks/misc/withAuth' import { IS_PLATFORM } from '@/lib/constants' import { buildStudioPageTitle } from '@/lib/page-title' @@ -23,6 +22,7 @@ export interface AccountLayoutProps { const AccountLayout = ({ children, title }: PropsWithChildren) => { const router = useRouter() const appSnap = useAppStateSnapshot() + const { lastVisitedOrganization } = useLastVisitedOrganization() const { setContent: setMobileSheetContent, registerOpenMenu } = useMobileSheet() const currentPath = router.pathname @@ -32,11 +32,6 @@ const AccountLayout = ({ children, title }: PropsWithChildren 0 ? appSnap.lastRouteBeforeVisitingAccountPage diff --git a/apps/studio/components/layouts/DefaultLayout.tsx b/apps/studio/components/layouts/DefaultLayout.tsx index 55853c36e06..5670934aafc 100644 --- a/apps/studio/components/layouts/DefaultLayout.tsx +++ b/apps/studio/components/layouts/DefaultLayout.tsx @@ -1,4 +1,4 @@ -import { LOCAL_STORAGE_KEYS, useBreakpoint, useParams } from 'common' +import { useBreakpoint, useParams } from 'common' import { useRouter } from 'next/router' import { PropsWithChildren, useEffect, useState } from 'react' import { ResizablePanel, ResizablePanelGroup, SidebarProvider } from 'ui' @@ -13,7 +13,7 @@ import { LayoutSidebarProvider } from './ProjectLayout/LayoutSidebar/LayoutSideb import { ProjectContextProvider } from './ProjectLayout/ProjectContext' import { AppBannerWrapper } from '@/components/interfaces/App/AppBannerWrapper' import { Sidebar } from '@/components/interfaces/Sidebar' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import { useCheckLatestDeploy } from '@/hooks/use-check-latest-deploy' import { IS_PLATFORM } from '@/lib/constants' import { useAppStateSnapshot } from '@/state/app-state' @@ -42,10 +42,7 @@ export const DefaultLayout = ({ const router = useRouter() const appSnap = useAppStateSnapshot() - const [lastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) + const { lastVisitedOrganization } = useLastVisitedOrganization() const backToDashboardURL = router.pathname.startsWith('/account') ? appSnap.lastRouteBeforeVisitingAccountPage.length > 0 diff --git a/apps/studio/components/layouts/Navigation/LayoutHeader/HomeIcon.tsx b/apps/studio/components/layouts/Navigation/LayoutHeader/HomeIcon.tsx index 2203a98c43d..0d3b227d3d6 100644 --- a/apps/studio/components/layouts/Navigation/LayoutHeader/HomeIcon.tsx +++ b/apps/studio/components/layouts/Navigation/LayoutHeader/HomeIcon.tsx @@ -1,11 +1,10 @@ -import { LOCAL_STORAGE_KEYS } from 'common' import Link from 'next/link' import { useRouter } from 'next/router' import { cn } from 'ui' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { IS_PLATFORM } from '@/lib/constants' import { useTrack } from '@/lib/telemetry/track' @@ -18,10 +17,7 @@ export const HomeIcon = ({ className }: { className?: string }) => { const largeLogo = useIsFeatureEnabled('branding:large_logo') const router = useRouter() - const [lastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) + const { lastVisitedOrganization } = useLastVisitedOrganization() const getDefaultOrgRedirect = () => { if (lastVisitedOrganization) return `/org/${lastVisitedOrganization}` diff --git a/apps/studio/hooks/misc/useLastVisitedOrganization.ts b/apps/studio/hooks/misc/useLastVisitedOrganization.ts new file mode 100644 index 00000000000..9459a6cea78 --- /dev/null +++ b/apps/studio/hooks/misc/useLastVisitedOrganization.ts @@ -0,0 +1,23 @@ +import { LOCAL_STORAGE_KEYS } from 'common' + +import { useLocalStorageQuery } from './useLocalStorage' +import { useProfile } from '@/lib/profile' + +export const useLastVisitedOrganization = () => { + const { profile, isLoading: isLoadingProfile, isSuccess: isSuccessProfile } = useProfile() + const [ + lastVisitedOrganization, + setLastVisitedOrganization, + { isLoading: isLoadingLocalStorage, isSuccess: isSuccessLocalStorage }, + ] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION(profile?.id), '') + + const isSuccess = isSuccessProfile && isSuccessLocalStorage + const isPending = isLoadingProfile || isLoadingLocalStorage + + return { + isSuccess, + isPending, + lastVisitedOrganization: isSuccess ? lastVisitedOrganization : undefined, + setLastVisitedOrganization, + } +} diff --git a/apps/studio/pages/500.tsx b/apps/studio/pages/500.tsx index 85f3e13ece6..a3e42efd702 100644 --- a/apps/studio/pages/500.tsx +++ b/apps/studio/pages/500.tsx @@ -1,4 +1,3 @@ -import { LOCAL_STORAGE_KEYS } from 'common' import { NextPage } from 'next' import { useTheme } from 'next-themes' import Image from 'next/legacy/image' @@ -7,7 +6,7 @@ import { useRouter } from 'next/router' import { Button } from 'ui' import { SupportLink } from '@/components/interfaces/Support/SupportLink' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import { useSignOut } from '@/lib/auth' const Error500: NextPage = () => { @@ -15,10 +14,7 @@ const Error500: NextPage = () => { const signOut = useSignOut() const { resolvedTheme } = useTheme() - const [lastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) + const { lastVisitedOrganization } = useLastVisitedOrganization() const onClickLogout = async () => { await signOut() diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index b700ec42b45..57ef2d799b3 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -1,6 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common' +import { useFlag, useParams } from 'common' import Head from 'next/head' import Link from 'next/link' import { useRouter } from 'next/router' @@ -64,7 +64,7 @@ import { useDataApiRevokeOnCreateDefaultEnabled, } from '@/hooks/misc/useDataApiRevokeOnCreateDefault' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { withAuth } from '@/hooks/misc/withAuth' import { usePHFlag } from '@/hooks/ui/useFlag' @@ -92,10 +92,7 @@ const Wizard: NextPageWithLayout = () => { const isFreePlan = currentOrg?.plan?.id === 'free' const canChooseInstanceSize = !isFreePlan - const [lastVisitedOrganization] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) + const { lastVisitedOrganization } = useLastVisitedOrganization() const { can: isAdmin } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects') const { can: canCreateGitHubConnection } = useAsyncCheckPermissions( PermissionAction.CREATE, diff --git a/apps/studio/pages/org/index.tsx b/apps/studio/pages/org/index.tsx index a9755ce8b28..dbe8af93bc0 100644 --- a/apps/studio/pages/org/index.tsx +++ b/apps/studio/pages/org/index.tsx @@ -1,4 +1,3 @@ -import { LOCAL_STORAGE_KEYS } from 'common' import { useRouter } from 'next/router' import { useEffect } from 'react' @@ -6,20 +5,20 @@ import { ShimmeringCard } from '@/components/interfaces/Home/ProjectList/Shimmer import { DefaultLayout } from '@/components/layouts/DefaultLayout' import OrganizationLayout from '@/components/layouts/OrganizationLayout' import { ScaffoldContainerLegacy } from '@/components/layouts/Scaffold' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import type { NextPageWithLayout } from '@/types' const OrgIndexPage: NextPageWithLayout = () => { const router = useRouter() - const [lastVisitedOrganization, _, { isSuccess }] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) + const { lastVisitedOrganization, isSuccess } = useLastVisitedOrganization() useEffect(() => { if (isSuccess) { - if (lastVisitedOrganization.length > 0) router.push(`/org/${lastVisitedOrganization}`) - else router.push('/organizations') + if (lastVisitedOrganization) { + router.push(`/org/${lastVisitedOrganization}`) + } else { + router.push('/organizations') + } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSuccess]) diff --git a/apps/studio/pages/project/_/[[...routeSlug]].tsx b/apps/studio/pages/project/_/[[...routeSlug]].tsx index 7f59242f587..61b83ea75e0 100644 --- a/apps/studio/pages/project/_/[[...routeSlug]].tsx +++ b/apps/studio/pages/project/_/[[...routeSlug]].tsx @@ -1,4 +1,4 @@ -import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useParams } from 'common' +import { IS_PLATFORM, useParams } from 'common' import { AlertTriangleIcon } from 'lucide-react' import { NextPage } from 'next' import { useRouter } from 'next/router' @@ -25,7 +25,7 @@ import { HomePageActions } from '@/components/interfaces/HomePageActions' import { PageLayout } from '@/components/layouts/PageLayout/PageLayout' import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' -import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' +import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization' import { withAuth } from '@/hooks/misc/withAuth' // [Joshen] I'd say we don't do route validation here, this page will act more @@ -37,10 +37,7 @@ const GenericProjectPage: NextPage = () => { const { slug } = useParams() const { routeSlug, ...queryParams } = router.query - const [lastVisitedOrgSlug] = useLocalStorageQuery( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, - '' - ) + const { lastVisitedOrganization } = useLastVisitedOrganization() const { data: organizations = [], @@ -52,7 +49,7 @@ const GenericProjectPage: NextPage = () => { }) const [selectedSlug, setSlug] = useState( - slug || lastVisitedOrgSlug || organizations[0]?.slug + slug || lastVisitedOrganization || organizations[0]?.slug ) const selectedOrganization = organizations.find((x) => x.slug === selectedSlug) @@ -74,13 +71,13 @@ const GenericProjectPage: NextPage = () => { } useEffect(() => { - if (!!lastVisitedOrgSlug) { - setSlug(lastVisitedOrgSlug) + if (!!lastVisitedOrganization) { + setSlug(lastVisitedOrganization) } else if (isSuccessOrganizations) { setSlug(organizations[0]?.slug) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [lastVisitedOrgSlug, isSuccessOrganizations]) + }, [lastVisitedOrganization, isSuccessOrganizations]) return (
diff --git a/apps/studio/tests/pages/aws-marketplace-onboarding.test.tsx b/apps/studio/tests/pages/aws-marketplace-onboarding.test.tsx index 3c5537feb30..1422fe9d8b9 100644 --- a/apps/studio/tests/pages/aws-marketplace-onboarding.test.tsx +++ b/apps/studio/tests/pages/aws-marketplace-onboarding.test.tsx @@ -273,7 +273,7 @@ describe('AwsMarketplaceOnboardingScreen', () => { ] window.localStorage.setItem( - LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION, + LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION(DEFAULT_PROFILE_CONTEXT.profile?.id), JSON.stringify('zeta-team') ) mockAwsEndpoints({ diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index 2c370016f34..40ac85123ab 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -87,7 +87,7 @@ export const LOCAL_STORAGE_KEYS = { // Shortcut preferences SHORTCUT_STORAGE_KEY: 'supabase-shortcut-preferences', - LAST_VISITED_ORGANIZATION: 'last-visited-organization', + LAST_VISITED_ORGANIZATION: (uid?: number) => `last-visited-organization-${uid}`, // user impersonation selector previous searches USER_IMPERSONATION_SELECTOR_PREVIOUS_SEARCHES: (ref: string) =>