-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add deactivating partner feature #2915
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughExpands enrollment status handling across APIs and UI to include “deactivated” (and sometimes “rejected”). Adds partner deactivate/reactivate flows with new server actions, modals, and menu options. Updates audit-log schema, status badges, and Prisma enum. Minor import/path and param handling adjustments. UI texts and tooltips updated accordingly. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant UI as DeactivatePartnerModal
participant Action as deactivatePartnerAction
participant DB as Prisma (TX)
participant BG as waitUntil
participant Cache as linkCache
participant Logs as audit.log
User->>UI: Confirm "deactivate partner"
UI->>Action: workspaceId, partnerId
Action->>DB: Verify enrollment (must not already be deactivated)
DB-->>Action: Enrollment OK
Action->>DB: TX: expire links + set enrollment.status=deactivated
DB-->>Action: TX committed
Action->>BG: enqueue post-tasks
BG->>Cache: expireMany(partner links)
BG->>Logs: record "partner.deactivated"
UI-->>User: Success toast and refresh
sequenceDiagram
autonumber
actor User
participant UI as ReactivatePartnerModal
participant Action as reactivatePartnerAction
participant DB as Prisma (TX)
participant BG as waitUntil
participant Cache as linkCache
participant Logs as audit.log
User->>UI: Click Reactivate
UI->>Action: workspaceId, partnerId
Action->>DB: Require enrollment.status=deactivated
DB-->>Action: Enrollment fetched
Action->>DB: TX: unexpire links + set status=approved
DB-->>Action: TX committed
Action->>BG: enqueue post-tasks
BG->>Cache: expireMany(partner links)
BG->>Logs: record "partner.reactivated"
UI-->>User: Success toast and refresh
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
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)/api/partner-profile/programs/[programId]/links/[linkId]/route.ts (1)
31-36: Update error message to reflect both banned and deactivated states.The status check now includes both "banned" and "deactivated", but the error message only mentions "You are banned from this program." This is misleading for deactivated partners, as these states are semantically different (banned typically implies a permanent/punitive action, while deactivated suggests a temporary suspension).
Consider updating the error message to be more accurate:
- if (["banned", "deactivated"].includes(status)) { - throw new DubApiError({ - code: "forbidden", - message: "You are banned from this program.", - }); - } + if (["banned", "deactivated"].includes(status)) { + throw new DubApiError({ + code: "forbidden", + message: status === "banned" + ? "You are banned from this program." + : "Your account has been deactivated for this program.", + }); + }apps/web/app/(ee)/api/partner-profile/programs/[programId]/links/route.ts (1)
39-44: Error message doesn't match expanded status check.The status check now includes
["banned", "deactivated", "rejected"], but the error message hardcoded on line 42 only mentions "You are banned from this program." This will mislead users who are deactivated or rejected.Apply this diff to provide accurate feedback:
- if (["banned", "deactivated", "rejected"].includes(status)) { + const forbiddenStatuses = ["banned", "deactivated", "rejected"]; + if (forbiddenStatuses.includes(status)) { + const messages = { + banned: "You are banned from this program.", + deactivated: "Your enrollment in this program has been deactivated.", + rejected: "Your enrollment in this program has been rejected.", + }; throw new DubApiError({ code: "forbidden", - message: "You are banned from this program.", + message: messages[status] || "You cannot create links for this program.", }); }Alternatively, use a generic message that covers all cases:
if (["banned", "deactivated", "rejected"].includes(status)) { throw new DubApiError({ code: "forbidden", - message: "You are banned from this program.", + message: "You cannot create links for this program due to your enrollment status.", }); }
🧹 Nitpick comments (4)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/unapproved-program-page.tsx (3)
35-38: Consider enriching the deactivation message.The description "Your partnership has been deactivated." is accurate but minimal. Consider adding context about next steps or how the user can get more information, similar to how the contact link is shown below (lines 97-103).
For example:
deactivated: () => ({ title: "Partnership deactivated", - description: "Your partnership has been deactivated.", + description: "Your partnership with this program has been deactivated by the program administrator.", }),
39-42: Reintroduction of rejected status looks good.The "rejected" status has been properly reintroduced with clear messaging. Like the "deactivated" status, you might consider enriching the description to provide more context.
Optional enhancement:
rejected: () => ({ title: "Application rejected", - description: "Your application has been rejected.", + description: "Your application to join this program has been rejected.", }),
93-105: Contact link logic updated correctly for new status.The expansion to include "deactivated" alongside "banned" and "rejected" is appropriate and consistent with the PR's objective. The updated messaging ("if you have any questions") is more neutral and suitable for all three statuses.
Optional: Extract status array to constant.
The array literal is recreated on every render. Consider extracting it to a constant for better performance:
+const CONTACT_STATUSES = ["banned", "deactivated", "rejected"] as const; + export function UnapprovedProgramPage({ programEnrollment, }: { programEnrollment: ProgramEnrollmentProps; }) { // ... rest of component - {["banned", "deactivated", "rejected"].includes( - programEnrollment.status, - ) && ( + {CONTACT_STATUSES.includes(programEnrollment.status) && (apps/web/app/(ee)/api/embed/referrals/links/[linkId]/route.ts (1)
36-42: Consider updating pre-existing error message.The error message says "before creating a link" but this is a PATCH endpoint for updating links. While this is a pre-existing issue not introduced in this PR, consider updating it for consistency.
Apply this diff if addressing the pre-existing issue:
throw new DubApiError({ code: "bad_request", message: - "This program needs a domain and URL set before creating a link.", + "This program needs a domain and URL set before updating a link.", });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
apps/web/app/(ee)/api/embed/referrals/links/[linkId]/route.ts(1 hunks)apps/web/app/(ee)/api/embed/referrals/links/route.ts(1 hunks)apps/web/app/(ee)/api/partner-profile/programs/[programId]/links/[linkId]/route.ts(1 hunks)apps/web/app/(ee)/api/partner-profile/programs/[programId]/links/route.ts(1 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx(1 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/auth.tsx(1 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/links/page-client.tsx(2 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/unapproved-program-page.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/layout.tsx(4 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/links/page-client.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx(8 hunks)apps/web/lib/actions/partners/deactivate-partner.ts(1 hunks)apps/web/lib/actions/partners/reactivate-partner.ts(1 hunks)apps/web/lib/api/audit-logs/schemas.ts(1 hunks)apps/web/lib/auth/partner.ts(1 hunks)apps/web/lib/zod/schemas/partners.ts(1 hunks)apps/web/ui/modals/deactivate-partner-modal.tsx(1 hunks)apps/web/ui/modals/reactivate-partner-modal.tsx(1 hunks)apps/web/ui/partners/partner-status-badges.ts(1 hunks)apps/web/ui/partners/program-card.tsx(2 hunks)packages/prisma/schema/program.prisma(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
PR: dubinc/dub#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
📚 Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
PR: dubinc/dub#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/partners/partners-table.tsx
🧬 Code graph analysis (8)
apps/web/lib/actions/partners/deactivate-partner.ts (5)
apps/web/lib/actions/safe-action.ts (1)
authActionClient(33-82)apps/web/lib/zod/schemas/partners.ts (1)
deactivatePartnerSchema(755-758)apps/web/lib/api/programs/get-program-enrollment-or-throw.ts (1)
getProgramEnrollmentOrThrow(6-97)apps/web/lib/api/links/cache.ts (1)
linkCache(113-113)apps/web/lib/api/audit-logs/record-audit-log.ts (1)
recordAuditLog(47-73)
apps/web/app/(ee)/api/embed/referrals/links/route.ts (1)
apps/web/lib/api/errors.ts (1)
DubApiError(75-92)
apps/web/ui/modals/deactivate-partner-modal.tsx (3)
apps/web/lib/types.ts (1)
PartnerProps(432-432)apps/web/lib/actions/partners/deactivate-partner.ts (1)
deactivatePartnerAction(14-86)packages/utils/src/constants/misc.ts (1)
OG_AVATAR_URL(29-29)
apps/web/app/(ee)/api/embed/referrals/links/[linkId]/route.ts (1)
apps/web/lib/api/errors.ts (1)
DubApiError(75-92)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (2)
apps/web/ui/modals/deactivate-partner-modal.tsx (1)
useDeactivatePartnerModal(142-167)apps/web/ui/modals/reactivate-partner-modal.tsx (1)
useReactivatePartnerModal(88-113)
apps/web/lib/actions/partners/reactivate-partner.ts (4)
apps/web/lib/actions/safe-action.ts (1)
authActionClient(33-82)apps/web/lib/zod/schemas/partners.ts (1)
deactivatePartnerSchema(755-758)apps/web/lib/api/links/cache.ts (1)
linkCache(113-113)apps/web/lib/api/audit-logs/record-audit-log.ts (1)
recordAuditLog(47-73)
apps/web/ui/modals/reactivate-partner-modal.tsx (3)
apps/web/lib/types.ts (1)
PartnerProps(432-432)apps/web/lib/actions/partners/reactivate-partner.ts (1)
reactivatePartnerAction(12-87)packages/utils/src/constants/misc.ts (1)
OG_AVATAR_URL(29-29)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/layout.tsx (3)
apps/web/ui/modals/deactivate-partner-modal.tsx (1)
useDeactivatePartnerModal(142-167)apps/web/ui/modals/reactivate-partner-modal.tsx (1)
useReactivatePartnerModal(88-113)packages/ui/src/menu-item.tsx (1)
MenuItem(43-86)
⏰ 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 (12)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/auth.tsx (2)
5-5: LGTM! Clean removal of unused import.The removal of
usePathnameis appropriate as it's no longer used in the component logic.
20-29: Deactivated status handling is correct.
UnapprovedProgramPage explicitly supports “deactivated” with its own title, description, and contact link, and auth.tsx’s catch-all renders it appropriately. No changes required.apps/web/ui/partners/program-card.tsx (2)
82-93: Consider if the generic messaging is appropriate for all non-pending statuses.The refactored messaging now uses a generic "if you have any questions" for all statuses (banned, rejected, deactivated), removing the previous "to appeal" language. While this works well for "deactivated," users who are "banned" or "rejected" might still need the ability to appeal their status. Consider whether different messaging should be used for different statuses, or if the generic approach is intentional.
If appeal functionality is important for banned/rejected users, you may want to restore status-specific messaging:
- {` ${statusDescription} `} - <Link - href={`/messages/${program.slug}`} - className="text-neutral-400 underline decoration-dotted underline-offset-2 hover:text-neutral-700" - > - Reach out to the {program.name} team - </Link>{" "} - if you have any questions. + {` ${statusDescription} `} + <Link + href={`/messages/${program.slug}`} + className="text-neutral-400 underline decoration-dotted underline-offset-2 hover:text-neutral-700" + > + Reach out to the {program.name} team + </Link>{" "} + {status === "deactivated" ? "if you have any questions." : "to appeal."}
39-44: No badge needed for “deactivated” status: program-card.tsx renders all non-approved statuses (including “deactivated”) via the statusDescriptions map, so ProgramEnrollmentStatusBadges intentionally excludes it.Likely an incorrect or invalid review comment.
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/links/page-client.tsx (1)
81-85: Simplified tooltip logic is cleaner.The removal of ban-specific tooltip messaging improves code clarity and aligns with moving status validation to the server layer. The remaining tooltip conditions clearly communicate the two client-side constraints: link limit and program permissions.
Note: Ensure that when a banned/deactivated partner attempts link creation (if the button is enabled), the error response from the server provides clear feedback about their enrollment status.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/links/page-client.tsx (1)
6-6: LGTM!The import path update aligns with the modal system reorganization mentioned in the PR.
apps/web/lib/api/audit-logs/schemas.ts (1)
58-61: LGTM!The reordering groups related partner lifecycle actions together, and the new
partner.deactivatedandpartner.reactivatedentries properly support the expanded status workflow.apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (1)
140-140: LGTM!The status array reordering is functionally equivalent since
.includes()performs membership checking regardless of order. This change is cosmetic.apps/web/lib/auth/partner.ts (1)
30-32: LGTM!The async params handling aligns with Next.js patterns and includes a safe fallback to an empty object. The handler continues to receive a concrete
Record<string, string>.apps/web/ui/partners/partner-status-badges.ts (1)
38-47: LGTM!The styling updates clearly differentiate partner statuses:
declinednow uses amber (invitation-related)deactivateduses neutral (manually disabled, distinct from banned)Icon choices are semantically appropriate for each status.
apps/web/ui/modals/reactivate-partner-modal.tsx (1)
1-114: LGTM!The modal implementation follows established patterns:
- Proper error and loading state handling
- Cache invalidation after successful action
- Clear UI with partner information and descriptive text
- Memoized component for optimization
apps/web/lib/zod/schemas/partners.ts (1)
755-758: LGTM!The
deactivatePartnerSchemafollows the established pattern for partner action schemas and includes the necessary fields for partner deactivation/reactivation operations.
| if ( | ||
| ["banned", "deactivated", "rejected"].includes(programEnrollment.status) | ||
| ) { | ||
| throw new DubApiError({ | ||
| code: "forbidden", | ||
| message: "You are banned from this program hence cannot create links.", | ||
| message: `You are ${programEnrollment.status} from this program hence cannot create links.`, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix misleading error message.
The error message says "cannot create links" but this is a PATCH endpoint for updating links. The message should reflect the actual operation being blocked.
Apply this diff to fix the error message:
throw new DubApiError({
code: "forbidden",
- message: `You are ${programEnrollment.status} from this program hence cannot create links.`,
+ message: `You are ${programEnrollment.status} from this program hence cannot update links.`,
});🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/embed/referrals/links/[linkId]/route.ts around lines 18
to 25, the error message incorrectly says "cannot create links" for a PATCH
(update) endpoint; update the thrown DubApiError message to reflect the blocked
operation (e.g., "You are {status} from this program hence cannot update
links.") so it correctly indicates the update action being prevented.
| if (["banned", "deactivated"].includes(programEnrollment.status)) { | ||
| throw new DubApiError({ | ||
| code: "forbidden", | ||
| message: "You are banned from this program hence cannot create links.", | ||
| message: `You are ${programEnrollment.status} from this program hence cannot create links.`, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Fix grammar and verify status completeness.
The expanded status check and dynamic error message are correct improvements. However, there are two concerns:
-
Grammar issue: "You are deactivated from this program" is awkward. Consider:
- "You have been deactivated from this program and cannot create links."
- Or: "Your enrollment status is '{status}' and you cannot create links."
-
Status completeness: The AI summary mentions "rejected" status is blocked in other PATCH routes. Verify whether "rejected" should also be included here for consistency.
Apply this diff to improve the error message grammar:
- message: `You are ${programEnrollment.status} from this program hence cannot create links.`,
+ message: `You have been ${programEnrollment.status} from this program and cannot create links.`,Run the following script to verify if "rejected" status is blocked elsewhere and should be included here:
🏁 Script executed:
#!/bin/bash
# Description: Check if "rejected" status is blocked in other partner API routes
# Search for status checks in partner-related API routes
rg -n --type=ts -C3 '\["banned",.*"deactivated".*"rejected"\]' apps/web/app
# Also check for any other status validation patterns
rg -n --type=ts -C3 'programEnrollment\.status.*rejected' apps/web/appLength of output: 2523
Include "rejected" status and improve error message grammar.
- Add
"rejected"to the blocked statuses for consistency with other routes. - Change the message to:
You have been ${programEnrollment.status} from this program and cannot create links.
- if (["banned", "deactivated"].includes(programEnrollment.status)) {
+ if (["banned", "deactivated", "rejected"].includes(programEnrollment.status)) {
throw new DubApiError({
code: "forbidden",
- message: `You are ${programEnrollment.status} from this program hence cannot create links.`,
+ message: `You have been ${programEnrollment.status} from this program and cannot create links.`,
});
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (["banned", "deactivated"].includes(programEnrollment.status)) { | |
| throw new DubApiError({ | |
| code: "forbidden", | |
| message: "You are banned from this program hence cannot create links.", | |
| message: `You are ${programEnrollment.status} from this program hence cannot create links.`, | |
| }); | |
| } | |
| if (["banned", "deactivated", "rejected"].includes(programEnrollment.status)) { | |
| throw new DubApiError({ | |
| code: "forbidden", | |
| message: `You have been ${programEnrollment.status} from this program and cannot create links.`, | |
| }); | |
| } |
🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/embed/referrals/links/route.ts around lines 27 to 32,
the blocked-status check should include "rejected" and the error message grammar
should be corrected; update the array to include "rejected" alongside "banned"
and "deactivated", and change the DubApiError message to: "You have been
${programEnrollment.status} from this program and cannot create links." so it
matches other routes and reads correctly.
| !hasLinksLimitReached && | ||
| hasAdditionalLinks && | ||
| programEnrollment?.status !== "banned"; | ||
| const canCreateNewLink = !hasLinksLimitReached && hasAdditionalLinks; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify that enrollment status validation exists at the server layer.
The removal of the programEnrollment?.status !== "banned" check from canCreateNewLink means the UI will no longer prevent banned/deactivated partners from attempting to create links. While moving validation to the server is a security best practice, ensure that:
- The server action or API endpoint that creates partner links validates enrollment status
- Error handling provides clear feedback when a banned/deactivated partner attempts link creation
- This aligns with the PR objective of adding the "deactivating partner feature"
Run the following script to verify server-side status validation:
🏁 Script executed:
#!/bin/bash
# Description: Find server actions or API routes that handle partner link creation
# and verify they validate enrollment status
# Search for partner link creation endpoints/actions
echo "=== Searching for partner link creation server actions/APIs ==="
rg -n --type=ts --type=tsx -C5 'createPartnerLink|create.*link' -g '!node_modules' -g '!*.test.*' -g '!*.spec.*'
echo -e "\n=== Searching for enrollment status validation in server code ==="
rg -n --type=ts --type=tsx -C3 'enrollment.*status|status.*banned|status.*deactivated' -g '!node_modules' -g '!*.test.*' -g '!*.spec.*' -g '**/app/**/route.ts' -g '**/lib/actions/**' -g '**/app/**/actions.ts'Length of output: 186
🏁 Script executed:
#!/bin/bash
echo "=== Searching for createPartnerLink definitions ==="
rg -n 'createPartnerLink' --glob '*.ts' --glob '*.tsx' --glob '!node_modules/*' --glob '!*.test.*' --glob '!*.spec.*'
echo -e "\n=== Searching for Next.js POST route handlers ==="
rg -n "export\s+async\s+function\s+POST" -g 'route.ts' --glob '!node_modules/*'
echo -e "\n=== Searching for enrollment status validation ==="
rg -nE 'status.*(banned|deactivated)' --glob '*.ts' --glob '*.tsx' --glob '!node_modules/*' --glob '!*.test.*' --glob '!*.spec.*'Length of output: 7432
🏁 Script executed:
#!/bin/bash
echo "=== Displaying first 200 lines of partner links route ==="
sed -n '1,200p' 'apps/web/app/(ee)/api/partners/links/route.ts'
echo -e "\n=== Searching for status checks in this route file ==="
rg -n 'status' 'apps/web/app/(ee)/api/partners/links/route.ts'Length of output: 5120
Enforce enrollment status in link-creation API
The POST handler in apps/web/app/(ee)/api/partners/links/route.ts doesn’t check programEnrollment.status (eg. “banned”/“deactivated”), so banned partners can still create links. Add a status guard before processing:
if (partner.status === "banned" || partner.status === "deactivated") {
throw new DubApiError({ code: "forbidden", message: "Partner is deactivated." });
}🤖 Prompt for AI Agents
In
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/links/page-client.tsx
around line 59 and the POST handler file
apps/web/app/(ee)/api/partners/links/route.ts, the API does not enforce partner
enrollment status allowing banned/deactivated partners to create links; add a
status guard in the POST handler that checks partner.programEnrollment.status
(or partner.status) and, if it equals "banned" or "deactivated", throw a
DubApiError with code "forbidden" and message "Partner is deactivated." before
any further processing so creation is blocked for those statuses.
| await Promise.allSettled([ | ||
| // TODO send email to partner | ||
| linkCache.expireMany(links), | ||
| recordAuditLog({ | ||
| workspaceId: workspace.id, | ||
| programId, | ||
| action: "partner.unbanned", | ||
| description: `Partner ${partnerId} unbanned`, | ||
| actor: user, | ||
| targets: [ | ||
| { | ||
| type: "partner", | ||
| id: partnerId, | ||
| metadata: programEnrollment.partner, | ||
| }, | ||
| ], | ||
| }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the correct audit-log event for reactivation
This action is logging partner.unbanned, which mislabels the event and breaks any consumers expecting a distinct “reactivated” audit entry. Please switch the action/description to the new reactivation event so audit trails stay accurate.
- recordAuditLog({
+ recordAuditLog({
workspaceId: workspace.id,
programId,
- action: "partner.unbanned",
- description: `Partner ${partnerId} unbanned`,
+ action: "partner.reactivated",
+ description: `Partner ${partnerId} reactivated`,
actor: user,
targets: [📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await Promise.allSettled([ | |
| // TODO send email to partner | |
| linkCache.expireMany(links), | |
| recordAuditLog({ | |
| workspaceId: workspace.id, | |
| programId, | |
| action: "partner.unbanned", | |
| description: `Partner ${partnerId} unbanned`, | |
| actor: user, | |
| targets: [ | |
| { | |
| type: "partner", | |
| id: partnerId, | |
| metadata: programEnrollment.partner, | |
| }, | |
| ], | |
| }), | |
| await Promise.allSettled([ | |
| // TODO send email to partner | |
| linkCache.expireMany(links), | |
| recordAuditLog({ | |
| workspaceId: workspace.id, | |
| programId, | |
| action: "partner.reactivated", | |
| description: `Partner ${partnerId} reactivated`, | |
| actor: user, | |
| targets: [ | |
| { | |
| type: "partner", | |
| id: partnerId, | |
| metadata: programEnrollment.partner, | |
| }, | |
| ], | |
| }), |
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/reactivate-partner.ts around lines 67 to 83,
the audit log call uses action "partner.unbanned" and description "Partner
${partnerId} unbanned" which mislabels the operation; change the action to the
designated reactivation event (e.g., "partner.reactivated") and update the
description to reflect reactivation (e.g., "Partner ${partnerId} reactivated")
so the audit entry correctly represents the operation and downstream consumers
receive the correct event.
| const { executeAsync, isPending } = useAction(deactivatePartnerAction, { | ||
| onSuccess: async () => { | ||
| toast.success("Partner deactivated successfully!"); | ||
| setShowDeactivatePartnerModal(false); | ||
| mutatePrefix("/api/partners"); | ||
| }, | ||
| onError({ error }) { | ||
| toast.error(error.serverError); | ||
| }, | ||
| }); | ||
|
|
||
| const onSubmit = useCallback(async () => { | ||
| if (!workspaceId || !partner.id) { | ||
| return; | ||
| } | ||
|
|
||
| await executeAsync({ | ||
| workspaceId, | ||
| partnerId: partner.id, | ||
| }); | ||
| }, [executeAsync, partner.id, workspaceId]); | ||
|
|
||
| const isDisabled = useMemo(() => { | ||
| return ( | ||
| !workspaceId || !partner.id || confirm !== "confirm deactivate partner" | ||
| ); | ||
| }, [workspaceId, partner.id, confirm]); | ||
|
|
||
| return ( | ||
| <Modal | ||
| showModal={showDeactivatePartnerModal} | ||
| setShowModal={setShowDeactivatePartnerModal} | ||
| > | ||
| <div className="flex flex-col items-center justify-center space-y-3 border-b border-neutral-200 px-4 py-8 sm:px-10"> | ||
| <img | ||
| src={partner.image || `${OG_AVATAR_URL}${partner.name}`} | ||
| alt={partner.name} | ||
| className="size-12 rounded-full" | ||
| /> | ||
|
|
||
| <div className="flex flex-col text-center"> | ||
| <h3 className="text-lg font-semibold leading-7">{partner.name}</h3> | ||
| <p className="text-sm font-medium leading-5 text-neutral-500"> | ||
| {partner.email} | ||
| </p> | ||
| </div> | ||
|
|
||
| <p className="text-balance text-center text-sm font-normal leading-5 text-neutral-600"> | ||
| This will deactivate the partner and disable all their active links. | ||
| Their commissions and payouts will remain intact. You can reactivate | ||
| them later if needed.{" "} | ||
| <span className="font-semibold"> | ||
| Are you sure you want to continue? | ||
| </span> | ||
| </p> | ||
| </div> | ||
|
|
||
| <form | ||
| onSubmit={handleSubmit(onSubmit)} | ||
| className="grid gap-4 bg-neutral-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-12" | ||
| > | ||
| <div> | ||
| <label htmlFor="name" className="flex items-center space-x-2"> | ||
| <h2 className="text-sm font-medium text-neutral-900"> | ||
| To verify, type <strong>confirm deactivate partner</strong> below | ||
| </h2> | ||
| </label> | ||
| <div className="relative mt-2 rounded-md shadow-sm"> | ||
| <input | ||
| className={cn( | ||
| "block w-full rounded-md border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm", | ||
| errors.confirm && "border-red-600", | ||
| )} | ||
| placeholder="confirm deactivate partner" | ||
| type="text" | ||
| autoComplete="off" | ||
| {...register("confirm", { | ||
| required: true, | ||
| })} | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <Button | ||
| type="submit" | ||
| className="w-full" | ||
| variant="danger" | ||
| text="Confirm deactivate" | ||
| disabled={isDisabled} | ||
| loading={isPending} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reset the confirmation field after closing
Because the Modal keeps this form mounted while hidden, the typed “confirm deactivate partner” string persists after you close the dialog. Re‑opening the modal leaves the input prefilled, so the destructive action can be triggered without retyping the guard phrase. Please call reset() (or manually clear confirm) on success/close so every attempt requires a fresh confirmation.
const {
register,
handleSubmit,
watch,
+ reset,
formState: { errors },
} = useForm<DeactivatePartnerFormData>({
...
const { executeAsync, isPending } = useAction(deactivatePartnerAction, {
onSuccess: async () => {
toast.success("Partner deactivated successfully!");
setShowDeactivatePartnerModal(false);
+ reset();
mutatePrefix("/api/partners");
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { executeAsync, isPending } = useAction(deactivatePartnerAction, { | |
| onSuccess: async () => { | |
| toast.success("Partner deactivated successfully!"); | |
| setShowDeactivatePartnerModal(false); | |
| mutatePrefix("/api/partners"); | |
| }, | |
| onError({ error }) { | |
| toast.error(error.serverError); | |
| }, | |
| }); | |
| const onSubmit = useCallback(async () => { | |
| if (!workspaceId || !partner.id) { | |
| return; | |
| } | |
| await executeAsync({ | |
| workspaceId, | |
| partnerId: partner.id, | |
| }); | |
| }, [executeAsync, partner.id, workspaceId]); | |
| const isDisabled = useMemo(() => { | |
| return ( | |
| !workspaceId || !partner.id || confirm !== "confirm deactivate partner" | |
| ); | |
| }, [workspaceId, partner.id, confirm]); | |
| return ( | |
| <Modal | |
| showModal={showDeactivatePartnerModal} | |
| setShowModal={setShowDeactivatePartnerModal} | |
| > | |
| <div className="flex flex-col items-center justify-center space-y-3 border-b border-neutral-200 px-4 py-8 sm:px-10"> | |
| <img | |
| src={partner.image || `${OG_AVATAR_URL}${partner.name}`} | |
| alt={partner.name} | |
| className="size-12 rounded-full" | |
| /> | |
| <div className="flex flex-col text-center"> | |
| <h3 className="text-lg font-semibold leading-7">{partner.name}</h3> | |
| <p className="text-sm font-medium leading-5 text-neutral-500"> | |
| {partner.email} | |
| </p> | |
| </div> | |
| <p className="text-balance text-center text-sm font-normal leading-5 text-neutral-600"> | |
| This will deactivate the partner and disable all their active links. | |
| Their commissions and payouts will remain intact. You can reactivate | |
| them later if needed.{" "} | |
| <span className="font-semibold"> | |
| Are you sure you want to continue? | |
| </span> | |
| </p> | |
| </div> | |
| <form | |
| onSubmit={handleSubmit(onSubmit)} | |
| className="grid gap-4 bg-neutral-50 px-4 py-8 text-left sm:rounded-b-2xl sm:px-12" | |
| > | |
| <div> | |
| <label htmlFor="name" className="flex items-center space-x-2"> | |
| <h2 className="text-sm font-medium text-neutral-900"> | |
| To verify, type <strong>confirm deactivate partner</strong> below | |
| </h2> | |
| </label> | |
| <div className="relative mt-2 rounded-md shadow-sm"> | |
| <input | |
| className={cn( | |
| "block w-full rounded-md border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm", | |
| errors.confirm && "border-red-600", | |
| )} | |
| placeholder="confirm deactivate partner" | |
| type="text" | |
| autoComplete="off" | |
| {...register("confirm", { | |
| required: true, | |
| })} | |
| /> | |
| </div> | |
| </div> | |
| <Button | |
| type="submit" | |
| className="w-full" | |
| variant="danger" | |
| text="Confirm deactivate" | |
| disabled={isDisabled} | |
| loading={isPending} | |
| const { | |
| register, | |
| handleSubmit, | |
| watch, | |
| reset, | |
| formState: { errors }, | |
| } = useForm<DeactivatePartnerFormData>({ | |
| defaultValues: { | |
| confirm: "", | |
| }, | |
| }); | |
| const { executeAsync, isPending } = useAction(deactivatePartnerAction, { | |
| onSuccess: async () => { | |
| toast.success("Partner deactivated successfully!"); | |
| setShowDeactivatePartnerModal(false); | |
| reset(); | |
| mutatePrefix("/api/partners"); | |
| }, | |
| onError({ error }) { | |
| toast.error(error.serverError); | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In apps/web/ui/modals/deactivate-partner-modal.tsx around lines 46-135 the
confirmation input value persists because the modal stays mounted; after a
successful deactivation or when the modal is closed you must clear the form
state — call reset() from react-hook-form (or set the confirm field to empty)
immediately after setShowDeactivatePartnerModal(false) in the onSuccess handler
and also in any handler that closes the modal so the next open starts with a
blank confirmation field.
| @@ -0,0 +1,113 @@ | |||
| import { reactivatePartnerAction } from "@/lib/actions/partners/reactivate-partner"; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify audit log action name consistency.
The referenced reactivatePartnerAction (apps/web/lib/actions/partners/reactivate-partner.ts:73) logs the action as "partner.unbanned" instead of "partner.reactivated". This is inconsistent with the new audit schema entry "partner.reactivated" added in apps/web/lib/api/audit-logs/schemas.ts.
Please verify the correct audit log action name:
🏁 Script executed:
#!/bin/bash
# Description: Check audit log references in reactivate-partner action
# Find the audit log call in reactivate-partner action
rg -A 5 "recordAuditLog" apps/web/lib/actions/partners/reactivate-partner.ts
# Verify the schema includes both entries
rg '"partner\.(reactivated|unbanned)"' apps/web/lib/api/audit-logs/schemas.tsLength of output: 636
Fix audit log action name in reactivate-partner action
In apps/web/lib/actions/partners/reactivate-partner.ts, change the recordAuditLog call’s action value from "partner.unbanned" to "partner.reactivated" to align with the audit schema.
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/reactivate-partner.ts (locate the
recordAuditLog call near the partner reactivation flow), the audit log action is
currently set to "partner.unbanned"; update that value to "partner.reactivated"
to match the audit schema; ensure the rest of the recordAuditLog payload and
callsite remain unchanged and run tests/lint to confirm no other references need
updating.
| rejected // program rejected the partner | ||
| invited // partner who has been invited | ||
| declined // partner declined the invite | ||
| deactivated // partner is deactivated by the program |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
LGTM! Verify that the migration file exists.
The new deactivated status is logically positioned between declined and banned, expanding the partner lifecycle management capabilities.
Please ensure that a Prisma migration file has been generated and committed for this schema change:
🏁 Script executed:
#!/bin/bash
# Description: Verify that a migration file exists for the deactivated status
# Check for recent migration files that might contain the deactivated status
fd -e sql . packages/prisma/migrations --exec head -20 {} \; | rg -i "deactivated"Length of output: 113
🏁 Script executed:
#!/bin/bash
# Locate all migrations directories
echo "Migrations directories:"
fd -t d migrations
# Search for 'deactivated' in all SQL files under any migrations directory
echo "Searching for 'deactivated' in migration SQL files:"
find . -type f -path '*/migrations/*/*.sql' -exec grep -Hn "deactivated" {} +Length of output: 104
Missing Prisma migration for the new deactivated status
I found no SQL migration containing “deactivated”; run npx prisma migrate dev --name add-deactivated-status and commit the generated files under packages/prisma/migrations.
🤖 Prompt for AI Agents
In packages/prisma/schema/program.prisma around line 7, the new enum/value
comment "deactivated" was added but there is no corresponding Prisma migration;
run `npx prisma migrate dev --name add-deactivated-status` from the repo root
(or from packages/prisma if your setup requires) to generate the SQL migration
and updated Prisma client, then commit the new migration folder/files under
packages/prisma/migrations (and any updated prisma client artifacts) to the
repo.
There was a problem hiding this 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 (1)
apps/web/ui/modals/unban-partner-modal.tsx (1)
46-144: Reset the confirmation field whenever the modal closes.Because this modal stays mounted while hidden, the typed
confirm unban …string persists after closing. Reopening keeps the guard prefilled, so the destructive action can be confirmed without retyping. ImportresetfromuseFormand call it when the modal closes or succeeds (e.g.,useEffectonshowUnbanPartnerModalor inside both the success handler and Cancel click) to reinstate the empty confirmation state.
♻️ Duplicate comments (1)
apps/web/ui/modals/deactivate-partner-modal.tsx (1)
46-145: Reset the confirmation guard on close.The confirmation input stays populated after closing, so reopening the modal leaves
confirm deactivate partneralready filled and the action can fire without retyping. Please invokereset()(or manually clear the field) whenever the modal closes/succeeds—e.g., by pullingresetfromuseFormand calling it inside the success handler and any close path (Cancel button,useEffectonshowDeactivatePartnerModal, etc.).
🧹 Nitpick comments (2)
apps/web/ui/modals/ban-partner-modal.tsx (2)
65-67: Silent early return could mask issues.The early return when
workspaceIdorpartner.idis missing provides no user feedback. While this should never occur in normal flows (the submit button is also disabled), consider logging an error or showing a toast to aid debugging if this state is somehow reached.Apply this diff to add defensive logging:
async (data: BanPartnerFormData) => { if (!workspaceId || !partner.id) { + console.error("Missing workspaceId or partner.id in ban partner form"); + toast.error("Unable to ban partner. Please refresh and try again."); return; }
192-200: Remove unnecessary dependency from useCallback.Including
showBanPartnerModalin the dependency array causes the callback to regenerate on every modal open/close, even though the modal component receives it as a prop and will always get the current value. Removing it improves memoization efficiency.Apply this diff:
const BanPartnerModalCallback = useCallback(() => { return ( <BanPartnerModal showBanPartnerModal={showBanPartnerModal} setShowBanPartnerModal={setShowBanPartnerModal} partner={partner} /> ); - }, [showBanPartnerModal, setShowBanPartnerModal, partner]); + }, [setShowBanPartnerModal, partner]);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/layout.tsx(4 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx(5 hunks)apps/web/ui/modals/archive-partner-modal.tsx(2 hunks)apps/web/ui/modals/ban-partner-modal.tsx(1 hunks)apps/web/ui/modals/deactivate-partner-modal.tsx(1 hunks)apps/web/ui/modals/reactivate-partner-modal.tsx(1 hunks)apps/web/ui/modals/unban-partner-modal.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
PR: dubinc/dub#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/partners/partners-table.tsx
🧬 Code graph analysis (7)
apps/web/ui/modals/unban-partner-modal.tsx (1)
packages/utils/src/constants/misc.ts (1)
OG_AVATAR_URL(29-29)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx (4)
apps/web/ui/modals/deactivate-partner-modal.tsx (1)
useDeactivatePartnerModal(151-176)apps/web/ui/modals/reactivate-partner-modal.tsx (1)
useReactivatePartnerModal(104-129)packages/ui/src/popover.tsx (1)
Popover(25-102)packages/ui/src/menu-item.tsx (1)
MenuItem(43-86)
apps/web/ui/modals/reactivate-partner-modal.tsx (3)
apps/web/lib/types.ts (1)
PartnerProps(432-432)apps/web/lib/actions/partners/reactivate-partner.ts (1)
reactivatePartnerAction(12-87)packages/utils/src/constants/misc.ts (1)
OG_AVATAR_URL(29-29)
apps/web/ui/modals/ban-partner-modal.tsx (4)
apps/web/lib/zod/schemas/partners.ts (2)
banPartnerSchema(729-738)BAN_PARTNER_REASONS(67-74)apps/web/lib/types.ts (1)
PartnerProps(432-432)apps/web/lib/actions/partners/ban-partner.ts (1)
banPartnerAction(20-158)packages/utils/src/constants/misc.ts (1)
OG_AVATAR_URL(29-29)
apps/web/ui/modals/archive-partner-modal.tsx (1)
packages/utils/src/constants/misc.ts (1)
OG_AVATAR_URL(29-29)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/layout.tsx (4)
apps/web/ui/modals/deactivate-partner-modal.tsx (1)
useDeactivatePartnerModal(151-176)apps/web/ui/modals/reactivate-partner-modal.tsx (1)
useReactivatePartnerModal(104-129)apps/web/ui/modals/archive-partner-modal.tsx (1)
useArchivePartnerModal(108-132)packages/ui/src/menu-item.tsx (1)
MenuItem(43-86)
apps/web/ui/modals/deactivate-partner-modal.tsx (3)
apps/web/lib/types.ts (1)
PartnerProps(432-432)apps/web/lib/actions/partners/deactivate-partner.ts (1)
deactivatePartnerAction(14-86)packages/utils/src/constants/misc.ts (1)
OG_AVATAR_URL(29-29)
⏰ 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 (10)
apps/web/ui/modals/archive-partner-modal.tsx (5)
1-15: LGTM!Imports are clean and all dependencies are properly utilized throughout the component.
17-54: LGTM!The component logic correctly handles both archive and unarchive flows:
- Proper status-based action text/description
- Validation guards before action execution
- Appropriate cache invalidation on success
- Clean error handling
61-87: LGTM!The UI refactor improves visual hierarchy:
- Partner details are now in a distinct card with appropriate borders and background
- Avatar styling with white background ensures proper display
- Action description is appropriately positioned outside the info card
- Consistent use of neutral color palette
89-103: LGTM!The action bar is well-structured:
- Proper loading state handling during async operations
- Dynamic button text matches the action context
- Consistent styling with the modal's neutral theme
108-132: LGTM!The custom hook follows React best practices:
- Proper memoization with
useCallbackanduseMemo- Correct dependency arrays
- Clean API surface for consumers
- Prevents unnecessary re-renders
apps/web/ui/modals/ban-partner-modal.tsx (5)
1-21: LGTM!All imports are properly scoped and necessary for the modal's functionality.
23-25: LGTM!Type definition correctly extends the Zod schema with the additional confirmation field.
82-182: LGTM!The modal UI is well-structured with proper validation feedback, destructive action warnings, and confirmation requirements. The form handling and state management are implemented correctly.
185-209: LGTM!The hook implementation follows a solid pattern for modal state management and provides a clean API for consumers. The memoization ensures stable references for the returned values.
96-96: No encoding needed for avatar URL fallback
partner.nameis intentionally concatenated unencoded—this matches the established codebase pattern for OG_AVATAR_URL.Likely an incorrect or invalid review comment.
Summary by CodeRabbit
New Features
Style