Skip to content
Merged
6 changes: 6 additions & 0 deletions .server-changes/vercel-staging-gating.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: fix
---

The Staging branch setting now shows an upgrade prompt on plans that don't include a Staging environment, instead of looking editable and then silently doing nothing when saved.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
};
Expand All @@ -68,7 +67,6 @@ export function BuildSettingsFields({
onAutoPromoteChange,
currentTriggerVersion,
currentTriggerVersionFetchFailed,
hideSectionToggles,
showAtomicDeployments = true,
layout = "card",
}: BuildSettingsFieldsProps) {
Expand Down Expand Up @@ -222,7 +220,7 @@ export function BuildSettingsFields({
<div className="mb-2">
<div className="flex items-center justify-between">
<Label>Pull env vars before build</Label>
{!hideSectionToggles && availableEnvSlugs.length > 1 && (
{availableEnvSlugs.length > 1 && (
<Switch
variant="small"
checked={
Expand Down Expand Up @@ -292,7 +290,7 @@ export function BuildSettingsFields({
<div className="mb-2">
<div className="flex items-center justify-between">
<Label>Discover new env vars</Label>
{!hideSectionToggles && availableEnvSlugs.length > 1 && (
{availableEnvSlugs.length > 1 && (
<Switch
variant="small"
checked={
Expand Down
69 changes: 28 additions & 41 deletions apps/webapp/app/components/integrations/VercelOnboardingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ChevronDownIcon,
ChevronUpIcon,
} from "@heroicons/react/20/solid";
import { useFetcher, useNavigation, useSearchParams } from "@remix-run/react";
import { useFetcher, useSearchParams } from "@remix-run/react";
import { useTypedFetcher } from "remix-typedjson";
import { Dialog, DialogContent, DialogHeader } from "~/components/primitives/Dialog";
import { Button, LinkButton } from "~/components/primitives/Buttons";
Expand Down Expand Up @@ -34,13 +34,11 @@ import {
type EnvSlug,
ALL_ENV_SLUGS,
shouldSyncEnvVarForAnyEnvironment,
getAvailableEnvSlugs,
getAvailableEnvSlugsForBuildSettings,
} from "~/v3/vercel/vercelProjectIntegrationSchema";
import { type VercelCustomEnvironment } from "~/models/vercelIntegration.server";
import { type VercelOnboardingData } from "~/presenters/v3/VercelSettingsPresenter.server";
import {
vercelAppInstallPath,
v3ProjectSettingsIntegrationsPath,
githubAppInstallPath,
vercelResourcePath,
Expand Down Expand Up @@ -78,7 +76,6 @@ function formatVercelTargets(targets: string[]): string {

type OnboardingState =
| "idle"
| "installing"
| "loading-projects"
| "project-selection"
| "loading-env-mapping"
Expand All @@ -99,6 +96,7 @@ export function VercelOnboardingModal({
hasStagingEnvironment,
hasPreviewEnvironment,
hasOrgIntegration,
onboardingDataUnavailable = false,
nextUrl,
onDataReload,
vercelManageAccessUrl,
Expand All @@ -112,16 +110,15 @@ export function VercelOnboardingModal({
hasStagingEnvironment: boolean;
hasPreviewEnvironment: boolean;
hasOrgIntegration: boolean;
onboardingDataUnavailable?: boolean;
nextUrl?: string;
onDataReload?: (vercelStagingEnvironment?: string) => void;
vercelManageAccessUrl?: string;
}) {
const { capture, startSessionRecording } = usePostHogTracking();
const navigation = useNavigation();
const fetcher = useTypedFetcher<typeof loader>();
const envMappingFetcher = useFetcher();
const completeOnboardingFetcher = useFetcher();
const { Form: _CompleteOnboardingForm } = completeOnboardingFetcher;
const [searchParams] = useSearchParams();
const origin = searchParams.get("origin");
const fromMarketplaceContext = origin === "marketplace";
Expand All @@ -130,7 +127,6 @@ export function VercelOnboardingModal({
() => onboardingData?.availableProjects ?? [],
[onboardingData?.availableProjects]
);
const _hasProjectSelected = onboardingData?.hasProjectSelected ?? false;
const customEnvironments = useMemo(
() => onboardingData?.customEnvironments ?? [],
[onboardingData?.customEnvironments]
Expand Down Expand Up @@ -224,10 +220,6 @@ export function VercelOnboardingModal({
environmentId: string;
displayName: string;
} | null>(null);
const _availableEnvSlugsForOnboarding = getAvailableEnvSlugs(
hasStagingEnvironment,
hasPreviewEnvironment
);
const availableEnvSlugsForOnboardingBuildSettings = getAvailableEnvSlugsForBuildSettings(
hasStagingEnvironment,
hasPreviewEnvironment
Expand Down Expand Up @@ -375,7 +367,6 @@ export function VercelOnboardingModal({
}
break;

case "installing":
case "project-selection":
case "env-mapping":
case "env-var-sync":
Expand Down Expand Up @@ -459,8 +450,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) => {
Expand Down Expand Up @@ -634,19 +623,6 @@ export function VercelOnboardingModal({
gitHubAppInstallations.length,
]);

const _handleFinishOnboarding = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.currentTarget;
const formData = new FormData(form);
completeOnboardingFetcher.submit(formData, {
method: "post",
action: actionUrl,
});
},
[completeOnboardingFetcher, actionUrl]
);

useEffect(() => {
if (
completeOnboardingFetcher.data &&
Expand Down Expand Up @@ -698,13 +674,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 &&
Expand Down Expand Up @@ -749,7 +718,6 @@ export function VercelOnboardingModal({
state === "loading-projects" ||
state === "loading-env-mapping" ||
state === "loading-env-vars" ||
state === "installing" ||
(state === "idle" && !onboardingData);

if (isLoadingState) {
Expand All @@ -758,9 +726,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();
}
}}
Expand All @@ -772,9 +738,30 @@ export function VercelOnboardingModal({
<span>Set up Vercel Integration</span>
</div>
</DialogHeader>
<div className="flex items-center justify-center py-8">
<Spinner color="blue" className="size-6" />
</div>
{onboardingDataUnavailable ? (
<div className="flex flex-col items-start gap-3 py-4">
<Paragraph variant="small">
We couldn't load your Vercel projects. The integration may have been removed or lost
access to this organization on Vercel.
</Paragraph>
<div className="flex items-center gap-2">
{onDataReload && (
<Button variant="secondary/small" onClick={() => onDataReload()}>
Try again
</Button>
)}
{vercelManageAccessUrl && (
<LinkButton to={vercelManageAccessUrl} target="_blank" variant="tertiary/small">
Manage access on Vercel
</LinkButton>
)}
</div>
</div>
) : (
<div className="flex items-center justify-center py-8">
<Spinner color="blue" className="size-6" />
</div>
)}
</DialogContent>
</Dialog>
);
Expand Down
43 changes: 36 additions & 7 deletions apps/webapp/app/presenters/v3/GitHubSettingsPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class GitHubSettingsPresenter extends BasePresenter {
connectedRepository: undefined,
installations: undefined,
isPreviewEnvironmentEnabled: undefined,
isStagingEnvironmentEnabled: undefined,
});
}

Expand Down Expand Up @@ -114,7 +115,8 @@ export class GitHubSettingsPresenter extends BasePresenter {
},
where: {
projectId: projectId,
slug: "preview",
type: "PREVIEW",
parentEnvironmentId: null,
},
}),
(error) => ({
Expand All @@ -123,15 +125,42 @@ export class GitHubSettingsPresenter extends BasePresenter {
})
).map((previewEnvironment) => previewEnvironment !== null);

const isStagingEnvironmentEnabled = () =>
fromPromise(
(this._replica as PrismaClient).runtimeEnvironment.findFirst({
select: {
id: true,
},
where: {
projectId: projectId,
type: "STAGING",
parentEnvironmentId: null,
},
}),
(error) => ({
type: "other" as const,
cause: error,
})
).map((stagingEnvironment) => stagingEnvironment !== null);

return ResultAsync.combine([
isPreviewEnvironmentEnabled(),
isStagingEnvironmentEnabled(),
findConnectedGithubRepository(),
listGithubAppInstallations(),
]).map(([isPreviewEnvironmentEnabled, connectedGithubRepository, githubAppInstallations]) => ({
enabled: true,
connectedRepository: connectedGithubRepository,
installations: githubAppInstallations,
isPreviewEnvironmentEnabled,
}));
]).map(
([
isPreviewEnvironmentEnabled,
isStagingEnvironmentEnabled,
connectedGithubRepository,
githubAppInstallations,
]) => ({
enabled: true,
connectedRepository: connectedGithubRepository,
installations: githubAppInstallations,
isPreviewEnvironmentEnabled,
isStagingEnvironmentEnabled,
})
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export class VercelSettingsPresenter extends BasePresenter {
where: {
projectId,
type: "STAGING",
parentEnvironmentId: null,
},
}),
(error) => ({
Expand All @@ -199,6 +200,7 @@ export class VercelSettingsPresenter extends BasePresenter {
where: {
projectId,
type: "PREVIEW",
parentEnvironmentId: null,
},
}),
(error) => ({
Expand Down
Loading
Loading