From 69570a357d97ba8c15894faa9a3df41bbe2bf2cf Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:00:37 -0400 Subject: [PATCH] fix(studio): route vercel deploy-button params to create despite marketplace source (#48258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Bug fix for the Vercel Deploy Button → Studio handoff. ## What is the current behavior? Vercel sometimes opens our install popup with `source=marketplace` while still sending Deploy Button params (`currentProjectId`, `external-id`). We trust `source` alone, so users are routed to choose-project (connect) instead of create — which is why create never gets reached in the Deploy Button flow. ## What is the new behavior? - When both Deploy Button signals (`currentProjectId` + `externalId`) are present, route to create even if Vercel sent `source=marketplace` / `external` - Hide Skip (and related empty-state copy) on choose-project when those signals are present, so Deploy Button users can't continue without linking ## Additional context Stacked on #48230. Test plan: - [ ] Unit tests for `resolveVercelInstallSource` / `hasVercelDeployButtonSignals` pass - [ ] Deploy Button flow with mislabeled `source=marketplace` + both params → lands on create after org install/continue - [ ] Genuine marketplace install (no `currentProjectId`/`external-id`) → still lands on choose-project with Skip available - [ ] If choose-project is opened with both Deploy Button params, Skip is hidden ## Summary by CodeRabbit - **New Features** - Improved Vercel installation handling for Deploy Button workflows, ensuring the correct setup path is selected. - Added clearer project-connection guidance when no projects are available (including conditional skip copy). - **Bug Fixes** - Prevented Deploy Button installations from incorrectly offering a skip option. - Preserved the skip-and-connect-later guidance for other Vercel installation flows. - Improved recognition of Deploy Button installations even when the reported Vercel source differs. --------- Co-authored-by: Joshen Lim --- .../VercelGithub/ProjectLinker.tsx | 11 ++-- .../lib/integrations/vercel-install.utils.ts | 28 ++++++++++- .../[slug]/marketplace/choose-project.tsx | 18 ++++--- .../pages/integrations/vercel/install.tsx | 8 ++- .../integrations/vercel/install.utils.test.ts | 50 +++++++++++++++---- 5 files changed, 93 insertions(+), 22 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx b/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx index 8ea17f52cf8..2a491957170 100644 --- a/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx +++ b/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx @@ -117,7 +117,8 @@ export const ProjectLinker = ({ ) : showNoEntitiesState && (noSupabaseProjects || noForeignProjects) ? (
No {missingEntity} projects found. Create a {missingEntity} project to link to a{' '} - {oppositeMissingEntity} project, or skip and connect later. + {oppositeMissingEntity} project + {onSkip !== undefined ? ', or skip and connect later.' : '.'}
) : ( <> @@ -192,8 +193,12 @@ export const ProjectLinker = ({

You will need to create a {missingEntity} Project to link to a {oppositeMissingEntity}{' '} Project. -
- You can skip this and create a Project Connection later. + {onSkip !== undefined && ( + <> +
+ You can skip this and create a Project Connection later. + + )}

) : ( diff --git a/apps/studio/lib/integrations/vercel-install.utils.ts b/apps/studio/lib/integrations/vercel-install.utils.ts index 837ffda50dc..8eaac03c080 100644 --- a/apps/studio/lib/integrations/vercel-install.utils.ts +++ b/apps/studio/lib/integrations/vercel-install.utils.ts @@ -13,9 +13,33 @@ export function getErrorMessage(error: unknown): string | undefined { export type VercelInstallSource = 'deploy-button' | 'marketplace' | 'external' -export function getVercelInstallSource( +/** + * Vercel sometimes sends source=marketplace with Deploy Button params + * (currentProjectId + external-id). Treat that as deploy-button for routing. + */ +export function hasVercelDeployButtonSignals({ + currentProjectId, + externalId, +}: { + currentProjectId?: string + externalId?: string +}): boolean { + return Boolean(currentProjectId && externalId) +} + +export function resolveVercelInstallSource({ + source, + currentProjectId, + externalId, +}: { source: string | undefined -): VercelInstallSource | undefined { + currentProjectId?: string + externalId?: string +}): VercelInstallSource | undefined { + if (hasVercelDeployButtonSignals({ currentProjectId, externalId })) { + return 'deploy-button' + } + switch (source) { case 'deploy-button': case 'marketplace': diff --git a/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx b/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx index be6674e039f..7febfc533af 100644 --- a/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx +++ b/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx @@ -28,6 +28,7 @@ import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import { withAuth } from '@/hooks/misc/withAuth' import { BASE_PATH } from '@/lib/constants' import { getErrorMessage } from '@/lib/get-error-message' +import { hasVercelDeployButtonSignals } from '@/lib/integrations/vercel-install.utils' import { buildStudioPageTitle } from '@/lib/page-title' import { useProfileNameAndPicture } from '@/lib/profile' import { EMPTY_ARR } from '@/lib/void' @@ -40,9 +41,10 @@ const PAGE_TITLE = buildStudioPageTitle({ }) const VercelChooseProjectPage: NextPageWithLayout = () => { - const { slug, configurationId, next, currentProjectId } = useParams() + const { slug, configurationId, next, currentProjectId, externalId } = useParams() const { username, primaryEmail, avatarUrl } = useProfileNameAndPicture() const displayName = primaryEmail ?? username ?? '' + const isDeployButtonFlow = hasVercelDeployButtonSignals({ currentProjectId, externalId }) const { data: integrationData, @@ -199,11 +201,15 @@ const VercelChooseProjectPage: NextPageWithLayout = () => { getForeignProjectIcon={getForeignProjectIcon} choosePrompt="Choose Vercel project" defaultForeignProjectId={currentProjectId} - onSkip={() => { - if (next && isVercelUrl(next)) { - window.location.href = next - } - }} + onSkip={ + isDeployButtonFlow + ? undefined + : () => { + if (next && isVercelUrl(next)) { + window.location.href = next + } + } + } loadingForeignProjects={isLoadingVercelProjectsData} mode="Vercel" /> diff --git a/apps/studio/pages/integrations/vercel/install.tsx b/apps/studio/pages/integrations/vercel/install.tsx index 9defd01e649..abc1c711021 100644 --- a/apps/studio/pages/integrations/vercel/install.tsx +++ b/apps/studio/pages/integrations/vercel/install.tsx @@ -32,7 +32,7 @@ import { withAuth } from '@/hooks/misc/withAuth' import { buildVercelInstallRouteQuery, getErrorMessage, - getVercelInstallSource, + resolveVercelInstallSource, } from '@/lib/integrations/vercel-install.utils' import { buildStudioPageTitle } from '@/lib/page-title' import { useProfileNameAndPicture } from '@/lib/profile' @@ -115,7 +115,11 @@ const VercelIntegration: NextPageWithLayout = () => { */ function handleRouteChange() { const orgSlug = selectedOrg?.slug - const vercelInstallSource = getVercelInstallSource(source) + const vercelInstallSource = resolveVercelInstallSource({ + source, + currentProjectId, + externalId, + }) const query = buildVercelInstallRouteQuery({ source: vercelInstallSource, organizationSlug: orgSlug, diff --git a/apps/studio/tests/pages/integrations/vercel/install.utils.test.ts b/apps/studio/tests/pages/integrations/vercel/install.utils.test.ts index b8109105f60..7d96716d14d 100644 --- a/apps/studio/tests/pages/integrations/vercel/install.utils.test.ts +++ b/apps/studio/tests/pages/integrations/vercel/install.utils.test.ts @@ -3,7 +3,8 @@ import { describe, expect, test } from 'vitest' import { buildVercelInstallRouteQuery, getErrorMessage, - getVercelInstallSource, + hasVercelDeployButtonSignals, + resolveVercelInstallSource, } from '@/lib/integrations/vercel-install.utils' describe('getErrorMessage', () => { @@ -89,15 +90,46 @@ describe('buildVercelInstallRouteQuery', () => { }) }) -describe('getVercelInstallSource', () => { - test('returns supported Vercel install sources', () => { - expect(getVercelInstallSource('deploy-button')).toBe('deploy-button') - expect(getVercelInstallSource('marketplace')).toBe('marketplace') - expect(getVercelInstallSource('external')).toBe('external') +describe('hasVercelDeployButtonSignals', () => { + test('requires both currentProjectId and externalId', () => { + expect( + hasVercelDeployButtonSignals({ + currentProjectId: 'prj_123', + externalId: 'https://github.com/org/repo', + }) + ).toBe(true) + expect(hasVercelDeployButtonSignals({ currentProjectId: 'prj_123' })).toBe(false) + expect(hasVercelDeployButtonSignals({ externalId: 'https://github.com/org/repo' })).toBe(false) + expect(hasVercelDeployButtonSignals({})).toBe(false) }) +}) - test('returns undefined for unsupported sources', () => { - expect(getVercelInstallSource('deploybutton')).toBeUndefined() - expect(getVercelInstallSource(undefined)).toBeUndefined() +describe('resolveVercelInstallSource', () => { + test('overrides marketplace and external when deploy-button signals are present', () => { + expect( + resolveVercelInstallSource({ + source: 'marketplace', + currentProjectId: 'prj_123', + externalId: 'https://github.com/org/repo', + }) + ).toBe('deploy-button') + expect( + resolveVercelInstallSource({ + source: 'external', + currentProjectId: 'prj_123', + externalId: 'https://github.com/org/repo', + }) + ).toBe('deploy-button') + }) + + test('keeps the declared source when deploy-button signals are incomplete', () => { + expect( + resolveVercelInstallSource({ + source: 'marketplace', + currentProjectId: 'prj_123', + }) + ).toBe('marketplace') + expect(resolveVercelInstallSource({ source: 'deploy-button' })).toBe('deploy-button') + expect(resolveVercelInstallSource({ source: undefined })).toBeUndefined() }) })