Skip to content

Staging & sandbox workspaces#3919

Open
devkiran wants to merge 73 commits into
mainfrom
staging-sandbox
Open

Staging & sandbox workspaces#3919
devkiran wants to merge 73 commits into
mainfrom
staging-sandbox

Conversation

@devkiran
Copy link
Copy Markdown
Collaborator

@devkiran devkiran commented May 18, 2026

Summary by CodeRabbit

  • New Features

    • Workspace environments (production/staging/sandbox) with top environment banners, environment switcher, and environment-aware workspace selector.
    • Copy-to-live workflows and UI modals to copy discounts and rewards from staging to live.
    • Staging/sandbox tooling: create staging/sandbox workspaces, sync hooks, mock payment provider, and server actions for copy-to-live.
  • Improvements

    • Environment-aware gating across UI, billing, tokens, payments, payouts, partner emails, and webhooks (production-only side effects; sandbox/demo fallbacks).
    • Standardized webhook input/output and safer staging guards.

Review Change Stack

devkiran added 3 commits May 18, 2026 16:51
Expose stagingWorkspaceId on workspaces, set staging environment on create, and add sidebar UI to switch between live and staging.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dub Error Error May 25, 2026 5:49am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds WorkspaceEnvironment enum and fields; implements staging/sandbox workspaces and sync; gates payments/payouts and API/UI flows by environment; adds mock providers, copy-to-live actions and UI, workspace deletion/backfill/seed scripts, webhook workspace resolution, and provider/type updates.

Changes

Workspace Environment & Staging Infrastructure

Layer / File(s) Summary
Schema & data model
packages/prisma/schema/workspace.prisma, packages/prisma/client.ts, apps/web/lib/zod/schemas/workspaces.ts
Introduces WorkspaceEnvironment enum and adds environment and stagingWorkspaceId to Project.
Client access gating
apps/web/lib/client-access-check.ts
Adds environment and stagingBehavior inputs and staging-specific denial messages.
Staging & sandbox creation
apps/web/lib/sandbox/create-staging-workspace.ts, apps/web/lib/sandbox/create-sandbox-workspace.ts
Creates staging/sandbox workspace rows, copies users, links stagingWorkspaceId, and provisions sandbox defaults.
Sync & guards
apps/web/lib/sandbox/sync-workspace.ts, apps/web/lib/sandbox/throw-if-staging-workspace.ts
Helpers to add/update/remove members in staging, sync settings, and throw on forbidden staging mutations.
Copy-to-live server actions
apps/web/lib/sandbox/copy-discount-to-live.ts, apps/web/lib/sandbox/copy-reward-to-live.ts, apps/web/lib/sandbox/schemas.ts
Server actions to copy discounts/rewards from staging to production with validation, provider calls, and transactional creates.
Sandbox mocks & helpers
apps/web/lib/sandbox/mock-payment-provider.ts, apps/web/lib/sandbox/mock-payout-completion.ts
Mock payment provider and mock payout completion for non-production flows.
Payout/payment flows
apps/web/lib/actions/partners/confirm-payouts.ts, apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts, apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts
Gate Stripe/PayPal/stablecoin and payment-intent creation to production; simulate payouts in non-production.
Workspace deletion & cleanup
apps/web/app/(ee)/api/cron/workspaces/delete/delete-workspace.ts, apps/web/lib/api/workspaces/delete-workspace.ts
Adds Stripe subscription cancellation and R2 logo deletion in cron; deleteWorkspace accepts stagingWorkspaceId and schedules deletions for both.

Discounts & rewards, provider updates

