Skip to content

Slack request invite#3795

Open
pepeladeira wants to merge 10 commits intomainfrom
slack-request-invite
Open

Slack request invite#3795
pepeladeira wants to merge 10 commits intomainfrom
slack-request-invite

Conversation

@pepeladeira
Copy link
Copy Markdown
Collaborator

@pepeladeira pepeladeira commented Apr 21, 2026

Summary by CodeRabbit

  • New Features

    • Priority Slack support invites for Advanced/Enterprise plans (request flow, onboarding button, and admin manual invites)
    • Workspace settings card to request or dismiss Slack invites
    • Admin dashboard section to send manual Slack support invites
    • Rate limits applied to Slack support requests (per-workspace daily and per-user hourly)
  • UI Enhancements

    • Two new Slack icon variants added
  • Pricing

    • “Advanced” plan now exposes a “request an invite” CTA for Slack support

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 21, 2026

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Apr 24, 2026 11:23pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

Adds a Slack support invite feature: admin UI and API, workspace/user-facing API with rate limits and plan gating, Slack integration library for channel creation and invites, onboarding/settings UI controls, new icons, and related config/type updates.

Changes

Cohort / File(s) Summary
Environment
apps/web/.env.example
Added SLACK_SUPPORT_BOT_TOKEN and SLACK_SUPPORT_INTERNAL_USER_IDS.
Admin UI & Page
apps/web/app/(ee)/admin.dub.co/(dashboard)/components/slack-support-invite.tsx, apps/web/app/(ee)/admin.dub.co/(dashboard)/page.tsx
New SlackSupportInvite component and admin dashboard section for manual Slack support invites (optional channel ID).
Admin API Route
apps/web/app/(ee)/api/admin/slack-support-invite/route.ts
Admin-protected POST handler that validates input and delegates to inviteToSlackSupportChannel, returns 409 on name conflicts or 200 with inviteId.
Workspace API Route
apps/web/app/api/workspaces/[idOrSlug]/support/slack-invite/route.ts
User-facing POST with plan gating, daily workspace and hourly per-user Upstash rate limits, email validation, and delegation to requestSlackConnectSupportInvite.
Core Slack Integration
apps/web/lib/slack/support-invite.ts
New Slack helper with timeout and error mapping, shared channel naming, internal user pre-invite (from env), and exported flows: requestSlackConnectSupportInvite, inviteToSlackSupportChannel, sharedSupportChannelName.
Onboarding Success UI
apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/success/page-client.tsx, .../page.tsx
Added plan-gated Slack invite row, persisted per-workspace invite state, in-flight lock, and parent page now fetches/passes workspace.plan.
Workspace Settings Card
apps/web/ui/workspaces/slack-support-settings-card.tsx
New SlackSupportSettingsCard with plan/permission/trial gating, persisted dismissal, invite request flow and toasts.
Plan Capabilities
apps/web/lib/plan-capabilities.ts
Added canRequestSlackSupportInvite capability (enterprise/advanced gated).
Icons
packages/ui/src/icons/slack-2.tsx, packages/ui/src/icons/slack-colorful.tsx, packages/ui/src/icons/index.tsx
Added two Slack icon components and re-exports in the icons barrel.
Pricing Types / Data
packages/utils/src/constants/pricing/pricing-plans.tsx
Made tooltip.cta/tooltip.href optional and added optional ctaLink; Advanced plan's slack feature uses ctaLink.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Admin
    participant Web as Web UI
    participant API as API Route
    participant SlackLib as Slack Support Lib
    participant SlackAPI as Slack Web API

    User->>Web: Click "Request Invite" or submit admin form
    Web->>API: POST { email, workspaceSlug, channelId? }
    API->>API: Validate, auth, plan/permission checks, rate limit
    API->>SlackLib: request/invite call
    SlackLib->>SlackLib: Ensure channel (create or use channelId)
    SlackLib->>SlackAPI: conversations.create or conversations.inviteShared
    SlackAPI-->>SlackLib: Success or error (e.g., name_taken)
    alt name_taken
        SlackLib-->>API: { nameTaken: true }
        API-->>Web: 409 conflict + error
        Web->>User: show error toast / prompt channelId
    else success
        SlackLib-->>API: { inviteId }
        API-->>Web: 200 { inviteId }
        Web->>User: show success toast
    end
