From 53bd2f5a2752b12a6be44e96c16f434c1951c767 Mon Sep 17 00:00:00 2001 From: Christian Alfoni Date: Tue, 16 Jun 2026 15:35:30 +0200 Subject: [PATCH 1/6] feat(dashboard): add Devbox deprecation banner and blocking modal Mirror the branch-creation deprecation pattern for Devboxes (v2 sandboxes), behind a new server-provided blockDevboxCreation team feature flag: - Add blockDevboxCreation to the TeamFeatureFlags GraphQL type, fragments, and the useWorkspaceFeatureFlags hook - Show a DevboxDeprecationStripe warning banner on the Recent and Sandboxes dashboard pages when the flag is set - Add a devboxCreationDeprecated modal (close-only, mirroring the banner copy) - Guard the forkSandbox action: when creating or forking a Devbox (body.v2) with the flag set, open the deprecation modal instead of forking - Pass v2: sandbox.isV2 from the SandboxMenu fork entry points so forking an existing Devbox is caught by the action guard Copy intentionally omits a removal date (TBD) and uses a placeholder docs link. Co-Authored-By: Claude Opus 4.8 --- packages/app/src/app/graphql/types.ts | 6 ++++ .../src/app/hooks/useWorkspaceFeatureFlags.ts | 3 ++ packages/app/src/app/overmind/actions.ts | 3 +- .../effects/gql/dashboard/fragments.ts | 2 ++ .../overmind/namespaces/dashboard/actions.ts | 7 +++++ .../Selection/ContextMenus/SandboxMenu.tsx | 4 +++ .../shared/DevboxDeprecationStripe.tsx | 19 ++++++++++++ .../Dashboard/Content/routes/Recent/index.tsx | 11 ++++++- .../Content/routes/Sandboxes/index.tsx | 10 ++++++ .../DevboxCreationDeprecatedModal/index.tsx | 31 +++++++++++++++++++ .../app/src/app/pages/common/Modals/index.tsx | 5 +++ 11 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 packages/app/src/app/pages/Dashboard/Components/shared/DevboxDeprecationStripe.tsx create mode 100644 packages/app/src/app/pages/common/Modals/DevboxCreationDeprecatedModal/index.tsx diff --git a/packages/app/src/app/graphql/types.ts b/packages/app/src/app/graphql/types.ts index 1defc80aca0..01b5794340a 100644 --- a/packages/app/src/app/graphql/types.ts +++ b/packages/app/src/app/graphql/types.ts @@ -506,6 +506,7 @@ export type TeamFeatureFlags = { __typename?: 'TeamFeatureFlags'; blockRepoImport: Scalars['Boolean']; blockBranchCreation: Scalars['Boolean']; + blockDevboxCreation: Scalars['Boolean']; friendOfCsb: Scalars['Boolean']; ubbBeta: Scalars['Boolean']; }; @@ -3575,6 +3576,7 @@ export type TeamFragmentDashboardFragment = { __typename?: 'TeamFeatureFlags'; blockRepoImport: boolean; blockBranchCreation: boolean; + blockDevboxCreation: boolean; ubbBeta: boolean; friendOfCsb: boolean; }; @@ -3699,6 +3701,7 @@ export type CurrentTeamInfoFragmentFragment = { __typename?: 'TeamFeatureFlags'; blockRepoImport: boolean; blockBranchCreation: boolean; + blockDevboxCreation: boolean; ubbBeta: boolean; friendOfCsb: boolean; }; @@ -3874,6 +3877,7 @@ export type _CreateTeamMutation = { __typename?: 'TeamFeatureFlags'; blockRepoImport: boolean; blockBranchCreation: boolean; + blockDevboxCreation: boolean; ubbBeta: boolean; friendOfCsb: boolean; }; @@ -4733,6 +4737,7 @@ export type AllTeamsQuery = { __typename?: 'TeamFeatureFlags'; blockRepoImport: boolean; blockBranchCreation: boolean; + blockDevboxCreation: boolean; ubbBeta: boolean; friendOfCsb: boolean; }; @@ -5184,6 +5189,7 @@ export type GetTeamQuery = { __typename?: 'TeamFeatureFlags'; blockRepoImport: boolean; blockBranchCreation: boolean; + blockDevboxCreation: boolean; ubbBeta: boolean; friendOfCsb: boolean; }; diff --git a/packages/app/src/app/hooks/useWorkspaceFeatureFlags.ts b/packages/app/src/app/hooks/useWorkspaceFeatureFlags.ts index 119ad7f2ce2..c1d9af5d723 100644 --- a/packages/app/src/app/hooks/useWorkspaceFeatureFlags.ts +++ b/packages/app/src/app/hooks/useWorkspaceFeatureFlags.ts @@ -3,6 +3,7 @@ import { useAppState } from 'app/overmind'; export type FeatureFlags = { blockRepoImport: boolean; blockBranchCreation: boolean; + blockDevboxCreation: boolean; ubbBeta: boolean; friendOfCsb: boolean; }; @@ -14,6 +15,7 @@ export const useWorkspaceFeatureFlags = (): FeatureFlags => { return { blockRepoImport: false, blockBranchCreation: false, + blockDevboxCreation: false, ubbBeta: false, friendOfCsb: false, }; @@ -22,6 +24,7 @@ export const useWorkspaceFeatureFlags = (): FeatureFlags => { return { blockRepoImport: activeTeamInfo.featureFlags.blockRepoImport, blockBranchCreation: activeTeamInfo.featureFlags.blockBranchCreation, + blockDevboxCreation: activeTeamInfo.featureFlags.blockDevboxCreation, ubbBeta: activeTeamInfo.featureFlags.ubbBeta, friendOfCsb: activeTeamInfo.featureFlags.friendOfCsb, }; diff --git a/packages/app/src/app/overmind/actions.ts b/packages/app/src/app/overmind/actions.ts index 58af0a64e15..dc8439653de 100755 --- a/packages/app/src/app/overmind/actions.ts +++ b/packages/app/src/app/overmind/actions.ts @@ -160,7 +160,8 @@ type ModalName = | 'minimumPrivacy' | 'import' | 'create' - | 'branchCreationDeprecated'; + | 'branchCreationDeprecated' + | 'devboxCreationDeprecated'; export const modalOpened = ( { state, effects }: Context, diff --git a/packages/app/src/app/overmind/effects/gql/dashboard/fragments.ts b/packages/app/src/app/overmind/effects/gql/dashboard/fragments.ts index f3b68b6c124..cdcc359f2f5 100644 --- a/packages/app/src/app/overmind/effects/gql/dashboard/fragments.ts +++ b/packages/app/src/app/overmind/effects/gql/dashboard/fragments.ts @@ -162,6 +162,7 @@ export const teamFragmentDashboard = gql` featureFlags { blockRepoImport blockBranchCreation + blockDevboxCreation ubbBeta friendOfCsb } @@ -281,6 +282,7 @@ export const currentTeamInfoFragment = gql` featureFlags { blockRepoImport blockBranchCreation + blockDevboxCreation ubbBeta friendOfCsb } diff --git a/packages/app/src/app/overmind/namespaces/dashboard/actions.ts b/packages/app/src/app/overmind/namespaces/dashboard/actions.ts index 97dcaa3ccff..75348e767f9 100755 --- a/packages/app/src/app/overmind/namespaces/dashboard/actions.ts +++ b/packages/app/src/app/overmind/namespaces/dashboard/actions.ts @@ -1868,6 +1868,13 @@ export const forkSandbox = async ( }; } ) => { + // Devboxes (v2) are being deprecated. Block creating and forking them and + // surface the deprecation modal instead, mirroring the banner content. + if (body?.v2 && state.activeTeamInfo?.featureFlags.blockDevboxCreation) { + actions.modalOpened({ modal: 'devboxCreationDeprecated' }); + return undefined; + } + effects.analytics.track('Fork Sandbox', { type: 'external', sandboxId }); const usedBody: ForkSandboxBody = body || {}; diff --git a/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/SandboxMenu.tsx b/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/SandboxMenu.tsx index 5cb7bde2567..a41ba1b784e 100644 --- a/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/SandboxMenu.tsx +++ b/packages/app/src/app/pages/Dashboard/Components/Selection/ContextMenus/SandboxMenu.tsx @@ -102,6 +102,9 @@ export const SandboxMenu: React.FC = ({ sandboxId: sandbox.id, openInNewWindow: true, redirectAfterFork: true, + body: { + v2: sandbox.isV2, + }, }); }} > @@ -149,6 +152,7 @@ export const SandboxMenu: React.FC = ({ openInNewWindow: true, redirectAfterFork: true, body: { + v2: sandbox.isV2, privacy: 'privacy' in sandbox ? sandbox.privacy as 2 | 1 | 0 : undefined, collectionId: 'draft' in sandbox && sandbox.draft ? undefined : 'collection' in sandbox && sandbox.collection.id, }, diff --git a/packages/app/src/app/pages/Dashboard/Components/shared/DevboxDeprecationStripe.tsx b/packages/app/src/app/pages/Dashboard/Components/shared/DevboxDeprecationStripe.tsx new file mode 100644 index 00000000000..663f6847c9e --- /dev/null +++ b/packages/app/src/app/pages/Dashboard/Components/shared/DevboxDeprecationStripe.tsx @@ -0,0 +1,19 @@ +import { MessageStripe } from '@codesandbox/components'; +import React from 'react'; + +export const DevboxDeprecationStripe: React.FC = () => { + return ( + + Devboxes are being deprecated and will be removed soon. Make sure to + export or migrate any work you want to keep before then. + + Learn more + + + ); +}; diff --git a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/index.tsx b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/index.tsx index 4efd2381ed4..577cda5e3bc 100644 --- a/packages/app/src/app/pages/Dashboard/Content/routes/Recent/index.tsx +++ b/packages/app/src/app/pages/Dashboard/Content/routes/Recent/index.tsx @@ -3,11 +3,13 @@ import { useAppState, useActions } from 'app/overmind'; import { sandboxesTypes } from 'app/overmind/namespaces/dashboard/types'; import { Helmet } from 'react-helmet'; import { DashboardBranch, DashboardSandbox } from 'app/pages/Dashboard/types'; -import { Loading, Stack } from '@codesandbox/components'; +import { Element, Loading, Stack } from '@codesandbox/components'; import { RepoInfo } from 'app/overmind/namespaces/sidebar/types'; import { StyledContentWrapper } from 'app/pages/Dashboard/Components/shared/elements'; import { ContentSection } from 'app/pages/Dashboard/Components/shared/ContentSection'; +import { DevboxDeprecationStripe } from 'app/pages/Dashboard/Components/shared/DevboxDeprecationStripe'; import { useWorkspaceLimits } from 'app/hooks/useWorkspaceLimits'; +import { useWorkspaceFeatureFlags } from 'app/hooks/useWorkspaceFeatureFlags'; import { TopBanner } from './TopBanner'; import { CreateBranchesRow } from './CreateBranchesRow'; import { ItemsGrid } from './ItemsGrid'; @@ -20,6 +22,7 @@ export const Recent = () => { dashboard: { sandboxes }, } = useAppState(); const { isFrozen } = useWorkspaceLimits(); + const { blockDevboxCreation } = useWorkspaceFeatureFlags(); const { dashboard: { getPage, getWorkspaceSandboxes }, } = useActions(); @@ -143,6 +146,12 @@ export const Recent = () => { + {blockDevboxCreation && ( + + + + )} + {recentRepos.length > 0 && ( { @@ -22,6 +25,7 @@ export const SandboxesPage = () => { const items = useFilteredItems(currentPath, cleanParam, level); const actions = useActions(); const { isFrozen } = useWorkspaceLimits(); + const { blockDevboxCreation } = useWorkspaceFeatureFlags(); const { hasEditorAccess } = useWorkspaceAuthorization(); const { dashboard: { allCollections }, @@ -86,6 +90,12 @@ export const SandboxesPage = () => { showSortOptions={!isEmpty && Boolean(currentPath)} /> + {blockDevboxCreation && ( + + + + )} + {isEmpty ? ( diff --git a/packages/app/src/app/pages/common/Modals/DevboxCreationDeprecatedModal/index.tsx b/packages/app/src/app/pages/common/Modals/DevboxCreationDeprecatedModal/index.tsx new file mode 100644 index 00000000000..96f5eea7460 --- /dev/null +++ b/packages/app/src/app/pages/common/Modals/DevboxCreationDeprecatedModal/index.tsx @@ -0,0 +1,31 @@ +import React, { FunctionComponent } from 'react'; +import { useActions } from 'app/overmind'; +import { Alert } from '../Common/Alert'; + +export const DevboxCreationDeprecatedModal: FunctionComponent = () => { + const { modalClosed } = useActions(); + + return ( + + Creating and forking Devboxes is being deprecated and will be removed + soon. Make sure to export or migrate any work you want to keep before + then.{' '} + + Learn more. + + + } + confirmMessage="Close" + type="secondary" + onPrimaryAction={modalClosed} + /> + ); +}; diff --git a/packages/app/src/app/pages/common/Modals/index.tsx b/packages/app/src/app/pages/common/Modals/index.tsx index 047c5613929..334ba0adbdd 100644 --- a/packages/app/src/app/pages/common/Modals/index.tsx +++ b/packages/app/src/app/pages/common/Modals/index.tsx @@ -22,6 +22,7 @@ import { AccountDeletionConfirmationModal } from './AccountDeletion/DeletedConfi import { UndoAccountDeletionModal } from './UndoAccountDeletion'; import { UndoAccountDeletionConfirmationModal } from './UndoAccountDeletion/UndoDeletedConfirmation'; import { BranchCreationDeprecatedModal } from './BranchCreationDeprecatedModal'; +import { DevboxCreationDeprecatedModal } from './DevboxCreationDeprecatedModal'; const modals = { preferences: { @@ -97,6 +98,10 @@ const modals = { Component: BranchCreationDeprecatedModal, width: 450, }, + devboxCreationDeprecated: { + Component: DevboxCreationDeprecatedModal, + width: 450, + }, }; const Modals: FunctionComponent = () => { From e6ea94ab59f002022445ba82d39323f49bbfd7d0 Mon Sep 17 00:00:00 2001 From: Christian Alfoni Date: Wed, 17 Jun 2026 12:18:35 +0200 Subject: [PATCH 2/6] feat(dashboard): block Devbox creation inline in the Create modal Showing the deprecation modal from the forkSandbox action while the Create modal was open just closed the Create modal (modals can't stack), so creating a Devbox appeared to silently do nothing. Instead, surface the deprecation banner inline in the Create form when the Devbox runtime is selected and disable the Create button while the blockDevboxCreation flag is set. The forkSandbox action guard remains as the block for the dashboard "Fork" context-menu path. Co-Authored-By: Claude Opus 4.8 --- .../app/components/Create/CreateBox/CreateBoxForm.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/app/src/app/components/Create/CreateBox/CreateBoxForm.tsx b/packages/app/src/app/components/Create/CreateBox/CreateBoxForm.tsx index 42449fe6e89..a2ffc281297 100644 --- a/packages/app/src/app/components/Create/CreateBox/CreateBoxForm.tsx +++ b/packages/app/src/app/components/Create/CreateBox/CreateBoxForm.tsx @@ -11,6 +11,8 @@ import { } from '@codesandbox/components'; import { useActions, useAppState, useEffects } from 'app/overmind'; +import { useWorkspaceFeatureFlags } from 'app/hooks/useWorkspaceFeatureFlags'; +import { DevboxDeprecationStripe } from 'app/pages/Dashboard/Components/shared/DevboxDeprecationStripe'; import { useWorkspaceSubscription } from 'app/hooks/useWorkspaceSubscription'; import { useGlobalPersistedState } from 'app/hooks/usePersistedState'; import { PATHED_SANDBOXES_FOLDER_QUERY } from 'app/pages/Dashboard/queries'; @@ -53,9 +55,14 @@ export const CreateBoxForm: React.FC = ({ const label = runtime === 'browser' ? 'Sandbox' : 'Devbox'; + // Devboxes are being deprecated. When the team has the flag set, block + // creating one and surface the deprecation banner instead. + const devboxDeprecated = runtime === 'vm' && blockDevboxCreation; + const { activeTeamInfo, activeTeam, hasLogIn } = useAppState(); const { signInClicked } = useActions(); const { highestAllowedVMTier, isFrozen } = useWorkspaceLimits(); + const { blockDevboxCreation } = useWorkspaceFeatureFlags(); const [name, setName] = useState(); const effects = useEffects(); const nameInputRef = useRef(null); @@ -310,6 +317,8 @@ export const CreateBoxForm: React.FC = ({ + {devboxDeprecated && } + {runtime === 'vm' && ( <> @@ -377,7 +386,7 @@ export const CreateBoxForm: React.FC = ({