Layer / File(s) Summary
Provider contracts & types
apps/web/lib/discounts/*
Provider APIs and internal types updated to include environment; some parameter types narrowed.
Cron and generators
apps/web/app/(ee)/api/cron/discount-codes/*, apps/web/lib/discounts/generate-discount-code-for-partner.ts
Cron endpoints now select/passthrough workspace environment to providers.
Program fetcher & plan capabilities
apps/web/lib/fetchers/get-program.ts, apps/web/lib/plan-capabilities.ts
Program fetcher includes workspace.environment; capability canUseStagingWorkspace added.

APIs & UI gating

Layer / File(s) Summary
Workspace APIs & invites
apps/web/app/api/workspaces/*, apps/web/app/api/user/route.ts, invite pages
Added staging guards, staging-aware invite/user flows, staging sync enqueueing, and default-workspace production restriction.
Payment methods endpoint
apps/web/app/api/workspaces/[idOrSlug]/billing/payment-methods/route.ts
Returns sandbox payment method for non-production workspaces.
UI components & banners
apps/web/lib/sandbox/components/*, apps/web/lib/hooks/use-dashboard-banner-visible.ts, apps/web/app/app.dub.co/(dashboard)/layout.tsx, apps/web/ui/*
Adds environment banners, switcher, copy-to-live modals/hooks, environment-aware workspace selector and layout adjustments; UI checks wired to clientAccessCheck with environment.
Icons & emails
packages/ui/src/icons/nucleo/*, packages/email/*
Adds isolated-cube icon and environment banner in partner payout email templates, plus types.
Scripts
apps/web/scripts/migrations/backfill-staging-workspaces.ts, apps/web/scripts/dev/seed-installation.ts
Backfill script to create staging workspaces and dev seed for installed integration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • dubinc/dub#3885: Overlaps changes to checkout-session-completed webhook flow and related onboarding/email logic.
  • dubinc/dub#3739: Refactors Stripe webhook handler signatures to typed input contracts; overlaps webhook changes here.
  • dubinc/dub#3679: Related edits to discount-code cron flows and provider integration handling.

Suggested reviewers

  • steven-tey

"🐰 I hopped through branches, nose a-twitch,
Banners and staging sewn without a glitch.
Copy-to-live seeds, mocks in a row,
Payments gated where production must go.
Hop, patch, and ship — a carrot-shaped glow!"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch staging-sandbox

devkiran added 2 commits May 19, 2026 15:26
Non-live workspaces use mock payout completion in the process cron, skip Stripe
payment validation when confirming payouts, and get a demo card from the
payment-methods API. Payout updates run before hybrid totals; apply success shows
the program environment banner with refreshed banner styling.
devkiran added 3 commits May 20, 2026 10:25
Introduce createSandboxWorkspace with trial limits, rename
createStagingWorkspace, and apply TRIAL_LIMITS when provisioning staging.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/web/app/(ee)/api/stripe/integration/webhook/customer-created.ts (1)

66-67: createNewCustomer(event) aligns with the resolved workspace for projectConnectId (so the drift concern for staging/sandbox mapping is unlikely).

  • resolveWebhookWorkspace resolves by stripeConnectId: event.account and, even in test-mode staging fallback, keeps stripeConnectId equal to the same stripeAccountId.
  • createNewCustomer sets projectConnectId: event.account, matching the update path’s workspace.stripeConnectId.
    Optional refactor: thread workspace (or workspace.stripeConnectId) into createNewCustomer for consistency/guarding against future resolver changes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(ee)/api/stripe/integration/webhook/customer-created.ts around
lines 66 - 67, The current flow relies on resolveWebhookWorkspace(event.account)
matching the projectConnectId used in createNewCustomer(event), but to guard
against future resolver changes thread the resolved workspace (or at minimum
workspace.stripeConnectId) into createNewCustomer so the create path explicitly
uses the same workspace context; update the call site to await
createNewCustomer(event, workspace) (or createNewCustomer(event,
workspace.stripeConnectId)) and adjust the createNewCustomer signature and
internal use of projectConnectId accordingly so both create and update paths use
the identical workspace.stripeConnectId value.
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (1)

37-43: Confirm connect-id source consistency between checkout-session-completed and invoice-paid

  • apps/web/app/(ee)/api/stripe/integration/webhook/route.ts resolves workspace via resolveWebhookWorkspace({ stripeAccountId: event.account!, mode }), and resolveWebhookWorkspace looks up Project by stripeConnectId: stripeAccountId—so workspace.stripeConnectId should match event.account for this handler.
  • In checkout-session-completed.ts, persisting projectConnectId uses stripeAccountId in one path (projectConnectId: stripeAccountId, ~line 106) and workspace.stripeConnectId in another (projectConnectId: workspace.stripeConnectId, ~line 656); given the resolver, these are currently the same, but using workspace.stripeConnectId consistently would prevent future drift.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
around lines 37 - 43, The handler checkoutSessionCompleted mixes sources for the
Stripe connect id when setting projectConnectId (sometimes using
stripeAccountId, other times workspace.stripeConnectId); since route.ts resolves
workspace via resolveWebhookWorkspace({ stripeAccountId: event.account!, mode })
and Projects are looked up by stripeConnectId, update checkoutSessionCompleted
to always use workspace.stripeConnectId when persisting projectConnectId
(replace any usage of stripeAccountId for projectConnectId with
workspace.stripeConnectId) to keep the connect-id source consistent with
invoice-paid and prevent future drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/lib/sandbox/components/workspace-environment-banner.tsx`:
- Around line 12-15: The exit-link currently rebuilds the href using only
usePathname(), dropping the query string; update WorkspaceEnvironmentBanner to
also read the current search params (useSearchParams from next/navigation), and
append them to the computed href so queries are preserved when switching
staging→live. Locate the computation that uses pathname.replace(...) inside
WorkspaceEnvironmentBanner and concatenate the existing search params (if
non-empty) as ?{searchParams.toString()} (or omit the ? when empty); ensure the
replacement logic (the string you remove/replace from pathname) remains
unchanged so only the query string behavior is fixed.

---

Nitpick comments:
In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/checkout-session-completed.ts:
- Around line 37-43: The handler checkoutSessionCompleted mixes sources for the
Stripe connect id when setting projectConnectId (sometimes using
stripeAccountId, other times workspace.stripeConnectId); since route.ts resolves
workspace via resolveWebhookWorkspace({ stripeAccountId: event.account!, mode })
and Projects are looked up by stripeConnectId, update checkoutSessionCompleted
to always use workspace.stripeConnectId when persisting projectConnectId
(replace any usage of stripeAccountId for projectConnectId with
workspace.stripeConnectId) to keep the connect-id source consistent with
invoice-paid and prevent future drift.

In `@apps/web/app/`(ee)/api/stripe/integration/webhook/customer-created.ts:
- Around line 66-67: The current flow relies on
resolveWebhookWorkspace(event.account) matching the projectConnectId used in
createNewCustomer(event), but to guard against future resolver changes thread
the resolved workspace (or at minimum workspace.stripeConnectId) into
createNewCustomer so the create path explicitly uses the same workspace context;
update the call site to await createNewCustomer(event, workspace) (or
createNewCustomer(event, workspace.stripeConnectId)) and adjust the
createNewCustomer signature and internal use of projectConnectId accordingly so
both create and update paths use the identical workspace.stripeConnectId value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dfe7d507-dab2-4c3c-90c7-89ac3e15ba56

📥 Commits

Reviewing files that changed from the base of the PR and between aaab1bc and 9dc6fa5.

📒 Files selected for processing (14)
  • apps/web/app/(ee)/api/stripe/integration/webhook/account-application-deauthorized.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/charge-refunded.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/coupon-deleted.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/customer-created.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/customer-subscription-created.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/customer-subscription-deleted.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/customer-updated.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/promotion-code-updated.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/route.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/resolve-webhook-workspace.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/types.ts
  • apps/web/lib/sandbox/components/workspace-environment-banner.tsx
✅ Files skipped from review due to trivial changes (2)
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/types.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/resolve-webhook-workspace.ts

Comment thread apps/web/lib/sandbox/components/workspace-environment-banner.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/lib/sandbox/mock-payout-completion.ts`:
- Around line 117-121: The call to trackCommissionStatusUpdatesByProgram is
awaited before sendBatchEmail so if it throws the payout emails are skipped;
change the flow to ensure sendBatchEmail always runs by wrapping the tracking
call in a try/catch (or running it non-blocking) and logging the error without
rethrowing, then proceed to await sendBatchEmail as before; reference
trackCommissionStatusUpdatesByProgram (called with commissions, payouts,
newStatus: "paid") and sendBatchEmail so reviewers can find and update the calls
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b6e4f5d-20d3-453b-8352-ab78e1709daf

📥 Commits

Reviewing files that changed from the base of the PR and between 9dc6fa5 and 1f070b3.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/layout.tsx
  • apps/web/lib/sandbox/mock-payout-completion.ts
  • apps/web/ui/layout/main-nav.tsx

Comment thread apps/web/lib/sandbox/mock-payout-completion.ts
@devkiran
Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review please

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

@devkiran: Sure! Triggering another full review of PR #3919 now. 🚀

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/app/api/workspaces/[idOrSlug]/users/route.ts (1)

178-207: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not swallow failures in workspace-member removal flow.

Promise.allSettled is awaited but never inspected, so projectUsers.delete/restrictedToken.deleteMany failures can still return 200 and leave inconsistent state.

Suggested fix
-    await Promise.allSettled([
+    const results = await Promise.allSettled([
       // Remove the user from the workspace
       prisma.projectUsers.delete({
         where: {
           userId_projectId: {
             projectId: workspace.id,
             userId,
           },
         },
       }),
@@
       workspace.slug === projectUser.user.defaultWorkspace &&
         prisma.user.update({
@@
         }),
     ]);
+
+    const rejected = results.filter((r) => r.status === "rejected");
+    if (rejected.length > 0) {
+      throw new DubApiError({
+        code: "internal_server_error",
+        message: "Failed to fully remove member from workspace.",
+      });
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/api/workspaces/`[idOrSlug]/users/route.ts around lines 178 -
207, The code currently calls Promise.allSettled for the deletion/update
operations (prisma.projectUsers.delete, prisma.restrictedToken.deleteMany,
prisma.user.update) but never checks the settled results, which can swallow
errors and return 200 on partial failure; replace the Promise.allSettled call
with logic that either uses Promise.all (so any rejection bubbles up) or
inspects the Promise.allSettled results and throws an error when any
result.status === "rejected" (including the rejection reason) so the request
fails on partial failures; ensure the check also handles the conditional
workspace.slug === projectUser.user.defaultWorkspace branch and surfaces its
error if it rejects.
♻️ Duplicate comments (5)
apps/web/lib/sandbox/components/workspace-environment-banner.tsx (1)

12-12: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve query params when building the “Exit staging” link.

Line 68 rebuilds href from pathname only, so active query state is dropped when switching workspaces.

💡 Minimal fix
-import { usePathname } from "next/navigation";
+import { usePathname, useSearchParams } from "next/navigation";
@@
   const pathname = usePathname();
+  const searchParams = useSearchParams();
@@
+  const query = searchParams.toString();
+  const liveWorkspaceHref = liveWorkspace
+    ? `${pathname.replace(currentWorkspace.slug, liveWorkspace.slug)}${query ? `?${query}` : ""}`
+    : "";
@@
-          href={pathname.replace(currentWorkspace.slug, liveWorkspace.slug)}
+          href={liveWorkspaceHref}

Also applies to: 15-15, 68-68

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/sandbox/components/workspace-environment-banner.tsx` at line 12,
The "Exit staging" link in WorkspaceEnvironmentBanner rebuilds href from
usePathname() only and drops query parameters; preserve the active query state
by reading current search params (via useSearchParams() or equivalent) and
appending them to the href when constructing the link (keep the existing
pathname value but append '?' + searchParams.toString() when non-empty). Update
the code that constructs the href in the WorkspaceEnvironmentBanner component
(the variable named href used for the Exit staging link) so query strings are
preserved when switching workspaces.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/rewards/group-rewards.tsx (1)

187-196: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve modifier-click/new-tab behavior in the row click handler.

Line 188 calls preventDefault() before intent checks, so modified clicks cannot open a new tab/window.

💡 Minimal fix
         onClick={(e) => {
-          e.preventDefault();
-          if (isClickOnInteractiveChild(e)) return;
+          if (isClickOnInteractiveChild(e)) return;
+          if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) {
+            return;
+          }
+          e.preventDefault();
           queryParams({
             set: {
               rewardId: reward?.id ?? `new-${event}`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/rewards/group-rewards.tsx
around lines 187 - 196, The row onClick handler currently calls
e.preventDefault() before checking intent, which blocks modifier-click/new-tab
behavior; update the handler (the onClick arrow, referencing
isClickOnInteractiveChild and queryParams) so you first detect and bail out for
intentful clicks (if isClickOnInteractiveChild(e) returns true OR e.metaKey ||
e.ctrlKey || e.shiftKey || e.altKey OR e.button === 1), and only call
e.preventDefault() when none of those are true; then proceed to call queryParams
with reward?.id ?? `new-${event}` and scroll: false.
apps/web/lib/sandbox/create-sandbox-workspace.ts (1)

26-45: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject partial user resolution before creating the sandbox workspace.

This currently aborts only when zero users are found. If some requested emails are missing, the workspace is still created with a partial member list.

Suggested fix
   if (users.length > 0) {
     usersFound = await prisma.user.findMany({
@@
     });

-    if (usersFound.length === 0) {
-      console.error(
-        "No users found. Please ensure the users exist in the database.",
-      );
-      return;
-    }
+    const foundEmails = new Set(usersFound.map((u) => u.email));
+    const missingEmails = users
+      .map((u) => u.email)
+      .filter((email) => !foundEmails.has(email));
+
+    if (missingEmails.length > 0) {
+      throw new Error(
+        `Cannot create sandbox workspace; missing users: ${missingEmails.join(", ")}`,
+      );
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/sandbox/create-sandbox-workspace.ts` around lines 26 - 45, The
code in create-sandbox-workspace aborts only when usersFound.length === 0,
allowing creation with a partial member list; change the logic after the
prisma.user.findMany call (symbols: users, usersFound, prisma.user.findMany) to
verify that all requested emails were resolved (compare usersFound.length to
users.length), compute which emails are missing, log a clear error listing the
missing emails, and return/throw to prevent workspace creation when any
requested user is not found.
apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts (1)

246-255: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make staging-workspace creation retryable after partial webhook success.

This is still keyed to the transition edge. If project.update succeeds but createStagingWorkspace fails, retries will see workspace.plan === newPlanName and skip creation forever.

Suggested fix
-    if (
-      !workspace.stagingWorkspaceId &&
-      wouldGainPartnerAccess({
-        currentPlan: workspace.plan,
-        newPlan: newPlanName,
-      })
-    ) {
+    const targetPlanHasPartnerAccess = wouldGainPartnerAccess({
+      currentPlan: "free",
+      newPlan: newPlanName,
+    });
+
+    if (!workspace.stagingWorkspaceId && targetPlanHasPartnerAccess) {
       await createStagingWorkspace(workspace.id);
       console.log(`Created staging workspace for workspace ${workspace.id}.`);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(ee)/api/stripe/webhook/utils/update-workspace-plan.ts around
lines 246 - 255, The current check uses wouldGainPartnerAccess({currentPlan:
workspace.plan, newPlan: newPlanName}) so if workspace.plan already equals
newPlanName (e.g. project.update succeeded but createStagingWorkspace failed)
the branch never runs on retry; change the guard to create staging whenever the
target plan grants partner access and there is no stagingWorkspaceId (i.e. if
(!workspace.stagingWorkspaceId && planGrantsPartnerAccess(newPlanName)) ...), or
replace wouldGainPartnerAccess with a helper that only checks the new plan's
entitlement; keep using createStagingWorkspace(workspace.id) so retries will
succeed if prior creation failed. Ensure you reference
workspace.stagingWorkspaceId, workspace.plan, newPlanName,
wouldGainPartnerAccess and createStagingWorkspace in the modification.
apps/web/lib/sandbox/mock-payout-completion.ts (1)

117-121: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't let activity-log failures block payout notification emails.

trackCommissionStatusUpdatesByProgram is awaited before sendBatchEmail. If tracking throws, payouts are already committed but partner emails are skipped.

Consider wrapping in a try/catch to log and continue:

🛠️ Suggested fix
-  await trackCommissionStatusUpdatesByProgram({
-    commissions,
-    payouts,
-    newStatus: "paid",
-  });
+  try {
+    await trackCommissionStatusUpdatesByProgram({
+      commissions,
+      payouts,
+      newStatus: "paid",
+    });
+  } catch (error) {
+    console.error("Failed to track commission status updates", {
+      invoiceId: invoice.id,
+      error,
+    });
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/sandbox/mock-payout-completion.ts` around lines 117 - 121, The
call to trackCommissionStatusUpdatesByProgram is awaited before sendBatchEmail
so if tracking throws partner notification emails are skipped; wrap the
trackCommissionStatusUpdatesByProgram invocation in a try/catch around the
await, log the error (using the existing logger or console) inside the catch,
and continue execution so sendBatchEmail still runs for the committed payouts;
keep the same arguments and ensure the catch does not rethrow.
🧹 Nitpick comments (2)
packages/email/src/components/environment-banner.tsx (1)

17-20: 💤 Low value

Consider distinct styling for sandbox environment.

Currently, only staging gets amber styling (Line 19), while all other non-production environments (including sandbox) get blue. If sandbox should have its own visual identity, add a third condition.

🎨 Proposed enhancement
     <Section
       className={cn(
         "mb-4 rounded-lg px-4 py-2",
-        environment === "staging" ? "bg-amber-200" : "bg-blue-200",
+        environment === "staging" 
+          ? "bg-amber-200" 
+          : environment === "sandbox"
+          ? "bg-blue-200"
+          : "bg-purple-200",
       )}
     >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/email/src/components/environment-banner.tsx` around lines 17 - 20,
The conditional styling in EnvironmentBanner uses a two-way ternary on the
environment variable (in the className call using cn) so only "staging" gets
amber and every other non-production (including "sandbox") falls back to blue;
update the conditional to explicitly handle a "sandbox" case (e.g., a third
branch that returns a distinct utility class such as a different bg- color) so
sandbox has its own visual identity, keeping the check for "staging" and the
fallback for production/others intact.
apps/web/lib/sandbox/mock-payment-provider.ts (1)

1-20: 💤 Low value

Inconsistent payment method types in mock provider.

SANDBOX_PAYMENT_METHOD declares type: "card" with card details, but retrievePaymentMethod always returns type: "us_bank_account". If these are meant to represent different test scenarios, consider documenting the intent or aligning the types for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/sandbox/mock-payment-provider.ts` around lines 1 - 20,
SANDBOX_PAYMENT_METHOD and MockPaymentProvider.retrievePaymentMethod return
inconsistent payment types (card vs us_bank_account); update
retrievePaymentMethod in MockPaymentProvider so it returns a matching card-typed
object (including card.brand and card.last4) when the id matches
SANDBOX_PAYMENT_METHOD.id, or else add branching to return different fixtures
based on id/flag and document the behavior; reference SANDBOX_PAYMENT_METHOD,
MockPaymentProvider.retrievePaymentMethod, and mockPaymentProvider when making
the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/api/workspaces/`[idOrSlug]/billing/payment-methods/route.ts:
- Around line 24-26: The POST handler in apps/web/.../payment-methods/route.ts
currently creates real Stripe sessions for non-production workspaces; add the
same environment guard used for GET (check workspace.environment !==
WorkspaceEnvironment.production) at the top of the exported POST function and
return the sandbox response (e.g., NextResponse.json([SANDBOX_PAYMENT_METHOD])
or appropriate sandbox status) instead of creating Stripe sessions when not in
production; ensure you reference WorkspaceEnvironment.production,
SANDBOX_PAYMENT_METHOD and the POST handler so the early-return mirrors the GET
behavior.

In `@apps/web/app/api/workspaces/`[idOrSlug]/invites/reset/route.ts:
- Line 2: The helper throwIfStagingWorkspace currently only blocks
WorkspaceEnvironment.staging but the invite reset must also block sandbox
workspaces; update the guard logic inside throwIfStagingWorkspace (or
create/rename a new helper) to check for WorkspaceEnvironment.sandbox as well
(e.g., treat sandbox the same as staging) so that route handlers like the reset
invite route call a helper that prevents both staging and sandbox environments.
Ensure you reference the throwIfStagingWorkspace function/method and adjust any
callers if you rename or split the helper.

In `@apps/web/lib/sandbox/components/program-environment-banner.tsx`:
- Line 17: The banner's tailwind z-index string in
program-environment-banner.tsx ("fixed left-0 right-0 top-0 z-50 ...") places it
above the modal layer; change that z-50 to z-30 (to match
workspace-environment-banner) so the banner no longer obscures Radix Dialog
overlays/contents (or alternatively coordinate with packages/ui/src/modal.tsx to
raise the modal z to >50 if you prefer the banner to stay on top).

In `@apps/web/lib/zod/schemas/workspaces.ts`:
- Line 187: The WorkspaceSchema currently declares environment as nullable which
contradicts the Prisma workspace model and weakens type safety; update the
schema by making the environment field non-nullable (remove .nullable()) so
environment: z.enum(WorkspaceEnvironment) (or equivalent non-null zod enum)
matches the Prisma defaulted non-null WorkspaceEnvironment type, and ensure any
callers/validators rely on the non-null contract (e.g., places referencing
WorkspaceSchema.environment).

In `@apps/web/scripts/dev/seed-installation.ts`:
- Line 30: The script calls main() directly which drops errors and doesn't
guarantee Prisma disconnection; update the invocation to handle promise
rejection and always call prisma.$disconnect(): call main().catch(err => { /*
log or rethrow err */ }).finally(() => prisma.$disconnect()) or use an async
IIFE that try/catch/finally around await main() so that any thrown error is
logged/handled and prisma.$disconnect() is invoked in the finally block;
reference the main() function and the prisma instance in your change.
- Around line 1-3: The seed script imports prisma (which constructs new
PrismaClient at module scope) before loading environment variables, so
DATABASE_URL can be unset/stale; move the dotenv loader import
("dotenv-flow/config") to the very top of
apps/web/scripts/dev/seed-installation.ts so environment variables are
initialized before importing prisma or any other modules (e.g., references like
prisma and STRIPE_INTEGRATION_ID), ensuring the PrismaClient is created with the
correct env values.