Loading
sequenceDiagram
    participant Admin as Admin User
    participant AdminUI as Admin Dashboard
    participant AdminAPI as Admin API
    participant SlackLib as Slack Support Lib
    participant SlackAPI as Slack Web API

    Admin->>AdminUI: Fill and submit invite form
    AdminUI->>AdminAPI: POST payload
    AdminAPI->>AdminAPI: withAdmin auth
    AdminAPI->>SlackLib: inviteToSlackSupportChannel(...)
    SlackLib->>SlackAPI: create/invite calls, pre-invite internal users
    SlackAPI-->>SlackLib: responses (success or errors)
    alt name_taken
        SlackLib-->>AdminAPI: { nameTaken: true }
        AdminAPI-->>AdminUI: 409 with message
        AdminUI->>AdminUI: set needsChannelId = true, show error
    else success
        SlackLib-->>AdminAPI: { inviteId }
        AdminAPI-->>AdminUI: 200 { inviteId }
        AdminUI->>AdminUI: clear form, show success
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • devkiran
  • steven-tey

Poem

🐰 With eager paws I hop and write,

A slack invite set just right.
From dashboard, onboarding, settings too,
Channels bloom for help to come through.
Hooray — support hops closer to you!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature: enabling Slack request/invite functionality. It accurately reflects the comprehensive changes across environment config, API routes, UI components, and utilities.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slack-request-invite

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 8

🧹 Nitpick comments (2)
apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/success/page-client.tsx (1)

50-55: Minor: slackInviteInFlight ref is redundant with slackInviting state.

