From 9d4911820f81c428235f99404e8e02e17b2e75a3 Mon Sep 17 00:00:00 2001 From: nams1570 Date: Wed, 28 Jan 2026 17:31:48 -0800 Subject: [PATCH 1/6] fix: issues with app enable behavior Before, an app on explore app would show enable app even when enabled. Also, the open page button behavior was buggy. We now prevent random redirects behind the modal. --- .../@modal/(.)apps/[appId]/page-client.tsx | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx index 906cb0697e..83783d5233 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx @@ -7,32 +7,59 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui"; import { ALL_APPS_FRONTEND, getAppPath } from "@/lib/apps-frontend"; import { useUpdateConfig } from "@/lib/config-update"; import { AppId } from "@stackframe/stack-shared/dist/apps/apps-config"; -import { wait } from "@stackframe/stack-shared/dist/utils/promises"; +import { useEffect, useRef, useState } from "react"; export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { const router = useRouter(); + const [isOpen, setIsOpen] = useState(true); + const [navigateTo, setNavigateTo] = useState(null); + const hasNavigatedRef = useRef(false); const adminApp = useAdminApp(); const project = adminApp.useProject(); + const config = project.useConfig(); const updateConfig = useUpdateConfig(); + const isEnabled = config.apps.installed[appId]?.enabled ?? false; + + useEffect(() => { + if (!isOpen && navigateTo && !hasNavigatedRef.current) { + hasNavigatedRef.current = true; + router.replace(navigateTo); + } + }, [isOpen, navigateTo, router]); + const handleEnable = async () => { - await wait(1000); await updateConfig({ adminApp, configUpdate: { [`apps.installed.${appId}.enabled`]: true }, pushable: true, }); + }; + + const handleOpen = () => { const path = getAppPath(project.id, ALL_APPS_FRONTEND[appId]); - router.push(path); + setNavigateTo(path); + setIsOpen(false); + }; + + const handleOpenChange = (open: boolean) => { + if (!open) { + setIsOpen(false); + if (!navigateTo) { + router.replace(`/projects/${project.id}/apps`); + } + } }; return ( - !open && router.back()} modal> + From 07ca67304101269d697cd41af65a8dee39bb9d89 Mon Sep 17 00:00:00 2001 From: nams1570 Date: Thu, 29 Jan 2026 09:52:35 -0800 Subject: [PATCH 2/6] fix: modal reopens correctly isOpen was sued to render modal. It was never reset to true. However, pathname still updated on clicking the appentry. So we use a change in pathname to update isOpen. We check to make sure that pathname change is to an app entry, else modal would reopen randomly on going to the base exporeapps page too. --- .../@modal/(.)apps/[appId]/page-client.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx index 83783d5233..626b0c91bb 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx @@ -7,10 +7,12 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui"; import { ALL_APPS_FRONTEND, getAppPath } from "@/lib/apps-frontend"; import { useUpdateConfig } from "@/lib/config-update"; import { AppId } from "@stackframe/stack-shared/dist/apps/apps-config"; +import { usePathname } from "next/navigation"; import { useEffect, useRef, useState } from "react"; export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { const router = useRouter(); + const pathname = usePathname(); const [isOpen, setIsOpen] = useState(true); const [navigateTo, setNavigateTo] = useState(null); const hasNavigatedRef = useRef(false); @@ -22,6 +24,15 @@ export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { const isEnabled = config.apps.installed[appId]?.enabled ?? false; + useEffect(() => { + const isModalRoute = /\/apps\/[^/]+$/.test(pathname); + if (isModalRoute) { + setIsOpen(true); + setNavigateTo(null); + hasNavigatedRef.current = true; + } + }, [pathname]); + useEffect(() => { if (!isOpen && navigateTo && !hasNavigatedRef.current) { hasNavigatedRef.current = true; @@ -39,6 +50,7 @@ export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { const handleOpen = () => { const path = getAppPath(project.id, ALL_APPS_FRONTEND[appId]); + hasNavigatedRef.current = false; setNavigateTo(path); setIsOpen(false); }; From d6171013e596fbcf5bbe2e920929767967a06554 Mon Sep 17 00:00:00 2001 From: nams1570 Date: Thu, 29 Jan 2026 11:07:13 -0800 Subject: [PATCH 3/6] chore: add minor comments explaining ref --- .../[projectId]/@modal/(.)apps/[appId]/page-client.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx index 626b0c91bb..b18a7d07b3 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx @@ -15,6 +15,7 @@ export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { const pathname = usePathname(); const [isOpen, setIsOpen] = useState(true); const [navigateTo, setNavigateTo] = useState(null); + // Tracks whether we've already navigated to prevent duplicate navigations const hasNavigatedRef = useRef(false); const adminApp = useAdminApp(); @@ -29,12 +30,14 @@ export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { if (isModalRoute) { setIsOpen(true); setNavigateTo(null); + // Block any stale navigation from previous session's navigateTo value hasNavigatedRef.current = true; } }, [pathname]); useEffect(() => { if (!isOpen && navigateTo && !hasNavigatedRef.current) { + // Mark as navigated to prevent duplicate navigation on re-renders hasNavigatedRef.current = true; router.replace(navigateTo); } @@ -50,6 +53,7 @@ export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { const handleOpen = () => { const path = getAppPath(project.id, ALL_APPS_FRONTEND[appId]); + // Allow navigation by resetting the flag (was set to true by pathname effect) hasNavigatedRef.current = false; setNavigateTo(path); setIsOpen(false); From cd182b5e335be2fed2ef3233ff7c4ae7b3b6ba80 Mon Sep 17 00:00:00 2001 From: nams1570 Date: Thu, 29 Jan 2026 11:39:36 -0800 Subject: [PATCH 4/6] chore: make regex more specific --- .../projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx index b18a7d07b3..4aaca49d68 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx @@ -26,7 +26,7 @@ export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { const isEnabled = config.apps.installed[appId]?.enabled ?? false; useEffect(() => { - const isModalRoute = /\/apps\/[^/]+$/.test(pathname); + const isModalRoute = /^\/projects\/[^/]+\/apps\/[^/]+$/.test(pathname); if (isModalRoute) { setIsOpen(true); setNavigateTo(null); From 6f005a58bd531037d3da2b703af0ab1d330073f8 Mon Sep 17 00:00:00 2001 From: nams1570 Date: Mon, 2 Feb 2026 09:23:07 -0800 Subject: [PATCH 5/6] feat: add disable button to apps --- .../[projectId]/@modal/(.)apps/[appId]/page-client.tsx | 9 +++++++++ apps/dashboard/src/components/app-store-entry.tsx | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx index 4aaca49d68..8deb3d38a1 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx @@ -51,6 +51,14 @@ export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { }); }; + const handleDisable = async () => { + await updateConfig({ + adminApp, + configUpdate: { [`apps.installed.${appId}.enabled`]: false }, + pushable: true, + }); + }; + const handleOpen = () => { const path = getAppPath(project.id, ALL_APPS_FRONTEND[appId]); // Allow navigation by resetting the flag (was set to true by pathname effect) @@ -75,6 +83,7 @@ export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { appId={appId} isEnabled={isEnabled} onEnable={handleEnable} + onDisable={handleDisable} onOpen={handleOpen} titleComponent={DialogTitle} /> diff --git a/apps/dashboard/src/components/app-store-entry.tsx b/apps/dashboard/src/components/app-store-entry.tsx index 66116146d9..e6178c3297 100644 --- a/apps/dashboard/src/components/app-store-entry.tsx +++ b/apps/dashboard/src/components/app-store-entry.tsx @@ -156,8 +156,9 @@ export function AppStoreEntry({ {onDisable && ( From 731db26511ed4b4fc2eafb7dc8c6cf7ab06f85bb Mon Sep 17 00:00:00 2001 From: nams1570 Date: Mon, 2 Feb 2026 09:43:06 -0800 Subject: [PATCH 6/6] refactor: simplify modal handling This deals with issues in our old approach. If a confirmation dialog pops up, and the user declines to navigate, the modal will not be closed. --- .../@modal/(.)apps/[appId]/page-client.tsx | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx index 8deb3d38a1..0061981e00 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/@modal/(.)apps/[appId]/page-client.tsx @@ -8,15 +8,12 @@ import { ALL_APPS_FRONTEND, getAppPath } from "@/lib/apps-frontend"; import { useUpdateConfig } from "@/lib/config-update"; import { AppId } from "@stackframe/stack-shared/dist/apps/apps-config"; import { usePathname } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { const router = useRouter(); const pathname = usePathname(); const [isOpen, setIsOpen] = useState(true); - const [navigateTo, setNavigateTo] = useState(null); - // Tracks whether we've already navigated to prevent duplicate navigations - const hasNavigatedRef = useRef(false); const adminApp = useAdminApp(); const project = adminApp.useProject(); @@ -25,24 +22,14 @@ export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { const isEnabled = config.apps.installed[appId]?.enabled ?? false; + // Control modal visibility based on whether we're on a modal route. + // This ensures the modal only closes when navigation actually succeeds, + // preventing issues if router.replace is vetoed by a confirmation dialog. useEffect(() => { const isModalRoute = /^\/projects\/[^/]+\/apps\/[^/]+$/.test(pathname); - if (isModalRoute) { - setIsOpen(true); - setNavigateTo(null); - // Block any stale navigation from previous session's navigateTo value - hasNavigatedRef.current = true; - } + setIsOpen(isModalRoute); }, [pathname]); - useEffect(() => { - if (!isOpen && navigateTo && !hasNavigatedRef.current) { - // Mark as navigated to prevent duplicate navigation on re-renders - hasNavigatedRef.current = true; - router.replace(navigateTo); - } - }, [isOpen, navigateTo, router]); - const handleEnable = async () => { await updateConfig({ adminApp, @@ -61,18 +48,14 @@ export default function AppDetailsModalPageClient({ appId }: { appId: AppId }) { const handleOpen = () => { const path = getAppPath(project.id, ALL_APPS_FRONTEND[appId]); - // Allow navigation by resetting the flag (was set to true by pathname effect) - hasNavigatedRef.current = false; - setNavigateTo(path); - setIsOpen(false); + // Navigate to the app page. Modal stays open until pathname changes. + router.replace(path); }; const handleOpenChange = (open: boolean) => { if (!open) { - setIsOpen(false); - if (!navigateTo) { - router.replace(`/projects/${project.id}/apps`); - } + // Navigate back to apps list. Modal stays open until pathname changes. + router.replace(`/projects/${project.id}/apps`); } };