From b491e832a47a8855f4c7e7ee0d1a7560756ab0e3 Mon Sep 17 00:00:00 2001 From: Developing-Gamer Date: Sat, 4 Apr 2026 21:45:40 -0400 Subject: [PATCH] Add Neon project transfer confirmation page and refactor transfer confirmation components - Introduced a new `neon-transfer-confirm-page.tsx` for handling project transfer confirmations specific to Neon, maintaining legacy UI and behavior. - Refactored existing `transfer-confirm-page.tsx` to support a new `TransferConfirmMissingCodeView` for handling cases where the transfer code is missing. - Updated integration pages for both Neon and custom transfers to utilize the new components, enhancing code organization and reusability. - Added a new `project-transfer-confirm-view.tsx` component to standardize the UI for project transfer confirmations across different integrations. These changes improve the user experience during project transfers and streamline the integration process for different services. --- .../custom/projects/transfer/confirm/page.tsx | 8 +- .../neon-transfer-confirm-page.tsx | 151 ++++++++++++++++ .../neon/projects/transfer/confirm/page.tsx | 4 +- .../integrations/transfer-confirm-page.tsx | 171 ++++++------------ .../project-transfer-confirm-view.tsx | 132 ++++++++++++++ 5 files changed, 348 insertions(+), 118 deletions(-) create mode 100644 apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx create mode 100644 apps/dashboard/src/components/project-transfer-confirm-view.tsx diff --git a/apps/dashboard/src/app/(main)/integrations/custom/projects/transfer/confirm/page.tsx b/apps/dashboard/src/app/(main)/integrations/custom/projects/transfer/confirm/page.tsx index f8b73e9a78..1202b2d260 100644 --- a/apps/dashboard/src/app/(main)/integrations/custom/projects/transfer/confirm/page.tsx +++ b/apps/dashboard/src/app/(main)/integrations/custom/projects/transfer/confirm/page.tsx @@ -1,4 +1,4 @@ -import IntegrationProjectTransferConfirmPageClient from "@/app/(main)/integrations/transfer-confirm-page"; +import IntegrationProjectTransferConfirmPageClient, { TransferConfirmMissingCodeView } from "@/app/(main)/integrations/transfer-confirm-page"; export const metadata = { title: "Project transfer", @@ -7,14 +7,12 @@ export const metadata = { export default async function Page(props: { searchParams: Promise<{ code?: string }> }) { const transferCode = (await props.searchParams).code; if (!transferCode) { - return <> -
Error: No transfer code provided.
- ; + return ; } return ( <> - + ); } diff --git a/apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx b/apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx new file mode 100644 index 0000000000..79461420aa --- /dev/null +++ b/apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { Logo } from "@/components/logo"; +import { useRouter } from "@/components/router"; +import { Button, Card, CardContent, CardFooter, CardHeader, Input, Typography } from "@/components/ui"; +import { stackAppInternalsSymbol } from "@/lib/stack-app-internals"; +import { useStackApp, useUser } from "@stackframe/stack"; +import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises"; +import Image from "next/image"; +import { useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import NeonLogo from "../../../../public/neon.png"; + +type NeonTransferState = "loading" | "success" | { type: "error", message: string }; + +/** + * Neon project transfer confirmation — legacy UI and copy (unchanged from pre–custom-redesign behavior). + */ +export default function NeonIntegrationProjectTransferConfirmPageClient() { + const app = useStackApp(); + const user = useUser({ projectIdMustMatch: "internal" }); + const router = useRouter(); + const searchParams = useSearchParams(); + + const [state, setState] = useState("loading"); + + useEffect(() => { + runAsynchronously(async () => { + try { + await (app as any)[stackAppInternalsSymbol].sendRequest("/integrations/neon/projects/transfer/confirm/check", { + method: "POST", + body: JSON.stringify({ + code: searchParams.get("code"), + }), + headers: { + "Content-Type": "application/json", + }, + }); + setState("success"); + } catch (err: any) { + setState({ type: "error", message: err.message }); + } + }); + }, [app, searchParams]); + + const currentUrl = new URL(window.location.href); + const signUpSearchParams = new URLSearchParams(); + signUpSearchParams.set("after_auth_return_to", currentUrl.pathname + currentUrl.search + currentUrl.hash); + const signUpUrl = `/handler/signup?${signUpSearchParams.toString()}`; + + return ( + + + Neon +
+
+
+
+
+
+
+
+ + + +

+ Project transfer +

+ {state === "success" && <> + + Neon would like to transfer a Stack Auth project and link it to your own account. This will let you access the project from Stack Auth's dashboard. + + {user ? ( + <> + + Which Stack Auth account would you like to transfer the project to? (You'll still be able to access your project from Neon's dashboard.) + + } value={`Signed in as ${user.primaryEmail || user.displayName || "Unnamed user"}`} /> + + + ) : ( + + To continue, please sign in or create a Stack Auth account. + + )} + } + + {typeof state !== "string" && <> + + {state.message} + + } + +
+ {state === "success" && +
+ + +
+
} + + ); +} diff --git a/apps/dashboard/src/app/(main)/integrations/neon/projects/transfer/confirm/page.tsx b/apps/dashboard/src/app/(main)/integrations/neon/projects/transfer/confirm/page.tsx index f4cae6b4d2..27401cbf02 100644 --- a/apps/dashboard/src/app/(main)/integrations/neon/projects/transfer/confirm/page.tsx +++ b/apps/dashboard/src/app/(main)/integrations/neon/projects/transfer/confirm/page.tsx @@ -1,4 +1,4 @@ -import IntegrationProjectTransferConfirmPageClient from "@/app/(main)/integrations/transfer-confirm-page"; +import NeonIntegrationProjectTransferConfirmPageClient from "@/app/(main)/integrations/neon-transfer-confirm-page"; export const metadata = { title: "Project transfer", @@ -14,7 +14,7 @@ export default async function Page(props: { searchParams: Promise<{ code?: strin return ( <> - + ); } diff --git a/apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx b/apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx index 6b19aa3dd7..bd0e0da2ee 100644 --- a/apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx +++ b/apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx @@ -1,28 +1,40 @@ "use client"; -import { Logo } from "@/components/logo"; +import { DesignAlert } from "@/components/design-components/alert"; +import { ProjectTransferConfirmView, type ProjectTransferConfirmUiState } from "@/components/project-transfer-confirm-view"; import { useRouter } from "@/components/router"; -import { Button, Card, CardContent, CardFooter, CardHeader, Input, Typography } from "@/components/ui"; import { stackAppInternalsSymbol } from "@/lib/stack-app-internals"; import { useStackApp, useUser } from "@stackframe/stack"; import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises"; -import Image from "next/image"; import { useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; -import NeonLogo from "../../../../public/neon.png"; -export default function IntegrationProjectTransferConfirmPageClient(props: { type: "neon" | "custom" }) { +export function TransferConfirmMissingCodeView() { + return ( +
+ +
+ ); +} + +/** Custom integration project transfer — design-components UI. Neon uses `neon-transfer-confirm-page`. */ +export default function IntegrationProjectTransferConfirmPageClient() { const app = useStackApp(); const user = useUser({ projectIdMustMatch: "internal" }); const router = useRouter(); const searchParams = useSearchParams(); - const [state, setState] = useState<'loading'|'success'|{type: 'error', message: string}>('loading'); + const [state, setState] = useState("loading"); useEffect(() => { runAsynchronously(async () => { try { - await (app as any)[stackAppInternalsSymbol].sendRequest(`/integrations/${props.type}/projects/transfer/confirm/check`, { + await (app as any)[stackAppInternalsSymbol].sendRequest("/integrations/custom/projects/transfer/confirm/check", { method: "POST", body: JSON.stringify({ code: searchParams.get("code"), @@ -31,119 +43,56 @@ export default function IntegrationProjectTransferConfirmPageClient(props: { typ "Content-Type": "application/json", }, }); - setState('success'); + setState("success"); } catch (err: any) { - setState({ type: 'error', message: err.message }); + setState({ type: "error", message: err.message }); } }); - - }, [app, searchParams, props.type]); + }, [app, searchParams]); const currentUrl = new URL(window.location.href); const signUpSearchParams = new URLSearchParams(); signUpSearchParams.set("after_auth_return_to", currentUrl.pathname + currentUrl.search + currentUrl.hash); const signUpUrl = `/handler/signup?${signUpSearchParams.toString()}`; - return ( - - - {props.type === "neon" && (<> - Neon -
-
-
-
-
-
-
-
- )} - - - -

- Project transfer -

- {state === 'success' && <> - - {props.type === "neon" ? "Neon" : "A third party"} would like to transfer a Stack Auth project and link it to your own account. This will let you access the project from Stack Auth's dashboard. - - {user ? ( - <> - - Which Stack Auth account would you like to transfer the project to? (You'll still be able to access your project from {props.type === "neon" ? "Neon" : "the third party"}'s dashboard.) - - } value={`Signed in as ${user.primaryEmail || user.displayName || "Unnamed user"}`} /> - - - ) : ( - - To continue, please sign in or create a Stack Auth account. - - )} - } - - {typeof state !== 'string' && <> - - {state.message} - - } + const signedIn = user != null; + const accountLabel = user + ? `Signed in as ${user.primaryEmail ?? user.displayName ?? "Unnamed user"}` + : undefined; -
- {state === 'success' && -
- - -
-
} - + return ( + { + window.close(); + }} + onPrimary={async () => { + if (user) { + const confirmRes = await (app as any)[stackAppInternalsSymbol].sendRequest("/integrations/custom/projects/transfer/confirm", { + method: "POST", + body: JSON.stringify({ + code: searchParams.get("code"), + }), + headers: { + "Content-Type": "application/json", + }, + }); + const confirmResJson = await confirmRes.json(); + router.push(`/projects/${confirmResJson.project_id}`); + await wait(3000); + } else { + router.push(signUpUrl); + await wait(3000); + } + }} + onSwitchAccount={async () => { + if (user == null) { + return; + } + await user.signOut({ redirectUrl: signUpUrl }); + }} + /> ); } diff --git a/apps/dashboard/src/components/project-transfer-confirm-view.tsx b/apps/dashboard/src/components/project-transfer-confirm-view.tsx new file mode 100644 index 0000000000..7de5e391c7 --- /dev/null +++ b/apps/dashboard/src/components/project-transfer-confirm-view.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { DesignAlert } from "@/components/design-components/alert"; +import { DesignButton } from "@/components/design-components/button"; +import { DesignCard } from "@/components/design-components/card"; +import { DesignInput } from "@/components/design-components/input"; +import { Logo } from "@/components/logo"; +import { Spinner } from "@/components/ui"; +import { ArrowsLeftRightIcon } from "@phosphor-icons/react"; + +export type ProjectTransferConfirmUiState = "loading" | "success" | { type: "error", message: string }; + +export type ProjectTransferConfirmViewProps = { + state: ProjectTransferConfirmUiState, + /** When `state === "success"`, whether the “signed in” branch is shown. */ + signedIn: boolean, + /** Label for the disabled “Receiving account” field when signed in. */ + signedInAsLabel?: string, + onCancel?: () => void | Promise, + onPrimary?: () => void | Promise, + onSwitchAccount?: () => void | Promise, +}; + +/** Presentational shell for the custom integration project transfer confirmation screen. */ +export function ProjectTransferConfirmView(props: ProjectTransferConfirmViewProps) { + const { + state, + signedIn, + signedInAsLabel = "Signed in as preview@example.com", + onCancel, + onPrimary, + onSwitchAccount, + } = props; + + const primaryLabel = signedIn ? "Accept transfer" : "Sign in"; + + return ( +
+ + +
+ )} + > + {state === "loading" && ( +
+ +

Verifying this transfer link…

+
+ )} + + {state === "success" && ( +
+ {signedIn ? ( + <> +

+ You'll still be able to open this project from the third party's dashboard after you accept. +

+
+ + Receiving account + + } + value={signedInAsLabel} + /> +
+ { + await onSwitchAccount?.(); + }} + > + Use a different account + + + ) : ( + + )} +
+ )} + + {typeof state !== "string" && ( + + )} + + {state === "success" && ( +
+ { + await onCancel?.(); + }} + > + Cancel + + { + await onPrimary?.(); + }} + > + {primaryLabel} + +
+ )} + +
+ ); +}