Project transfer page redesign#1309
Conversation
…firmation 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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughSplits Neon and Custom transfer-confirm flows into separate client components, introduces a reusable ChangesProject transfer UI
Neon integration
Custom integration
Utilities & minor runtime tweaks
Sequence DiagramsequenceDiagram
participant Browser as Browser
participant Client as Integration Client
participant AppServer as AppServer (API)
participant Auth as Auth (session)
participant Router as Router
Browser->>Client: load page with ?code=...
Client->>AppServer: POST /integrations/*/projects/transfer/confirm/check (code)
AppServer-->>Client: 200 OK / error
alt check success
Client->>Auth: read current user session
Note right of Client: render success UI (signed-in or guest)
Browser->>Client: user clicks Primary
alt signed-in
Client->>AppServer: POST /integrations/*/projects/transfer/confirm
AppServer-->>Client: { project_id }
Client->>Router: navigate -> /projects/{project_id}
else guest
Client->>Browser: redirect -> /handler/signup?after_auth_return_to=...
end
else check error
Client->>Browser: render error message
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsxParsing error: error TS5012: Cannot read file '/tsconfig.json': ENOENT: no such file or directory, open '/tsconfig.json'. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Redesigns the custom integration project transfer confirmation experience in the Dashboard by introducing a new design-components-based view, while keeping the Neon flow on the legacy UI via a dedicated client component.
Changes:
- Add
ProjectTransferConfirmViewpresentational component (design-components UI) for transfer confirmation states. - Update custom integration transfer confirm page to use the new view and custom endpoints; add a richer “missing code” UI.
- Split Neon transfer confirm page into its own legacy client component and update routing to use it.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/dashboard/src/components/project-transfer-confirm-view.tsx | New design-components presentational shell for custom transfer confirm (loading/success/error). |
| apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx | Custom integration client logic updated to render the new view and call custom endpoints. |
| apps/dashboard/src/app/(main)/integrations/custom/projects/transfer/confirm/page.tsx | Server route updated to use redesigned custom client page and improved missing-code UX. |
| apps/dashboard/src/app/(main)/integrations/neon/projects/transfer/confirm/page.tsx | Route updated to use Neon-specific client component. |
| apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx | New Neon-specific legacy client page extracted from the previous shared implementation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Greptile SummaryThis PR redesigns the custom-integration project-transfer confirmation page onto the new design-components system (
Confidence Score: 4/5Safe to merge with minor UX polish outstanding — no data-loss or auth-boundary changes. The core logic (API calls, redirect, auth state wiring) is unchanged from the pre-refactor code and the new view component correctly wraps all async handlers with runAsynchronouslyWithAlert. The one notable gap is that the error state in ProjectTransferConfirmView renders no footer buttons, so a user who lands with an invalid code has no UI action to leave. apps/dashboard/src/components/project-transfer-confirm-view.tsx — the error/loading render paths have no footer actions. Important Files Changed
Sequence DiagramsequenceDiagram
participant Browser
participant CustomPage as custom/confirm page.tsx (Server)
participant CustomClient as transfer-confirm-page.tsx (Client)
participant View as ProjectTransferConfirmView
participant NeonPage as neon/confirm page.tsx (Server)
participant NeonClient as neon-transfer-confirm-page.tsx (Client)
participant API as Stack Auth API
Browser->>CustomPage: "GET /integrations/custom/.../confirm?code=xxx"
alt no code param
CustomPage-->>Browser: MissingCodeView (styled error card)
else code present
CustomPage-->>Browser: CustomIntegrationProjectTransferConfirmPageClient
CustomClient->>View: "render(state=loading)"
CustomClient->>API: POST /integrations/custom/.../confirm/check
alt check succeeds
API-->>CustomClient: 200 OK
CustomClient->>View: "render(state=success, signedIn=...)"
alt user clicks Accept
View->>CustomClient: onPrimary()
CustomClient->>API: POST /integrations/custom/.../confirm
API-->>CustomClient: project_id
CustomClient->>Browser: "router.push(/projects/{id})"
else user clicks Cancel
View->>CustomClient: onCancel()
CustomClient->>Browser: window.close()
end
else check fails
API-->>CustomClient: error
CustomClient->>View: "render(state=error)"
end
end
Browser->>NeonPage: "GET /integrations/neon/.../confirm?code=xxx"
alt no code param
NeonPage-->>Browser: plain text error div
else code present
NeonPage-->>Browser: NeonIntegrationProjectTransferConfirmPageClient
NeonClient->>API: POST /integrations/neon/.../confirm/check
API-->>NeonClient: result
NeonClient-->>Browser: Legacy Neon x Stack co-branded card
end
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
apps/dashboard/src/components/project-transfer-confirm-view.tsx:112-145
**Error state leaves user with no exit action**
When the `/check` call fails the component renders the `DesignAlert` inside the card but the footer (Cancel / Accept buttons) is gated on `state === "success"` and is never shown. A user who arrives with an invalid or expired code sees the error message and then has no UI button to leave — the only escape is the browser's back button. Consider adding a minimal footer (e.g. a single "Close" / "Go back" button) that is also rendered in the error state.
### Issue 2 of 2
apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx:17-22
`buildSignUpUrl` is defined identically in both this file and `transfer-confirm-page.tsx`. Extracting it to a shared module (e.g. `@/lib/transfer-utils`) would remove the duplication and make a future change (e.g. changing the redirect param name) a one-place edit.
```suggestion
// TODO: extract to a shared util (same implementation lives in transfer-confirm-page.tsx)
function buildSignUpUrl(): string {
const currentUrl = new URL(window.location.href);
const signUpSearchParams = new URLSearchParams();
signUpSearchParams.set("after_auth_return_to", currentUrl.pathname + currentUrl.search + currentUrl.hash);
return `/handler/signup?${signUpSearchParams.toString()}`;
}
```
Reviews (1): Last reviewed commit: "bot comments" | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx (1)
35-49:⚠️ Potential issue | 🟠 MajorGuard error types and validate API response shape before routing.
Line 47 silently treats every failure as an
Error—if a non-Error value is thrown (e.g., a string ornull), accessing.messageproducesundefined. Lines 82–83 bypass response validation entirely; if the API contract changes or returns malformed data,confirmResJson.project_idbecomesundefined, routing the user to/projects/undefined.Per the coding guidelines ("Fail early, fail loud. Fail fast with an error instead of silently continuing."), guard both the caught error type and the JSON response shape:
Suggested hardening
} catch (err: any) { - setState({ type: "error", message: err.message }); + } catch (err: unknown) { + setState({ + type: "error", + message: err instanceof Error ? err.message : "Failed to verify the transfer link", + }); } @@ - const confirmResJson = await confirmRes.json(); - router.push(`/projects/${confirmResJson.project_id}`); + const confirmResJson: unknown = await confirmRes.json(); + const projectId = + typeof confirmResJson === "object" && + confirmResJson != null && + "project_id" in confirmResJson && + typeof confirmResJson["project_id"] === "string" + ? confirmResJson["project_id"] + : null; + if (projectId == null) { + throw new Error("Transfer confirm response missing `project_id`"); + } + router.push(`/projects/${encodeURIComponent(projectId)}`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/integrations/transfer-confirm-page.tsx around lines 35 - 49, The catch block in the runAsynchronously call and the subsequent handling of confirmResJson need hardening: when catching errors from (app as any)[stackAppInternalsSymbol].sendRequest, normalize the error message (e.g., use err?.message ?? String(err) or assert instanceof Error) before calling setState so you never pass undefined, and when handling the response from sendRequest validate the parsed JSON (confirmResJson) and assert that confirmResJson.project_id is a non-empty string (otherwise throw a clear Error) before routing to `/projects/${project_id}`; update the setState error paths to use the normalized message and throw/handle a descriptive error when the API response shape is invalid.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/dashboard/src/app/`(main)/integrations/neon-transfer-confirm-page.tsx:
- Around line 29-41: The catch block and response handling need explicit
validation: when catching errors from (app as
any)[stackAppInternalsSymbol].sendRequest, check if err is an Error (e.g., err
instanceof Error) and use err.message, otherwise use String(err) or throw a new
Error("Unexpected error from sendRequest"); similarly, after parsing the confirm
response (confirmResJson), validate that confirmResJson and
confirmResJson.project_id exist and are of the expected type before calling
navigate(`/projects/${confirmResJson.project_id}`) — if missing, throw a clear
Error("Missing project_id in confirm response") and setState({type: "error",
message: ...}) so we fail loud; finally, remove or justify the any casts (app as
any and catch err: any) by either adding a brief explanatory comment per
guidelines or converting to proper types so casts are not needed (referencing
stackAppInternalsSymbol, sendRequest, setState, and confirmResJson.project_id).
- Around line 46-49: The code builds signUpUrl during render using
window.location (currentUrl, signUpSearchParams, signUpUrl), which can cause
"window is not defined" on server render; move that construction out of the
render path by either 1) creating a small helper function buildSignUpUrl() that
reads window.location and returns the `/handler/signup?...` string and call it
inside the click handlers that currently use signUpUrl (the handlers referenced
around where signUpUrl is used), or 2) compute and store signUpUrl in a
useEffect into state (useState + useEffect) so it runs only on the client; then
remove the top-level construction of currentUrl/signUpSearchParams/signUpUrl
from the component body. Ensure the handlers use the new helper or state value.
In `@apps/dashboard/src/app/`(main)/integrations/transfer-confirm-page.tsx:
- Around line 71-95: The code reads window.location.href at render and captures
signUpUrl for the onPrimary and onSwitchAccount callbacks, which risks SSR
crashes; move the currentUrl/signUpUrl computation out of render and into a
client-only effect or ref: create a state/ref (e.g., signUpUrlRef or signUpUrl
state) and set it inside a useEffect that runs on mount, or compute signUpUrl
inside the onPrimary/onSwitchAccount handlers themselves before using it; update
references inside onPrimary and onSwitchAccount (and any usage of currentUrl) to
use the ref/state or freshly computed value so no window access happens during
server render.
In `@apps/dashboard/src/components/project-transfer-confirm-view.tsx`:
- Around line 13-21: When rendering ProjectTransferConfirmView, enforce its
success-state contract by removing the fallback "Signed in as
preview@example.com" label and adding runtime validations in the component
(e.g., at the start of ProjectTransferConfirmView render): if state ===
"success" ensure onCancel and onPrimary are non-null and throw descriptive
Errors if missing; additionally if state === "success" && signedIn === true
ensure signedInAsLabel and onSwitchAccount are non-null and throw Errors if
missing. Make these checks reference the props by name (state, signedIn,
signedInAsLabel, onCancel, onPrimary, onSwitchAccount) so callers fail loud
instead of rendering disabled buttons or fake labels.
---
Outside diff comments:
In `@apps/dashboard/src/app/`(main)/integrations/transfer-confirm-page.tsx:
- Around line 35-49: The catch block in the runAsynchronously call and the
subsequent handling of confirmResJson need hardening: when catching errors from
(app as any)[stackAppInternalsSymbol].sendRequest, normalize the error message
(e.g., use err?.message ?? String(err) or assert instanceof Error) before
calling setState so you never pass undefined, and when handling the response
from sendRequest validate the parsed JSON (confirmResJson) and assert that
confirmResJson.project_id is a non-empty string (otherwise throw a clear Error)
before routing to `/projects/${project_id}`; update the setState error paths to
use the normalized message and throw/handle a descriptive error when the API
response shape is invalid.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e09c23fe-5da6-46e1-9316-0c489fee819b
📒 Files selected for processing (5)
apps/dashboard/src/app/(main)/integrations/custom/projects/transfer/confirm/page.tsxapps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsxapps/dashboard/src/app/(main)/integrations/neon/projects/transfer/confirm/page.tsxapps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsxapps/dashboard/src/components/project-transfer-confirm-view.tsx
|
@greptile review again |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/dashboard/src/app/(main)/integrations/custom/projects/transfer/confirm/page.tsx (1)
35-37: ⚡ Quick winAvoid dynamic
searchParamsin this app route; use a client component instead.This page can remain static by delegating the transfer code check to a client component that uses
useSearchParams. Move the conditional rendering logic to a client component wrapper, keeping only the metadata export in the server component if needed.As per coding guidelines: "NEVER use Next.js dynamic functions if you can avoid them. Instead, prefer using a client component to ensure the page remains static."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/app/`(main)/integrations/custom/projects/transfer/confirm/page.tsx around lines 35 - 37, The Page server component currently awaits props.searchParams and reads transferCode (in Page and transferCode variable) which makes the route dynamic; instead keep Page static and move the conditional transfer-code check and rendering into a client component that uses useSearchParams (create a ClientTransferConfirm or similar client wrapper that calls useSearchParams to get code and handles the conditional rendering/redirect/UI). Leave any metadata export in the server Page if required, remove awaiting of props.searchParams and references to transferCode from Page, and import/render the new client wrapper from Page so the route stays static.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/dashboard/src/app/`(main)/integrations/transfer-confirm-page.tsx:
- Line 31: The code uses an untyped cast (app as any)[stackAppInternalsSymbol]
to call sendRequest, which should be fixed by either adding a brief explanatory
comment above the access that states why the SDK internals are not expressible
in TS and how runtime safety is ensured, or by introducing a small typed
accessor helper (e.g., a function like getStackAppInternals(app): { sendRequest:
(...) => Promise<any> } that encapsulates the cast and returns a typed object)
and replacing direct (app as any)[stackAppInternalsSymbol].sendRequest calls
with getStackAppInternals(app).sendRequest to centralize and document the unsafe
cast (reference symbols: app, stackAppInternalsSymbol, sendRequest,
transfer-confirm-page.tsx).
---
Nitpick comments:
In
`@apps/dashboard/src/app/`(main)/integrations/custom/projects/transfer/confirm/page.tsx:
- Around line 35-37: The Page server component currently awaits
props.searchParams and reads transferCode (in Page and transferCode variable)
which makes the route dynamic; instead keep Page static and move the conditional
transfer-code check and rendering into a client component that uses
useSearchParams (create a ClientTransferConfirm or similar client wrapper that
calls useSearchParams to get code and handles the conditional
rendering/redirect/UI). Leave any metadata export in the server Page if
required, remove awaiting of props.searchParams and references to transferCode
from Page, and import/render the new client wrapper from Page so the route stays
static.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4e4fda67-6678-4532-b54a-4a8231a88cab
📒 Files selected for processing (5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsxapps/dashboard/src/app/(main)/integrations/custom/projects/transfer/confirm/page.tsxapps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsxapps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsxapps/dashboard/src/components/project-transfer-confirm-view.tsx
✅ Files skipped from review due to trivial changes (1)
- apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx
- apps/dashboard/src/components/project-transfer-confirm-view.tsx
| {typeof state !== "string" && ( | ||
| <DesignAlert | ||
| variant="error" | ||
| title="This transfer can’t continue" | ||
| description={state.message} | ||
| glassmorphic | ||
| /> | ||
| )} | ||
|
|
||
| {state === "success" && ( | ||
| <div className="flex flex-col-reverse gap-2 border-t border-black/[0.08] pt-5 dark:border-white/[0.08] sm:flex-row sm:justify-end"> | ||
| <DesignButton | ||
| variant="outline" | ||
| className="transition-colors duration-150 hover:transition-none sm:min-w-[6.5rem]" | ||
| onClick={() => { | ||
| runAsynchronouslyWithAlert(async () => { | ||
| await onCancel?.(); | ||
| }); | ||
| }} | ||
| > | ||
| Cancel | ||
| </DesignButton> | ||
| <DesignButton | ||
| className="sm:min-w-[6.5rem]" | ||
| onClick={() => { | ||
| runAsynchronouslyWithAlert(async () => { | ||
| await onPrimary?.(); | ||
| }); | ||
| }} | ||
| > | ||
| {primaryLabel} | ||
| </DesignButton> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
Error state leaves user with no exit action
When the /check call fails the component renders the DesignAlert inside the card but the footer (Cancel / Accept buttons) is gated on state === "success" and is never shown. A user who arrives with an invalid or expired code sees the error message and then has no UI button to leave — the only escape is the browser's back button. Consider adding a minimal footer (e.g. a single "Close" / "Go back" button) that is also rendered in the error state.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/dashboard/src/components/project-transfer-confirm-view.tsx
Line: 112-145
Comment:
**Error state leaves user with no exit action**
When the `/check` call fails the component renders the `DesignAlert` inside the card but the footer (Cancel / Accept buttons) is gated on `state === "success"` and is never shown. A user who arrives with an invalid or expired code sees the error message and then has no UI button to leave — the only escape is the browser's back button. Consider adding a minimal footer (e.g. a single "Close" / "Go back" button) that is also rendered in the error state.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
apps/dashboard/src/components/project-transfer-confirm-view.tsx (1)
93-93:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winType-narrow
signedInAsLabelto satisfy TypeScript and follow coding guidelines.Although runtime validation at line 48 ensures
signedInAsLabelis non-null whensignedInis true, TypeScript cannot infer this guarantee at the usage site. This creates a type mismatch ifDesignInput'svalueprop requires a non-optional string.Per coding guidelines, prefer
?? throwErr(...)over non-null assertions for defensive coding with explicit error messages.🛡️ Suggested fix using guideline-compliant pattern
Extract a validated variable immediately after the runtime checks:
if (signedIn && (signedInAsLabel == null || onSwitchAccount == null)) { throw new StackAssertionError("ProjectTransferConfirmView requires `signedInAsLabel` and `onSwitchAccount` when `signedIn` is true in the success state"); } } +const validatedSignedInLabel = signedIn + ? signedInAsLabel ?? throwErr("signedInAsLabel must be non-null when signedIn is true", StackAssertionError) + : undefined; + const primaryLabel = signedIn ? "Accept transfer" : "Sign in";Then at line 93:
- value={signedInAsLabel} + value={validatedSignedInLabel}Alternatively, if you prefer inline defensive coding:
- value={signedInAsLabel} + value={signedInAsLabel ?? throwErr("signedInAsLabel must be non-null when signedIn is true", StackAssertionError)}Based on coding guidelines: "Prefer
?? throwErr(...)over non-null assertions, with good error messages explicitly stating the assumption that must've been violated for the error to be thrown."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/dashboard/src/components/project-transfer-confirm-view.tsx` at line 93, TypeScript can't see the earlier runtime check that guarantees signedInAsLabel when signedIn is true, so replace the direct usage to a narrowed/defensive value: after the runtime check for signedIn, create a local validated variable (e.g., const validatedSignedInAs = signedInAsLabel ?? throwErr("expected signedInAsLabel to be set when signedIn is true")) and use that validatedSignedInAs as the value prop for DesignInput (instead of signedInAsLabel); alternatively apply the inline defensive pattern value={signedInAsLabel ?? throwErr("expected signedInAsLabel to be set when signedIn is true")}. This satisfies TypeScript and follows the guideline preferring ?? throwErr(...) over non-null assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/dashboard/src/lib/transfer-utils.ts`:
- Around line 3-7: buildTransferSignUpUrl currently builds the signup URL with a
template literal; instead create a URL object for "/handler/signup" using new
URL('/handler/signup', window.location.origin), set its search via the existing
signUpSearchParams (e.g., signupUrl.search = signUpSearchParams.toString() or
use signupUrl.searchParams directly), and return the relative path + search
(e.g., signupUrl.pathname + signupUrl.search) so the function uses the URL API;
update references to currentUrl and signUpSearchParams inside
buildTransferSignUpUrl accordingly.
- Around line 18-20: The getStackAppInternals function currently casts blindly
and may return undefined; change it to validate that app is a non-null object
and that the stackAppInternalsSymbol key exists before returning. Implement a
type-guard style check inside getStackAppInternals: verify typeof app ===
"object" && app !== null and Object.prototype.hasOwnProperty.call(app as object,
stackAppInternalsSymbol), then retrieve the value, assert it matches
StackAppInternals (or narrow its type) and return it; if the checks fail, throw
a clear Error like "Missing StackAppInternals on app" so callers fail fast.
Ensure you reference stackAppInternalsSymbol and getStackAppInternals in your
fix.
---
Duplicate comments:
In `@apps/dashboard/src/components/project-transfer-confirm-view.tsx`:
- Line 93: TypeScript can't see the earlier runtime check that guarantees
signedInAsLabel when signedIn is true, so replace the direct usage to a
narrowed/defensive value: after the runtime check for signedIn, create a local
validated variable (e.g., const validatedSignedInAs = signedInAsLabel ??
throwErr("expected signedInAsLabel to be set when signedIn is true")) and use
that validatedSignedInAs as the value prop for DesignInput (instead of
signedInAsLabel); alternatively apply the inline defensive pattern
value={signedInAsLabel ?? throwErr("expected signedInAsLabel to be set when
signedIn is true")}. This satisfies TypeScript and follows the guideline
preferring ?? throwErr(...) over non-null assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6c7d1581-f427-4570-b17b-2c25a885d695
📒 Files selected for processing (4)
apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsxapps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsxapps/dashboard/src/components/project-transfer-confirm-view.tsxapps/dashboard/src/lib/transfer-utils.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx
- apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx
| export function buildTransferSignUpUrl(): string { | ||
| const currentUrl = new URL(window.location.href); | ||
| const signUpSearchParams = new URLSearchParams(); | ||
| signUpSearchParams.set("after_auth_return_to", currentUrl.pathname + currentUrl.search + currentUrl.hash); | ||
| return `/handler/signup?${signUpSearchParams.toString()}`; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, verify the file exists and read its contents
cat -n apps/dashboard/src/lib/transfer-utils.tsRepository: hexclave/stack-auth
Length of output: 927
🏁 Script executed:
# Check if there are any server/client-specific markers in the file or directory
head -20 apps/dashboard/src/lib/transfer-utils.tsRepository: hexclave/stack-auth
Length of output: 787
🏁 Script executed:
# Check the directory structure to understand if this is a shared lib
ls -la apps/dashboard/src/lib/Repository: hexclave/stack-auth
Length of output: 1897
🏁 Script executed:
# Search for other usages of buildTransferSignUpUrl to understand context
rg "buildTransferSignUpUrl" --type ts --type tsx -B2 -A2Repository: hexclave/stack-auth
Length of output: 91
🏁 Script executed:
# Search for usages of buildTransferSignUpUrl without using file type
rg "buildTransferSignUpUrl" -B2 -A2Repository: hexclave/stack-auth
Length of output: 4058
🏁 Script executed:
# Check if this is within a client component or page (check parent directory structure)
find apps/dashboard/src -type f -name "*.tsx" -o -name "*.ts" | head -20Repository: hexclave/stack-auth
Length of output: 1739
🏁 Script executed:
# Verify the specific context - check if apps/dashboard is client-only
cat apps/dashboard/src/lib/transfer-utils.tsRepository: hexclave/stack-auth
Length of output: 787
🏁 Script executed:
# Check the project structure to understand if apps/dashboard is exclusively client-side
ls -la apps/dashboard/Repository: hexclave/stack-auth
Length of output: 1314
Prefer URL API over string interpolation when constructing the signup URL.
The function currently constructs the URL using template literal interpolation (/handler/signup?${...}). Per coding guidelines, use URL APIs for URL construction. While buildTransferSignUpUrl() is currently only called from client components and won't encounter a server context, the guideline recommends using new URL() with proper APIs rather than string concatenation.
export function buildTransferSignUpUrl(): string {
const currentUrl = new URL(window.location.href);
const signUpSearchParams = new URLSearchParams();
signUpSearchParams.set("after_auth_return_to", currentUrl.pathname + currentUrl.search + currentUrl.hash);
- return `/handler/signup?${signUpSearchParams.toString()}`;
+ const signUpUrl = new URL("/handler/signup", window.location.origin);
+ signUpUrl.search = signUpSearchParams.toString();
+ return signUpUrl.pathname + signUpUrl.search;
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/dashboard/src/lib/transfer-utils.ts` around lines 3 - 7,
buildTransferSignUpUrl currently builds the signup URL with a template literal;
instead create a URL object for "/handler/signup" using new
URL('/handler/signup', window.location.origin), set its search via the existing
signUpSearchParams (e.g., signupUrl.search = signUpSearchParams.toString() or
use signupUrl.searchParams directly), and return the relative path + search
(e.g., signupUrl.pathname + signupUrl.search) so the function uses the URL API;
update references to currentUrl and signUpSearchParams inside
buildTransferSignUpUrl accordingly.
| export function getStackAppInternals(app: unknown): StackAppInternals { | ||
| return (app as Record<symbol, StackAppInternals>)[stackAppInternalsSymbol]; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/dashboard/src/lib/transfer-utils.tsRepository: hexclave/stack-auth
Length of output: 927
🏁 Script executed:
rg -A 3 "getStackAppInternals" --type ts --type tsxRepository: hexclave/stack-auth
Length of output: 91
🏁 Script executed:
rg "getStackAppInternals" -A 3Repository: hexclave/stack-auth
Length of output: 6432
🏁 Script executed:
cat -n apps/dashboard/src/app/\(main\)/\(protected\)/\(outside-dashboard\)/projects/page-client.tsx | sed -n '1,50p'Repository: hexclave/stack-auth
Length of output: 2720
🏁 Script executed:
cat -n apps/dashboard/src/app/\(main\)/\(protected\)/\(outside-dashboard\)/new-project/page-client.tsx | sed -n '1,60p'Repository: hexclave/stack-auth
Length of output: 1985
🏁 Script executed:
rg "function getStackAppInternals" apps/dashboard/src/app/\(main\)/\(protected\)/\(outside-dashboard\)/new-project/page-client.tsx -A 10Repository: hexclave/stack-auth
Length of output: 469
🏁 Script executed:
cat -n apps/dashboard/src/app/\(main\)/integrations/transfer-confirm-page.tsx | head -30Repository: hexclave/stack-auth
Length of output: 1664
🏁 Script executed:
cat -n apps/dashboard/src/lib/stack-app-internals.tsRepository: hexclave/stack-auth
Length of output: 186
Don't bypass typing here—validate internals before returning.
This cast can return undefined as StackAppInternals and defer failure to call sites. Validate object/symbol presence before returning, then throw a clear error if assumptions are violated. Other parts of the codebase already implement this pattern correctly with type guards.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/dashboard/src/lib/transfer-utils.ts` around lines 18 - 20, The
getStackAppInternals function currently casts blindly and may return undefined;
change it to validate that app is a non-null object and that the
stackAppInternalsSymbol key exists before returning. Implement a type-guard
style check inside getStackAppInternals: verify typeof app === "object" && app
!== null and Object.prototype.hasOwnProperty.call(app as object,
stackAppInternalsSymbol), then retrieve the value, assert it matches
StackAppInternals (or narrow its type) and return it; if the checks fail, throw
a clear Error like "Missing StackAppInternals on app" so callers fail fast.
Ensure you reference stackAppInternalsSymbol and getStackAppInternals in your
fix.
dev already had a deliberate fix for the same TS2722 issue in e680159 (N2D4, "Fix lint"). The bot-comment fix on this branch diverged from it; restoring dev's `{props.reset && ... !()}` form so the merge stays clean.
| const signUpSearchParams = new URLSearchParams(); | ||
| signUpSearchParams.set("after_auth_return_to", currentUrl.pathname + currentUrl.search + currentUrl.hash); | ||
| return `/handler/signup?${signUpSearchParams.toString()}`; |
| export function getStackAppInternals(app: unknown): StackAppInternals { | ||
| return (app as Record<symbol, StackAppInternals>)[stackAppInternalsSymbol]; |
Summary by CodeRabbit
New Features
Bug Fixes
Summary
Redesigns the custom integration project-transfer confirmation page (
/integrations/custom/projects/transfer/confirm) onto the new design-components system (DesignCard+DesignAlert+DesignButton+DesignInput). The presentational shell is extracted into a reusableProjectTransferConfirmViewso the route file only handles state + API calls. The legacy Neon transfer page is split out unchanged into its own client component to keep the existing Neon × Stack co-branded UI intact.Screenshots — before and after
Custom integration — missing transfer code
Visiting
/integrations/custom/projects/transfer/confirmwith no?code=…query param.dev)Before was a raw
"Error: No transfer code provided."line. After is a dedicatedDesignAlertwith an explanation and recovery instructions.Custom integration — invalid / expired code (check endpoint fails)
dev)Before showed the raw backend error string (
Request validation failed on POST …). After uses aDesignCardwith theArrowsLeftRightIcon, a friendlier "This transfer can't continue" copy in an inlineDesignAlert, the Stack Auth logomark in the actions slot, and an explicit Close button to dismiss.Neon integration — legacy UI preserved
The Neon page (
/integrations/neon/projects/transfer/confirm) was deliberately not redesigned — it still uses the Neon × Stack co-branded card so partner-facing copy/branding stay identical. It's now its own client component (neon-transfer-confirm-page.tsx) instead of sharing the redesigned one.dev)Same shell on both sides — copy was tightened slightly ("Return to your Neon dashboard and start the transfer again") and the raw API error string is gone.
What changed
apps/dashboard/src/components/project-transfer-confirm-view.tsx— purely presentationalProjectTransferConfirmView. Owns the design-components shell, the loading spinner, the signed-in vs signed-out branches of the success state (withDesignInput+ "Use a different account" button), and the error / missing-code alerts.apps/dashboard/src/app/(main)/integrations/neon-transfer-confirm-page.tsx— extraction of the legacy Neon UI (Neon logo, Stack logo, "Project transfer" header, Card / CardContent / CardFooter). Behaviour and copy match the previoustransfer-confirm-pageexactly whentype === "neon".apps/dashboard/src/app/(main)/integrations/transfer-confirm-page.tsx— now hard-coded to thecustomintegration (no moretypeprop), defers UI toProjectTransferConfirmView, and exports aTransferConfirmMissingCodeViewused by the route whencodeis absent from the URL.app/(main)/integrations/custom/projects/transfer/confirm/page.tsx— renders the redesigned flow, falls back toTransferConfirmMissingCodeViewwhencodeis missing.app/(main)/integrations/neon/projects/transfer/confirm/page.tsx— points at the new dedicated Neon client component.apps/dashboard/src/lib/stack-app-internals.ts— consolidates the symbol-keyedgetStackAppInternals(app)helper (andstackAppInternalsSymbol) into one module with a JSDoc explainer + runtime type guard, replacing scatteredas anycasts.apps/dashboard/src/lib/transfer-utils.ts—buildTransferSignUpUrl()helper so the route file + the view stay in sync on the/handler/signup?after_auth_return_to=…query construction.Bot review follow-ups addressed in this PR
ProjectTransferConfirmView(StackAssertionErrorinstead of silent no-op).window.locationread into client-only handlers /useEffects — the page was previously evaluating it at module load./checkendpoint throws — replaces the rawKnownError<…>message with "This transfer link is invalid, has expired, or has already been used. Open the original link from the partner or integrations dashboard, or start the transfer again."runAsynchronouslyWithAlertaround every asynconClick(Transfer, Sign in, Switch account, Close) so unhandled rejections surface to the user.'was a string-attribute literal, not a JSX expression — converted to a JSX expression so it renders as'.window.close()removal in error state — replaced with a Close button that resets local state, so users on a fresh tab (no opener) aren't stuck.getStackAppInternalsconsolidated — previously three independent copies (here + two inprojects/page-client.tsx). Now one helper with a runtime type guard instead ofas any, plus a comment explaining the symbol-keyed SDK escape hatch.dev(N2D4,e68015909d "Fix lint"). Reverted infe92689ebso we don't fight that fix.Notes for reviewers
components/project-transfer-confirm-view.tsx. Everything reviewer-interesting is in the props shape (ProjectTransferConfirmUiStateunion,onPrimary/onCancel/onSwitchAccountcallbacks). The route file just wires those to thegetStackAppInternals(app).sendRequest(...)calls./integrations/custom/projects/transfer/confirm/checkand/integrations/custom/projects/transfer/confirmendpoints, same request bodies, same redirect to/projects/{project_id}on success./checkendpoint validates the code against the DB). It uses the sameDesignCardshell with either aDesignInputshowing the receiving account + a "Use a different account" outline button (signed-in branch), or aDesignAlert variant="info"prompting sign-in (signed-out branch). Worth manually testing on a real transfer before merging.Test plan
/integrations/custom/projects/transfer/confirmwith nocode→ renders the "transfer link is incomplete" alert (screenshots above)/integrations/custom/projects/transfer/confirm?code=invalid→ renders the redesigned card with the friendly error inside aDesignAlert variant="error"and a working Close button/handler/signup?after_auth_return_to=…while signed out/integrations/neon/projects/transfer/confirm?code=…→ unchanged legacy Neon × Stack co-branded card