The button at line 301 is already disabled={slackInviteDone || slackInviting}, and setSlackInviting(true) runs synchronously before the fetch. The additional useRef guard is unnecessary unless you anticipate programmatic callers; consider removing it to reduce complexity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/app.dub.co/`(onboarding)/onboarding/(steps)/success/page-client.tsx
around lines 50 - 55, Remove the redundant slackInviteInFlight useRef and all
checks/assignments that reference it; rely on the existing slackInviting
useState (which is set synchronously before the fetch) and the button disabled
prop (disabled={slackInviteDone || slackInviting}) to prevent re-entrancy.
Update the invite handler (the function that calls setSlackInviting and performs
the fetch) to no longer read or write slackInviteInFlight, and delete the
slackInviteInFlight declaration and imports so only slackInviting controls
in-flight guarding.
apps/web/ui/workspaces/slack-support-settings-card.tsx (1)

65-75: AnimatePresence has no effect here.

The motion.div is unconditionally rendered (the early return null happens above), so AnimatePresence never sees a child being removed and the exit animation will not run on dismiss. Either wrap a conditional ({!dismissed && <motion.div ... />}) inside AnimatePresence (and move the dismissal checks into the JSX), or drop AnimatePresence entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/ui/workspaces/slack-support-settings-card.tsx` around lines 65 - 75,
The AnimatePresence wrapper currently has no effect because motion.div is always
rendered after the early return; update the JSX so AnimatePresence controls the
conditional rendering of the motion.div (e.g., move the dismissal logic into the
render and render {!dismissed && <motion.div ... />} inside AnimatePresence) so
exit animations run when dismissed, or if you prefer, remove the AnimatePresence
import/wrapper and keep the unconditional motion.div; reference AnimatePresence,
motion.div and the dismissed check to locate where to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/web/app/`(ee)/admin.dub.co/(dashboard)/components/slack-support-invite.tsx:
- Around line 14-42: The form action's fetch block doesn't handle non-OK HTTP
statuses or network/JSON parse errors, which can lead to false success toasts
and unhandled rejections; wrap the fetch/res.json sequence in a try/catch, check
res.ok before treating the response as success, attempt to read an error message
from the parsed body when res.ok is false (fall back to a generic message), and
only call toast.success with json.inviteId when res.ok is true and inviteId is
present; update the inline action function that calls
fetch("/api/admin/slack-support-invite") and keep using setNeedsChannelId,
toast.error/toast.success, and Form as-is.

In `@apps/web/app/`(ee)/api/admin/slack-support-invite/route.ts:
- Around line 10-24: Wrap the call to inviteToSlackSupportChannel in a try/catch
that catches DubApiError and uses handleAndReturnErrorResponse to return a
structured JSON error (or rethrow other errors); ensure this is done inside the
POST handler defined via withAdmin. Replace the manual presence checks for email
and workspaceSlug with a zod schema (validate email format and non-empty
strings) at the top of the POST handler, parse req.json() with that schema, and
return the schema error response shape when validation fails so the route
matches other admin endpoints.

In `@apps/web/app/api/workspaces/`[idOrSlug]/support/slack-invite/route.ts:
- Around line 28-38: The current per-user key in ratelimit
(ratelimit(...).limit(`slack-support-invite:${workspace.id}:${session.user.id}`))
doesn't prevent many invites from being generated by different users in the same
workspace, so add a second, coarser workspace-level rate check before
proceeding: call ratelimit(5, "1
day").limit(`slack-support-invite:workspace:${workspace.id}`) (or your chosen
cap/window), and if that check fails throw the same DubApiError; keep the
existing per-user check as well (order can be workspace-first then user) so both
checks must pass before creating the Slack channel/invite.

In
`@apps/web/app/app.dub.co/`(onboarding)/onboarding/(steps)/success/page-client.tsx:
- Around line 57-83: The bug is that setSlackInviteDone(true) is called in the
finally block of requestSlackSupportInvite, which marks the invite as completed
even on failure; change requestSlackSupportInvite so setSlackInviteDone(true) is
only called inside the success branch (where res.ok is true) and remove it from
the finally block, while keeping slackInviteInFlight.current = false and
setSlackInviting(false) in finally to always clear in-flight state.

In `@apps/web/lib/slack/support-invite.ts`:
- Around line 241-251: The current flow treats a "nameTaken"/nameTaken result
from createSharedCustomerChannel as a hard conflict and throws a DubApiError,
which makes repeated self-serve invites fail; instead, make the flow idempotent
by resolving the existing channel and sending the invite: when
createSharedCustomerChannel returns the nameTaken variant, lookup or fetch the
existing channel ID for the channel name (e.g., "shared-{workspaceSlug}") or
return the channelId if provided, and then call
sendSlackConnectInvite(channelId, email) just like the success branch;
alternatively persist the created channelId from createSharedCustomerChannel (or
call a resolver function) and use that persisted/found channelId to call
sendSlackConnectInvite so retries and subsequent invites succeed rather than
throwing from the nameTaken branch.
- Around line 90-99: The slackWebApi wrapper must be hardened: add an
AbortController with a configurable timeout (e.g., 5–10s) around the fetch to
prevent hangs, then check response.ok and handle non-2xx statuses (treat 429
specially) before attempting to parse JSON; wrap the response.json() call in
try/catch to convert any parsing or network errors into a DubApiError and
include structured fields (status, statusText, method, API path, raw body/text
when parse fails) to preserve debugging context. Update slackWebApi to throw a
DubApiError on timeout, non-ok HTTP responses, or JSON parse failures so all
callers receive a bounded, typed error.

In `@apps/web/ui/workspaces/slack-support-settings-card.tsx`:
- Around line 42-62: Remove the unreachable "conflict" branch inside the error
handling after the fetch to `/api/workspaces/${slug}/support/slack-invite`:
delete the `if (json?.error?.code === "conflict") { setDismissed(true); }` check
so only the existing toast.error(json?.error?.message ?? "...") runs on non-ok
responses; keep the existing success path that calls toast.success(...) and
setDismissed(true). This touches the error handling around the fetch/await json,
toast.error usage and the setDismissed calls in the Slack invite handler.

In `@packages/email/src/templates/upgrade-email.tsx`:
- Around line 27-38: The default workspaceSlug value should not be the sample
"acme" because missing callers (like the Stripe webhook) will incorrectly
produce /acme links; update the function signature to default workspaceSlug to
null instead of "acme" and change the fallback settingsUrl to use APP_DOMAIN
(i.e., when workspaceSlug is falsy use `${APP_DOMAIN}/settings`); locate the
workspaceSlug parameter and the settingsUrl computation in upgrade-email.tsx
(see getPlanDetails, workspaceSlug, settingsUrl, and APP_DOMAIN) and make those
two changes.

---

Nitpick comments:
In
`@apps/web/app/app.dub.co/`(onboarding)/onboarding/(steps)/success/page-client.tsx:
- Around line 50-55: Remove the redundant slackInviteInFlight useRef and all
checks/assignments that reference it; rely on the existing slackInviting
useState (which is set synchronously before the fetch) and the button disabled
prop (disabled={slackInviteDone || slackInviting}) to prevent re-entrancy.
Update the invite handler (the function that calls setSlackInviting and performs
the fetch) to no longer read or write slackInviteInFlight, and delete the
slackInviteInFlight declaration and imports so only slackInviting controls
in-flight guarding.

In `@apps/web/ui/workspaces/slack-support-settings-card.tsx`:
- Around line 65-75: The AnimatePresence wrapper currently has no effect because
motion.div is always rendered after the early return; update the JSX so
AnimatePresence controls the conditional rendering of the motion.div (e.g., move
the dismissal logic into the render and render {!dismissed && <motion.div ...
/>} inside AnimatePresence) so exit animations run when dismissed, or if you
prefer, remove the AnimatePresence import/wrapper and keep the unconditional
motion.div; reference AnimatePresence, motion.div and the dismissed check to
locate where to change.
🪄 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: 2196fed5-7c2d-4969-92de-1fe1382c4ba7

📥 Commits

Reviewing files that changed from the base of the PR and between 16e5c55 and 1b04a47.

📒 Files selected for processing (17)
  • apps/web/.env.example
  • apps/web/app/(ee)/admin.dub.co/(dashboard)/components/slack-support-invite.tsx
  • apps/web/app/(ee)/admin.dub.co/(dashboard)/page.tsx
  • apps/web/app/(ee)/api/admin/slack-support-invite/route.ts
  • apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts
  • apps/web/app/api/workspaces/[idOrSlug]/support/slack-invite/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/page-client.tsx
  • apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/success/page-client.tsx
  • apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/success/page.tsx
  • apps/web/lib/plan-capabilities.ts
  • apps/web/lib/slack/support-invite.ts
  • apps/web/ui/workspaces/slack-support-settings-card.tsx
  • packages/email/src/templates/upgrade-email.tsx
  • packages/ui/src/icons/index.tsx
  • packages/ui/src/icons/slack-2.tsx
  • packages/ui/src/icons/slack-colorful.tsx
  • packages/utils/src/constants/pricing/pricing-plans.tsx

Comment thread apps/web/app/(ee)/api/admin/slack-support-invite/route.ts
Comment thread apps/web/app/api/workspaces/[idOrSlug]/support/slack-invite/route.ts Outdated
Comment thread apps/web/lib/slack/support-invite.ts Outdated
Comment thread apps/web/lib/slack/support-invite.ts
Comment thread apps/web/ui/workspaces/slack-support-settings-card.tsx
Comment thread packages/email/src/templates/upgrade-email.tsx Outdated
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.

♻️ Duplicate comments (1)
apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/success/page-client.tsx (1)

69-89: ⚠️ Potential issue | 🟠 Major

Bug still present: invite is marked "done" on failure, blocking retries.

setSlackInviteDone(true) is still inside the finally block (line 87), so any failure path (network error, rate-limit hit, transient 5xx, plan/trial race, etc.) permanently flips the persisted slack-support-dismissed:${workspace.slug} flag to true. The user then sees a generic error toast and the CTA becomes "Invite sent" forever, with the early-return at line 64 preventing any retry.

Move the setSlackInviteDone(true) call into the success branch only:

🐛 Proposed fix
     try {
       const res = await fetch(
         `/api/workspaces/${workspace.slug}/support/slack-invite`,
         { method: "POST" },
       );
       const json = await res.json().catch(() => ({}));
       if (res.ok) {
         toast.success(
           "Check your email for your Slack Connect invitation to our team.",
         );
+        setSlackInviteDone(true);
       } else {
         toast.error(
           json?.error?.message ?? "Contact support for help connecting Slack.",
         );
       }
     } finally {
       slackInviteInFlight.current = false;
       setSlackInviting(false);
-      setSlackInviteDone(true);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/app.dub.co/`(onboarding)/onboarding/(steps)/success/page-client.tsx
around lines 69 - 89, The invite flow marks the invite as "done" even on failure
because setSlackInviteDone(true) is in the finally block; move
setSlackInviteDone(true) into the success branch (inside the res.ok block) and
remove it from the finally block so failures don't persist the persisted
slack-support-dismissed:${workspace.slug} flag; keep slackInviteInFlight.current
= false and setSlackInviting(false) in the finally block so in-flight state is
always cleared but only mark done when the POST to
/api/workspaces/.../support/slack-invite succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@apps/web/app/app.dub.co/`(onboarding)/onboarding/(steps)/success/page-client.tsx:
- Around line 69-89: The invite flow marks the invite as "done" even on failure
because setSlackInviteDone(true) is in the finally block; move
setSlackInviteDone(true) into the success branch (inside the res.ok block) and
remove it from the finally block so failures don't persist the persisted
slack-support-dismissed:${workspace.slug} flag; keep slackInviteInFlight.current
= false and setSlackInviting(false) in the finally block so in-flight state is
always cleared but only mark done when the POST to
/api/workspaces/.../support/slack-invite succeeds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: df749fa6-c1dd-451e-9afe-f1128eb77505

📥 Commits

Reviewing files that changed from the base of the PR and between abaf227 and 381a323.

📒 Files selected for processing (3)
  • apps/web/app/api/workspaces/[idOrSlug]/support/slack-invite/route.ts
  • apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/success/page-client.tsx
  • apps/web/ui/workspaces/slack-support-settings-card.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/ui/workspaces/slack-support-settings-card.tsx

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