---

Outside diff comments:
In `@apps/web/app/api/workspaces/`[idOrSlug]/users/route.ts:
- Around line 178-207: The code currently calls Promise.allSettled for the
deletion/update operations (prisma.projectUsers.delete,
prisma.restrictedToken.deleteMany, prisma.user.update) but never checks the
settled results, which can swallow errors and return 200 on partial failure;
replace the Promise.allSettled call with logic that either uses Promise.all (so
any rejection bubbles up) or inspects the Promise.allSettled results and throws
an error when any result.status === "rejected" (including the rejection reason)
so the request fails on partial failures; ensure the check also handles the
conditional workspace.slug === projectUser.user.defaultWorkspace branch and
surfaces its error if it rejects.

---

Duplicate comments:
In `@apps/web/app/`(ee)/api/stripe/webhook/utils/update-workspace-plan.ts:
- Around line 246-255: The current check uses
wouldGainPartnerAccess({currentPlan: workspace.plan, newPlan: newPlanName}) so
if workspace.plan already equals newPlanName (e.g. project.update succeeded but
createStagingWorkspace failed) the branch never runs on retry; change the guard
to create staging whenever the target plan grants partner access and there is no
stagingWorkspaceId (i.e. if (!workspace.stagingWorkspaceId &&
planGrantsPartnerAccess(newPlanName)) ...), or replace wouldGainPartnerAccess
with a helper that only checks the new plan's entitlement; keep using
createStagingWorkspace(workspace.id) so retries will succeed if prior creation
failed. Ensure you reference workspace.stagingWorkspaceId, workspace.plan,
newPlanName, wouldGainPartnerAccess and createStagingWorkspace in the
modification.

