Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@TWilson023
Copy link
Collaborator

@TWilson023 TWilson023 commented Nov 12, 2025

  • Programs can message partners not currently enrolled in the program
  • Programs can initiate a conversation with a partner even if messaging is disabled
  • Programs and partners can continue messaging after messaging is disabled if a conversation was already started
Screenshot 2025-11-12 at 3 35 32β€―PM Screenshot 2025-11-12 at 2 30 14β€―PM

Summary by CodeRabbit

  • New Features

    • Added Message and View Program/Partner navigation buttons across messaging UIs and partner sheets.
  • Bug Fixes

    • Fixed message retrieval, per-entity limits, and recent-sorting so lists reflect actual program/partner relationships.
    • Expanded notification selection to include all recent message types; stabilized redirects when enrollment or message data is missing.
  • Improvements

    • Enrollment-aware UI gating and conditional data fetching; support email and right-panel actions respect enrollment state.
    • Track partner messaging status to improve invite limits and messaging behavior.
  • Behavior Change

    • Removed in-app "Respond in Dub" campaign reply and campaign-generated message records.

@vercel
Copy link
Contributor

vercel bot commented Nov 12, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Nov 13, 2025 0:18am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 12, 2025

Walkthrough

Replaces enrollment-centered queries with direct Prisma partner/program queries across message APIs and actions; adds an enabled gate to useProgramEnrollment; exposes program.messagingEnabledAt in message schemas; shifts client UI gating to derive enrollment from message payloads; adds messagedAt to DiscoveredPartner.

Changes

