From 630212c138982b31220ff84e25375957d2911df9 Mon Sep 17 00:00:00 2001 From: Oskar Otwinowski Date: Wed, 26 Aug 2026 14:04:20 +0200 Subject: [PATCH 1/4] fix(webapp): show build settings when the GitHub app is disabled The integrations page wrapped the Git section, the Vercel section and the build settings in a single `githubAppEnabled` guard, so with the GitHub app off the page rendered an empty container. The Vercel section genuinely depends on GitHub - it cannot sync environment variables or link deployments without a connected repo - so it stays inside the guard. Build settings do not: they also apply to CLI deploys run with --native-build-server, exactly as the section describes. They now render regardless. TRI-13488 --- .../route.tsx | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx index 864cc300fa4..0e46cacf856 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx @@ -375,24 +375,26 @@ export default function IntegrationsSettingsPage() { /> )} - - - - Applies to deployments triggered from GitHub, and CLI deployments run with the{" "} - - --native-build-server - {" "} - flag. - - } - /> - - )} + + {/* Build settings also drive CLI deploys, so they stay available when the + GitHub app is disabled and the Git/Vercel sections above are hidden. */} + + + Applies to deployments triggered from GitHub, and CLI deployments run with the{" "} + + --native-build-server + {" "} + flag. + + } + /> + + {/* Vercel Onboarding Modal */} From fbf5e02a51acc5d93809a5dc1d6b99c399d73fa7 Mon Sep 17 00:00:00 2001 From: Oskar Otwinowski Date: Wed, 26 Aug 2026 14:05:18 +0200 Subject: [PATCH 2/4] fix(webapp): stop the Vercel onboarding modal spinning forever `computeInitialState` starts in "loading-projects" whenever the org has a Vercel integration but no onboarding data yet. The effect that escapes that state waits for `availableProjects !== undefined`, so when `getOnboardingData` returns null - it does that on any thrown error, and when the org integration row is missing - nothing ever arrives and the modal spins indefinitely with no explanation. The route knows the difference between "still loading" and "loaded nothing", since its fetcher always requests the onboarding data. It now passes that down, and the modal shows what went wrong plus a way to retry or check the integration's access on Vercel. TRI-13488 --- .../integrations/VercelOnboardingModal.tsx | 35 +++++++++++++++++-- .../route.tsx | 5 +++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx b/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx index f658424b51e..2808fb552f2 100644 --- a/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx +++ b/apps/webapp/app/components/integrations/VercelOnboardingModal.tsx @@ -99,6 +99,7 @@ export function VercelOnboardingModal({ hasStagingEnvironment, hasPreviewEnvironment, hasOrgIntegration, + onboardingDataUnavailable = false, nextUrl, onDataReload, vercelManageAccessUrl, @@ -112,6 +113,8 @@ export function VercelOnboardingModal({ hasStagingEnvironment: boolean; hasPreviewEnvironment: boolean; hasOrgIntegration: boolean; + /** The onboarding fetch settled without returning data - show an error instead of spinning. */ + onboardingDataUnavailable?: boolean; nextUrl?: string; onDataReload?: (vercelStagingEnvironment?: string) => void; vercelManageAccessUrl?: string; @@ -772,9 +775,35 @@ export function VercelOnboardingModal({ Set up Vercel Integration -
- -
+ {onboardingDataUnavailable ? ( +
+ + We couldn't load your Vercel projects. The integration may have been removed or lost + access to this organization on Vercel. + +
+ {onDataReload && ( + + )} + {vercelManageAccessUrl && ( + + Manage access on Vercel + + )} +
+
+ ) : ( +
+ +
+ )} ); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx index 0e46cacf856..b6c697187a6 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx @@ -223,6 +223,10 @@ export default function IntegrationsSettingsPage() { const loadVercelOnboarding = vercelFetcher.load; const onboardingData = vercelFetcher.data?.onboardingData ?? null; const hasVercelFetcherData = vercelFetcher.data !== undefined; + // The fetcher always requests `?vercelOnboarding=true`, so a settled load with no + // onboardingData means the presenter returned null - not that we simply didn't ask. + const onboardingDataUnavailable = + hasVercelFetcherData && vercelFetcher.state === "idle" && onboardingData === null; const vercelOnboardingPath = `${vercelResourcePath( organization.slug, project.slug, @@ -409,6 +413,7 @@ export default function IntegrationsSettingsPage() { hasStagingEnvironment={vercelFetcher.data?.hasStagingEnvironment ?? false} hasPreviewEnvironment={vercelFetcher.data?.hasPreviewEnvironment ?? false} hasOrgIntegration={vercelFetcher.data?.hasOrgIntegration ?? false} + onboardingDataUnavailable={onboardingDataUnavailable} nextUrl={nextUrl ?? undefined} vercelManageAccessUrl={vercelFetcher.data?.vercelManageAccessUrl} onDataReload={(vercelEnvironmentId) => { From 3aded7b1e4d62f9eff17e2916dcc5b611d774069 Mon Sep 17 00:00:00 2001 From: Oskar Otwinowski Date: Wed, 26 Aug 2026 14:05:53 +0200 Subject: [PATCH 3/4] fix(webapp): match preview environment on slug, not type VercelSettingsPresenter asked for a PREVIEW-typed environment with no filter on the parent, so any preview *branch* row satisfied it - branches are PREVIEW rows too. GitHubSettingsPresenter and ProjectSettingsService ask the same question via the "preview" slug, which only the branchable parent carries, so the two presenters feeding the same page could disagree. All four checks now match on slug. TRI-13488 --- .../app/presenters/v3/VercelSettingsPresenter.server.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts b/apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts index 10a46c01b3a..4c9cdda07b5 100644 --- a/apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts @@ -173,6 +173,9 @@ export class VercelSettingsPresenter extends BasePresenter { }) ).map((repo) => repo !== null); + // Match on slug, not type: preview branches are PREVIEW rows too, and only the + // branchable parent carries the "preview" slug. Keeps this in step with + // GitHubSettingsPresenter and ProjectSettingsService, which ask the same question. const checkStagingEnvironment = () => fromPromise( (this._replica as PrismaClient).runtimeEnvironment.findFirst({ @@ -181,7 +184,7 @@ export class VercelSettingsPresenter extends BasePresenter { }, where: { projectId, - type: "STAGING", + slug: "stg", }, }), (error) => ({ @@ -198,7 +201,7 @@ export class VercelSettingsPresenter extends BasePresenter { }, where: { projectId, - type: "PREVIEW", + slug: "preview", }, }), (error) => ({ From 172de2395a2a8e806284cb913efb53e7b5054d0e Mon Sep 17 00:00:00 2001 From: Oskar Otwinowski Date: Wed, 26 Aug 2026 14:11:35 +0200 Subject: [PATCH 4/4] chore(webapp): remove the remaining dead code in the Vercel integration UI Follows the two unreachable panels removed in the parent branch. None of this is reachable either: - The `"installing"` OnboardingState is unproducible - no setState call ever yields it - so its redirect effect, switch arm, isLoadingState conjunct and the vercelAppInstallPath import it was the only user of are all dead. - `(state as string) !== "completed"` is inside a branch where TypeScript has already narrowed "completed" out; the cast is what let it compile. - `hideSectionToggles` was only ever passed alongside layout="settings" but only read inside layout="card" blocks, so it could never take effect. - A handful of unused bindings and the helpers only they referenced: envSlugLabel, _formatSelectedEnvs, _CompleteOnboardingForm, _handleFinishOnboarding and friends. No behaviour change. TRI-13488 --- .../integrations/VercelBuildSettings.tsx | 6 +-- .../integrations/VercelOnboardingModal.tsx | 47 ++----------------- ...cts.$projectParam.env.$envParam.vercel.tsx | 33 +------------ 3 files changed, 7 insertions(+), 79 deletions(-) diff --git a/apps/webapp/app/components/integrations/VercelBuildSettings.tsx b/apps/webapp/app/components/integrations/VercelBuildSettings.tsx index e3be9a4f90e..56bc785368c 100644 --- a/apps/webapp/app/components/integrations/VercelBuildSettings.tsx +++ b/apps/webapp/app/components/integrations/VercelBuildSettings.tsx @@ -49,7 +49,6 @@ type BuildSettingsFieldsProps = { * the pin status is unknown — distinct from "not set". */ currentTriggerVersionFetchFailed?: boolean; /** Hide the section-level master toggles for "Pull env vars" and "Discover new env vars". */ - hideSectionToggles?: boolean; showAtomicDeployments?: boolean; layout?: "settings" | "card"; }; @@ -68,7 +67,6 @@ export function BuildSettingsFields({ onAutoPromoteChange, currentTriggerVersion, currentTriggerVersionFetchFailed, - hideSectionToggles, showAtomicDeployments = true, layout = "card", }: BuildSettingsFieldsProps) { @@ -222,7 +220,7 @@ export function BuildSettingsFields({
- {!hideSectionToggles && availableEnvSlugs.length > 1 && ( + {availableEnvSlugs.length > 1 && (
- {!hideSectionToggles && availableEnvSlugs.length > 1 && ( + {availableEnvSlugs.length > 1 && ( (); const envMappingFetcher = useFetcher(); const completeOnboardingFetcher = useFetcher(); - const { Form: _CompleteOnboardingForm } = completeOnboardingFetcher; const [searchParams] = useSearchParams(); const origin = searchParams.get("origin"); const fromMarketplaceContext = origin === "marketplace"; @@ -133,7 +128,6 @@ export function VercelOnboardingModal({ () => onboardingData?.availableProjects ?? [], [onboardingData?.availableProjects] ); - const _hasProjectSelected = onboardingData?.hasProjectSelected ?? false; const customEnvironments = useMemo( () => onboardingData?.customEnvironments ?? [], [onboardingData?.customEnvironments] @@ -227,10 +221,6 @@ export function VercelOnboardingModal({ environmentId: string; displayName: string; } | null>(null); - const _availableEnvSlugsForOnboarding = getAvailableEnvSlugs( - hasStagingEnvironment, - hasPreviewEnvironment - ); const availableEnvSlugsForOnboardingBuildSettings = getAvailableEnvSlugsForBuildSettings( hasStagingEnvironment, hasPreviewEnvironment @@ -378,7 +368,6 @@ export function VercelOnboardingModal({ } break; - case "installing": case "project-selection": case "env-mapping": case "env-var-sync": @@ -462,8 +451,6 @@ export function VercelOnboardingModal({ const overlappingEnvVarsCount = enabledEnvVars.filter((v) => existingVars[v.key]).length; - const _isSubmitting = navigation.state === "submitting" || navigation.state === "loading"; - const actionUrl = vercelResourcePath(organizationSlug, projectSlug, environmentSlug); const handleToggleEnvVar = useCallback((key: string, enabled: boolean) => { @@ -637,19 +624,6 @@ export function VercelOnboardingModal({ gitHubAppInstallations.length, ]); - const _handleFinishOnboarding = useCallback( - (e: React.FormEvent) => { - e.preventDefault(); - const form = e.currentTarget; - const formData = new FormData(form); - completeOnboardingFetcher.submit(formData, { - method: "post", - action: actionUrl, - }); - }, - [completeOnboardingFetcher, actionUrl] - ); - useEffect(() => { if ( completeOnboardingFetcher.data && @@ -701,13 +675,6 @@ export function VercelOnboardingModal({ } }, [state, onClose, trackOnboarding, isGitHubConnectedForOnboarding]); - useEffect(() => { - if (state === "installing") { - const installUrl = vercelAppInstallPath(organizationSlug, projectSlug); - window.location.href = installUrl; - } - }, [state, organizationSlug, projectSlug]); - useEffect(() => { if ( envMappingFetcher.data && @@ -752,7 +719,6 @@ export function VercelOnboardingModal({ state === "loading-projects" || state === "loading-env-mapping" || state === "loading-env-vars" || - state === "installing" || (state === "idle" && !onboardingData); if (isLoadingState) { @@ -761,9 +727,7 @@ export function VercelOnboardingModal({ open={isOpen} onOpenChange={(open) => { if (!open && !fromMarketplaceContext) { - if ((state as string) !== "completed") { - trackOnboarding("vercel onboarding abandoned"); - } + trackOnboarding("vercel onboarding abandoned"); onClose(); } }} @@ -788,12 +752,7 @@ export function VercelOnboardingModal({ )} {vercelManageAccessUrl && ( - + Manage access on Vercel )} diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx index 88fe46b7ba5..bc5e7187cac 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx @@ -2,7 +2,7 @@ import { getFormProps, useForm } from "@conform-to/react"; import { parseWithZod } from "@conform-to/zod"; import { CheckCircleIcon } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; -import { Form, useActionData, useFetcher, useLocation, useNavigation } from "@remix-run/react"; +import { Form, useActionData, useFetcher, 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"; @@ -62,7 +62,6 @@ import { type SyncEnvVarsMapping, type VercelProjectIntegrationData, envSlugArrayField, - getAvailableEnvSlugs, getAvailableEnvSlugsForBuildSettings, } from "~/v3/vercel/vercelProjectIntegrationSchema"; import { sanitizeVercelNextUrl } from "~/v3/vercel/vercelUrls.server"; @@ -596,7 +595,6 @@ function VercelLoadingIcon() { function VercelSettingsRows({ organizationSlug, projectSlug, - environmentSlug: _environmentSlug, hasOrgIntegration, isGitHubConnected, onOpenModal, @@ -605,7 +603,6 @@ function VercelSettingsRows({ }: { organizationSlug: string; projectSlug: string; - environmentSlug: string; hasOrgIntegration: boolean; isGitHubConnected: boolean; onOpenModal?: () => void; @@ -698,19 +695,6 @@ function VercelGitHubWarning() { ); } -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, @@ -774,7 +758,7 @@ function ConnectedVercelProjectForm({ stagingEnvChanged || autoPromoteChanged; - const [configForm, _fields] = useForm({ + const [configForm] = useForm({ id: "update-vercel-config", lastResult: lastSubmission, shouldRevalidate: "onSubmit", @@ -833,7 +817,6 @@ function ConnectedVercelProjectForm({ const actionUrl = vercelResourcePath(organizationSlug, projectSlug, environmentSlug); - const availableEnvSlugs = getAvailableEnvSlugs(hasStagingEnvironment, hasPreviewEnvironment); const availableEnvSlugsForBuildSettings = getAvailableEnvSlugsForBuildSettings( hasStagingEnvironment, hasPreviewEnvironment @@ -844,15 +827,6 @@ function ConnectedVercelProjectForm({ ? { stg: "Set a Vercel environment for Staging first." } : 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 ( <> @@ -1205,7 +1178,6 @@ function VercelSettingsPanel({ }) { const fetcher = useTypedFetcher(); const { load } = fetcher; - const _location = useLocation(); const data = fetcher.data; const [hasFetched, setHasFetched] = useState(false); @@ -1270,7 +1242,6 @@ function VercelSettingsPanel({