In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/rewards/group-rewards.tsx:
- Around line 187-196: The row onClick handler currently calls
e.preventDefault() before checking intent, which blocks modifier-click/new-tab
behavior; update the handler (the onClick arrow, referencing
isClickOnInteractiveChild and queryParams) so you first detect and bail out for
intentful clicks (if isClickOnInteractiveChild(e) returns true OR e.metaKey ||
e.ctrlKey || e.shiftKey || e.altKey OR e.button === 1), and only call
e.preventDefault() when none of those are true; then proceed to call queryParams
with reward?.id ?? `new-${event}` and scroll: false.

In `@apps/web/lib/sandbox/components/workspace-environment-banner.tsx`:
- Line 12: The "Exit staging" link in WorkspaceEnvironmentBanner rebuilds href
from usePathname() only and drops query parameters; preserve the active query
state by reading current search params (via useSearchParams() or equivalent) and
appending them to the href when constructing the link (keep the existing
pathname value but append '?' + searchParams.toString() when non-empty). Update
the code that constructs the href in the WorkspaceEnvironmentBanner component
(the variable named href used for the Exit staging link) so query strings are
preserved when switching workspaces.

In `@apps/web/lib/sandbox/create-sandbox-workspace.ts`:
- Around line 26-45: The code in create-sandbox-workspace aborts only when
usersFound.length === 0, allowing creation with a partial member list; change
the logic after the prisma.user.findMany call (symbols: users, usersFound,
prisma.user.findMany) to verify that all requested emails were resolved (compare
usersFound.length to users.length), compute which emails are missing, log a
clear error listing the missing emails, and return/throw to prevent workspace
creation when any requested user is not found.

