Conversation
…nd Team+ tier fixes NV-7403 Email agent integration depends on domain management which is already Business-tier only. This aligns the agent email channel with that constraint: FREE/PRO orgs see the option with a locked badge and an upgrade tooltip; Business+ orgs are fully unblocked. Made-with: Cursor
✅ Deploy Preview for dashboard-v2-novu-staging ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughThis PR implements tier-based access control for the agent email integration feature by introducing a feature gate across the API and dashboard layers. It adds the feature flag definition, tier mappings, tier-based validation in the backend use case, a decorator on the test-email endpoint, and UI restrictions with upgrade prompts on the dashboard provider dropdown. Changes
Sequence DiagramsequenceDiagram
actor User
participant Dashboard
participant Hook as useIsAgentEmailAvailable
participant Subscription
participant FeatureConfig
participant API
participant DB as Organization Repo
User->>Dashboard: Select Novu Email provider
Dashboard->>Hook: Check if agent email available?
Hook->>Subscription: Fetch subscription data
Subscription->>FeatureConfig: Get feature enabled for tier?
FeatureConfig-->>Hook: false (if not Business+)
Hook-->>Dashboard: Feature not available
Dashboard-->>User: Show locked state + Upgrade button
alt User attempts API connection
User->>API: POST add-agent-integration (NovuAgent)
API->>DB: Get organization
DB-->>API: Return org with FREE/PRO tier
API->>FeatureConfig: Validate AGENT_EMAIL_INTEGRATION for tier
FeatureConfig-->>API: Access denied for tier
API-->>User: 402 Payment Required
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/api/src/app/agents/usecases/add-agent-integration/add-agent-integration.usecase.ts (1)
76-84: Consider a more descriptive error message for the 402 response.The
'Payment Required'string is generic and will surface to API consumers/devs without indicating which feature or tier is required. A more specific message (e.g., naming the feature and the required tier) would improve debuggability without leaking sensitive info.🛠️ Suggested change
if (!allowed) { - throw new HttpException('Payment Required', HttpStatus.PAYMENT_REQUIRED); + throw new HttpException( + 'Agent email integration requires the Team plan or higher. Please upgrade your subscription.', + HttpStatus.PAYMENT_REQUIRED + ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/api/src/app/agents/usecases/add-agent-integration/add-agent-integration.usecase.ts` around lines 76 - 84, The 402 uses a generic 'Payment Required' message; update enforceEmailTier to throw a more descriptive HttpException that names the feature (FeatureNameEnum.AGENT_EMAIL_INTEGRATION) and the required tier (use ApiServiceLevelEnum value expected for this feature) so consumers know which feature/tier is missing; locate enforceEmailTier (calls organizationRepository.findById and getFeatureForTierAsBoolean) and replace the generic message with one like "Agent email integration requires <REQUIRED_TIER> plan; current plan: <current_tier>" (include the actual enum values dynamically) while keeping the HttpStatus.PAYMENT_REQUIRED status.apps/dashboard/src/components/agents/provider-dropdown.tsx (1)
386-474: Extract the duplicated "Team+" badge markup.The gradient-text "Team+" / "Team feature" badge appears twice (lines 387–400 and 439–452) with near-identical inline
styleblocks. A small local component (e.g.,TeamTierBadge) would de-duplicate the markup and centralize the gradient styling. Tailwind arbitrary-value utilities (bg-[linear-gradient(...)] bg-clip-text text-transparent) can also replace the inlinestyleper the dashboard styling guideline to avoid inline styles for values expressible as utilities.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/agents/provider-dropdown.tsx` around lines 386 - 474, Create a small presentational component (e.g., TeamTierBadge) to replace the duplicated gradient "Team+" / "Team feature" badge markup found in the rowContent block and inside the TooltipContent; extract the repeated structure (icon RiLockStarLine + gradient text span) into that component and use it in both places (references: rowContent, TooltipContent, CommandItem render). Move the inline style to Tailwind utilities (use bg-[linear-gradient(225deg,_#FF884D_23.17%,_#E300BD_80.17%)] bg-clip-text text-transparent or bg-clip-text with WebKit equivalents if needed) so no inline style is used, and ensure the component accepts props for the copy ("Team+" vs "Team feature") and any sizing classes so it fits both usages without duplicating markup.
🤖 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/dashboard/src/components/agents/provider-dropdown.tsx`:
- Around line 413-480: The locked rows are rendered with disabled={isBusy ||
isLocked}, which makes them unreachable to keyboard users; remove disabling for
the isLocked case on the CommandItem (keep disabled only for isBusy) and instead
handle the locked case in the onSelect handler: if isLocked, prevent selection
(no-op) and open/focus the tooltip/upgrade CTA (e.g., call setOpen(true) or
focus the TooltipTrigger) so the tooltip and its "Upgrade plan" button become
reachable via keyboard; update handleSelect/onSelect logic to early-return for
locked items and ensure the TooltipTrigger wrapping rowContent remains
focusable.
---
Nitpick comments:
In
`@apps/api/src/app/agents/usecases/add-agent-integration/add-agent-integration.usecase.ts`:
- Around line 76-84: The 402 uses a generic 'Payment Required' message; update
enforceEmailTier to throw a more descriptive HttpException that names the
feature (FeatureNameEnum.AGENT_EMAIL_INTEGRATION) and the required tier (use
ApiServiceLevelEnum value expected for this feature) so consumers know which
feature/tier is missing; locate enforceEmailTier (calls
organizationRepository.findById and getFeatureForTierAsBoolean) and replace the
generic message with one like "Agent email integration requires <REQUIRED_TIER>
plan; current plan: <current_tier>" (include the actual enum values dynamically)
while keeping the HttpStatus.PAYMENT_REQUIRED status.
In `@apps/dashboard/src/components/agents/provider-dropdown.tsx`:
- Around line 386-474: Create a small presentational component (e.g.,
TeamTierBadge) to replace the duplicated gradient "Team+" / "Team feature" badge
markup found in the rowContent block and inside the TooltipContent; extract the
repeated structure (icon RiLockStarLine + gradient text span) into that
component and use it in both places (references: rowContent, TooltipContent,
CommandItem render). Move the inline style to Tailwind utilities (use
bg-[linear-gradient(225deg,_#FF884D_23.17%,_#E300BD_80.17%)] bg-clip-text
text-transparent or bg-clip-text with WebKit equivalents if needed) so no inline
style is used, and ensure the component accepts props for the copy ("Team+" vs
"Team feature") and any sizing classes so it fits both usages without
duplicating markup.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dbfa8af0-4198-40c7-bb8e-0d0fc40933a8
📒 Files selected for processing (8)
apps/api/src/app/agents/agents.controller.tsapps/api/src/app/agents/usecases/add-agent-integration/add-agent-integration.usecase.tsapps/dashboard/src/components/agents/provider-dropdown.tsxapps/dashboard/src/hooks/use-is-agent-email-available.tspackages/shared/src/consts/feature-tiers-constants.tspackages/shared/src/consts/productFeatureEnabledForServiceLevel.tspackages/shared/src/consts/providers/conversational-providers.tspackages/shared/src/types/billing.ts
| return ( | ||
| <CommandItem | ||
| key={itemKey} | ||
| value={`${item.displayName} ${item.providerId}${item.integration ? ` ${item.integration.identifier}` : ''}`} | ||
| disabled={isBusy} | ||
| disabled={isBusy || isLocked} | ||
| onSelect={() => { | ||
| void handleSelect(item, index); | ||
| }} | ||
| className={cn( | ||
| 'flex items-center gap-2 rounded-md p-1', | ||
| item.integration?._id === selectedIntegrationId && 'bg-bg-muted' | ||
| item.integration?._id === selectedIntegrationId && 'bg-bg-muted', | ||
| isLocked && '!pointer-events-auto opacity-60' | ||
| )} | ||
| > | ||
| <div className="flex flex-1 items-center gap-1"> | ||
| <ProviderIcon | ||
| providerId={item.providerId} | ||
| providerDisplayName={item.displayName} | ||
| className="size-4 shrink-0" | ||
| /> | ||
| <span className="text-text-sub text-label-xs flex-1 font-medium leading-4"> | ||
| {item.displayName} | ||
| </span> | ||
| </div> | ||
|
|
||
| {isRowPending && ( | ||
| <RiLoader4Line className="text-text-soft size-3 shrink-0 animate-spin" aria-hidden /> | ||
| )} | ||
| {!isRowPending && item.integration && item.providerId !== EmailProviderIdEnum.NovuAgent && ( | ||
| <span className="font-code text-text-sub shrink-0 text-[10px] leading-[15px] tracking-[-0.2px]"> | ||
| {item.integration.identifier} | ||
| </span> | ||
| )} | ||
| {!isRowPending && !item.integration && ( | ||
| <RiAddLine className="text-text-soft size-3 shrink-0" /> | ||
| {isLocked ? ( | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| {rowContent} | ||
| </TooltipTrigger> | ||
| <TooltipContent | ||
| side="right" | ||
| align="start" | ||
| variant="light" | ||
| size="lg" | ||
| className="flex w-64 flex-col items-start gap-3 border border-neutral-100 p-2 shadow-md" | ||
| > | ||
| <div className="flex items-center gap-1 rounded bg-red-50 px-2 py-1"> | ||
| <RiLockStarLine className="h-3 w-3 text-pink-600" /> | ||
| <span | ||
| className="text-[10px] font-medium uppercase leading-normal" | ||
| style={{ | ||
| background: 'linear-gradient(225deg, #FF884D 23.17%, #E300BD 80.17%)', | ||
| WebkitBackgroundClip: 'text', | ||
| WebkitTextFillColor: 'transparent', | ||
| backgroundClip: 'text', | ||
| }} | ||
| > | ||
| Team feature | ||
| </span> | ||
| </div> | ||
| <div className="flex flex-col items-start gap-3"> | ||
| <p className="text-xs text-neutral-500"> | ||
| Agent email requires the Team plan. Upgrade to connect an inbound email address. | ||
| </p> | ||
| <button | ||
| type="button" | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| setOpen(false); | ||
| if (IS_SELF_HOSTED) { | ||
| openInNewTab( | ||
| `${SELF_HOSTED_UPGRADE_REDIRECT_URL}?utm_campaign=agent_email_integration` | ||
| ); | ||
| } else { | ||
| navigate(`${ROUTES.SETTINGS_BILLING}?utm_source=agent_provider_dropdown`); | ||
| } | ||
| }} | ||
| className="flex items-center gap-1 text-xs font-medium text-neutral-900 hover:underline" | ||
| > | ||
| Upgrade plan <RiExternalLinkLine className="h-3 w-3" /> | ||
| </button> | ||
| </div> | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| ) : ( | ||
| rowContent | ||
| )} | ||
| </CommandItem> |
There was a problem hiding this comment.
Keyboard/a11y: locked rows may be unreachable, hiding the upgrade CTA.
Disabled CommandItems are typically skipped by cmdk's keyboard navigation, which means keyboard-only users won't be able to focus the locked row, trigger the tooltip, or reach the "Upgrade plan" button. The !pointer-events-auto class lets mouse hover work but does not help keyboard access.
Consider one of:
- Keep the row focusable (not
disabled) and handle the "no-op" inonSelect/handleSelectonly, so the tooltip opens on focus and the upgrade button is reachable via Tab. - Or render a standalone always-visible "Upgrade" link/button next to the locked provider (outside the tooltip), so the CTA is always reachable regardless of input modality.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/dashboard/src/components/agents/provider-dropdown.tsx` around lines 413
- 480, The locked rows are rendered with disabled={isBusy || isLocked}, which
makes them unreachable to keyboard users; remove disabling for the isLocked case
on the CommandItem (keep disabled only for isBusy) and instead handle the locked
case in the onSelect handler: if isLocked, prevent selection (no-op) and
open/focus the tooltip/upgrade CTA (e.g., call setOpen(true) or focus the
TooltipTrigger) so the tooltip and its "Upgrade plan" button become reachable
via keyboard; update handleSelect/onSelect logic to early-return for locked
items and ensure the TooltipTrigger wrapping rowContent remains focusable.
…team-business-tier
commit: |
Why
Agent email integration depends on domain management (inbound routing, DNS verification, custom domains) which is already locked to the Business (Team+) tier. Allowing FREE/PRO orgs to connect agent email creates a broken experience — the underlying infrastructure simply isn't available to them.
What changed
packages/sharedAGENT_EMAIL_INTEGRATIONtoFeatureNameEnumwith tier values:falsefor FREE/PRO,truefor BUSINESS/ENTERPRISE/UNLIMITEDAGENT_EMAIL_INTEGRATIONtoProductFeatureKeyEnumand wired it intoproductFeatureEnabledForServiceLevelrequiresBusinessTier?: booleanflag to theConversationalProvidertype; set it on theNovuAgent(Novu Email) entryapps/apiadd-agent-integration.usecase.ts— InjectedCommunityOrganizationRepository; addedenforceEmailTier()that checksapiServiceLeveland throws HTTP 402 whenAGENT_EMAIL_INTEGRATIONis not enabled for the org's tier. Called before any NovuAgent email setup.agents.controller.ts— Added@ProductFeature(ProductFeatureKeyEnum.AGENT_EMAIL_INTEGRATION)to thePOST /:identifier/test-emailendpoint so theProductFeatureInterceptorreturns 402 for FREE/PRO orgs.apps/dashboardhooks/use-is-agent-email-available.ts— New hook usinguseFetchSubscription+getFeatureForTierAsBooleanprovider-dropdown.tsx— Locked Novu Email row for lower tiers: disabled styling (opacity-60), hover tooltip with "Team feature" badge + explanation + "Upgrade plan →" link (navigates to billing or self-hosted upgrade URL). Click on the row does nothing; the CTA is inside the tooltip.Architecture
Screenshots
Team+gradient badge,opacity-60. Hovering anywhere on the row shows tooltip with upgrade CTA. Clicking the row does nothing. "Upgrade plan →" in tooltip navigates to billing.Related
Made with Cursor
What changed
The PR gates the Novu agent email integration behind the Business (Team+) tier by introducing a feature flag, server-side validation, and UI controls. This is necessary because the integration depends on domain management features (DNS verification, custom domains, inbound routing) that are only available in the Business tier and above.
Affected areas
shared: Added
AGENT_EMAIL_INTEGRATIONfeature flag toFeatureNameEnumandProductFeatureKeyEnum, enabled for BUSINESS/ENTERPRISE/UNLIMITED tiers and disabled for FREE/PRO. AddedrequiresBusinessTieroptional property toConversationalProvidertype, set totruefor Novu Email provider.api: Added tier enforcement in
add-agent-integration.usecase.tsby injectingCommunityOrganizationRepositoryand validating the organization'sapiServiceLevelagainst the feature; returns HTTP 402 (PAYMENT_REQUIRED) for ineligible tiers. Applied@ProductFeaturedecorator to the test-email endpoint to enforce the same restriction at the controller level.dashboard: Added
useIsAgentEmailAvailable()hook to check if the feature is enabled for the current subscription tier. Updatedprovider-dropdown.tsxto disable the Novu Email row for FREE/PRO organizations, display a "Team+" lock badge, show a tooltip explaining the tier requirement, and provide an "Upgrade plan" link that routes to billing (hosted) or self-hosted upgrade URL.Key technical decisions
@ProductFeaturedecorator pattern for declarative API endpoint gating.Testing
No new tests are explicitly mentioned. This change would benefit from integration tests verifying that FREE/PRO organizations receive HTTP 402 when attempting to add agent email integration, and that BUSINESS+ organizations can proceed normally.