Files
supabase/apps/studio/lib/integrations/vercel-install.utils.ts
Danny White 69570a357d 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>
2026-07-25 00:00:37 +10:00

98 lines
2.3 KiB
TypeScript

export function getErrorMessage(error: unknown): string | undefined {
if (error instanceof Error) return error.message
if (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as { message: unknown }).message === 'string'
) {
return (error as { message: string }).message
}
return undefined
}
export type VercelInstallSource = 'deploy-button' | 'marketplace' | 'external'
/**
* 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
currentProjectId?: string
externalId?: string
}): VercelInstallSource | undefined {
if (hasVercelDeployButtonSignals({ currentProjectId, externalId })) {
return 'deploy-button'
}
switch (source) {
case 'deploy-button':
case 'marketplace':
case 'external':
return source
default:
return undefined
}
}
type BuildVercelInstallRouteQueryArgs = {
source?: VercelInstallSource
organizationSlug?: string
configurationId?: string
currentProjectId?: string
externalId?: string
next?: string
}
function removeUndefinedValues(query: Record<string, string | undefined>) {
return Object.fromEntries(
Object.entries(query).filter((entry): entry is [string, string] => entry[1] !== undefined)
)
}
export function buildVercelInstallRouteQuery({
source,
organizationSlug,
configurationId,
currentProjectId,
externalId,
next,
}: BuildVercelInstallRouteQueryArgs) {
switch (source) {
case 'deploy-button':
return removeUndefinedValues({
organizationSlug,
currentProjectId,
externalId,
next,
})
case 'marketplace':
case 'external':
// Keep Deploy Button ids when present so choose-project → create can seed + link.
return removeUndefinedValues({
organizationSlug,
configurationId,
currentProjectId,
externalId,
next,
})
default:
return removeUndefinedValues({ organizationSlug })
}
}