In `@apps/web/lib/sandbox/mock-payout-completion.ts`:
- Around line 117-121: The call to trackCommissionStatusUpdatesByProgram is
awaited before sendBatchEmail so if tracking throws partner notification emails
are skipped; wrap the trackCommissionStatusUpdatesByProgram invocation in a
try/catch around the await, log the error (using the existing logger or console)
inside the catch, and continue execution so sendBatchEmail still runs for the
committed payouts; keep the same arguments and ensure the catch does not
rethrow.

---

Nitpick comments:
In `@apps/web/lib/sandbox/mock-payment-provider.ts`:
- Around line 1-20: SANDBOX_PAYMENT_METHOD and
MockPaymentProvider.retrievePaymentMethod return inconsistent payment types
(card vs us_bank_account); update retrievePaymentMethod in MockPaymentProvider
so it returns a matching card-typed object (including card.brand and card.last4)
when the id matches SANDBOX_PAYMENT_METHOD.id, or else add branching to return
different fixtures based on id/flag and document the behavior; reference
SANDBOX_PAYMENT_METHOD, MockPaymentProvider.retrievePaymentMethod, and
mockPaymentProvider when making the change.

In `@packages/email/src/components/environment-banner.tsx`:
- Around line 17-20: The conditional styling in EnvironmentBanner uses a two-way
ternary on the environment variable (in the className call using cn) so only
"staging" gets amber and every other non-production (including "sandbox") falls
back to blue; update the conditional to explicitly handle a "sandbox" case
(e.g., a third branch that returns a distinct utility class such as a different
bg- color) so sandbox has its own visual identity, keeping the check for
"staging" and the fallback for production/others intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2ad010ed-c982-483e-9f73-3c654475c800

