fix(studio): route vercel deploy-button params to create despite marketplace source (#48258)

## 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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Danny WhiteandJoshen Lim authored and GitHub committed 2026-07-25 00:00:37 +10:00
1 parent 2f0a582198
commit 69570a357d
5 files changed
+93 -22

No files matched your search

@@ -117,7 +117,8 @@ export const ProjectLinker = ({
) : showNoEntitiesState && (noSupabaseProjects || noForeignProjects) ? (
<div className="text-sm text-foreground-lighter text-balance">
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.' : '.'}
</div>
) : (
<>
@@ -192,8 +193,12 @@ export const ProjectLinker = ({
<p className="text-foreground-light text-sm">
You will need to create a {missingEntity} Project to link to a {oppositeMissingEntity}{' '}
Project.
<br />
You can skip this and create a Project Connection later.
{onSkip !== undefined && (
<>
<br />
You can skip this and create a Project Connection later.
</>
)}
</p>
</div>
) : (
@@ -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':
@@ -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"
/>
@@ -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,
@@ -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()
})
})