Cohort / File(s) Change Summary
API: messages endpoints
apps/web/app/(ee)/api/messages/route.ts, apps/web/app/(ee)/api/partner-profile/messages/route.ts
Swap enrollment-based fetches for direct prisma queries on partner/program; apply OR filters (discoverable
Actions: message & read flows
apps/web/lib/actions/partners/mark-program-messages-read.ts, apps/web/lib/actions/partners/message-partner.ts, apps/web/lib/actions/partners/message-program.ts
Remove getProgramEnrollmentOrThrow; inline Prisma validations to locate program/partner, enforce ban/enrollment/message-based acceptance paths, use program.id for updates/creates, upsert DiscoveredPartner with messagedAt, and enforce network-invite limits.
SWR hook
apps/web/lib/swr/use-program-enrollment.ts
Add enabled?: boolean (default true) to hook signature and gate the SWR fetch on enabled.
Schemas & zod
apps/web/lib/zod/schemas/messages.ts
Include messagingEnabledAt in ProgramMessagesSchema pick.
Client pages β€” enrollment β†’ enrolled, gating & UI*
apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
Derive enrolledProgram/enrolledPartner from programEnrollment or programMessages/partnerMessages; use swrOpts.shouldRetryOnError; gate right-panel actions on enrollment presence; add ViewProgramButton/ViewPartnerButton; remove mobile-based initial open-state logic and adjust redirect/error gating.
Layout & MessagesDisabled
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/layout.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/page-client.tsx
Move messagingEnabledAt check into page-client; simplify MessagesDisabled markup and add explicit bottom action link; layout no longer short-circuits render by program.messagingEnabledAt.
Sidebar & callers of useProgramEnrollment
apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx
Conditionally call useProgramEnrollment({ enabled: isEnrolledProgramPage }) to defer enrollment fetch when not on an enrolled program page.
UI: partner network sheet
apps/web/ui/partners/partner-network/network-partner-sheet.tsx
Add a header "Message" Link button opening the internal partner messages page in a new tab (hidden on small screens).
Misc: campaign workflow & UI cleanup
apps/web/lib/api/workflows/execute-send-campaign-workflow.ts, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx
Remove DB Message record creation for campaign sends and remove a transactional campaign "Respond in Dub" UI block; email send/render flow remains.
Cron: notify partner
apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts
Broaden messages filter by removing the type === "direct" restriction; include all recent message types for unread-email selection.
Schema: prisma network
packages/prisma/schema/network.prisma
Add optional messagedAt DateTime? field to DiscoveredPartner.
API: network invites usage
apps/web/lib/api/partners/get-network-invites-usage.ts
Broaden usage filter to OR on invitedAt > billingStart or messagedAt > billingStart.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant PageClient
  participant API_Route
  participant Prisma
  Note right of PageClient `#F0F7FF`: derive enrolledProgram/enrolledPartner\nfrom programEnrollment OR messages payload
  Browser->>PageClient: open messages page (programSlug/partnerId)
  PageClient->>API_Route: fetch messages payload (partners|programs)
  API_Route->>Prisma: query partners/programs with OR filters\n(discoverable || enrolled || has messages)
  Prisma-->>API_Route: return entities with limited, ordered messages
  API_Route-->>PageClient: return shaped payload ({ partner|program, messages })
  PageClient->>Browser: render UI (gate right-panel on enrolledProgram/enrolledPartner)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay extra attention to:
    • Prisma where/include correctness and per-entity message take/ordering in API routes.
    • OR-condition logic replacing enrollment guards in message actions and read/update flows.
    • Call-sites that assumed useProgramEnrollment always ran and returned data (now can be disabled).
    • UI gating and redirect changes where enrollment is derived from messages instead of a dedicated enrollment endpoint.

Possibly related PRs

  • MessagesΒ #2781 β€” prior work on messages API/routes and message model; overlaps messaging query/create flows.
  • Partner NetworkΒ #2886 β€” changes around DiscoveredPartner/discoverable partner-network and messaged/upsert flows; directly related to messagedAt and discovered partner upsert.
  • Partner network invite limitsΒ #2935 β€” modifies network-invites usage and invite limits; relates to invite/messaging usage checks added here.

Poem

πŸ‡
I nibbled through Prisma, hopped past the gate,
Swapped enrollments for partners and sorted each slate.
Messages tidy, panels wait for the sign,
A carrot for code β€” small, clever, and fine. πŸ₯•

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
βœ… Passed checks (2 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title 'Messaging unenrolled partners' accurately summarizes the main change: enabling programs to message partners not currently enrolled in their program.
✨ Finishing touches
  • πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch message-unenrolled-partners

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.

@TWilson023 TWilson023 marked this pull request as ready for review November 12, 2025 20:12
Copy link
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

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/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (1)

28-271: Missing messaging enabled check contradicts PR objectives.

The AI summary states "messaging disabled checks are relocated to page-client," but there is no check for program.messagingEnabledAt or similar in this file. The MessagesPanel and send message functionality are always active regardless of whether messaging is enabled for the program.

Based on learnings, the team uses messagingEnabledAt timestamps to control messaging availability. This check should be implemented to prevent messages from being sent when messaging is disabled.

Consider adding a check like this before rendering the MessagesPanel:

const isMessagingEnabled = program?.messagingEnabledAt !== null;

// Then in the render:
<MessagesPanel
  messages={messages && user ? messages : undefined}
  error={errorMessages}
  currentUserType="user"
  currentUserId={user?.id || ""}
  program={program}
  partner={partner}
  disabled={!isMessagingEnabled}
  onSendMessage={!isMessagingEnabled ? undefined : async (message) => {
    // existing implementation
  }}
/>

Would you like me to open an issue to track implementing the messaging enabled check?

🧹 Nitpick comments (3)
apps/web/lib/actions/partners/message-program.ts (1)

21-48: Verify the security implications of allowing continued messaging via message history.

The second OR condition (lines 39-45) allows partners to continue messaging a program indefinitely if they have any message history, even if:

  • They've been unenrolled from the program
  • Messaging has been disabled (messagingEnabledAt set to null)

While this supports conversation continuity, it effectively bypasses both enrollment and messaging-enabled checks for partners with prior messages. Please confirm this is the intended behavior.

Additionally, consider improving error handling. When findFirstOrThrow fails, it throws a generic Prisma error that may not clearly communicate to the user why they cannot message (e.g., "not enrolled", "messaging disabled", or "program not found"). Consider catching the error and returning a more specific message:

try {
  const program = await prisma.program.findFirstOrThrow({
    // ... existing query
  });
} catch (error) {
  throw new Error(
    "You must be enrolled in this program with messaging enabled, or have an existing conversation."
  );
}
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/layout.tsx (1)

33-33: Remove unused program variable.

The program variable is extracted but never used in CapableLayout. According to the AI summary, the messagingEnabledAt check that previously used this value has been removed from this file.

Apply this diff to remove the unused variable:

 function CapableLayout({ children }: { children: ReactNode }) {
   const { slug: workspaceSlug } = useWorkspace();
   const { partnerId } = useParams() as { partnerId?: string };
-  const { program } = useProgram();
 
   const router = useRouter();
apps/web/lib/actions/partners/message-partner.ts (1)

38-64: Consider providing more specific error messages.

The findFirstOrThrow call will throw a generic Prisma error ("Record not found") for both non-existent partners and partners that don't meet the access criteria.

For better UX, consider catching the error and providing specific messages like "Partner not found" vs "You don't have access to message this partner."

Apply this diff to add specific error handling:

-    // Make sure partner is either discoverable, enrolled in the program, or already has a message with the program
-    await prisma.partner.findFirstOrThrow({
+    const partner = await prisma.partner.findFirst({
       where: {
         id: partnerId,
         OR: [
           {
             discoverableAt: {
               not: null,
             },
           },
           {
             programs: {
               some: {
                 programId,
               },
             },
           },
           {
             messages: {
               some: {
                 programId,
               },
             },
           },
         ],
       },
     });
+
+    if (!partner) {
+      const partnerExists = await prisma.partner.findUnique({
+        where: { id: partnerId },
+        select: { id: true },
+      });
+
+      throw new DubApiError({
+        code: "not_found",
+        message: partnerExists
+          ? "You don't have access to message this partner. They must be discoverable, enrolled in your program, or already in conversation with you."
+          : "Partner not found.",
+      });
+    }
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 7e438be and 675e5f8.

πŸ“’ Files selected for processing (13)
  • apps/web/app/(ee)/api/messages/route.ts (1 hunks)
  • apps/web/app/(ee)/api/partner-profile/messages/route.ts (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (6 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (4 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/layout.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/page-client.tsx (1 hunks)
  • apps/web/lib/actions/partners/mark-program-messages-read.ts (1 hunks)
  • apps/web/lib/actions/partners/message-partner.ts (1 hunks)
  • apps/web/lib/actions/partners/message-program.ts (2 hunks)
  • apps/web/lib/swr/use-program-enrollment.ts (2 hunks)
  • apps/web/lib/zod/schemas/messages.ts (1 hunks)
  • apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (7)
πŸ““ Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:254-254
Timestamp: 2025-05-29T04:49:42.842Z
Learning: In the Dub codebase, it's acceptable to keep `partners.dub.co` hardcoded rather than making it configurable for different environments.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
πŸ“š Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/layout.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
πŸ“š Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/page-client.tsx
  • apps/web/app/(ee)/api/messages/route.ts
  • apps/web/lib/actions/partners/message-program.ts
  • apps/web/lib/zod/schemas/messages.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/layout.tsx
  • apps/web/lib/actions/partners/mark-program-messages-read.ts
  • apps/web/lib/actions/partners/message-partner.ts
  • apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
  • apps/web/app/(ee)/api/partner-profile/messages/route.ts
πŸ“š Learning: 2025-09-18T16:33:17.719Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2858
File: apps/web/ui/partners/partner-application-tabs.tsx:1-1
Timestamp: 2025-09-18T16:33:17.719Z
Learning: When a React component in Next.js App Router uses non-serializable props (like setState functions), adding "use client" directive can cause serialization warnings. If the component is only imported by Client Components, it's better to omit the "use client" directive to avoid these warnings while still getting client-side execution through promotion.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx
πŸ“š Learning: 2025-08-25T21:03:24.285Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx:1-1
Timestamp: 2025-08-25T21:03:24.285Z
Learning: In Next.js App Router, Server Components that use hooks can work without "use client" directive if they are only imported by Client Components, as they get "promoted" to run on the client side within the Client Component boundary.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx
πŸ“š Learning: 2025-08-26T15:05:55.081Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/swr/use-bounty.ts:11-16
Timestamp: 2025-08-26T15:05:55.081Z
Learning: In the Dub codebase, workspace authentication and route structures prevent endless loading states when workspaceId or similar route parameters are missing, so gating SWR loading states on parameter availability is often unnecessary.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx
πŸ“š Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.

Applied to files:

  • apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx
🧬 Code graph analysis (11)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/page-client.tsx (2)
apps/web/lib/swr/use-program.ts (1)
  • useProgram (6-40)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx (1)
  • MessagesDisabled (8-42)
apps/web/app/(ee)/api/messages/route.ts (2)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/zod/schemas/messages.ts (1)
  • PartnerMessagesSchema (36-45)
apps/web/lib/actions/partners/message-program.ts (2)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/api/create-id.ts (1)
  • createId (66-71)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx (2)
packages/ui/src/icons/nucleo/msgs-dotted.tsx (1)
  • MsgsDotted (3-35)
packages/ui/src/button.tsx (1)
  • buttonVariants (7-28)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (5)
apps/web/lib/swr/use-partner.ts (1)
  • usePartner (6-29)
apps/web/ui/layout/page-content/toggle-side-panel-button.tsx (1)
  • ToggleSidePanelButton (4-66)
apps/web/ui/partners/partner-info-section.tsx (1)
  • PartnerInfoSection (7-71)
apps/web/ui/partners/partner-info-stats.tsx (1)
  • PartnerInfoStats (6-79)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-46)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/layout.tsx (4)
apps/web/lib/swr/use-program.ts (1)
  • useProgram (6-40)
apps/web/lib/plan-capabilities.ts (1)
  • getPlanCapabilities (4-21)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-upsell.tsx (1)
  • MessagesUpsell (8-70)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-46)
apps/web/lib/actions/partners/mark-program-messages-read.ts (1)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/actions/partners/message-partner.ts (1)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx (1)
apps/web/lib/swr/use-program-enrollment.ts (1)
  • useProgramEnrollment (8-45)
apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (2)
apps/web/lib/swr/use-program-enrollment.ts (1)
  • useProgramEnrollment (8-45)
apps/web/ui/layout/page-content/toggle-side-panel-button.tsx (1)
  • ToggleSidePanelButton (4-66)
apps/web/app/(ee)/api/partner-profile/messages/route.ts (1)
apps/web/lib/zod/schemas/messages.ts (1)
  • ProgramMessagesSchema (73-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
πŸ”‡ Additional comments (15)
apps/web/lib/actions/partners/message-program.ts (2)

50-64: LGTM!

The message creation correctly uses program.id from the validated program, consistent with the refactored validation logic.


66-76: LGTM!

The notification payload correctly uses program.id, maintaining consistency with the refactored approach.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/layout.tsx (2)

18-28: LGTM! Clean feature gating logic.

The loading state and plan capability checks provide appropriate gates for the messaging feature.


18-28: Verification confirmed: messagingEnabledAt validation is properly relocated to page-client.

The check has been moved to apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/page-client.tsx (line 12), where it returns <MessagesDisabled /> when program?.messagingEnabledAt === null. This prevents users from accessing messaging when the feature is disabled, while the layout correctly relies on the plan capability check (canMessagePartners).

apps/web/lib/actions/partners/message-partner.ts (1)

38-64: Code is correct; discoverable partner messaging is intentional and properly gated.

The verification confirms this change implements the intended behavior. Messaging access is controlled at three levels: (1) plan capability (canMessagePartners), (2) program-level enablement (messagingEnabledAt), and (3) partner accessibility (discoverable, enrolled, or in-conversation). The discoverable flag is opt-in by partners, and this same pattern is consistently deployed across the messaging system. No additional access controls are needed.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (5)

35-38: Good addition of retry skip for 404 errors.

The shouldRetryOnError option prevents unnecessary retries when a partner is not enrolled, improving performance and reducing API load. The renamed variables (enrolledPartner, enrolledPartnerError) also better convey the enrollment-specific nature of this data.


70-70: LGTM!

Deriving partner data from messages provides a sensible fallback for non-enrolled partners, ensuring the UI can still display basic partner information when direct enrollment data is unavailable.


78-78: Verify redirect logic handles all error scenarios.

The redirect now only triggers on errorMessages, but what happens if enrolledPartnerError exists but errorMessages is undefined? The page may render in an incomplete state. Consider whether a redirect should also occur when partner data fails to load without message errors.


273-298: LGTM!

The ViewPartnerButton component correctly routes to the appropriate profile page based on enrollment status. The implementation is clean and uses proper Next.js Link patterns with external tab opening.


121-128: Review comment verification complete β€” concern is invalid.

The error handling in this code correctly distinguishes non-enrollment (404) from other error types. Here's why:

The usePartner hook is configured with shouldRetryOnError: (err) => err.status !== 404, which means:

  • 404 errors: Stop retrying immediately (error becomes permanent)
  • Non-404 errors (network, permission, etc.): Continue retrying indefinitely

The API returns 404 only when getPartnerForProgram finds no enrollment matching the partnerId_programId composite key, and this error code maps to HTTP 404 status.

Therefore, enrolledPartnerError is truthy exclusively when the partner is not enrolled in the program. Other error types will continue to be retried by SWR and won't become permanent, so the conditional logic at lines 121–128 is correct.

apps/web/lib/zod/schemas/messages.ts (1)

75-81: Expose messagingEnabledAt for consumer gating.

Thanks for threading the field through this schema so downstream messaging flows can respect disabled programs.

apps/web/lib/actions/partners/mark-program-messages-read.ts (1)

18-47: Guard program lookup by partner/message relationship.

Nice pivot away from enrollment-only checks; this keeps the authorization tight while letting historical conversations stay readable.

apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx (1)

276-279: Gate SWR fetch behind enrolled pages.

Appreciate the enabled flag so we skip the enrollment fetch when we’re outside the program views.

apps/web/lib/swr/use-program-enrollment.ts (1)

9-26: enabled hook option keeps API flexible.

Optional flag keeps legacy callers untouched while letting consumers defer the network work when desired.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx (1)

12-40: Simpler empty state layout reads well.

The streamlined copy and CTA make the β€œenable messaging” path obvious.

Copy link
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: 0

Caution

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

⚠️ Outside diff range comments (2)
apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (1)

166-178: Use program instead of enrolledProgram for support email.

For unenrolled partners who have existing messages but messaging is now disabled, enrolledProgram will be undefined, preventing them from seeing the support email button. Since program (from message data) should also contain the support email, use it instead for consistency.

Apply this diff:

-            {enrolledProgram?.supportEmail && (
+            {program?.supportEmail && (
               <Link
-                href={`mailto:${enrolledProgram.supportEmail}`}
+                href={`mailto:${program.supportEmail}`}
                 target="_blank"
               >
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (1)

96-118: Avatar/name button has confusing behavior for non-enrolled partners.

The onClick handler at line 98 toggles isRightPanelOpen unconditionally, but the right panel only renders when enrolledPartner exists (line 215). For non-enrolled partners, clicking the avatar/name appears to do nothing because the panel never renders.

Apply this diff to conditionally enable the onClick based on enrollment status:

            <button
              type="button"
-              onClick={() => setIsRightPanelOpen((o) => !o)}
+              onClick={enrolledPartner ? () => setIsRightPanelOpen((o) => !o) : undefined}
              className="-mx-2 -my-1 flex items-center gap-2 rounded-lg px-2 py-1 transition-colors duration-100 hover:bg-black/5 active:bg-black/10"
+              disabled={!enrolledPartner}
            >

Alternatively, consider navigating to the partner's network profile page when clicked for non-enrolled partners.

♻️ Duplicate comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (1)

215-249: Critical: Right panel doesn't render for non-enrolled partners.

The right panel is wrapped in {enrolledPartner && (...)}, which prevents it from rendering entirely when the partner is not enrolled. According to the AI summary and past review comments, the panel should render with a limited/pending view for non-enrolled partners.

The past review comment suggested changing the outer conditional to allow the panel to render for both enrolled and non-enrolled partners:

-      {enrolledPartner && (
+      {(enrolledPartner || partner) && (

However, this would require additional conditional rendering inside the panel, as components like PartnerInfoStats expect full EnrolledPartnerProps. Consider either:

  1. Implementing the limited view for non-enrolled partners as intended, OR
  2. Confirming that the current behavior (no right panel for non-enrolled partners) is the desired design

The ViewPartnerButton in the header (line 126) currently serves as the only way to access non-enrolled partner info, which may be sufficient if that's the intended UX.

🧹 Nitpick comments (1)
apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (1)

85-85: Consider adding a fallback for enrolled partners with no messages yet.

If a partner is enrolled but hasn't messaged the program yet, program will be undefined while enrolledProgram is defined. This could cause the UI to show loading states unnecessarily.

Consider using a fallback pattern:

-  const program = programMessages?.[0]?.program;
+  const program = programMessages?.[0]?.program ?? enrolledProgram;

This ensures program data is always available when the partner is enrolled, regardless of message history.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 675e5f8 and dc1707d.

πŸ“’ Files selected for processing (2)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (7 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (5 hunks)
🧰 Additional context used
🧠 Learnings (8)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
πŸ“š Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
πŸ“š Learning: 2025-10-08T21:33:23.553Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2936
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/add-hostname-modal.tsx:28-34
Timestamp: 2025-10-08T21:33:23.553Z
Learning: In the dub/ui Button component, when the `disabledTooltip` prop is set to a non-undefined value (e.g., a string), the button is automatically disabled. Therefore, it's not necessary to also add the same condition to the `disabled` propβ€”setting `disabledTooltip={permissionsError || undefined}` is sufficient to disable the button when there's a permissions error.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
πŸ“š Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
🧬 Code graph analysis (2)
apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (2)
apps/web/lib/swr/use-program-enrollment.ts (1)
  • useProgramEnrollment (8-45)
apps/web/ui/layout/page-content/toggle-side-panel-button.tsx (1)
  • ToggleSidePanelButton (4-66)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (6)
apps/web/lib/swr/use-partner.ts (1)
  • usePartner (6-29)
apps/web/ui/layout/page-content/toggle-side-panel-button.tsx (1)
  • ToggleSidePanelButton (4-66)
apps/web/ui/partners/partner-info-section.tsx (1)
  • PartnerInfoSection (7-71)
apps/web/ui/partners/partner-info-group.tsx (1)
  • PartnerInfoGroup (11-86)
apps/web/ui/partners/partner-info-stats.tsx (1)
  • PartnerInfoStats (6-79)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
πŸ”‡ Additional comments (9)
apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (6)

48-54: LGTM! Good error handling for unenrolled partners.

The shouldRetryOnError logic correctly avoids retrying 404 errors, which now represent a valid state (unenrolled partner with existing messages).


93-98: LGTM! Redirect logic correctly supports unenrolled messaging.

The condition properly allows unenrolled partners to access the page when they have existing messages, while redirecting when neither enrollment nor message history exists.


143-150: LGTM! Appropriate conditional rendering based on enrollment status.

The conditional logic correctly shows different actions based on whether the partner is enrolled, handling the loading state gracefully by rendering null.


152-155: LGTM! Correctly implements continued messaging for existing conversations.

The condition properly allows existing conversations to continue even when messagingEnabledAt is null, while preventing new conversations. This aligns with the PR objective.


270-299: LGTM! Appropriate gating of enrollment-specific panel.

Conditionally rendering the right panel only for enrolled partners is correct, as it displays enrollment-specific information like referral links and rewards that wouldn't be relevant for unenrolled partners.


470-480: LGTM! Clean component extraction.

Nice extraction of the ViewProgramButton into a reusable component, following DRY principles.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (3)

34-77: LGTM!

The data fetching logic is well-structured:

  • shouldRetryOnError correctly prevents infinite retries on 404 responses for non-enrolled partners
  • Partner data is appropriately sourced from the messages API response
  • Error handling redirects to the messages index when message fetching fails

120-127: LGTM!

The conditional rendering logic for header buttons is well-designed:

  • Shows the toggle button for enrolled partners
  • Shows a view button for non-enrolled partners (when error indicates 404)
  • Shows nothing while loading (prevents layout shift)

254-279: LGTM!

The ViewPartnerButton component is well-implemented:

  • Correctly routes enrolled partners to their profile page
  • Routes non-enrolled partners to the network page
  • Uses appropriate button text for each case
  • Opens in a new tab to preserve the messages context

Copy link
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/ui/partners/partner-network/network-partner-sheet.tsx (1)

78-88: Consider adding screen reader indication for new tab behavior.

While the implementation is clean and functional, screen reader users won't be informed that the link opens in a new tab. This is a minor accessibility enhancement.

          <Link
            href={`/${workspaceSlug}/program/messages/${partner.id}`}
            target="_blank"
+           aria-label="Message partner (opens in new tab)"
          >
            <Button
              variant="secondary"
              text="Message"
              icon={<Msgs className="size-4 shrink-0" />}
              className="hidden h-9 rounded-lg px-4 sm:flex"
            />
          </Link>
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (1)

120-127: Consider adding a loading state placeholder.

When enrolledPartner is still loading (neither exists nor errored), the header renders null, creating a visual gap. While this is functional, showing a loading skeleton would improve the user experience.

Apply this diff to add a loading indicator:

         {enrolledPartner ? (
           <ToggleSidePanelButton
             isOpen={isRightPanelOpen}
             onClick={() => setIsRightPanelOpen((o) => !o)}
           />
         ) : enrolledPartnerError ? (
           <ViewPartnerButton partnerId={partnerId} isEnrolled={false} />
-        ) : null}
+        ) : (
+          <div className="size-9 animate-pulse rounded-lg bg-neutral-200" />
+        )}
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between dc1707d and e6bfae7.

πŸ“’ Files selected for processing (2)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (5 hunks)
  • apps/web/ui/partners/partner-network/network-partner-sheet.tsx (3 hunks)
🧰 Additional context used
🧠 Learnings (6)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
πŸ“š Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/ui/partners/partner-network/network-partner-sheet.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
🧬 Code graph analysis (2)
apps/web/ui/partners/partner-network/network-partner-sheet.tsx (1)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-46)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (3)
apps/web/lib/swr/use-partner.ts (1)
  • usePartner (6-29)
apps/web/ui/layout/page-content/toggle-side-panel-button.tsx (1)
  • ToggleSidePanelButton (4-66)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
πŸ”‡ Additional comments (4)
apps/web/ui/partners/partner-network/network-partner-sheet.tsx (2)

6-6: LGTM: Imports for messaging feature.

The new imports (useWorkspace, Msgs, Link) are appropriate and necessary for the messaging button implementation.

Also applies to: 14-14, 21-21


43-43: Add null check for workspaceSlug.

The useWorkspace hook can return undefined for workspace properties during loading or error states. Using workspaceSlug without validation (line 79) will create an invalid href like /undefined/program/messages/.... The existing code pattern at line 224 demonstrates checking workspace properties before use.

Consider one of these approaches:

Option 1: Conditionally render the button

  const { slug: workspaceSlug } = useWorkspace();
+
  const [currentTabId, setCurrentTabId] = useState<string>("about");

Then wrap the Link in a conditional:

        <div className="flex items-center gap-4">
-         <Link
-           href={`/${workspaceSlug}/program/messages/${partner.id}`}
-           target="_blank"
-         >
-           <Button
-             variant="secondary"
-             text="Message"
-             icon={<Msgs className="size-4 shrink-0" />}
-             className="hidden h-9 rounded-lg px-4 sm:flex"
-           />
-         </Link>
+         {workspaceSlug && (
+           <Link
+             href={`/${workspaceSlug}/program/messages/${partner.id}`}
+             target="_blank"
+           >
+             <Button
+               variant="secondary"
+               text="Message"
+               icon={<Msgs className="size-4 shrink-0" />}
+               className="hidden h-9 rounded-lg px-4 sm:flex"
+             />
+           </Link>
+         )}
          <div className="flex items-center">

Option 2: Provide a fallback or disable the button

          <Link
-           href={`/${workspaceSlug}/program/messages/${partner.id}`}
+           href={workspaceSlug ? `/${workspaceSlug}/program/messages/${partner.id}` : '#'}
            target="_blank"
          >
            <Button
              variant="secondary"
              text="Message"
              icon={<Msgs className="size-4 shrink-0" />}
              className="hidden h-9 rounded-lg px-4 sm:flex"
+             disabled={!workspaceSlug}
            />
          </Link>
β›” Skipped due to learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2538
File: apps/web/ui/partners/overview/blocks/commissions-block.tsx:16-27
Timestamp: 2025-06-18T20:26:25.177Z
Learning: In the Dub codebase, components that use workspace data (workspaceId, defaultProgramId) are wrapped in `WorkspaceAuth` which ensures these values are always available, making non-null assertions safe. This is acknowledged as a common pattern in their codebase, though not ideal.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/swr/use-bounty.ts:11-16
Timestamp: 2025-08-26T15:05:55.081Z
Learning: In the Dub codebase, workspace authentication and route structures prevent endless loading states when workspaceId or similar route parameters are missing, so gating SWR loading states on parameter availability is often unnecessary.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (2)

34-37: LGTM! Error handling correctly differentiates enrollment status.

The shouldRetryOnError configuration prevents retrying on 404 errors, allowing the component to distinguish between enrolled and non-enrolled partners. When a partner is not enrolled, enrolledPartnerError will be set, enabling the conditional rendering logic at lines 125-126 to show the appropriate fallback UI.


215-249: Right panel correctly restricted to enrolled partners.

The conditional rendering {enrolledPartner && (...)} ensures the profile panel only displays for enrolled partners, which aligns with the PR objectives. For non-enrolled partners, users can view basic info via the "View partner" button in the header (line 126), while the messaging functionality remains available in the left panel.

Note: The past review comment about unreachable code at lines 250-264 is no longer applicable to this version of the code.

Copy link
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

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between e6bfae7 and 8e3150a.

πŸ“’ Files selected for processing (5)
  • apps/web/app/(ee)/api/messages/route.ts (1 hunks)
  • apps/web/app/(ee)/api/partner-profile/messages/route.ts (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (8 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (6 hunks)
  • apps/web/lib/actions/partners/message-program.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (10)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
πŸ“š Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
  • apps/web/app/(ee)/api/partner-profile/messages/route.ts
  • apps/web/lib/actions/partners/message-program.ts
  • apps/web/app/(ee)/api/messages/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
πŸ“š Learning: 2025-10-08T21:33:23.553Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2936
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/add-hostname-modal.tsx:28-34
Timestamp: 2025-10-08T21:33:23.553Z
Learning: In the dub/ui Button component, when the `disabledTooltip` prop is set to a non-undefined value (e.g., a string), the button is automatically disabled. Therefore, it's not necessary to also add the same condition to the `disabled` propβ€”setting `disabledTooltip={permissionsError || undefined}` is sufficient to disable the button when there's a permissions error.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
πŸ“š Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-06-18T20:23:38.835Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2538
File: apps/web/ui/partners/overview/blocks/traffic-sources-block.tsx:50-82
Timestamp: 2025-06-18T20:23:38.835Z
Learning: Internal links within the same application that use target="_blank" may not require rel="noopener noreferrer" according to the team's security standards, even though it's generally considered a best practice for any target="_blank" link.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-09-24T15:50:16.414Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
🧬 Code graph analysis (5)
apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (1)
apps/web/lib/swr/use-program-enrollment.ts (1)
  • useProgramEnrollment (8-45)
apps/web/app/(ee)/api/partner-profile/messages/route.ts (2)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/zod/schemas/messages.ts (1)
  • ProgramMessagesSchema (73-84)
apps/web/lib/actions/partners/message-program.ts (2)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/api/create-id.ts (1)
  • createId (66-71)
apps/web/app/(ee)/api/messages/route.ts (2)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/zod/schemas/messages.ts (1)
  • PartnerMessagesSchema (36-45)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (6)
apps/web/lib/swr/use-partner.ts (1)
  • usePartner (6-29)
apps/web/ui/layout/page-content/toggle-side-panel-button.tsx (1)
  • ToggleSidePanelButton (4-66)
apps/web/ui/partners/partner-info-section.tsx (1)
  • PartnerInfoSection (7-71)
apps/web/ui/partners/partner-info-group.tsx (1)
  • PartnerInfoGroup (11-86)
apps/web/ui/partners/partner-info-stats.tsx (1)
  • PartnerInfoStats (6-79)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
πŸ”‡ Additional comments (11)
apps/web/app/(ee)/api/messages/route.ts (2)

24-59: Query logic correctly enables messaging unenrolled partners.

The partner-based query structure properly implements the PR objective of allowing programs to message partners not currently enrolled. The OR conditions appropriately cover discoverable partners, enrolled partners, and partners with existing message threads.


63-76: Data transformation correctly aligns with new query structure.

The sorting and mapping logic properly handles the restructured data, using the messages path directly and producing the expected {partner, messages} format.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (3)

34-37: Partner data fetching correctly handles enrollment status.

The combination of usePartner with 404 error handling and deriving basic partner info from messages enables the UI to gracefully handle both enrolled and non-enrolled partners.

Also applies to: 69-69


77-77: Redirect logic appropriately handles error states.

The redirect only triggers on message errors, allowing the page to render even when enrollment data is unavailable (404). This aligns with the PR objective of enabling messaging to non-enrolled partners.


121-128: Enrollment-aware UI correctly restricts profile access.

The implementation appropriately gates detailed profile information (stats, group, actions) to enrolled partners only, while providing a navigation path to view non-enrolled partners in the network. This aligns with the access control model for partner data.

Also applies to: 216-250, 255-280

apps/web/app/(ee)/api/partner-profile/messages/route.ts (2)

20-97: Query logic correctly implements partner-side message access.

The program-based query properly enforces access control:

  • Ensures partners are not banned (lines 22-28)
  • Allows enrolled partners to access program messages
  • Allows non-enrolled partners to access messages if the program initiated contact
  • Filters results appropriately based on whether a specific program is requested

101-114: Data transformation aligns with schema expectations.

The sorting by most recent message and mapping to {program, messages} format correctly produces the structure expected by ProgramMessagesSchema.

apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (4)

48-54: Program data fetching correctly handles enrollment status.

The dual-source approach (enrollment data for enrolled partners, message-derived data for non-enrolled) enables the UI to gracefully handle both scenarios while maintaining appropriate access control.

Also applies to: 85-85


94-98: Redirect logic appropriately balances access control.

The conditional redirect allows non-enrolled partners to access existing message threads while preventing access when there's no enrollment and no message history. This correctly implements the messaging access model.


153-180: External support fallback correctly uses enrollment data.

The conditional rendering for external support appropriately checks enrollment status and messaging availability, using enrolledProgram.supportEmail to provide contact information when messaging is disabled.


144-151: Enrollment-aware UI correctly restricts program details.

The implementation appropriately gates detailed program information (performance stats, rewards, referral links) to enrolled partners, while providing a navigation path for non-enrolled partners to view the public program page.

Also applies to: 271-300, 471-481

Copy link
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: 2

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 1b0b9e2 and 2796482.

πŸ“’ Files selected for processing (7)
  • apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts (0 hunks)
  • apps/web/app/(ee)/api/messages/route.ts (1 hunks)
  • apps/web/app/(ee)/api/partner-profile/messages/route.ts (1 hunks)
  • apps/web/lib/actions/partners/message-partner.ts (2 hunks)
  • apps/web/lib/actions/partners/message-program.ts (2 hunks)
  • apps/web/lib/api/partners/get-network-invites-usage.ts (1 hunks)
  • packages/prisma/schema/network.prisma (1 hunks)
πŸ’€ Files with no reviewable changes (1)
  • apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts
🧰 Additional context used
🧠 Learnings (5)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 3098
File: apps/web/lib/actions/partners/message-program.ts:49-58
Timestamp: 2025-11-12T22:23:10.390Z
Learning: In apps/web/lib/actions/partners/message-program.ts, when checking if a partner can continue messaging after messaging is disabled, the code intentionally requires `senderPartnerId: null` (program-initiated messages) to prevent partners from continuing to send junk messages. Only conversations started by the program should continue after messaging is disabled, as a spam prevention mechanism.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
πŸ“š Learning: 2025-11-12T22:23:10.390Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 3098
File: apps/web/lib/actions/partners/message-program.ts:49-58
Timestamp: 2025-11-12T22:23:10.390Z
Learning: In apps/web/lib/actions/partners/message-program.ts, when checking if a partner can continue messaging after messaging is disabled, the code intentionally requires `senderPartnerId: null` (program-initiated messages) to prevent partners from continuing to send junk messages. Only conversations started by the program should continue after messaging is disabled, as a spam prevention mechanism.

Applied to files:

  • apps/web/lib/actions/partners/message-program.ts
  • apps/web/app/(ee)/api/messages/route.ts
  • apps/web/lib/actions/partners/message-partner.ts
  • apps/web/app/(ee)/api/partner-profile/messages/route.ts
πŸ“š Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/lib/actions/partners/message-program.ts
  • apps/web/app/(ee)/api/messages/route.ts
  • apps/web/lib/api/partners/get-network-invites-usage.ts
  • apps/web/lib/actions/partners/message-partner.ts
  • apps/web/app/(ee)/api/partner-profile/messages/route.ts
πŸ“š Learning: 2025-09-24T15:50:16.414Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.

Applied to files:

  • apps/web/lib/actions/partners/message-program.ts
πŸ“š Learning: 2025-10-06T15:48:14.205Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.205Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.

Applied to files:

  • apps/web/lib/api/partners/get-network-invites-usage.ts
  • apps/web/lib/actions/partners/message-partner.ts
🧬 Code graph analysis (4)
apps/web/lib/actions/partners/message-program.ts (2)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/api/create-id.ts (1)
  • createId (66-71)
apps/web/app/(ee)/api/messages/route.ts (2)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/zod/schemas/messages.ts (1)
  • PartnerMessagesSchema (36-45)
apps/web/lib/actions/partners/message-partner.ts (4)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/api/partners/get-network-invites-usage.ts (1)
  • getNetworkInvitesUsage (5-30)
apps/web/lib/api/errors.ts (1)
  • DubApiError (58-75)
apps/web/lib/api/create-id.ts (1)
  • createId (66-71)
apps/web/app/(ee)/api/partner-profile/messages/route.ts (2)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/zod/schemas/messages.ts (1)
  • ProgramMessagesSchema (73-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

@steven-tey
Copy link
Collaborator

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 13, 2025

βœ… Actions performed

Full review triggered.

@steven-tey steven-tey merged commit 488b07d into main Nov 13, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the message-unenrolled-partners branch November 13, 2025 00:28
Copy link
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: 0

♻️ Duplicate comments (2)
apps/web/app/(ee)/api/partner-profile/messages/route.ts (1)

31-52: Reinstate the messaging-disabled spam guard.

The partners.some branch still returns programs for any enrolled partner even when messagingEnabledAt is null. That was the path we explicitly closed to stop partners from pinging programs that turned messaging off unless the thread was program-initiated (senderPartnerId: null). Please keep enrollment-only access gated to active messaging and rely on the second branch for conversations the program started.

               {
                 partners: {
                   some: {
                     partnerId: partner.id,
                   },
                 },
+                messagingEnabledAt: {
+                  not: null,
+                },
               },

Based on learnings

apps/web/lib/actions/partners/message-partner.ts (1)

88-93: Guard unlimited network invites before comparing.

workspace.networkInvitesLimit is null for unlimited workspaces, and networkInvitesUsage >= null coerces null to 0, so every outreach attempt immediately throws the β€œlimit reached” error. Please reintroduce the null-check before comparing so unlimited plans keep working.

-      if (networkInvitesUsage >= workspace.networkInvitesLimit) {
+      if (
+        workspace.networkInvitesLimit != null &&
+        networkInvitesUsage >= workspace.networkInvitesLimit
+      ) {
         throw new DubApiError({
           code: "forbidden",
           message: "You have reached your partner network invitations limit.",
         });
       }
🧹 Nitpick comments (1)
apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (1)

167-169: Add fallback to program?.supportEmail for robust edge case coverage.

The component already has access to the program object from the message payload (used for logo, name, and messagingEnabledAt checks). Based on codebase patterns, this object includes supportEmail as a standard field.

Currently, the email link only renders when enrolledProgram?.supportEmail exists. In the narrow edge case where a non-enrolled user has an empty conversation with messaging disabled, enrolledProgram would be undefined and the support email wouldn't display, even though the program object would have it available.

Change line 167 to:

{(enrolledProgram?.supportEmail ?? program?.supportEmail) && (
  <Link
    href={`mailto:${enrolledProgram?.supportEmail ?? program?.supportEmail}`}

This improves robustness without affecting the common enrollment paths.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 2d0b924 and 2796482.

πŸ“’ Files selected for processing (19)
  • apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts (0 hunks)
  • apps/web/app/(ee)/api/messages/route.ts (1 hunks)
  • apps/web/app/(ee)/api/partner-profile/messages/route.ts (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (8 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx (0 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (6 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/layout.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/page-client.tsx (1 hunks)
  • apps/web/lib/actions/partners/mark-program-messages-read.ts (1 hunks)
  • apps/web/lib/actions/partners/message-partner.ts (2 hunks)
  • apps/web/lib/actions/partners/message-program.ts (2 hunks)
  • apps/web/lib/api/partners/get-network-invites-usage.ts (1 hunks)
  • apps/web/lib/api/workflows/execute-send-campaign-workflow.ts (0 hunks)
  • apps/web/lib/swr/use-program-enrollment.ts (2 hunks)
  • apps/web/lib/zod/schemas/messages.ts (1 hunks)
  • apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx (1 hunks)
  • apps/web/ui/partners/partner-network/network-partner-sheet.tsx (3 hunks)
  • packages/prisma/schema/network.prisma (1 hunks)
πŸ’€ Files with no reviewable changes (3)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx
  • apps/web/lib/api/workflows/execute-send-campaign-workflow.ts
  • apps/web/app/(ee)/api/cron/messages/notify-partner/route.ts
🧰 Additional context used
🧠 Learnings (16)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 3098
File: apps/web/lib/actions/partners/message-program.ts:49-58
Timestamp: 2025-11-12T22:23:10.390Z
Learning: In apps/web/lib/actions/partners/message-program.ts, when checking if a partner can continue messaging after messaging is disabled, the code intentionally requires `senderPartnerId: null` (program-initiated messages) to prevent partners from continuing to send junk messages. Only conversations started by the program should continue after messaging is disabled, as a spam prevention mechanism.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
πŸ“š Learning: 2025-11-12T22:23:10.390Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 3098
File: apps/web/lib/actions/partners/message-program.ts:49-58
Timestamp: 2025-11-12T22:23:10.390Z
Learning: In apps/web/lib/actions/partners/message-program.ts, when checking if a partner can continue messaging after messaging is disabled, the code intentionally requires `senderPartnerId: null` (program-initiated messages) to prevent partners from continuing to send junk messages. Only conversations started by the program should continue after messaging is disabled, as a spam prevention mechanism.

Applied to files:

  • apps/web/ui/partners/partner-network/network-partner-sheet.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx
  • apps/web/lib/zod/schemas/messages.ts
  • apps/web/lib/actions/partners/message-program.ts
  • apps/web/lib/actions/partners/message-partner.ts
  • apps/web/app/(ee)/api/partner-profile/messages/route.ts
  • apps/web/app/(ee)/api/messages/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
  • apps/web/lib/actions/partners/mark-program-messages-read.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/layout.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
πŸ“š Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/ui/partners/partner-network/network-partner-sheet.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/layout.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
πŸ“š Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx
  • apps/web/lib/zod/schemas/messages.ts
  • apps/web/lib/actions/partners/message-program.ts
  • apps/web/lib/swr/use-program-enrollment.ts
  • apps/web/lib/actions/partners/message-partner.ts
  • apps/web/app/(ee)/api/partner-profile/messages/route.ts
  • apps/web/app/(ee)/api/messages/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
  • apps/web/lib/api/partners/get-network-invites-usage.ts
  • apps/web/lib/actions/partners/mark-program-messages-read.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/layout.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
πŸ“š Learning: 2025-09-18T16:33:17.719Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2858
File: apps/web/ui/partners/partner-application-tabs.tsx:1-1
Timestamp: 2025-09-18T16:33:17.719Z
Learning: When a React component in Next.js App Router uses non-serializable props (like setState functions), adding "use client" directive can cause serialization warnings. If the component is only imported by Client Components, it's better to omit the "use client" directive to avoid these warnings while still getting client-side execution through promotion.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/page-client.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx
πŸ“š Learning: 2025-08-25T21:03:24.285Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/bounty-card.tsx:1-1
Timestamp: 2025-08-25T21:03:24.285Z
Learning: In Next.js App Router, Server Components that use hooks can work without "use client" directive if they are only imported by Client Components, as they get "promoted" to run on the client side within the Client Component boundary.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx
πŸ“š Learning: 2025-08-26T15:05:55.081Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/swr/use-bounty.ts:11-16
Timestamp: 2025-08-26T15:05:55.081Z
Learning: In the Dub codebase, workspace authentication and route structures prevent endless loading states when workspaceId or similar route parameters are missing, so gating SWR loading states on parameter availability is often unnecessary.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/messages-disabled.tsx
πŸ“š Learning: 2025-09-24T15:50:16.414Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.

Applied to files:

  • apps/web/lib/actions/partners/message-program.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-10-06T15:48:14.205Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.205Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.

Applied to files:

  • apps/web/lib/actions/partners/message-partner.ts
  • apps/web/lib/api/partners/get-network-invites-usage.ts
πŸ“š Learning: 2025-06-16T19:21:23.506Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2519
File: apps/web/ui/analytics/utils.ts:35-37
Timestamp: 2025-06-16T19:21:23.506Z
Learning: In the `useAnalyticsFilterOption` function in `apps/web/ui/analytics/utils.ts`, the pattern `options?.context ?? useContext(AnalyticsContext)` is intentionally designed as a complete replacement strategy, not a merge. When `options.context` is provided, it should contain all required fields (`baseApiPath`, `queryString`, `selectedTab`, `requiresUpgrade`) and completely replace the React context, not be merged with it. This is used for dependency injection or testing scenarios.

Applied to files:

  • apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx
πŸ“š Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-06-18T20:23:38.835Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2538
File: apps/web/ui/partners/overview/blocks/traffic-sources-block.tsx:50-82
Timestamp: 2025-06-18T20:23:38.835Z
Learning: Internal links within the same application that use target="_blank" may not require rel="noopener noreferrer" according to the team's security standards, even though it's generally considered a best practice for any target="_blank" link.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
πŸ“š Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
πŸ“š Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
πŸ“š Learning: 2025-10-08T21:33:23.553Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2936
File: apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/analytics/add-hostname-modal.tsx:28-34
Timestamp: 2025-10-08T21:33:23.553Z
Learning: In the dub/ui Button component, when the `disabledTooltip` prop is set to a non-undefined value (e.g., a string), the button is automatically disabled. Therefore, it's not necessary to also add the same condition to the `disabled` propβ€”setting `disabledTooltip={permissionsError || undefined}` is sufficient to disable the button when there's a permissions error.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx
πŸ”‡ Additional comments (11)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx (5)

34-37: Excellent error handling for non-enrolled partners.

The shouldRetryOnError callback that skips retries on 404 allows the UI to quickly determine enrollment status and show appropriate controls for non-enrolled partners. This is a clean approach.


69-69: Smart derivation of partner from message payload.

Deriving partner data from the messages API response allows the UI to display partner information for both enrolled and non-enrolled partners without requiring a separate enrolled-partner check for basic display data.


121-128: Clear enrollment-aware header controls.

The conditional rendering provides appropriate actions for each state: toggle panel for enrolled partners, navigation to network/profile page for non-enrolled partners, and clean loading state. The logic is straightforward and handles all cases.


216-250: Right panel rendering correctly simplified.

The previous critical issue about unreachable fallback code has been resolved. The current implementation cleanly limits the right panel to enrolled partners only, while non-enrolled partners use the ViewPartnerButton in the header to navigate to the network page for partner details. This is a clearer separation of concerns.


255-280: Clean navigation component for enrollment-aware routing.

The ViewPartnerButton component cleanly routes to the appropriate page based on enrollment status. The implementation is straightforward and provides good UX for viewing partner details in a new tab.

apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (6)

48-54: Good error handling for enrollment fetch.

The retry logic correctly distinguishes between "not found" (404, an expected state for unenrolled partners) and transient errors that should be retried. This aligns well with the PR's goal of supporting messaging for unenrolled partners.


85-85: Smart derivation of program from messages.

Deriving program from the message payload allows the UI to display program information even for unenrolled partners who have ongoing conversations. This is a key enabler for the feature.


94-98: Redirect logic correctly supports the new feature.

The condition allows unenrolled partners to view conversations (enrollment error but messages exist) while redirecting only when there's nothing to display. This properly implements the requirement that programs can message unenrolled partners.


121-122: Well-designed conditional rendering for enrollment states.

The header button gating is thoughtfully implemented:

  • Disabled when not enrolled (since the right panel shows enrollment-specific details)
  • Shows the toggle button when enrolled
  • Shows a ViewProgramButton when not enrolled but has messages (provides alternate action)
  • Shows nothing during loading (acceptable UX)

Also applies to: 144-151


271-300: Right panel gating is correctly implemented.

The panel displays enrollment-specific information (performance stats, referral links, rewards), so gating it behind enrolledProgram existence is the right approach. The inclusion of ViewProgramButton in the header provides users with a clear path to the full program page.


471-481: Clean implementation of ViewProgramButton.

The component is simple, reusable, and follows good UX patterns by opening the program page in a new tab. Nice addition to provide navigation for unenrolled users.

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.

3 participants