📥 Commits

Reviewing files that changed from the base of the PR and between d392759 and fa55e51.

📒 Files selected for processing (91)
  • apps/web/app/(ee)/api/cron/discount-codes/create/queue-batches/route.ts
  • apps/web/app/(ee)/api/cron/discount-codes/create/route.ts
  • apps/web/app/(ee)/api/cron/discount-codes/delete/route.ts
  • apps/web/app/(ee)/api/cron/groups/remap-discount-codes/route.ts
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/queue-external-payouts.ts
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts
  • apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts
  • apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts
  • apps/web/app/(ee)/api/cron/program-application-reminder/route.ts
  • apps/web/app/(ee)/api/cron/workspaces/delete/delete-workspace.ts
  • apps/web/app/(ee)/api/stripe/integration/route.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/account-application-deauthorized.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/charge-refunded.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/coupon-deleted.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/customer-created.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/customer-subscription-created.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/customer-subscription-deleted.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/customer-updated.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/promotion-code-updated.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/route.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/resolve-webhook-workspace.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/types.ts
  • apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts
  • apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/apply/page.tsx
  • apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/apply/success/page.tsx
  • apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/header.tsx
  • apps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/page.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/layout.tsx
  • apps/web/app/api/tokens/route.ts
  • apps/web/app/api/user/route.ts
  • apps/web/app/api/workspaces/[idOrSlug]/billing/payment-methods/route.ts
  • apps/web/app/api/workspaces/[idOrSlug]/invites/accept/route.ts
  • apps/web/app/api/workspaces/[idOrSlug]/invites/reset/route.ts
  • apps/web/app/api/workspaces/[idOrSlug]/invites/route.ts
  • apps/web/app/api/workspaces/[idOrSlug]/route.ts
  • apps/web/app/api/workspaces/[idOrSlug]/users/route.ts
  • apps/web/app/app.dub.co/(auth)/invites/[code]/page.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/discounts/group-discounts.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/rewards/group-rewards.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/members/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/layout.tsx
  • apps/web/lib/actions/partners/confirm-payouts.ts
  • apps/web/lib/api/activity-log/track-reward-activity-log.ts
  • apps/web/lib/api/workspaces/delete-workspace.ts
  • apps/web/lib/client-access-check.ts
  • apps/web/lib/discounts/create-discount-code.ts
  • apps/web/lib/discounts/discount-provider-shopify.ts
  • apps/web/lib/discounts/discount-provider-stripe.ts
  • apps/web/lib/discounts/generate-discount-code-for-partner.ts
  • apps/web/lib/fetchers/get-program.ts
  • apps/web/lib/hooks/use-dashboard-banner-visible.ts
  • apps/web/lib/partners/create-stablecoin-payout.ts
  • apps/web/lib/partners/create-stripe-transfer.ts
  • apps/web/lib/plan-capabilities.ts
  • apps/web/lib/sandbox/components/copy-discount-to-live-modal.tsx
  • apps/web/lib/sandbox/components/copy-reward-to-live-modal.tsx
  • apps/web/lib/sandbox/components/program-environment-banner.tsx
  • apps/web/lib/sandbox/components/workspace-environment-banner.tsx
  • apps/web/lib/sandbox/components/workspace-environment-switcher.tsx
  • apps/web/lib/sandbox/copy-discount-to-live.ts
  • apps/web/lib/sandbox/copy-reward-to-live.ts
  • apps/web/lib/sandbox/create-sandbox-workspace.ts
  • apps/web/lib/sandbox/create-staging-workspace.ts
  • apps/web/lib/sandbox/mock-payment-provider.ts
  • apps/web/lib/sandbox/mock-payout-completion.ts
  • apps/web/lib/sandbox/schemas.ts
  • apps/web/lib/sandbox/sync-workspace.ts
  • apps/web/lib/sandbox/throw-if-staging-workspace.ts
  • apps/web/lib/zod/schemas/workspaces.ts
  • apps/web/playwright/workspaces/billing-trial.spec.ts
  • apps/web/scripts/dev/seed-installation.ts
  • apps/web/scripts/migrations/backfill-staging-workspaces.ts
  • apps/web/ui/account/update-default-workspace.tsx
  • apps/web/ui/layout/main-nav.tsx
  • apps/web/ui/layout/sidebar/workspace-dropdown.tsx
  • apps/web/ui/layout/upgrade-banner.tsx
  • apps/web/ui/workspaces/delete-workspace.tsx
  • apps/web/ui/workspaces/upload-logo.tsx
  • apps/web/ui/workspaces/workspace-selector.tsx
  • packages/email/src/components/environment-banner.tsx
  • packages/email/src/templates/partner-payout-confirmed.tsx
  • packages/email/src/templates/partner-payout-processed.tsx
  • packages/email/src/types.ts
  • packages/prisma/client.ts
  • packages/prisma/schema/workspace.prisma
  • packages/ui/src/icons/nucleo/index.ts
  • packages/ui/src/icons/nucleo/isolated-cube.tsx

