-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Program marketplace #2985
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
Program marketplace #2985
Conversation
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
π§Ή Nitpick comments (1)
apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx (1)
104-109: Consider simplifying the isActive logic (optional).The current implementation using
.every()is functionally correct and maintainable if more exclusions are added later. However, for just two exclusions, a direct boolean expression might be slightly more readable:isActive: (pathname, href) => - pathname.startsWith(href) && - ["invitations", "marketplace"].every( - (k) => !pathname.startsWith(`${href}/${k}`), - ), + pathname.startsWith(href) && + !pathname.startsWith(`${href}/invitations`) && + !pathname.startsWith(`${href}/marketplace`),The current approach is fine if you anticipate adding more exclusions in the future.
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (6)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/layout.tsx(1 hunks)apps/web/app/api/user/referrals-token/route.ts(2 hunks)apps/web/lib/middleware/utils/partners-redirect.ts(1 hunks)apps/web/lib/network/get-discoverability-requirements.ts(4 hunks)apps/web/lib/network/throw-if-partner-cannot-view-marketplace.ts(1 hunks)apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx(8 hunks)
π§ Files skipped from review as they are similar to previous changes (4)
- apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/layout.tsx
- apps/web/lib/network/throw-if-partner-cannot-view-marketplace.ts
- apps/web/app/api/user/referrals-token/route.ts
- apps/web/lib/middleware/utils/partners-redirect.ts
π§° Additional context used
π§ Learnings (4)
π Common learnings
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-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/ui/layout/sidebar/partners-sidebar-nav.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/ui/layout/sidebar/partners-sidebar-nav.tsx
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
apps/web/lib/network/get-discoverability-requirements.ts
𧬠Code graph analysis (2)
apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx (3)
packages/ui/src/icons/nucleo/shop.tsx (1)
Shop(3-65)apps/web/lib/swr/use-partner-profile.ts (1)
usePartnerProfile(6-30)apps/web/lib/network/get-discoverability-requirements.ts (1)
partnerCanViewMarketplace(34-51)
apps/web/lib/network/get-discoverability-requirements.ts (3)
apps/web/lib/constants/partner-profile.ts (1)
PARTNER_NETWORK_MIN_COMMISSIONS_CENTS(15-15)apps/web/lib/types.ts (2)
PartnerProps(453-456)EnrolledPartnerProps(465-465)packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(7-22)
β° 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/lib/network/get-discoverability-requirements.ts (5)
2-10: LGTM!The import consolidation and path updates are clean refactoring changes that align with the new constant usage pattern.
44-46: Document the @dub.co email bypass rationale.The bypass grants unconditional marketplace access to any partner whose email ends with "@dub.co". While this appears intentional for internal testing, consider:
- Adding a code comment explaining this is for Dub employees
- Confirming that the email address is validated by the authentication layer to prevent spoofing
- If this is temporary for testing, consider using a feature flag instead
Is this bypass intended for production, or should it be gated behind a development/staging environment check?
107-107: LGTM!The currency formatting correctly uses the new constant and will display the commission threshold cleanly (e.g., "$10" instead of "$10.00").
23-23: Change is consistent and correct.The constant
PARTNER_NETWORK_MIN_COMMISSIONS_CENTSis properly centralized inapps/web/lib/constants/partner-profile.tsand used consistently across all commission threshold checks. Bothget-discoverability-requirements.tsandthrow-if-partner-cannot-view-marketplace.tsreference the same constant ($10), and no hardcoded commission values exist elsewhere in the codebase.
34-51: Breaking API change is complete across all call sites.The function signature change from single parameter to destructured object has been properly implemented. All three call sites (
partners-sidebar-nav.tsx:344,referrals-token/route.ts:40, andmarketplace/layout.tsx:15) have been updated to use the new{ partner, programEnrollments }signature.The
@dub.coemail bypass is an intentional pattern for internal employee access, consistent with similar checks found inthrow-if-partner-cannot-view-marketplace.tsand e2e test cleanup logic elsewhere in the codebase. Since the partner object originates from an authenticated session via theusePartnerProfilehook, email spoofing is mitigated at the authentication layer.apps/web/ui/layout/sidebar/partners-sidebar-nav.tsx (5)
3-21: LGTM!The new imports are well-organized and all are utilized within the component for the marketplace feature.
46-46: LGTM!The optional showMarketplace field appropriately extends the data model for conditional marketplace visibility.
110-119: LGTM!The conditional marketplace navigation item is well-implemented with appropriate icon, badge, and placement. The spread operator pattern ensures clean conditional rendering.
288-310: LGTM!The hooks are properly used following React rules, and the data is appropriately integrated into the visibility logic. The undefined fallback handling is safe.
344-347: LGTM!The call to
partnerCanViewMarketplacecorrectly uses the new signature and appropriately handles undefined data with safe fallbacks.
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
β»οΈ Duplicate comments (1)
apps/web/ui/partners/program-marketplace/program-category.tsx (1)
16-19: UsereplaceAllto replace all underscores in the label.
category.replace("_", " ")only replaces the first occurrence. Categories like"food_and_beverage"would render as"food and_beverage".const { icon: Icon, label } = categoryData ?? { icon: CircleInfo, - label: category.replace("_", " "), + label: category.replaceAll("_", " "), };
π§Ή Nitpick comments (3)
apps/web/ui/partners/program-marketplace/program-rewards-display.tsx (1)
99-106: Consider applyingiconClassNamein single-item case for consistency.In the multi-item case (line 131),
iconClassNameis passed toProgramRewardIcon, but in the single-item case here, it's not applied to the icon container. This inconsistency may cause styling differences depending on the number of items.<div className={cn( "text-content-default flex size-6 items-center justify-center rounded-md", isDarkImage && "text-content-inverted", + iconClassName, )} >apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/featured-program-card.tsx (2)
16-16: Consider a more robust dark image detection mechanism.The current approach relies on the presence of "-dark" in the URL string, which is fragile and error-prone. If the naming convention changes or isn't consistently followed, the dark mode styling will break.
Consider storing a boolean flag in the program data model or using a more explicit configuration approach.
115-132: Simplify first category rendering.Using
slice(0, 1).map()creates an unnecessary single-item array and then maps over it. Since you're only rendering the first category, consider accessing it directly for cleaner, more efficient code.Apply this diff:
- {program.categories.slice(0, 1)?.map((category) => ( + {program.categories[0] && ( <ProgramCategory - key={category} - category={category} + key={program.categories[0]} + category={program.categories[0]} onClick={() => queryParams({ set: { - category, + category: program.categories[0], }, del: "page", }) } className={cn( "hover:bg-bg-default/10 active:bg-bg-default/20", isDarkImage && "text-content-inverted", )} /> - ))} + )}
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (4)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/featured-program-card.tsx(1 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/program-card.tsx(1 hunks)apps/web/ui/partners/program-marketplace/program-category.tsx(1 hunks)apps/web/ui/partners/program-marketplace/program-rewards-display.tsx(1 hunks)
π§ Files skipped from review as they are similar to previous changes (1)
- apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/program-card.tsx
π§° Additional context used
π§ Learnings (4)
π Common learnings
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-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/partners/program-marketplace/program-rewards-display.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/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/featured-program-card.tsx
π Learning: 2025-11-24T16:36:36.196Z
Learnt from: CR
Repo: dubinc/dub PR: 0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-11-24T16:36:36.196Z
Learning: Applies to packages/hubspot-app/app/cards/**/*.{js,jsx,ts,tsx} : Only components exported from the `hubspot/ui-extensions` npm package can be used in card components
Applied to files:
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/featured-program-card.tsx
𧬠Code graph analysis (3)
apps/web/ui/partners/program-marketplace/program-rewards-display.tsx (3)
apps/web/lib/types.ts (2)
RewardProps(556-556)DiscountProps(483-483)apps/web/ui/partners/format-reward-description.ts (1)
formatRewardDescription(4-32)apps/web/ui/partners/format-discount-description.ts (1)
formatDiscountDescription(5-34)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/featured-program-card.tsx (6)
apps/web/lib/types.ts (1)
NetworkProgramProps(473-473)packages/utils/src/constants/misc.ts (1)
OG_AVATAR_URL(29-29)apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/program-status-badge.tsx (1)
ProgramStatusBadge(20-39)apps/web/ui/partners/program-marketplace/program-rewards-display.tsx (1)
ProgramRewardsDisplay(28-136)apps/web/ui/partners/program-marketplace/program-category.tsx (1)
ProgramCategory(6-43)packages/ui/src/tooltip.tsx (1)
Tooltip(68-119)
apps/web/ui/partners/program-marketplace/program-category.tsx (2)
apps/web/lib/network/program-categories.ts (1)
PROGRAM_CATEGORIES_MAP(79-83)packages/ui/src/icons/index.tsx (1)
Icon(82-82)
β° 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 (6)
apps/web/ui/partners/program-marketplace/program-category.tsx (1)
6-43: Component structure looks good.The dynamic element pattern (
As = onClick ? "button" : "div") and event handling withpreventDefault/stopPropagationare consistent with theProgramRewardIconcomponent pattern. The fallback for unmapped categories is a sensible defensive approach.apps/web/ui/partners/program-marketplace/program-rewards-display.tsx (3)
10-26: Type definitions are well-structured.The
RewardIteminternal type andProgramRewardsDisplayPropsinterface are clean with appropriate optional markers.
122-135: Multi-item rendering looks good.Clean delegation to
ProgramRewardIconwith proper key assignment and consistent styling. TheisDarkImagetheming is correctly applied via the className prop.
42-51: No action needed. The code is type-safe.Line 46 accesses
REWARD_EVENTS[reward.event].iconsafely becausereward.eventis typed asEventType(a Prisma enum with exactly three values:click,lead,sale), andREWARD_EVENTShas all three as keys. TheRewardSchemaZod validation usingz.nativeEnum(EventType)ensures only valid enum values reach this code, preventing any runtime errors.Likely an incorrect or invalid review comment.
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/marketplace/featured-program-card.tsx (2)
174-202: LGTM!The skeleton component properly mirrors the structure of the actual card with appropriate loading states and animations. The comments explaining sizing choices are helpful for maintainability.
25-29: Standard<img>tag usage is consistent across all marketplace components in the partners section for external/partner-provided images. This is an intentional pattern rather than an optimization gapβNext.js Image component is not used in the partners directory for external URLs with unknown dimensions. The established approach is appropriate for partner-provided marketplace images.Likely an incorrect or invalid review 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: 2
β»οΈ Duplicate comments (1)
apps/web/lib/api/programs/get-program-or-throw.ts (1)
33-43: Refine typing to avoid@ts-ignoreoncategoriestransformationThe runtime behavior looks right, but the
// @ts-ignoremasks type issues aroundprogram.categories. Itβd be better to model the two cases explicitly so TS understands whencategoriesexists, e.g. by branching before parse:- return ProgramSchema.extend({ - inviteEmailData: programInviteEmailDataSchema, - }).parse( - includeCategories - ? { - ...program, - // @ts-ignore conditionally including categories - categories: program.categories?.map(({ category }) => category) ?? [], - } - : program, - ); + const BaseProgramSchema = ProgramSchema.extend({ + inviteEmailData: programInviteEmailDataSchema, + }); + + if (includeCategories && "categories" in program) { + return BaseProgramSchema.parse({ + ...program, + categories: program.categories?.map(({ category }) => category) ?? [], + }); + } + + return BaseProgramSchema.parse(program);You may need to fineβtune the narrowing (e.g. a small helper type for
programwhen categories are included), but eliminating the blanket@ts-ignorewill make future type issues easier to catch.
π§Ή Nitpick comments (3)
apps/web/ui/modals/application-settings-modal.tsx (1)
62-79: Simplify error handling and remove redundant assertion.Line 63 already guards against missing
workspaceId, making the non-null assertion on line 66 redundant. Additionally, lines 70-78 set both a form error and show a toast using the same message, which may be redundant since theonErrorcallback (line 52-54) already displays server errors via toast.const onSubmit = handleSubmit(async (data) => { if (!workspaceId) return; const result = await executeAsync({ - workspaceId: workspaceId!, + workspaceId, ...data, }); if (result?.serverError || result?.validationErrors) { - setError("root.serverError", { - message: "Failed to update application settings", - }); toast.error( parseActionError(result, "Failed to update application settings"), ); return; } });apps/web/lib/api/programs/get-program-or-throw.ts (1)
15-23: Optionally narrow the Prismaincludeto only the neededcategoryfieldRight now
include: { categories: true }will load the full relation objects, but you only consume thecategoryproperty later. You could trim payload size a bit by selecting just that field:- ...(includeCategories && { - include: { - categories: true, - }, - }), + ...(includeCategories && { + include: { + categories: { + select: { category: true }, + }, + }, + }),This is non-blocking but can help keep the object shape and data volume tighter.
apps/web/lib/zod/schemas/programs.ts (1)
10-15: New ProgramSchema fields (description, marketplace timestamp, categories) look consistent; confirm update pathsImporting
Categoryand modeling:
description: z.string().nullish()addedToMarketplaceAt: z.date().nullish()categories: z.array(z.nativeEnum(Category)).nullish()fits well with the new marketplace/program metadata and with how
getProgramOrThrowmaps categories into an enum array. One thing to confirm: these fields are not present inupdateProgramSchema, so they appear readβonly from that API. If theyβre meant to be editable in the same βupdate programβ surface, youβll want to mirror them there (and in any corresponding forms); if theyβre managed via a separate marketplaceβspecific flow, the current separation is fine.Also applies to: 26-42
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (5)
apps/web/lib/actions/partners/update-application-settings.ts(1 hunks)apps/web/lib/api/programs/get-program-or-throw.ts(2 hunks)apps/web/lib/zod/schemas/program-network.ts(1 hunks)apps/web/lib/zod/schemas/programs.ts(2 hunks)apps/web/ui/modals/application-settings-modal.tsx(1 hunks)
π§ Files skipped from review as they are similar to previous changes (1)
- apps/web/lib/zod/schemas/program-network.ts
π§° Additional context used
π§ Learnings (12)
π Common learnings
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: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/ui/modals/application-settings-modal.tsx
π Learning: 2025-11-24T16:36:36.196Z
Learnt from: CR
Repo: dubinc/dub PR: 0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-11-24T16:36:36.196Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Only components exported from the `hubspot/ui-extensions` npm package can be used in settings components
Applied to files:
apps/web/ui/modals/application-settings-modal.tsx
π Learning: 2025-11-24T16:36:36.196Z
Learnt from: CR
Repo: dubinc/dub PR: 0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-11-24T16:36:36.196Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : React components from `hubspot/ui-extensions/crm` cannot be used in settings components
Applied to files:
apps/web/ui/modals/application-settings-modal.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/modals/application-settings-modal.tsx
π Learning: 2025-10-28T19:17:28.085Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2985
File: apps/web/app/(ee)/api/network/programs/[programSlug]/route.ts:20-27
Timestamp: 2025-10-28T19:17:28.085Z
Learning: In this codebase, Prisma's extendedWhereUnique feature is enabled (or Prisma 5.x is used), allowing findUniqueOrThrow to accept additional non-unique filters alongside unique fields like slug. This pattern is valid and acceptable.
Applied to files:
apps/web/lib/api/programs/get-program-or-throw.ts
π Learning: 2025-08-25T17:42:13.600Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/lib/api/get-workspace-users.ts:76-83
Timestamp: 2025-08-25T17:42:13.600Z
Learning: Business rule confirmed: Each workspace has exactly one program. The code should always return workspace.programs[0] since there's only one program per workspace.
Applied to files:
apps/web/lib/api/programs/get-program-or-throw.ts
π Learning: 2025-06-18T20:26:25.177Z
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.
Applied to files:
apps/web/lib/api/programs/get-program-or-throw.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/update-application-settings.ts
π Learning: 2025-08-26T14:20:23.943Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/app/api/workspaces/[idOrSlug]/notification-preferences/route.ts:13-14
Timestamp: 2025-08-26T14:20:23.943Z
Learning: The updateNotificationPreference action in apps/web/lib/actions/update-notification-preference.ts already handles all notification preference types dynamically, including newBountySubmitted, through its schema validation using the notificationTypes enum and Prisma's dynamic field update pattern.
Applied to files:
apps/web/lib/actions/partners/update-application-settings.ts
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
apps/web/lib/zod/schemas/programs.ts
π Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/lib/zod/schemas/programs.ts
𧬠Code graph analysis (3)
apps/web/lib/api/programs/get-program-or-throw.ts (1)
packages/prisma/index.ts (1)
prisma(3-9)
apps/web/lib/actions/partners/update-application-settings.ts (2)
apps/web/lib/actions/safe-action.ts (1)
authActionClient(39-88)packages/prisma/index.ts (1)
prisma(3-9)
apps/web/lib/zod/schemas/programs.ts (1)
apps/web/lib/zod/schemas/rewards.ts (1)
RewardSchema(166-176)
β° 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 (2)
apps/web/ui/modals/application-settings-modal.tsx (1)
163-183: LGTM: Clean hook pattern.The hook correctly uses
useState,useCallback, anduseMemoto expose a stable API for managing modal visibility and rendering. This pattern prevents unnecessary re-renders while maintaining encapsulation.apps/web/lib/api/programs/get-program-or-throw.ts (1)
6-14:includeCategoriesflag wiring is backwardβcompatible and clearThe new
includeCategoriesparameter with a default offalsekeeps existing callers working while allowing new call sites to opt into categories. The type and default align correctly with how itβs used below.
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: 1
β»οΈ Duplicate comments (1)
apps/web/app/(ee)/api/network/programs/route.ts (1)
64-82: Reward type multiβselect still behaves as AND, not ORThe
rewardTypefilter still builds a singlegroups.someobject with multiplesaleRewardId/leadRewardId/clickRewardId/discountIdconditions, so selecting["sale","lead"]will only match programs whose default group has both sale and lead rewards. Marketplace users generally expect multiβselect filters to be OR (βsale or leadβ).You can switch this to an
ORarray as previously suggested:...(rewardType && { groups: { some: { slug: DEFAULT_PARTNER_GROUP.slug, - ...(rewardType.includes("sale") && { - saleRewardId: { not: null }, - }), - ...(rewardType.includes("lead") && { - leadRewardId: { not: null }, - }), - ...(rewardType.includes("click") && { - clickRewardId: { not: null }, - }), - ...(rewardType.includes("discount") && { - discountId: { not: null }, - }), + OR: [ + ...(rewardType.includes("sale") + ? [{ saleRewardId: { not: null } }] + : []), + ...(rewardType.includes("lead") + ? [{ leadRewardId: { not: null } }] + : []), + ...(rewardType.includes("click") + ? [{ clickRewardId: { not: null } }] + : []), + ...(rewardType.includes("discount") + ? [{ discountId: { not: null } }] + : []), + ], }, }, }),
π§Ή Nitpick comments (3)
apps/web/ui/partners/program-marketplace/program-reward-icon.tsx (1)
37-37: Remove redundant optional chaining.The optional chaining operator is unnecessary here since this code is already inside a conditional block that checks for
onClick's existence (line 32).Apply this diff:
- onClick?.(); + onClick();apps/web/app/(ee)/api/network/programs/route.ts (2)
43-56: Search +featuredfilter semantics and case sensitivityTwo small UX nits to consider:
featured:...(featured && {...})meansfeatured=falsebehaves the same as omitting the param. If callers might ever sendfeatured=falseexpecting βnonβfeatured onlyβ, that wonβt happen today. If thatβs not a use case, this is fine.search:contains: searchis caseβsensitive by default in Prisma. If you want more forgiving search, addmode: "insensitive"on each searchable field.Example tweak for the search block:
- ...(search && { - OR: [ - { name: { contains: search } }, - { slug: { contains: search } }, - { domain: { contains: search } }, - { url: { contains: search } }, - ], - }), + ...(search && { + OR: [ + { name: { contains: search, mode: "insensitive" } }, + { slug: { contains: search, mode: "insensitive" } }, + { domain: { contains: search, mode: "insensitive" } }, + { url: { contains: search, mode: "insensitive" } }, + ], + }),
121-142: Optional: extractPROMOTED_PROGRAM_IDSand avoid mutatingprogramsThe promotion logic is fine functionally, but you could make it a bit cleaner:
- Hoist
PROMOTED_PROGRAM_IDSto a topβlevelconstso it isnβt reβallocated on every request.- Instead of mutating
programswithsplice, consider returning a new array for clarity.For example:
- if (sortBy === "popularity") { - const PROMOTED_PROGRAM_IDS = [/* ... */]; - const promoted = programs.filter((p) => - PROMOTED_PROGRAM_IDS.includes(p.id), - ); - const others = programs.filter((p) => !PROMOTED_PROGRAM_IDS.includes(p.id)); - promoted.sort( - (a, b) => - PROMOTED_PROGRAM_IDS.indexOf(a.id) - PROMOTED_PROGRAM_IDS.indexOf(b.id), - ); - programs.splice(0, programs.length, ...promoted, ...others); - } + if (sortBy === "popularity") { + const promoted = programs.filter((p) => + PROMOTED_PROGRAM_IDS.includes(p.id), + ); + const others = programs.filter((p) => !PROMOTED_PROGRAM_IDS.includes(p.id)); + promoted.sort( + (a, b) => + PROMOTED_PROGRAM_IDS.indexOf(a.id) - PROMOTED_PROGRAM_IDS.indexOf(b.id), + ); + programs = [...promoted, ...others]; + }(Adjust
programstoletif you go this route.)
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (2)
apps/web/app/(ee)/api/network/programs/route.ts(1 hunks)apps/web/ui/partners/program-marketplace/program-reward-icon.tsx(1 hunks)
π§° Additional context used
π§ Learnings (11)
π Common learnings
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.414Z
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.414Z
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/app/(ee)/api/network/programs/route.ts
π Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
Applied to files:
apps/web/app/(ee)/api/network/programs/route.ts
π Learning: 2025-11-24T09:10:12.536Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3089
File: apps/web/lib/api/fraud/fraud-rules-registry.ts:17-25
Timestamp: 2025-11-24T09:10:12.536Z
Learning: In apps/web/lib/api/fraud/fraud-rules-registry.ts, the fraud rules `partnerCrossProgramBan` and `partnerDuplicatePayoutMethod` intentionally have stub implementations that return `{ triggered: false }` because they are partner-scoped rules handled separately during partner application/onboarding flows (e.g., in detect-record-fraud-application.ts), rather than being evaluated per conversion event like other rules in the registry. The stubs exist only to satisfy the `Record<FraudRuleType, ...>` type constraint.
Applied to files:
apps/web/app/(ee)/api/network/programs/route.ts
π Learning: 2025-08-16T11:14:00.667Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2754
File: apps/web/lib/partnerstack/schemas.ts:47-52
Timestamp: 2025-08-16T11:14:00.667Z
Learning: The PartnerStack API always includes the `group` field in partner responses, so the schema should use `.nullable()` rather than `.nullish()` since the field is never omitted/undefined.
Applied to files:
apps/web/app/(ee)/api/network/programs/route.ts
π Learning: 2025-11-24T08:55:31.332Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3089
File: apps/web/app/(ee)/api/fraud-rules/route.ts:71-87
Timestamp: 2025-11-24T08:55:31.332Z
Learning: In apps/web/app/(ee)/api/fraud-rules/route.ts, fraud rules cannot be created in a disabled state. When using prisma.fraudRule.upsert, the create branch intentionally omits the disabledAt field (defaulting to null, meaning enabled), while the update branch allows toggling enabled/disabled state via the disabledAt field. This is a business logic constraint.
Applied to files:
apps/web/app/(ee)/api/network/programs/route.ts
π 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/(ee)/api/network/programs/route.ts
π Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).
Applied to files:
apps/web/app/(ee)/api/network/programs/route.ts
π Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/app/(ee)/api/network/programs/route.ts
π Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
apps/web/ui/partners/program-marketplace/program-reward-icon.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/ui/partners/program-marketplace/program-reward-icon.tsx
𧬠Code graph analysis (2)
apps/web/app/(ee)/api/network/programs/route.ts (4)
apps/web/lib/network/throw-if-partner-cannot-view-marketplace.ts (1)
throwIfPartnerCannotViewMarketplace(10-50)apps/web/lib/zod/schemas/program-network.ts (1)
getNetworkProgramsQuerySchema(35-59)packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/zod/schemas/groups.ts (1)
DEFAULT_PARTNER_GROUP(16-20)
apps/web/ui/partners/program-marketplace/program-reward-icon.tsx (1)
packages/ui/src/icons/index.tsx (1)
Icon(82-82)
β° 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 (6)
apps/web/ui/partners/program-marketplace/program-reward-icon.tsx (4)
1-3: LGTM!The imports are appropriate for the component's functionality.
5-15: LGTM!The component props are well-defined with appropriate types. The Icon naming pattern is consistent with the codebase conventions.
16-16: Verify TypeScript type safety with dynamic component pattern.The dynamic component pattern (
const As = onClick ? "button" : "div") may cause TypeScript issues since button and div elements accept different props. While the conditional spread on lines 32-39 attempts to handle this, TypeScript might not properly narrow the type, potentially allowing invalid prop combinations.Consider using a type assertion or refactoring to explicit conditional rendering if type errors occur.
35-36: Verify preventDefault and stopPropagation are intentional.The event handlers prevent default behavior and stop event bubbling, suggesting this component is used within a clickable parent element (e.g., a program card). Confirm this is the intended behavior, as it will prevent parent click handlers from executing.
apps/web/app/(ee)/api/network/programs/route.ts (2)
12-27: Auth guard + query parsing look solidThe partner access guard +
throwIfPartnerCannotViewMarketplaceand the Zod-based parsing ofsearchParamsare wired correctly and give you a nicely validated input surface for the rest of the handler. No issues here.
83-93: Status filter fix correctly handlesnullvsundefinedThe guard
status !== undefinedplus the innerstatus === null ? none : somelogic looks correct and fixes the earlier bug wherestatus === nullwas never applied. This should now properly distinguish βno enrollment yetβ from βfilter by a specific statusβ.
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: 1
π§Ή Nitpick comments (2)
apps/web/scripts/send-batch-emails.ts (2)
10-50: Add error handling for database operations.The main function performs database queries and email queueing without error handling. If the Prisma query fails, the error will be uncaught.
Apply this diff to add try-catch error handling:
async function main() { - const partners = await prisma.partner.findMany({ - where: { - email: "[email protected]", - users: { - some: {}, - }, - programs: { - some: { - programId: { - notIn: EXCLUDED_PROGRAM_IDS, - }, - status: "approved", - totalCommissions: { - gte: PARTNER_NETWORK_MIN_COMMISSIONS_CENTS, - }, - }, - none: { - status: "banned", - }, - }, - }, - }); - - const res = await queueBatchEmail<typeof ProgramMarketplaceAnnouncement>( - partners.map((partner) => ({ - to: partner.email!, - subject: "Introducing the Dub Program Marketplace", - variant: "marketing", - replyTo: "noreply", - templateName: "ProgramMarketplaceAnnouncement", - templateProps: { - email: partner.email!, - }, - })), - ); - - console.log({ res }); + try { + const partners = await prisma.partner.findMany({ + where: { + email: "[email protected]", + users: { + some: {}, + }, + programs: { + some: { + programId: { + notIn: EXCLUDED_PROGRAM_IDS, + }, + status: "approved", + totalCommissions: { + gte: PARTNER_NETWORK_MIN_COMMISSIONS_CENTS, + }, + }, + none: { + status: "banned", + }, + }, + }, + }); + + const res = await queueBatchEmail<typeof ProgramMarketplaceAnnouncement>( + partners.map((partner) => ({ + to: partner.email!, + subject: "Introducing the Dub Program Marketplace", + variant: "marketing", + replyTo: "noreply", + templateName: "ProgramMarketplaceAnnouncement", + templateProps: { + email: partner.email!, + }, + })), + ); + + console.log({ res }); + } catch (error) { + console.error("Failed to send batch emails:", error); + throw error; + } }
10-32: Consider adding logging before the operation starts.Adding a log statement before querying partners would help track script execution and show the partner count being processed.
async function main() { + console.log("Starting batch email send for Program Marketplace announcement..."); + const partners = await prisma.partner.findMany({ where: { email: "[email protected]",After the query, you could also log the count:
}); + + console.log(`Found ${partners.length} qualifying partner(s)`); const res = await queueBatchEmail<typeof ProgramMarketplaceAnnouncement>(
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π Files selected for processing (4)
apps/web/app/(ee)/api/network/programs/count/route.ts(1 hunks)apps/web/app/(ee)/api/network/programs/route.ts(1 hunks)apps/web/scripts/send-batch-emails.ts(1 hunks)apps/web/scripts/send-batch-emails.tsx(0 hunks)
π€ Files with no reviewable changes (1)
- apps/web/scripts/send-batch-emails.tsx
π§ Files skipped from review as they are similar to previous changes (2)
- apps/web/app/(ee)/api/network/programs/count/route.ts
- apps/web/app/(ee)/api/network/programs/route.ts
π§° Additional context used
π§ Learnings (3)
π Common learnings
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-17T05:19:11.972Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3113
File: apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:65-75
Timestamp: 2025-11-17T05:19:11.972Z
Learning: In the Dub codebase, `sendBatchEmail` (implemented in packages/email/src/send-via-resend.ts) handles filtering of emails with invalid `to` addresses internally. Call sites can safely use non-null assertions on email addresses because the email sending layer will filter out any entries with null/undefined `to` values before sending. This centralized validation pattern is intentional and removes the need for filtering at individual call sites.
Applied to files:
apps/web/scripts/send-batch-emails.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/scripts/send-batch-emails.ts
𧬠Code graph analysis (1)
apps/web/scripts/send-batch-emails.ts (4)
packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/constants/partner-profile.ts (2)
EXCLUDED_PROGRAM_IDS(10-14)PARTNER_NETWORK_MIN_COMMISSIONS_CENTS(15-15)apps/web/lib/email/queue-batch-email.ts (1)
queueBatchEmail(18-87)packages/email/src/templates/program-marketplace-announcement.tsx (1)
ProgramMarketplaceAnnouncement(17-132)
β° 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
Summary by CodeRabbit
New Features
UX
UI/Style
βοΈ Tip: You can customize this high-level summary in your review settings.