import { getFormProps, useForm } from "@conform-to/react"; import { parseWithZod } from "@conform-to/zod"; import { CheckCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react"; import { type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { Result, fromPromise } from "neverthrow"; import { useEffect, useRef, useState } from "react"; import { typedjson, useTypedFetcher } from "remix-typedjson"; import { z } from "zod"; import { BuildSettingsFields } from "~/components/integrations/VercelBuildSettings"; import { VercelLogo } from "~/components/integrations/VercelLogo"; import { Button } from "~/components/primitives/Buttons"; import { Callout } from "~/components/primitives/Callout"; import { DateTime } from "~/components/primitives/DateTime"; import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog"; import { Fieldset } from "~/components/primitives/Fieldset"; import { FormButtons } from "~/components/primitives/FormButtons"; import { FormError } from "~/components/primitives/FormError"; import { Hint } from "~/components/primitives/Hint"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; import { Paragraph } from "~/components/primitives/Paragraph"; import { PermissionLink } from "~/components/primitives/PermissionLink"; import { Select, SelectItem } from "~/components/primitives/Select"; import { SpinnerWhite } from "~/components/primitives/Spinner"; import { redirectBackWithErrorMessage, redirectWithErrorMessage, redirectWithSuccessMessage, } from "~/models/message.server"; import { resolveOrgIdFromSlug } from "~/models/organization.server"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { VercelIntegrationRepository } from "~/models/vercelIntegration.server"; import { type VercelOnboardingData, VercelSettingsPresenter, } from "~/presenters/v3/VercelSettingsPresenter.server"; import { logger } from "~/services/logger.server"; import { rbac } from "~/services/rbac.server"; import { dashboardAction } from "~/services/routeBuilders/dashboardBuilder"; import { requireUserId } from "~/services/session.server"; import { VercelIntegrationService } from "~/services/vercelIntegration.server"; import { EnvironmentParamSchema, v3ProjectSettingsIntegrationsPath, vercelAppInstallPath, vercelResourcePath, } from "~/utils/pathBuilder"; import { type EnvSlug, type SyncEnvVarsMapping, type VercelProjectIntegrationData, envSlugArrayField, getAvailableEnvSlugs, getAvailableEnvSlugsForBuildSettings, } from "~/v3/vercel/vercelProjectIntegrationSchema"; import { sanitizeVercelNextUrl } from "~/v3/vercel/vercelUrls.server"; export type ConnectedVercelProject = { id: string; vercelProjectId: string; vercelProjectName: string; vercelTeamId: string | null; integrationData: VercelProjectIntegrationData; createdAt: Date; }; const safeJsonParse = Result.fromThrowable( (val: string) => JSON.parse(val) as Record, () => null ); function parseVercelStagingEnvironment( value: string | null | undefined ): { environmentId: string; displayName: string } | null { if (!value) return null; return safeJsonParse(value).match( (parsed) => { if (typeof parsed?.environmentId === "string" && typeof parsed?.displayName === "string") { return { environmentId: parsed.environmentId, displayName: parsed.displayName }; } return null; }, () => null ); } // Sentinel values for the clearTriggerVersion hidden input. Used by the schema transform, // the input's defaultValue, and the modal's submit helper — keep all three reading the same // constants so they cannot drift. const CLEAR_TRIGGER_VERSION_YES = "true"; const CLEAR_TRIGGER_VERSION_NO = "false"; const UpdateVercelConfigFormSchema = z.object({ action: z.literal("update-config"), atomicBuilds: envSlugArrayField, pullEnvVarsBeforeBuild: envSlugArrayField, discoverEnvVars: envSlugArrayField, vercelStagingEnvironment: z.string().nullable().optional(), autoPromote: z .string() .optional() .transform((val) => val !== "false"), clearTriggerVersion: z .string() .optional() .transform((val) => val === CLEAR_TRIGGER_VERSION_YES), }); const DisconnectVercelFormSchema = z.object({ action: z.literal("disconnect"), }); const CompleteOnboardingFormSchema = z.object({ action: z.literal("complete-onboarding"), vercelStagingEnvironment: z.string().nullable().optional(), pullEnvVarsBeforeBuild: envSlugArrayField, atomicBuilds: envSlugArrayField, discoverEnvVars: envSlugArrayField, syncEnvVarsMapping: z.string().optional(), next: z.string().optional(), skipRedirect: z .string() .optional() .transform((val) => val === "true"), origin: z.string().optional(), }); const SkipOnboardingFormSchema = z.object({ action: z.literal("skip-onboarding"), }); const SelectVercelProjectFormSchema = z.object({ action: z.literal("select-vercel-project"), vercelProjectId: z.string().min(1, "Please select a Vercel project"), vercelProjectName: z.string().min(1), }); const UpdateEnvMappingFormSchema = z.object({ action: z.literal("update-env-mapping"), vercelStagingEnvironment: z.string().nullable().optional(), }); const DisableAutoAssignFormSchema = z.object({ action: z.literal("disable-auto-assign"), }); const VercelActionSchema = z.discriminatedUnion("action", [ UpdateVercelConfigFormSchema, DisconnectVercelFormSchema, CompleteOnboardingFormSchema, SkipOnboardingFormSchema, SelectVercelProjectFormSchema, UpdateEnvMappingFormSchema, DisableAutoAssignFormSchema, ]); export async function loader({ request, params }: LoaderFunctionArgs) { const userId = await requireUserId(request); const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { throw new Response("Not Found", { status: 404 }); } const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { throw new Response("Not Found", { status: 404 }); } const presenter = new VercelSettingsPresenter(); const resultOrFail = await presenter.call({ projectId: project.id, organizationId: project.organizationId, }); if (resultOrFail.isErr()) { logger.error("Failed to load Vercel settings", { url: request.url, params, error: resultOrFail.error, }); throw new Response("Failed to load Vercel settings", { status: 500 }); } const result = resultOrFail.value; const url = new URL(request.url); const needsOnboarding = url.searchParams.get("vercelOnboarding") === "true"; const vercelEnvironmentId = url.searchParams.get("vercelEnvironmentId") || undefined; let onboardingData: VercelOnboardingData | null = null; if (needsOnboarding) { onboardingData = await presenter.getOnboardingData( project.id, project.organizationId, vercelEnvironmentId ); } const authInvalid = onboardingData?.authInvalid || result.authInvalid || false; const authError = onboardingData?.authError || result.authError; // Display flag for the connect/disconnect/configure controls — the action // enforces write:vercel independently. Permissive in OSS. const sessionAuth = await rbac.authenticateSession(request, { userId, organizationId: project.organizationId, }); const canManageVercel = sessionAuth.ok ? sessionAuth.ability.can("write", { type: "vercel" }) : true; return typedjson({ ...result, authInvalid, authError, onboardingData, organizationSlug, projectSlug: projectParam, environmentSlug: envParam, projectId: project.id, organizationId: project.organizationId, canManageVercel, }); } export const action = dashboardAction( { params: EnvironmentParamSchema, context: async (params) => { const organizationId = await resolveOrgIdFromSlug(params.organizationSlug); return organizationId ? { organizationId } : {}; }, authorization: { action: "write", resource: { type: "vercel" } }, }, async ({ request, params, user }) => { const userId = user.id; const { organizationSlug, projectParam, envParam } = params; const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { throw new Response("Not Found", { status: 404 }); } const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { throw new Response("Not Found", { status: 404 }); } const formData = await request.formData(); const submission = parseWithZod(formData, { schema: VercelActionSchema }); if (submission.status !== "success") { return json(submission.reply()); } const settingsPath = v3ProjectSettingsIntegrationsPath( { slug: organizationSlug }, { slug: projectParam }, { slug: envParam } ); const vercelService = new VercelIntegrationService(); const { action: actionType } = submission.value; switch (actionType) { case "update-config": { const { atomicBuilds, pullEnvVarsBeforeBuild, discoverEnvVars, vercelStagingEnvironment, autoPromote, clearTriggerVersion, } = submission.value; const parsedStagingEnv = parseVercelStagingEnvironment(vercelStagingEnvironment); // Get the previous staging environment before updating const previousIntegration = await vercelService.getVercelProjectIntegration(project.id); const previousStagingEnvId = previousIntegration?.parsedIntegrationData.config?.vercelStagingEnvironment ?.environmentId ?? null; const newStagingEnvId = parsedStagingEnv?.environmentId ?? null; const result = await vercelService.updateVercelIntegrationConfig(project.id, { atomicBuilds, pullEnvVarsBeforeBuild, discoverEnvVars, vercelStagingEnvironment: parsedStagingEnv, autoPromote, }); if (result) { // Sync staging TRIGGER_SECRET_KEY if the custom environment changed if (previousStagingEnvId !== newStagingEnvId) { await vercelService.syncStagingKeyForCustomEnvironment( project.id, previousStagingEnvId, newStagingEnvId ); } // When atomic deployments are being disabled and the user confirmed clearing the pin, // remove TRIGGER_VERSION from Vercel production so future deploys don't stay pinned. // If the Vercel API call fails we still consider the settings save itself successful, // but tell the user so they can clear the env var manually from the Vercel dashboard. if (clearTriggerVersion && !atomicBuilds?.includes("prod")) { const cleared = await vercelService.clearTriggerVersionFromVercelProduction(project.id); if (!cleared) { return redirectWithErrorMessage( settingsPath, request, "Vercel settings saved, but failed to clear TRIGGER_VERSION on Vercel — please remove it manually from your Vercel project settings." ); } } return redirectWithSuccessMessage( settingsPath, request, "Vercel settings updated successfully" ); } return redirectWithErrorMessage(settingsPath, request, "Failed to update Vercel settings"); } case "disconnect": { const success = await vercelService.disconnectVercelProject(project.id); if (success) { return redirectWithSuccessMessage(settingsPath, request, "Vercel project disconnected"); } return redirectWithErrorMessage( settingsPath, request, "Failed to disconnect Vercel project" ); } case "complete-onboarding": { const { vercelStagingEnvironment, pullEnvVarsBeforeBuild, atomicBuilds, discoverEnvVars, syncEnvVarsMapping, next, skipRedirect, origin, } = submission.value; const parsedStagingEnv = parseVercelStagingEnvironment(vercelStagingEnvironment); const parsedSyncEnvVarsMapping = syncEnvVarsMapping ? (safeJsonParse(syncEnvVarsMapping).unwrapOr(undefined) as | SyncEnvVarsMapping | undefined) : undefined; const result = await vercelService.completeOnboarding(project.id, { vercelStagingEnvironment: parsedStagingEnv, pullEnvVarsBeforeBuild, atomicBuilds, discoverEnvVars, syncEnvVarsMapping: parsedSyncEnvVarsMapping, origin: origin === "marketplace" ? "marketplace" : "dashboard", }); if (result) { if (skipRedirect) { return json({ success: true }); } if (next) { const sanitizedNext = sanitizeVercelNextUrl(next); if (sanitizedNext) { return json({ success: true, redirectTo: sanitizedNext }); } logger.warn("Rejected next URL - not same-origin or vercel.com", { next }); } return json({ success: true, redirectTo: settingsPath }); } return redirectWithErrorMessage(settingsPath, request, "Failed to complete Vercel setup"); } case "update-env-mapping": { const { vercelStagingEnvironment } = submission.value; const parsedStagingEnv = parseVercelStagingEnvironment(vercelStagingEnvironment); const result = await vercelService.updateVercelIntegrationConfig(project.id, { vercelStagingEnvironment: parsedStagingEnv, }); if (result) { // During onboarding there's no previous custom environment — just upsert await vercelService.syncStagingKeyForCustomEnvironment( project.id, null, parsedStagingEnv?.environmentId ?? null ); return json({ success: true }); } return json( { success: false, error: "Failed to update environment mapping" }, { status: 400 } ); } case "skip-onboarding": { return redirectWithSuccessMessage( settingsPath, request, "Vercel integration setup skipped" ); } case "select-vercel-project": { const { vercelProjectId, vercelProjectName } = submission.value; const selectResult = await fromPromise( vercelService.selectVercelProject({ organizationId: project.organizationId, projectId: project.id, vercelProjectId, vercelProjectName, userId, }), (error) => error ); if (selectResult.isErr()) { logger.error("Failed to select Vercel project", { error: selectResult.error }); return json({ error: "Failed to connect Vercel project. Please try again.", }); } const { integration, syncResult } = selectResult.value; if (!syncResult.success && syncResult.errors.length > 0) { logger.warn("Failed to send trigger secrets to Vercel", { projectId: project.id, vercelProjectId, errors: syncResult.errors, }); } return json({ success: true, integrationId: integration.id, syncErrors: syncResult.errors, }); } case "disable-auto-assign": { const orgIntegration = await VercelIntegrationRepository.findVercelOrgIntegrationForProject( project.id ); if (!orgIntegration) { return redirectWithErrorMessage(settingsPath, request, "No Vercel integration found"); } const projectIntegration = await vercelService.getVercelProjectIntegration(project.id); if (!projectIntegration) { return redirectWithErrorMessage(settingsPath, request, "No Vercel project connected"); } const teamId = await VercelIntegrationRepository.getTeamIdFromIntegration(orgIntegration); const disableResult = await VercelIntegrationRepository.getVercelClient( orgIntegration ).andThen((client) => VercelIntegrationRepository.disableAutoAssignCustomDomains( client, projectIntegration.parsedIntegrationData.vercelProjectId, teamId ) ); if (disableResult.isErr()) { logger.error("Failed to disable auto-assign custom domains", { error: disableResult.error, }); return redirectWithErrorMessage( settingsPath, request, "Failed to disable auto-assign custom domains" ); } return redirectWithSuccessMessage( settingsPath, request, "Auto-assign custom domains disabled" ); } default: { submission.value satisfies never; return redirectBackWithErrorMessage(request, "Failed to process request"); } } } ); function VercelConnectionPrompt({ organizationSlug, projectSlug, environmentSlug, hasOrgIntegration, isGitHubConnected, onOpenModal, isLoading, canManageVercel = true, }: { organizationSlug: string; projectSlug: string; environmentSlug: string; hasOrgIntegration: boolean; isGitHubConnected: boolean; onOpenModal?: () => void; isLoading?: boolean; canManageVercel?: boolean; }) { const installPath = vercelAppInstallPath(organizationSlug, projectSlug); const handleConnectProject = () => { if (onOpenModal) { onOpenModal(); } }; const isLoadingProjects = isLoading ?? false; const isDisabled = isLoadingProjects || !onOpenModal; return (
{hasOrgIntegration ? ( <> Vercel app is installed {!onOpenModal && ( Please reconnect Vercel to continue )} ) : ( <> } > Install Vercel app )}
); } function VercelAuthInvalidBanner({ organizationSlug, projectSlug, canManageVercel = true, }: { organizationSlug: string; projectSlug: string; canManageVercel?: boolean; }) { const installUrl = vercelAppInstallPath(organizationSlug, projectSlug); return (

Vercel connection expired

Your Vercel access token has expired or been revoked. Please reconnect to restore functionality.

Reconnect Vercel
); } function VercelGitHubWarning() { return (

GitHub integration is not connected. Vercel integration cannot sync environment variables and link deployments without a properly installed GitHub integration.

); } function envSlugLabel(slug: EnvSlug): string { switch (slug) { case "prod": return "Production"; case "stg": return "Staging"; case "preview": return "Preview"; case "dev": return "Development"; } } function ConnectedVercelProjectForm({ connectedProject, hasStagingEnvironment, hasPreviewEnvironment, customEnvironments, autoAssignCustomDomains, currentTriggerVersion, currentTriggerVersionFetchFailed, organizationSlug, projectSlug, environmentSlug, canManageVercel = true, }: { connectedProject: ConnectedVercelProject; hasStagingEnvironment: boolean; hasPreviewEnvironment: boolean; customEnvironments: Array<{ id: string; slug: string }>; autoAssignCustomDomains: boolean | null; currentTriggerVersion: string | null; currentTriggerVersionFetchFailed: boolean; organizationSlug: string; projectSlug: string; environmentSlug: string; canManageVercel?: boolean; }) { const lastSubmission = useActionData() as any; const navigation = useNavigation(); const [hasConfigChanges, setHasConfigChanges] = useState(false); const [configValues, setConfigValues] = useState({ atomicBuilds: connectedProject.integrationData.config.atomicBuilds ?? [], pullEnvVarsBeforeBuild: connectedProject.integrationData.config.pullEnvVarsBeforeBuild ?? [], discoverEnvVars: connectedProject.integrationData.config.discoverEnvVars ?? [], vercelStagingEnvironment: connectedProject.integrationData.config.vercelStagingEnvironment ?? null, autoPromote: connectedProject.integrationData.config.autoPromote ?? true, }); const originalAtomicBuilds = connectedProject.integrationData.config.atomicBuilds ?? []; const originalPullEnvVars = connectedProject.integrationData.config.pullEnvVarsBeforeBuild ?? []; const originalDiscoverEnvVars = connectedProject.integrationData.config.discoverEnvVars ?? []; const originalStagingEnv = connectedProject.integrationData.config.vercelStagingEnvironment ?? null; const originalAutoPromote = connectedProject.integrationData.config.autoPromote ?? true; useEffect(() => { const atomicBuildsChanged = JSON.stringify([...configValues.atomicBuilds].sort()) !== JSON.stringify([...originalAtomicBuilds].sort()); const pullEnvVarsChanged = JSON.stringify([...configValues.pullEnvVarsBeforeBuild].sort()) !== JSON.stringify([...originalPullEnvVars].sort()); const discoverEnvVarsChanged = JSON.stringify([...configValues.discoverEnvVars].sort()) !== JSON.stringify([...originalDiscoverEnvVars].sort()); const stagingEnvChanged = configValues.vercelStagingEnvironment?.environmentId !== originalStagingEnv?.environmentId; const autoPromoteChanged = configValues.autoPromote !== originalAutoPromote; setHasConfigChanges( atomicBuildsChanged || pullEnvVarsChanged || discoverEnvVarsChanged || stagingEnvChanged || autoPromoteChanged ); }, [ configValues, originalAtomicBuilds, originalPullEnvVars, originalDiscoverEnvVars, originalStagingEnv, originalAutoPromote, ]); const [configForm, _fields] = useForm({ id: "update-vercel-config", lastResult: lastSubmission, shouldRevalidate: "onSubmit", onValidate({ formData }) { return parseWithZod(formData, { schema: UpdateVercelConfigFormSchema, }); }, }); const saveButtonRef = useRef(null); const clearTriggerVersionInputRef = useRef(null); const [showClearDialog, setShowClearDialog] = useState(false); // Modal trigger uses the page-load state of atomicBuilds, not whatever changed in-session, // because clearing TRIGGER_VERSION only makes sense when atomic was actually on at load time. // If the Vercel lookup failed we still prompt — we don't know whether a pin exists, so the // user needs to make the call explicitly rather than silently leaving prod pinned. const wasAtomicEnabledAtLoad = originalAtomicBuilds.includes("prod"); const isAtomicNowDisabled = !configValues.atomicBuilds.includes("prod"); const shouldPromptClearOnSave = wasAtomicEnabledAtLoad && isAtomicNowDisabled && (Boolean(currentTriggerVersion) || currentTriggerVersionFetchFailed); const submitWithClearChoice = (clear: boolean) => { if (clearTriggerVersionInputRef.current) { clearTriggerVersionInputRef.current.value = clear ? CLEAR_TRIGGER_VERSION_YES : CLEAR_TRIGGER_VERSION_NO; } setShowClearDialog(false); // Conform owns the form's React ref via {...configForm.props}, so look it up by id // (set via useForm({ id: "update-vercel-config" })) rather than fighting for the ref. const form = document.getElementById("update-vercel-config") as HTMLFormElement | null; form?.requestSubmit(saveButtonRef.current ?? undefined); }; const isConfigLoading = navigation.formData?.get("action") === "update-config" && (navigation.state === "submitting" || navigation.state === "loading"); const actionUrl = vercelResourcePath(organizationSlug, projectSlug, environmentSlug); const availableEnvSlugs = getAvailableEnvSlugs(hasStagingEnvironment, hasPreviewEnvironment); const availableEnvSlugsForBuildSettings = getAvailableEnvSlugsForBuildSettings( hasStagingEnvironment, hasPreviewEnvironment ); const disabledEnvSlugsForBuildSettings: Partial> | undefined = hasStagingEnvironment && !configValues.vercelStagingEnvironment ? { stg: "Map a custom Vercel environment to Staging to enable this" } : undefined; const _formatSelectedEnvs = ( selected: EnvSlug[], availableSlugs: EnvSlug[] = availableEnvSlugs ): string => { if (selected.length === 0) return "None selected"; if (selected.length === availableSlugs.length) return "All environments"; return selected.map(envSlugLabel).join(", "); }; return ( <>
{connectedProject.vercelProjectName}
Disconnect Vercel project
Are you sure you want to disconnect{" "} {connectedProject.vercelProjectName}? This will stop pulling environment variables and disable atomic deployments. } cancelButton={ } />
{/* Configuration form */}
{/* Flipped to CLEAR_TRIGGER_VERSION_YES by the clear-pinned-version modal on submit. */}
{/* Staging environment mapping */} {hasStagingEnvironment && customEnvironments && customEnvironments.length > 0 && (
Select which custom Vercel environment should map to Trigger.dev's Staging environment.
)} setConfigValues((prev) => ({ ...prev, pullEnvVarsBeforeBuild: slugs })) } discoverEnvVars={configValues.discoverEnvVars} onDiscoverEnvVarsChange={(slugs) => setConfigValues((prev) => ({ ...prev, discoverEnvVars: slugs })) } atomicBuilds={configValues.atomicBuilds} onAtomicBuildsChange={(slugs) => setConfigValues((prev) => ({ ...prev, atomicBuilds: slugs })) } envVarsConfigLink={`/orgs/${organizationSlug}/projects/${projectSlug}/env/${environmentSlug}/environment-variables`} disabledEnvSlugs={disabledEnvSlugsForBuildSettings} autoPromote={configValues.autoPromote} onAutoPromoteChange={(value) => setConfigValues((prev) => ({ ...prev, autoPromote: value })) } currentTriggerVersion={currentTriggerVersion} currentTriggerVersionFetchFailed={currentTriggerVersionFetchFailed} hideSectionToggles /> {/* Warning: autoAssignCustomDomains must be disabled for atomic deployments */} {autoAssignCustomDomains !== false && configValues.atomicBuilds.includes("prod") && (

Atomic deployments require the "Auto-assign Custom Domains" setting to be disabled on your Vercel project. Without this, Vercel will promote deployments before Trigger.dev is ready.

)}
{configForm.errors}
{ if (shouldPromptClearOnSave) { event.preventDefault(); setShowClearDialog(true); } }} > Save } />
Clear TRIGGER_VERSION from Vercel?
{currentTriggerVersion ? ( Atomic deployments are being turned off. The{" "} TRIGGER_VERSION env var on your Vercel production environment is currently set to{" "} {currentTriggerVersion}. ) : ( Atomic deployments are being turned off. We couldn't reach Vercel to confirm whether{" "} TRIGGER_VERSION is currently set on your Vercel production environment, so please verify in the Vercel dashboard. )} If you leave it, your Vercel project will stay pinned to this version. Since atomic deployments will be off, Trigger.dev will no longer update this variable, and future Vercel deploys will continue using this pinned version. We recommend clearing it.
} cancelButton={ } />
); } function VercelSettingsPanel({ organizationSlug, projectSlug, environmentSlug, onOpenVercelModal, isLoadingVercelData, }: { organizationSlug: string; projectSlug: string; environmentSlug: string; onOpenVercelModal?: () => void; isLoadingVercelData?: boolean; }) { const fetcher = useTypedFetcher(); const _location = useLocation(); const data = fetcher.data; const [hasError, _setHasError] = useState(false); const [hasFetched, setHasFetched] = useState(false); useEffect(() => { if (!data?.authInvalid && !hasError && !data && !hasFetched) { fetcher.load(vercelResourcePath(organizationSlug, projectSlug, environmentSlug)); setHasFetched(true); } }, [ organizationSlug, projectSlug, environmentSlug, data?.authInvalid, hasError, data, hasFetched, ]); if (hasError) { return (

Failed to load Vercel settings

There was an error loading the Vercel integration settings. Please refresh the page to try again.

); } if (fetcher.state === "loading" && !data) { return (
Loading Vercel settings...
); } if (!data || !data.enabled) { return null; } const showGitHubWarning = data.connectedProject && !data.isGitHubConnected; const showAuthInvalid = data.authInvalid || data.onboardingData?.authInvalid; if (data.connectedProject) { return ( <> {showAuthInvalid && ( )} {showGitHubWarning && } {!showAuthInvalid && ( )} ); } return (
{showAuthInvalid && ( )} {!showAuthInvalid && ( <> {data.hasOrgIntegration ? "Connect your Vercel project to pull environment variables and trigger builds automatically." : "Install the Vercel app to connect your projects and pull environment variables."} {!data.isGitHubConnected && ( GitHub integration is not connected. Vercel integration cannot sync environment variables and link deployments without a properly installed GitHub integration. )} )}
); } import { VercelOnboardingModal } from "~/components/integrations/VercelOnboardingModal"; export { VercelOnboardingModal, VercelSettingsPanel };