@@ -62,6 +62,8 @@ const VercelCallbackSchema = z
6262 configurationId : z . string ( ) . optional ( ) ,
6363 teamId : z . string ( ) . nullable ( ) . optional ( ) ,
6464 next : z . string ( ) . optional ( ) ,
65+ organizationId : z . string ( ) . optional ( ) ,
66+ projectId : z . string ( ) . optional ( ) ,
6567 } )
6668 . passthrough ( ) ;
6769
@@ -296,67 +298,72 @@ async function handleMarketplaceInvokedFlow(params: {
296298 code,
297299 configurationId,
298300 integration : "vercel" ,
301+ fromMarketplace : "true" ,
299302 } ) ;
300303 if ( nextUrl ) {
301304 onboardingParams . set ( "next" , nextUrl ) ;
302305 }
303306 return redirect ( `${ confirmBasicDetailsPath ( ) } ?${ onboardingParams . toString ( ) } ` ) ;
304307 }
305308
306- // Has organizations but no projects - redirect to project creation
309+ // Check if user has organizations with projects
307310 const hasProjects = userOrganizations . some ( ( org ) => org . projects . length > 0 ) ;
308311 if ( ! hasProjects ) {
309312 const firstOrg = userOrganizations [ 0 ] ;
310313 const projectParams = new URLSearchParams ( {
311314 code,
312315 configurationId,
313316 integration : "vercel" ,
317+ fromMarketplace : "true" ,
314318 } ) ;
315319 if ( nextUrl ) {
316320 projectParams . set ( "next" , nextUrl ) ;
317321 }
318322 return redirect ( `${ newProjectPath ( { slug : firstOrg . slug } ) } ?${ projectParams . toString ( ) } ` ) ;
319323 }
320324
321- // User has orgs and projects - complete the installation
322- const tokenResponse = await exchangeCodeForToken ( code ) ;
323- if ( ! tokenResponse ) {
324- return redirectWithErrorMessage (
325- "/" ,
326- request ,
327- "Failed to connect to Vercel. Please try again."
328- ) ;
325+ // Multiple organizations - redirect to onboarding
326+ if ( userOrganizations . length > 1 ) {
327+ const selectionParams = new URLSearchParams ( {
328+ code,
329+ configurationId,
330+ } ) ;
331+ if ( nextUrl ) {
332+ selectionParams . set ( "next" , nextUrl ) ;
333+ }
334+ return redirect ( `/onboarding/vercel?${ selectionParams . toString ( ) } ` ) ;
329335 }
330336
331- const config = await VercelIntegrationRepository . getVercelIntegrationConfiguration (
332- tokenResponse . accessToken ,
333- configurationId ,
334- tokenResponse . teamId ?? null
335- ) ;
337+ // Single organization - check project count
338+ const singleOrg = userOrganizations [ 0 ] ;
339+
340+ if ( singleOrg . projects . length > 1 ) {
341+ const projectParams = new URLSearchParams ( {
342+ organizationId : singleOrg . id ,
343+ code,
344+ configurationId,
345+ } ) ;
346+ if ( nextUrl ) {
347+ projectParams . set ( "next" , nextUrl ) ;
348+ }
349+ return redirect ( `/onboarding/vercel?${ projectParams . toString ( ) } ` ) ;
350+ }
336351
337- if ( ! config ) {
352+ // Single org with single project - complete installation directly
353+ const tokenResponse = await exchangeCodeForToken ( code ) ;
354+ if ( ! tokenResponse ) {
338355 return redirectWithErrorMessage (
339356 "/" ,
340357 request ,
341- "Failed to fetch Vercel integration configuration . Please try again."
358+ "Failed to connect to Vercel. Your session may have expired . Please try again from Vercel ."
342359 ) ;
343360 }
344361
345- const userOrg = userOrganizations [ 0 ] ;
346- const userProject = userOrg . projects [ 0 ] ;
347-
348- if ( ! userProject ) {
349- const projectParams = new URLSearchParams ( {
350- code,
351- configurationId,
352- integration : "vercel" ,
353- } ) ;
354- return redirect ( `${ newProjectPath ( { slug : userOrg . slug } ) } ?${ projectParams . toString ( ) } ` ) ;
355- }
362+ const singleProject = singleOrg . projects [ 0 ] ;
356363
357364 const environment = await prisma . runtimeEnvironment . findFirst ( {
358365 where : {
359- projectId : userProject . id ,
366+ projectId : singleProject . id ,
360367 slug : "prod" ,
361368 archivedAt : null ,
362369 } ,
@@ -371,11 +378,11 @@ async function handleMarketplaceInvokedFlow(params: {
371378 }
372379
373380 const stateData : StateData = {
374- organizationId : userOrg . id ,
375- projectId : userProject . id ,
381+ organizationId : singleOrg . id ,
382+ projectId : singleProject . id ,
376383 environmentSlug : environment . slug ,
377- organizationSlug : userOrg . slug ,
378- projectSlug : userProject . slug ,
384+ organizationSlug : singleOrg . slug ,
385+ projectSlug : singleProject . slug ,
379386 } ;
380387
381388 const project = await fetchProjectWithAccess ( stateData . projectId , stateData . organizationId , userId ) ;
@@ -420,7 +427,21 @@ async function handleSelfInvokedFlow(params: {
420427 logger . error ( "Invalid Vercel OAuth state JWT" , {
421428 error : validationResult . error ,
422429 } ) ;
423- throw new Response ( "Invalid state parameter" , { status : 400 } ) ;
430+
431+ // Check if JWT has expired
432+ if ( validationResult . error ?. includes ( "expired" ) || validationResult . error ?. includes ( "Token has expired" ) ) {
433+ return redirectWithErrorMessage (
434+ "/" ,
435+ request ,
436+ "Your installation session has expired. Please start the installation again."
437+ ) ;
438+ }
439+
440+ return redirectWithErrorMessage (
441+ "/" ,
442+ request ,
443+ "Invalid installation session. Please try again."
444+ ) ;
424445 }
425446
426447 const stateData = validationResult . state ;
@@ -500,6 +521,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
500521 error_description,
501522 configurationId,
502523 next : nextUrl ,
524+ organizationId,
525+ projectId,
503526 } = parsedParams . data ;
504527
505528 // Handle errors from Vercel
@@ -514,6 +537,67 @@ export async function loader({ request }: LoaderFunctionArgs) {
514537 throw new Response ( "Missing authorization code" , { status : 400 } ) ;
515538 }
516539
540+ // Handle return from project creation with org and project IDs
541+ if ( organizationId && projectId && configurationId && ! state ) {
542+ const project = await fetchProjectWithAccess ( projectId , organizationId , authenticatedUserId ) ;
543+
544+ if ( ! project ) {
545+ logger . error ( "Project not found or user does not have access" , {
546+ projectId,
547+ organizationId,
548+ userId : authenticatedUserId ,
549+ } ) ;
550+ return redirectWithErrorMessage (
551+ "/" ,
552+ request ,
553+ "Project not found. Please try again."
554+ ) ;
555+ }
556+
557+ const tokenResponse = await exchangeCodeForToken ( code ) ;
558+ if ( ! tokenResponse ) {
559+ return redirectWithErrorMessage (
560+ "/" ,
561+ request ,
562+ "Failed to connect to Vercel. Your session may have expired. Please try again from Vercel."
563+ ) ;
564+ }
565+
566+ const environment = await prisma . runtimeEnvironment . findFirst ( {
567+ where : {
568+ projectId : project . id ,
569+ slug : "prod" ,
570+ archivedAt : null ,
571+ } ,
572+ } ) ;
573+
574+ if ( ! environment ) {
575+ return redirectWithErrorMessage (
576+ "/" ,
577+ request ,
578+ "Failed to find project environment. Please try again."
579+ ) ;
580+ }
581+
582+ const stateData : StateData = {
583+ organizationId : project . organizationId ,
584+ projectId : project . id ,
585+ environmentSlug : environment . slug ,
586+ organizationSlug : project . organization . slug ,
587+ projectSlug : project . slug ,
588+ } ;
589+
590+ return completeIntegrationSetup ( {
591+ tokenResponse,
592+ project,
593+ stateData,
594+ configurationId,
595+ nextUrl,
596+ request,
597+ logContext : "after project creation" ,
598+ } ) ;
599+ }
600+
517601 // Route to appropriate handler based on presence of state parameter
518602 if ( state ) {
519603 // Self-invoked flow: user clicked connect from our app
0 commit comments