Skip to content

Commit b491e83

Browse files
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.
1 parent ce49eae commit b491e83

5 files changed

Lines changed: 348 additions & 118 deletions

File tree

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import IntegrationProjectTransferConfirmPageClient from "@/app/(main)/integrations/transfer-confirm-page";
1+
import IntegrationProjectTransferConfirmPageClient, { TransferConfirmMissingCodeView } from "@/app/(main)/integrations/transfer-confirm-page";
22

33
export const metadata = {
44
title: "Project transfer",
@@ -7,14 +7,12 @@ export const metadata = {
77
export default async function Page(props: { searchParams: Promise<{ code?: string }> }) {
88
const transferCode = (await props.searchParams).code;
99
if (!transferCode) {
10-
return <>
11-
<div>Error: No transfer code provided.</div>
12-
</>;
10+
return <TransferConfirmMissingCodeView />;
1311
}
1412

1513
return (
1614
<>
17-
<IntegrationProjectTransferConfirmPageClient type="custom" />
15+
<IntegrationProjectTransferConfirmPageClient />
1816
</>
1917
);
2018
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"use client";
2+
3+
import { Logo } from "@/components/logo";
4+
import { useRouter } from "@/components/router";
5+
import { Button, Card, CardContent, CardFooter, CardHeader, Input, Typography } from "@/components/ui";
6+
import { stackAppInternalsSymbol } from "@/lib/stack-app-internals";
7+
import { useStackApp, useUser } from "@stackframe/stack";
8+
import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises";
9+
import Image from "next/image";
10+
import { useSearchParams } from "next/navigation";
11+
import { useEffect, useState } from "react";
12+
import NeonLogo from "../../../../public/neon.png";
13+
14+
type NeonTransferState = "loading" | "success" | { type: "error", message: string };
15+
16+
/**
17+
* Neon project transfer confirmation — legacy UI and copy (unchanged from pre–custom-redesign behavior).
18+
*/
19+
export default function NeonIntegrationProjectTransferConfirmPageClient() {
20+
const app = useStackApp();
21+
const user = useUser({ projectIdMustMatch: "internal" });
22+
const router = useRouter();
23+
const searchParams = useSearchParams();
24+
25+
const [state, setState] = useState<NeonTransferState>("loading");
26+
27+
useEffect(() => {
28+
runAsynchronously(async () => {
29+
try {
30+
await (app as any)[stackAppInternalsSymbol].sendRequest("/integrations/neon/projects/transfer/confirm/check", {
31+
method: "POST",
32+
body: JSON.stringify({
33+
code: searchParams.get("code"),
34+
}),
35+
headers: {
36+
"Content-Type": "application/json",
37+
},
38+
});
39+
setState("success");
40+
} catch (err: any) {
41+
setState({ type: "error", message: err.message });
42+
}
43+
});
44+
}, [app, searchParams]);
45+
46+
const currentUrl = new URL(window.location.href);
47+
const signUpSearchParams = new URLSearchParams();
48+
signUpSearchParams.set("after_auth_return_to", currentUrl.pathname + currentUrl.search + currentUrl.hash);
49+
const signUpUrl = `/handler/signup?${signUpSearchParams.toString()}`;
50+
51+
return (
52+
<Card className="max-w-lg text-center">
53+
<CardHeader className="flex-row items-end justify-center gap-4">
54+
<Image src={NeonLogo} alt="Neon" width={55} />
55+
<div className="relative self-center w-10 hidden dark:block">
56+
<div style={{
57+
position: "absolute",
58+
width: 40,
59+
height: 6,
60+
backgroundImage: "repeating-linear-gradient(135deg, #ccc, #ccc)",
61+
transform: "rotate(-45deg)",
62+
}} />
63+
<div style={{
64+
position: "absolute",
65+
width: 40,
66+
height: 6,
67+
backgroundImage: "repeating-linear-gradient(45deg, #ccc, #ccc)",
68+
transform: "rotate(45deg)",
69+
}} />
70+
</div>
71+
<div className="relative self-center w-10 block dark:hidden">
72+
<div style={{
73+
position: "absolute",
74+
width: 40,
75+
height: 6,
76+
backgroundImage: "repeating-linear-gradient(135deg, #52525B, #52525B)",
77+
transform: "rotate(-45deg)",
78+
}} />
79+
<div style={{
80+
position: "absolute",
81+
width: 40,
82+
height: 6,
83+
backgroundImage: "repeating-linear-gradient(45deg, #52525B, #52525B)",
84+
transform: "rotate(45deg)",
85+
}} />
86+
</div>
87+
<Logo noLink alt="Stack" width={50} />
88+
</CardHeader>
89+
<CardContent className="flex flex-col gap-4">
90+
<h1 className="text-3xl font-semibold">
91+
Project transfer
92+
</h1>
93+
{state === "success" && <>
94+
<Typography className="text-sm">
95+
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&apos;s dashboard.
96+
</Typography>
97+
{user ? (
98+
<>
99+
<Typography className="mb-3 text-sm">
100+
Which Stack Auth account would you like to transfer the project to? (You&apos;ll still be able to access your project from Neon&apos;s dashboard.)
101+
</Typography>
102+
<Input type="text" disabled prefixItem={<Logo noLink width={15} height={15} />} value={`Signed in as ${user.primaryEmail || user.displayName || "Unnamed user"}`} />
103+
<Button variant="secondary" onClick={async () => await user.signOut({ redirectUrl: signUpUrl })}>
104+
Switch account
105+
</Button>
106+
</>
107+
) : (
108+
<Typography className="text-sm">
109+
To continue, please sign in or create a Stack Auth account.
110+
</Typography>
111+
)}
112+
</>}
113+
114+
{typeof state !== "string" && <>
115+
<Typography className="text-sm">
116+
{state.message}
117+
</Typography>
118+
</>}
119+
120+
</CardContent>
121+
{state === "success" && <CardFooter className="flex justify-end mt-4">
122+
<div className="flex gap-2 justify-center">
123+
<Button variant="secondary" onClick={() => { window.close(); }}>
124+
Cancel
125+
</Button>
126+
<Button onClick={async () => {
127+
if (user) {
128+
const confirmRes = await (app as any)[stackAppInternalsSymbol].sendRequest("/integrations/neon/projects/transfer/confirm", {
129+
method: "POST",
130+
body: JSON.stringify({
131+
code: searchParams.get("code"),
132+
}),
133+
headers: {
134+
"Content-Type": "application/json",
135+
},
136+
});
137+
const confirmResJson = await confirmRes.json();
138+
router.push(`/projects/${confirmResJson.project_id}`);
139+
await wait(3000);
140+
} else {
141+
router.push(signUpUrl);
142+
await wait(3000);
143+
}
144+
}}>
145+
{user ? "Transfer" : "Sign in"}
146+
</Button>
147+
</div>
148+
</CardFooter>}
149+
</Card>
150+
);
151+
}

apps/dashboard/src/app/(main)/integrations/neon/projects/transfer/confirm/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import IntegrationProjectTransferConfirmPageClient from "@/app/(main)/integrations/transfer-confirm-page";
1+
import NeonIntegrationProjectTransferConfirmPageClient from "@/app/(main)/integrations/neon-transfer-confirm-page";
22

33
export const metadata = {
44
title: "Project transfer",
@@ -14,7 +14,7 @@ export default async function Page(props: { searchParams: Promise<{ code?: strin
1414

1515
return (
1616
<>
17-
<IntegrationProjectTransferConfirmPageClient type="neon" />
17+
<NeonIntegrationProjectTransferConfirmPageClient />
1818
</>
1919
);
2020
}
Lines changed: 60 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,40 @@
11
"use client";
22

3-
import { Logo } from "@/components/logo";
3+
import { DesignAlert } from "@/components/design-components/alert";
4+
import { ProjectTransferConfirmView, type ProjectTransferConfirmUiState } from "@/components/project-transfer-confirm-view";
45
import { useRouter } from "@/components/router";
5-
import { Button, Card, CardContent, CardFooter, CardHeader, Input, Typography } from "@/components/ui";
66
import { stackAppInternalsSymbol } from "@/lib/stack-app-internals";
77
import { useStackApp, useUser } from "@stackframe/stack";
88
import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises";
9-
import Image from "next/image";
109
import { useSearchParams } from "next/navigation";
1110
import { useEffect, useState } from "react";
12-
import NeonLogo from "../../../../public/neon.png";
1311

14-
export default function IntegrationProjectTransferConfirmPageClient(props: { type: "neon" | "custom" }) {
12+
export function TransferConfirmMissingCodeView() {
13+
return (
14+
<div className="flex min-h-[100dvh] w-full items-center justify-center px-4 py-8 sm:px-6">
15+
<DesignAlert
16+
variant="error"
17+
title="This transfer link is incomplete"
18+
description="Open the full link you received (it includes a transfer code). If the link expired, go back to the partner or integrations screen and start the transfer again."
19+
className="max-w-md"
20+
/>
21+
</div>
22+
);
23+
}
24+
25+
/** Custom integration project transfer — design-components UI. Neon uses `neon-transfer-confirm-page`. */
26+
export default function IntegrationProjectTransferConfirmPageClient() {
1527
const app = useStackApp();
1628
const user = useUser({ projectIdMustMatch: "internal" });
1729
const router = useRouter();
1830
const searchParams = useSearchParams();
1931

20-
const [state, setState] = useState<'loading'|'success'|{type: 'error', message: string}>('loading');
32+
const [state, setState] = useState<ProjectTransferConfirmUiState>("loading");
2133

2234
useEffect(() => {
2335
runAsynchronously(async () => {
2436
try {
25-
await (app as any)[stackAppInternalsSymbol].sendRequest(`/integrations/${props.type}/projects/transfer/confirm/check`, {
37+
await (app as any)[stackAppInternalsSymbol].sendRequest("/integrations/custom/projects/transfer/confirm/check", {
2638
method: "POST",
2739
body: JSON.stringify({
2840
code: searchParams.get("code"),
@@ -31,119 +43,56 @@ export default function IntegrationProjectTransferConfirmPageClient(props: { typ
3143
"Content-Type": "application/json",
3244
},
3345
});
34-
setState('success');
46+
setState("success");
3547
} catch (err: any) {
36-
setState({ type: 'error', message: err.message });
48+
setState({ type: "error", message: err.message });
3749
}
3850
});
39-
40-
}, [app, searchParams, props.type]);
51+
}, [app, searchParams]);
4152

4253
const currentUrl = new URL(window.location.href);
4354
const signUpSearchParams = new URLSearchParams();
4455
signUpSearchParams.set("after_auth_return_to", currentUrl.pathname + currentUrl.search + currentUrl.hash);
4556
const signUpUrl = `/handler/signup?${signUpSearchParams.toString()}`;
4657

47-
return (
48-
<Card className="max-w-lg text-center">
49-
<CardHeader className="flex-row items-end justify-center gap-4">
50-
{props.type === "neon" && (<>
51-
<Image src={NeonLogo} alt="Neon" width={55} />
52-
<div className="relative self-center w-10 hidden dark:block">
53-
<div style={{
54-
position: "absolute",
55-
width: 40,
56-
height: 6,
57-
backgroundImage: "repeating-linear-gradient(135deg, #ccc, #ccc)",
58-
transform: "rotate(-45deg)",
59-
}} />
60-
<div style={{
61-
position: "absolute",
62-
width: 40,
63-
height: 6,
64-
backgroundImage: "repeating-linear-gradient(45deg, #ccc, #ccc)",
65-
transform: "rotate(45deg)",
66-
}} />
67-
</div>
68-
<div className="relative self-center w-10 block dark:hidden">
69-
<div style={{
70-
position: "absolute",
71-
width: 40,
72-
height: 6,
73-
backgroundImage: "repeating-linear-gradient(135deg, #52525B, #52525B)",
74-
transform: "rotate(-45deg)",
75-
}} />
76-
<div style={{
77-
position: "absolute",
78-
width: 40,
79-
height: 6,
80-
backgroundImage: "repeating-linear-gradient(45deg, #52525B, #52525B)",
81-
transform: "rotate(45deg)",
82-
}} />
83-
</div>
84-
</>)}
85-
<Logo noLink alt="Stack" width={50} />
86-
</CardHeader>
87-
<CardContent className="flex flex-col gap-4">
88-
<h1 className="text-3xl font-semibold">
89-
Project transfer
90-
</h1>
91-
{state === 'success' && <>
92-
<Typography className="text-sm">
93-
{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&apos;s dashboard.
94-
</Typography>
95-
{user ? (
96-
<>
97-
<Typography className="mb-3 text-sm">
98-
Which Stack Auth account would you like to transfer the project to? (You&apos;ll still be able to access your project from {props.type === "neon" ? "Neon" : "the third party"}&apos;s dashboard.)
99-
</Typography>
100-
<Input type="text" disabled prefixItem={<Logo noLink width={15} height={15} />} value={`Signed in as ${user.primaryEmail || user.displayName || "Unnamed user"}`} />
101-
<Button variant="secondary" onClick={async () => await user.signOut({ redirectUrl: signUpUrl })}>
102-
Switch account
103-
</Button>
104-
</>
105-
) : (
106-
<Typography className="text-sm">
107-
To continue, please sign in or create a Stack Auth account.
108-
</Typography>
109-
)}
110-
</>}
111-
112-
{typeof state !== 'string' && <>
113-
<Typography className="text-sm">
114-
{state.message}
115-
</Typography>
116-
</>}
58+
const signedIn = user != null;
59+
const accountLabel = user
60+
? `Signed in as ${user.primaryEmail ?? user.displayName ?? "Unnamed user"}`
61+
: undefined;
11762

118-
</CardContent>
119-
{state === 'success' && <CardFooter className="flex justify-end mt-4">
120-
<div className="flex gap-2 justify-center">
121-
<Button variant="secondary" onClick={() => { window.close(); }}>
122-
Cancel
123-
</Button>
124-
<Button onClick={async () => {
125-
if (user) {
126-
const confirmRes = await (app as any)[stackAppInternalsSymbol].sendRequest(`/integrations/${props.type}/projects/transfer/confirm`, {
127-
method: "POST",
128-
body: JSON.stringify({
129-
code: searchParams.get("code"),
130-
}),
131-
headers: {
132-
"Content-Type": "application/json",
133-
},
134-
});
135-
const confirmResJson = await confirmRes.json();
136-
router.push(`/projects/${confirmResJson.project_id}`);
137-
await wait(3000);
138-
} else {
139-
router.push(signUpUrl);
140-
await wait(3000);
141-
}
142-
}}>
143-
{user ? "Transfer" : "Sign in"}
144-
</Button>
145-
</div>
146-
</CardFooter>}
147-
</Card>
63+
return (
64+
<ProjectTransferConfirmView
65+
state={state}
66+
signedIn={signedIn}
67+
signedInAsLabel={accountLabel}
68+
onCancel={() => {
69+
window.close();
70+
}}
71+
onPrimary={async () => {
72+
if (user) {
73+
const confirmRes = await (app as any)[stackAppInternalsSymbol].sendRequest("/integrations/custom/projects/transfer/confirm", {
74+
method: "POST",
75+
body: JSON.stringify({
76+
code: searchParams.get("code"),
77+
}),
78+
headers: {
79+
"Content-Type": "application/json",
80+
},
81+
});
82+
const confirmResJson = await confirmRes.json();
83+
router.push(`/projects/${confirmResJson.project_id}`);
84+
await wait(3000);
85+
} else {
86+
router.push(signUpUrl);
87+
await wait(3000);
88+
}
89+
}}
90+
onSwitchAccount={async () => {
91+
if (user == null) {
92+
return;
93+
}
94+
await user.signOut({ redirectUrl: signUpUrl });
95+
}}
96+
/>
14897
);
14998
}

0 commit comments

Comments
 (0)