Comment thread apps/web/app/api/workspaces/[idOrSlug]/invites/reset/route.ts
return (
<div
className={cn(
"fixed left-0 right-0 top-0 z-50 flex h-12 items-center justify-between gap-4 overflow-hidden px-6 text-neutral-800",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for z-index usage in the codebase
rg -n 'z-\d+' --type=ts --type=tsx -g '!node_modules' | head -50

Repository: dubinc/dub

Length of output: 82


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find exact z-50 usages in TS/TSX
echo "== occurrences of z-50 =="

rg -n 'z-50' -S -g '*.ts' -g '*.tsx' -g '!node_modules/**' .

# 2) Find other numeric z-* usages (Tailwind default pattern)
echo
echo "== occurrences of z-[0-9]+ (numeric z-*) =="

rg -n 'z-[0-9]+' -S -g '*.ts' -g '*.tsx' -g '!node_modules/**' . || true

# 3) Find z-auto / arbitrary z-[...] usages
echo
echo "== occurrences of z-auto / arbitrary z-[...] =="

rg -n 'z-auto|z-\[.*?\]' -S -g '*.ts' -g '*.tsx' -g '!node_modules/**' . || true

# 4) Locate Tailwind config and inspect any custom zIndex scale
echo
echo "== Tailwind config files =="

fd -t f 'tailwind.config.*' . || true

echo
echo "== theme.extend.zIndex (any Tailwind config) =="

rg -n 'zIndex' -S -g 'tailwind.config.*' . || true

Repository: dubinc/dub

Length of output: 49255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the specific banner line region for context (kept small)
TARGET="apps/web/lib/sandbox/components/program-environment-banner.tsx"
if [ -f "$TARGET" ]; then
  echo "== banner file context =="
  nl -ba "$TARGET" | sed -n '1,120p'
else
  echo "Missing $TARGET"
fi

Repository: dubinc/dub

Length of output: 141


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print relevant snippets around each z-50 usage to understand intent/stacking.
files=(
  "apps/web/lib/sandbox/components/program-environment-banner.tsx"
  "apps/web/lib/sandbox/components/workspace-environment-banner.tsx"
  "apps/web/ui/layout/upgrade-banner.tsx"
  "apps/web/ui/layout/main-nav.tsx"
  "apps/web/ui/support/chat-bubble.tsx"
  "packages/ui/src/modal.tsx"
  "packages/ui/src/popover.tsx"
  "packages/ui/src/sheet.tsx"
)

for f in "${files[@]}"; do
  echo
  echo "===== $f ====="
  if [ -f "$f" ]; then
    # Show first 120 lines and also any line range likely to include the z-50 usage.
    # (Avoid huge output; these files should be small.)
    sed -n '1,140p' "$f" | head -n 140
  else
    echo "Missing: $f"
  fi
done

echo
echo "===== Tailwind config: check for custom zIndex ====="
tailwind_cfgs=(
  "apps/web/tailwind.config.ts"
  "packages/embeds/react/tailwind.config.ts"
  "packages/tailwind-config/tailwind.config.ts"
  "packages/ui/tailwind.config.ts"
)

for tc in "${tailwind_cfgs[@]}"; do
  echo
  echo "---- $tc (zIndex search) ----"
  if [ -f "$tc" ]; then
    rg -n "zIndex|theme.*zIndex" "$tc" || true
    rg -n "extend.*zIndex" "$tc" || true
  else
    echo "Missing: $tc"
  fi
done

Repository: dubinc/dub

Length of output: 26886


Align banner z-index with modal layers to avoid obscuring dialogs

apps/web/lib/sandbox/components/program-environment-banner.tsx uses z-50, but packages/ui/src/modal.tsx sets the desktop Radix Dialog.Overlay/Dialog.Content to z-40; with both fixed, this banner can stack above the modal backdrop/content. workspace-environment-banner.tsx already uses z-30, so match the sandbox banner (or raise the modal layer) to keep critical UI on top.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/lib/sandbox/components/program-environment-banner.tsx` at line 17,
The banner's tailwind z-index string in program-environment-banner.tsx ("fixed
left-0 right-0 top-0 z-50 ...") places it above the modal layer; change that
z-50 to z-30 (to match workspace-environment-banner) so the banner no longer
obscures Radix Dialog overlays/contents (or alternatively coordinate with
packages/ui/src/modal.tsx to raise the modal z to >50 if you prefer the banner
to stay on top).

Comment thread apps/web/lib/zod/schemas/workspaces.ts Outdated
Comment thread apps/web/scripts/dev/seed-installation.ts Outdated
Comment thread apps/web/scripts/dev/seed-installation.ts
Move program copy into createStagingProgram and trigger both steps
asynchronously from the Stripe plan upgrade webhook.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant