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

Skip to content

Conversation

@TWilson023
Copy link
Collaborator

@TWilson023 TWilson023 commented Jul 22, 2025

Summary by CodeRabbit

  • New Features

    • Introduced advanced reward logic with conditional reward modifiers, accessible for eligible plans.
    • Added interactive UI components for managing reward conditions, partner assignments, and reward logic.
    • Added inline badge popovers and new icons to enhance reward configuration.
    • Introduced a modal to facilitate upgrading to advanced partner plans.
    • Added a new reward icon component and improved partner selection UI with dynamic previews.
  • Enhancements

    • Reward creation and update actions now support conditional modifiers with plan-based validation.
    • Table component supports customizable styling for empty and error states.
    • Updated partner inclusion/exclusion labels and icons for clarity.
  • Refactor

    • Refined reward condition schemas for improved modularity and explicit typing.
    • Reorganized reward-related components and replaced legacy reward sheet with a new implementation.
  • Chores

    • Updated import paths and added new icon exports for consistency.

@vercel
Copy link
Contributor

vercel bot commented Jul 22, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
dub ✅ Ready (Inspect) Visit Preview Jul 30, 2025 3:24pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 22, 2025

Walkthrough

This change introduces advanced reward logic to the partner rewards system, enabling conditional reward modifiers based on customer or sale attributes. It adds new UI components for managing complex reward conditions, updates backend actions and validation schemas to support a modifiers field, and enforces plan-based access controls. Several new UI utilities and components are added, with major refactoring and relocation of the reward sheet logic.

Changes

Cohort / File(s) Change Summary
Backend reward logic and plan capabilities
apps/web/lib/actions/partners/create-reward.ts, apps/web/lib/actions/partners/update-reward.ts, apps/web/lib/plan-capabilities.ts
Added support for an optional modifiers field in reward creation and update actions. Enforced plan-based access for advanced reward logic using a new canUseAdvancedRewardLogic capability flag.
Reward condition schemas
apps/web/lib/zod/schemas/rewards.ts
Refactored and retyped reward condition schemas; constants for entities, attributes, and operators extracted; modifiers field in reward schema now uses explicit array schema.
Reward sheet UI refactor and relocation
apps/web/ui/partners/add-edit-reward-sheet.tsx, apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx
Old reward sheet component deleted; new, modularized reward sheet UI implemented under rewards/, supporting advanced modifiers, improved form handling, and new hooks.
Inline badge popover utilities
apps/web/ui/partners/rewards/inline-badge-popover.tsx
Introduced reusable inline badge popover components for menu and input controls, supporting dynamic selection and input lists within forms.
Reward partners UI enhancements
apps/web/ui/partners/rewards/reward-partners-card.tsx, apps/web/ui/partners/rewards/reward-partners-table.tsx
Added new partner card for managing included/excluded partners with preview/count, expanded/collapsed state, and improved iconography and labeling in the partners table.
Reward icon and logic components
apps/web/ui/partners/rewards/reward-icon-square.tsx, apps/web/ui/partners/rewards/rewards-logic.tsx
Added new icon component for rewards and a comprehensive system for building and displaying advanced reward logic conditions in the UI.
Rewards upgrade modal
apps/web/ui/partners/rewards/rewards-upgrade-modal.tsx
Added modal and hook for managing plan upgrades when advanced reward logic is accessed by users without appropriate plan.
UI icon exports
packages/ui/src/icons/nucleo/index.ts, packages/ui/src/icons/nucleo/user-minus.tsx
Added new UserMinus icon and exported it for use in partner table UI.
Table component enhancements
packages/ui/src/table/table.tsx, packages/ui/src/table/types.ts
Added optional emptyWrapperClassName prop to table component and type for improved empty/error state styling.
Reward sheet import update
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/rewards.tsx
Updated import path for useRewardSheet to new location under rewards/.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant RewardSheet
    participant Backend
    participant PlanCapabilities

    User->>RewardSheet: Open add/edit reward form
    RewardSheet->>Backend: Fetch workspace, partners, program data
    User->>RewardSheet: Fill in reward details, add modifiers (conditions)
    RewardSheet->>PlanCapabilities: Check if plan supports advanced logic
    alt Plan supports advanced logic
        User->>RewardSheet: Submit form with modifiers
        RewardSheet->>Backend: createRewardAction/updateRewardAction({modifiers, ...})
        Backend->>PlanCapabilities: Validate canUseAdvancedRewardLogic
        Backend->>Backend: Store reward with modifiers
        Backend-->>RewardSheet: Success response
        RewardSheet-->>User: Show success, update UI
    else Plan does not support advanced logic
        RewardSheet-->>User: Show upgrade modal
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

  • dubinc/dub#2645: Implements the core reward modifiers feature, including context-aware reward evaluation, condition schemas, and integration into partner commission logic.
  • dubinc/dub#2516: Introduces the new reward sheet UI component, directly related to the import path update and UI refactor in this PR.
  • dubinc/dub#2629: Modifies the RewardPartnersTable component used within the reward sheet UI, adding props and warning UI related to partner movement between rewards.

Poem

In fields of code where logic grows,
The rabbits hop where reward wind blows.
Modifiers bloom, conditions entwine,
Partners included—exclusions define!
With popovers bright and icons anew,
Advanced rewards now hop into view.
🐇✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch reward-logic

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@TWilson023 TWilson023 marked this pull request as ready for review July 30, 2025 15:03
@steven-tey
Copy link
Collaborator

@CodeRabbit final review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 30, 2025

@steven-tey I'll conduct a final comprehensive review of all the changes in this PR.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🔭 Outside diff range comments (1)
apps/web/lib/zod/schemas/rewards.ts (1)

67-67: Address the TODO: Fix modifiers type in RewardSchema

The TODO comment indicates that the modifiers field should use a proper type instead of z.any(). Since you've already defined rewardConditionsArraySchema, consider using it here for type consistency.

-  modifiers: z.any().nullish(), // TODO: Fix this
+  modifiers: rewardConditionsArraySchema.nullish(),
🧹 Nitpick comments (10)
apps/web/lib/actions/partners/create-reward.ts (1)

53-59: Consider validating plan capabilities before partner IDs

The modifiers validation currently happens after the partner ID validation. Consider moving this check earlier in the function to fail fast if the user doesn't have the required plan capabilities, avoiding unnecessary database queries.

     const programId = getDefaultProgramIdOrThrow(workspace);
 
+    if (
+      modifiers &&
+      !getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
+    )
+      throw new Error(
+        "Advanced reward structures are only available on the Advanced plan and above.",
+      );
+
     // Only one default reward is allowed for each event
     if (isDefault) {
       const defaultReward = await prisma.reward.findFirst({
         where: {
           programId,
           event,
           default: true,
         },
       });
 
       if (defaultReward) {
         throw new Error(
           `There is an existing default ${event} reward already. A program can only have one ${event} default reward.`,
         );
       }
     }
 
-    if (
-      modifiers &&
-      !getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
-    )
-      throw new Error(
-        "Advanced reward structures are only available on the Advanced plan and above.",
-      );
-
     const finalPartnerIds = [...includedPartnerIds, ...excludedPartnerIds];
apps/web/ui/partners/rewards/rewards-upgrade-modal.tsx (2)

26-78: Consider using features from PLANS constant

The features array is hardcoded in the component, which could lead to inconsistencies if the plan features change. Consider using the features directly from the PLANS constant and filtering/mapping as needed.

   const features = useMemo(
-    () => [
-      {
-        id: "payouts",
-        text: `$${nFormatter(plan.limits.payouts / 100)} partner payouts/mo`,
-        tooltip: {
-          title:
-            "Send payouts to your partners with 1-click (or automate it completely) – all across the world.",
-          cta: "Learn more.",
-          href: "https://dub.co/help/article/partner-payouts",
-        },
-      },
-      {
-        id: "flexiblerewards",
-        text: "Advanced reward structures",
-        tooltip: {
-          title:
-            "Create dynamic click, lead, or sale-based rewards with country and product-specific modifiers.",
-          cta: "Learn more.",
-          href: "https://dub.co/help/article/partner-rewards",
-        },
-      },
-      {
-        id: "whitelabel",
-        text: "White-labeling support",
-        tooltip: {
-          title:
-            "Embed a white-labeled referral dashboard directly in your app in just a few lines of code.",
-          cta: "Learn more.",
-          href: "https://dub.co/docs/partners/white-labeling",
-        },
-      },
-      {
-        id: "email",
-        text: "Email campaigns (beta)",
-      },
-      {
-        id: "api",
-        text: "Partners API",
-        tooltip: {
-          title:
-            "Leverage our partners API to build a bespoke, white-labeled referral program that lives within your app.",
-          cta: "Learn more.",
-          href: "https://dub.co/docs/api-reference/endpoint/create-a-partner",
-        },
-      },
-      {
-        id: "slack",
-        text: "Priority Slack support",
-      },
-    ],
+    () => {
+      const baseFeatures = plan.features || [];
+      // Add the custom advanced reward structures feature
+      const customFeatures = [
+        {
+          id: "flexiblerewards",
+          text: "Advanced reward structures",
+          tooltip: {
+            title:
+              "Create dynamic click, lead, or sale-based rewards with country and product-specific modifiers.",
+            cta: "Learn more.",
+            href: "https://dub.co/help/article/partner-rewards",
+          },
+        },
+        {
+          id: "email",
+          text: "Email campaigns (beta)",
+        },
+      ];
+      
+      // Merge base features with custom ones, avoiding duplicates
+      return [...baseFeatures.filter(f => f.id !== "flexiblerewards" && f.id !== "email"), ...customFeatures];
+    },
     [plan],
   );

98-98: Fix typo in description text

There's a missing word in the description. "get access higher payout" should be "get access to higher payout".

-            When you upgrade to Advanced, you&rsquo;ll get access higher payout
+            When you upgrade to Advanced, you&rsquo;ll get access to higher payout
apps/web/ui/partners/rewards/rewards-logic.tsx (2)

388-400: Simplify complex country selection logic

The nested ternary operators for handling country selection make the code hard to read and maintain. Consider extracting this logic into a separate function.

+                  const handleCountrySelect = (value: string) => {
+                    if (!isArrayValue) {
+                      return value;
+                    }
+                    
+                    const currentValues = Array.isArray(condition.value) 
+                      ? condition.value as string[]
+                      : [];
+                    
+                    return currentValues.includes(value)
+                      ? currentValues.filter(v => v !== value)
+                      : [...currentValues, value];
+                  };
+
                   onSelect={(value) => {
                     setValue(conditionKey, {
                       ...condition,
-                      value: isArrayValue
-                        ? Array.isArray(condition.value)
-                          ? (condition.value as string[]).includes(value)
-                            ? (condition.value.filter(
-                                (v) => v !== value,
-                              ) as string[])
-                            : ([...condition.value, value] as string[])
-                          : [value]
-                        : value,
+                      value: handleCountrySelect(value),
                     });
                   }}

40-572: Consider splitting this large component

The file is quite large (572 lines) with multiple nested components. Consider splitting it into smaller, more focused components in separate files for better maintainability:

  • ConditionBuilder.tsx for condition logic
  • OperatorSelector.tsx for operator dropdown
  • ResultTermsEditor.tsx for result terms

This would improve code organization and make testing easier.

apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (2)

234-236: Replace window.confirm with a proper confirmation modal

Using window.confirm is inconsistent with the modern UI design. Consider implementing a confirmation modal component for better UX.

Would you like me to create a confirmation modal component that matches your design system?


1-510: Consider splitting this file for better maintainability

This file contains 510 lines with multiple components and hooks. Consider splitting it into:

  • add-edit-reward-sheet.tsx - Main sheet component and form logic
  • reward-sheet-components.tsx - RewardSheetCard, VerticalLine, AmountInput
  • use-reward-sheet.tsx - Custom hooks

This would improve code organization and make testing easier.

apps/web/ui/partners/rewards/inline-badge-popover.tsx (3)

144-144: Potential search collision in Command.Item value.

The concatenation of text and value in the Command.Item value prop could cause unintended search matches if the value contains parts of other item texts.

Consider using only the text for search matching:

-                  value={`${text} ${value}`}
+                  value={text}

Or if you need both for search, consider a more distinctive separator:

-                  value={`${text} ${value}`}
+                  value={`${text}::${value}`}

216-221: Consider optimizing the values synchronization logic.

The current implementation has potential issues:

  1. JSON.stringify comparison could be inefficient for large arrays
  2. Generating new UUIDs on every external change might reset user focus

Consider a more targeted synchronization approach:

   useEffect(() => {
-    const currentValues = values.map((item) => item.value);
-    if (JSON.stringify(currentValues) !== JSON.stringify(valuesProp)) {
-      setValues(valuesProp.map((value) => ({ id: uuid(), value })));
-    }
-  }, [valuesProp, values]);
+    // Only update if lengths differ or if coming from empty state
+    if (valuesProp.length !== values.length || (values.length === 0 && valuesProp.length > 0)) {
+      setValues(valuesProp.map((value, index) => ({
+        id: values[index]?.id || uuid(),
+        value
+      })));
+    }
+  }, [valuesProp.length]);

This preserves existing IDs where possible and avoids constant regeneration.


177-200: Add displayName to forwardRef component for better debugging.

Add a displayName to the forwardRef component:

 export const InlineBadgePopoverInput = forwardRef<HTMLInputElement>(
   (props: HTMLProps<HTMLInputElement>, ref) => {
     const { setIsOpen } = useContext(InlineBadgePopoverContext);

     return (
       <div className="relative rounded-md shadow-sm">
         <input
           ref={ref}
           className={cn(
             "block w-full rounded-md border-neutral-300 px-1.5 py-1 text-neutral-900 placeholder-neutral-400 sm:w-32 sm:text-sm",
             "focus:border-neutral-500 focus:outline-none focus:ring-neutral-500",
           )}
           onKeyDown={(e) => {
             if (e.key === "Enter") {
               e.preventDefault();
               setIsOpen(false);
             }
           }}
           {...props}
         />
       </div>
     );
   },
 );
+
+InlineBadgePopoverInput.displayName = "InlineBadgePopoverInput";
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fd690d and 9429df8.

📒 Files selected for processing (17)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/rewards.tsx (1 hunks)
  • apps/web/lib/actions/partners/create-reward.ts (4 hunks)
  • apps/web/lib/actions/partners/update-reward.ts (4 hunks)
  • apps/web/lib/plan-capabilities.ts (1 hunks)
  • apps/web/lib/zod/schemas/rewards.ts (2 hunks)
  • apps/web/ui/partners/add-edit-reward-sheet.tsx (0 hunks)
  • apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (1 hunks)
  • apps/web/ui/partners/rewards/inline-badge-popover.tsx (1 hunks)
  • apps/web/ui/partners/rewards/reward-icon-square.tsx (1 hunks)
  • apps/web/ui/partners/rewards/reward-partners-card.tsx (1 hunks)
  • apps/web/ui/partners/rewards/reward-partners-table.tsx (5 hunks)
  • apps/web/ui/partners/rewards/rewards-logic.tsx (1 hunks)
  • apps/web/ui/partners/rewards/rewards-upgrade-modal.tsx (1 hunks)
  • packages/ui/src/icons/nucleo/index.ts (1 hunks)
  • packages/ui/src/icons/nucleo/user-minus.tsx (1 hunks)
  • packages/ui/src/table/table.tsx (2 hunks)
  • packages/ui/src/table/types.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/web/ui/partners/add-edit-reward-sheet.tsx
🧰 Additional context used
🧠 Learnings (3)
apps/web/ui/partners/rewards/reward-icon-square.tsx (1)

Learnt from: devkiran
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.

apps/web/ui/partners/rewards/reward-partners-table.tsx (2)

Learnt from: devkiran
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.

Learnt from: TWilson023
PR: #2614
File: apps/web/ui/partners/design/previews/lander-preview.tsx:181-181
Timestamp: 2025-07-09T20:52:56.592Z
Learning: In apps/web/ui/partners/design/previews/lander-preview.tsx, the ellipsis wave animation delay calculation 3 - i * -0.15 is intentionally designed to create negative delays that offset each dot's animation cycle. This pattern works correctly for the intended ellipsis effect and should not be changed to positive incremental delays.

apps/web/ui/partners/rewards/reward-partners-card.tsx (1)

Learnt from: TWilson023
PR: #2614
File: apps/web/ui/partners/design/previews/lander-preview.tsx:181-181
Timestamp: 2025-07-09T20:52:56.592Z
Learning: In apps/web/ui/partners/design/previews/lander-preview.tsx, the ellipsis wave animation delay calculation 3 - i * -0.15 is intentionally designed to create negative delays that offset each dot's animation cycle. This pattern works correctly for the intended ellipsis effect and should not be changed to positive incremental delays.

🧬 Code Graph Analysis (7)
apps/web/ui/partners/rewards/rewards-upgrade-modal.tsx (2)
packages/utils/src/constants/pricing.tsx (1)
  • PLANS (13-323)
packages/ui/src/tooltip.tsx (2)
  • Tooltip (32-88)
  • SimpleTooltipContent (130-158)
apps/web/ui/partners/rewards/reward-icon-square.tsx (1)
packages/ui/src/icons/index.tsx (1)
  • Icon (77-77)
apps/web/lib/actions/partners/create-reward.ts (2)
apps/web/lib/plan-capabilities.ts (1)
  • getPlanCapabilities (4-17)
packages/prisma/client.ts (1)
  • Prisma (16-16)
apps/web/ui/partners/rewards/rewards-logic.tsx (10)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-45)
apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (1)
  • useAddEditRewardForm (69-69)
apps/web/ui/partners/rewards/rewards-upgrade-modal.tsx (1)
  • useRewardsUpgradeModal (179-191)
apps/web/lib/plan-capabilities.ts (1)
  • getPlanCapabilities (4-17)
apps/web/ui/shared/icons/x.tsx (1)
  • X (3-21)
apps/web/ui/partners/rewards/reward-icon-square.tsx (1)
  • RewardIconSquare (3-7)
apps/web/lib/zod/schemas/rewards.ts (4)
  • CONDITION_CUSTOMER_ATTRIBUTES (20-20)
  • CONDITION_SALE_ATTRIBUTES (21-21)
  • CONDITION_ATTRIBUTES (22-25)
  • CONDITION_OPERATORS (27-34)
apps/web/ui/partners/rewards/inline-badge-popover.tsx (5)
  • InlineBadgePopover (35-72)
  • InlineBadgePopoverMenu (81-175)
  • InlineBadgePopoverInputs (202-276)
  • InlineBadgePopoverInput (177-200)
  • InlineBadgePopoverContext (27-33)
packages/ui/src/icons/nucleo/chevron-right.tsx (1)
  • ChevronRight (3-24)
apps/web/lib/api/sales/construct-reward-amount.ts (1)
  • constructRewardAmount (4-22)
apps/web/ui/partners/rewards/inline-badge-popover.tsx (1)
apps/web/ui/shared/icons/x.tsx (1)
  • X (3-21)
apps/web/lib/actions/partners/update-reward.ts (2)
apps/web/lib/plan-capabilities.ts (1)
  • getPlanCapabilities (4-17)
packages/prisma/client.ts (1)
  • Prisma (16-16)
apps/web/ui/partners/rewards/reward-partners-card.tsx (7)
apps/web/lib/types.ts (2)
  • EnrolledPartnerProps (414-414)
  • RewardProps (480-480)
apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (1)
  • useAddEditRewardForm (69-69)
apps/web/ui/partners/rewards/reward-icon-square.tsx (1)
  • RewardIconSquare (3-7)
packages/ui/src/icons/nucleo/chevron-right.tsx (1)
  • ChevronRight (3-24)
apps/web/ui/partners/rewards/reward-partners-table.tsx (1)
  • RewardPartnersTable (32-282)
apps/web/lib/swr/use-partners.ts (1)
  • usePartners (10-39)
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 (23)
packages/ui/src/table/types.ts (1)

66-66: LGTM!

The addition of emptyWrapperClassName follows the established pattern for className properties in the table component.

apps/web/lib/plan-capabilities.ts (1)

14-16: LGTM!

The new capability flag follows the established pattern and correctly gates advanced reward logic to enterprise and advanced plans.

packages/ui/src/icons/nucleo/user-minus.tsx (1)

1-46: LGTM!

Well-structured SVG icon component that follows the established pattern for icon components in the UI library.

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

9-9: LGTM!

Import path update correctly reflects the reorganization of reward-related components.

packages/ui/src/icons/nucleo/index.ts (1)

213-213: LGTM! Correctly added new icon export.

The user-minus icon export is properly placed in alphabetical order and follows the established pattern.

apps/web/ui/partners/rewards/reward-icon-square.tsx (1)

1-7: LGTM! Well-structured icon container component.

The component is cleanly implemented with proper TypeScript typing using the Icon type from @dub/ui. The consistent styling approach using Tailwind classes for the container and icon will provide a uniform appearance across the rewards UI.

packages/ui/src/table/table.tsx (2)

291-291: LGTM! New prop added correctly.

The emptyWrapperClassName prop is properly integrated into the function signature and maintains backward compatibility by being optional.


521-526: LGTM! Proper implementation of the new className prop.

The emptyWrapperClassName is correctly applied using the cn utility to merge with existing classes, providing flexibility for customizing the empty state container styling.

apps/web/lib/actions/partners/update-reward.ts (4)

6-6: LGTM! Correct imports for the new functionality.

The getPlanCapabilities import supports the plan-based access control, and the Prisma import enables proper JSON null handling for the modifiers field.

Also applies to: 12-12


28-28: LGTM! Modifiers field properly destructured.

The modifiers field is correctly added to the destructured parameters from parsedInput.


41-47: LGTM! Proper plan capability validation.

The validation correctly checks if modifiers are provided and enforces the plan requirement before proceeding with the update. The error message clearly explains the limitation to users.


80-80: LGTM! Correct Prisma JSON null handling.

The conditional assignment modifiers === null ? Prisma.JsonNull : modifiers properly handles the case where modifiers should be explicitly set to null in the JSON field, which is the correct approach for Prisma JSON columns.

apps/web/ui/partners/rewards/reward-partners-table.tsx (5)

12-13: LGTM! New icons imported for enhanced UI.

The UserMinus and UserPlus icons are correctly imported to provide visual distinction between inclusion and exclusion modes.


38-38: LGTM! Dynamic label logic implemented correctly.

The conditional logic properly maps the mode to appropriate labels: "Included partners" for include mode and "Excluded partners" for exclude mode, improving clarity for users.


247-254: LGTM! Icons enhance visual clarity.

The conditional icon rendering correctly uses UserPlus for include mode and UserMinus for exclude mode, providing clear visual indicators that complement the text labels.


236-238: LGTM! Styling improvements implemented correctly.

The containerClassName adds rounded corners and the emptyWrapperClassName sets appropriate height for the empty state, leveraging the new Table component functionality.


269-269: LGTM! Improved button styling.

The lighter border-neutral-200 color provides a more subtle appearance compared to the default border, improving the overall visual design.

apps/web/lib/actions/partners/create-reward.ts (1)

97-97: Correct usage of Prisma.JsonNull for JSON columns

Good use of Prisma.JsonNull to properly store null values in JSON columns. This ensures that the database correctly differentiates between null and undefined values.

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

18-56: Well-structured schema definitions

The new constants and schemas for reward conditions are well-organized and provide comprehensive validation for the advanced reward logic feature. The use of const assertions and proper typing ensures type safety throughout the application.

apps/web/ui/partners/rewards/reward-partners-card.tsx (1)

126-205: Well-implemented partner preview functionality

The PartnerPreviewOrCount component is well-designed with:

  • Efficient data fetching only for missing partners
  • Smooth transitions between count and avatar display
  • Proper fallback handling for missing partner data
  • Good use of memoization for performance
apps/web/ui/partners/rewards/rewards-logic.tsx (1)

40-96: Well-structured reward logic management

The RewardsLogic component effectively manages the dynamic reward modifiers with:

  • Proper plan capability checking with upgrade modal integration
  • Clean field array management using react-hook-form
  • Appropriate default values for new modifiers
apps/web/ui/partners/rewards/inline-badge-popover.tsx (2)

27-72: Well-structured context and popover implementation.

The context setup and InlineBadgePopover component are well-implemented with good UX considerations:

  • Clean context pattern for state management
  • Appropriate visual feedback with the invalid prop
  • Smart handling of scroll events in nested scenarios

177-200: Clean input component implementation.

The InlineBadgePopoverInput component is well-implemented with proper forwarding of refs and thoughtful keyboard interaction handling.

@steven-tey
Copy link
Collaborator

@CodeRabbit final review, like your life depends on it

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 30, 2025

🧩 Analysis chain

I'll conduct an absolutely thorough final review of this reward logic implementation, examining every aspect with meticulous attention to detail.

@steven-tey Here's my comprehensive final review of the reward logic implementation:

🎯 CRITICAL SECURITY ISSUES

1. Plan Capability Bypass Risk

File: apps/web/lib/actions/partners/create-reward.ts, update-reward.ts
Issue: The plan capability check occurs AFTER modifiers extraction but validation could potentially be bypassed if the workspace data is stale or manipulated.

// Potential issue: What if workspace.plan is outdated?
if (modifiers && !getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic) {
  throw new Error("Advanced reward logic is not available on your current plan.");
}

Recommendation: Add workspace plan validation refresh and consider server-side double-checking.

2. JSON Injection in Modifiers

File: apps/web/lib/actions/partners/create-reward.ts (Lines 50-52)
Issue: Raw modifiers are stored as Prisma.JsonValue without sanitization.

modifiers: modifiers ?? Prisma.JsonNull, // No sanitization!

Recommendation: Implement JSON schema validation and sanitization before database storage.

🔍 CODE QUALITY ISSUES

3. Type Safety Concerns

File: apps/web/ui/partners/rewards/rewards-logic.tsx
Issue: Generic types and form field arrays could cause runtime errors with malformed data.

4. Memory Leaks in Complex Forms

File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx
Issue: Large form with multiple useWatch calls and complex state could cause performance issues with frequent re-renders.

5. Missing Error Boundaries

Issue: Complex reward logic UI components lack error boundaries for graceful failure handling.

POSITIVE ASPECTS

  1. Excellent Schema Validation: The Zod schemas in rewards.ts are comprehensive and well-structured
  2. Plan Capability Architecture: Clean separation of plan-based feature access
  3. Modular UI Components: Well-organized component structure with clear separation of concerns
  4. Proper Form Integration: Good use of React Hook Form with proper validation

🛠 IMMEDIATE ACTION ITEMS

  1. Add JSON sanitization for modifiers before database storage
  2. Implement workspace plan refresh in capability checks
  3. Add error boundaries around complex UI components
  4. Add input sanitization for user-provided condition values
  5. Implement rate limiting for reward creation/update actions

📊 OVERALL ASSESSMENT

This is a substantial and well-architected feature with some critical security gaps that need immediate attention. The code demonstrates good practices in most areas but requires security hardening before production deployment.

Risk Level: MEDIUM-HIGH (due to plan bypass potential and JSON injection)
Code Quality: GOOD (well-structured but needs security improvements)


🏁 Script executed:

#!/bin/bash
# Get comprehensive overview of the reward logic implementation
echo "=== Reward Logic Files Overview ==="
fd -t f "reward" --exec ls -la {}

echo -e "\n=== Database Schema for Rewards ==="
fd -t f "schema.prisma" --exec grep -A 20 -B 5 "model Reward" {} || echo "Schema not found in current directory"

echo -e "\n=== Plan Capabilities Implementation ==="
cat apps/web/lib/plan-capabilities.ts

echo -e "\n=== Security: Check for direct database access patterns ==="
rg "prisma\." apps/web/lib/actions/partners/ -A 2 -B 2

echo -e "\n=== Validation: Check Zod schema consistency ==="
cat apps/web/lib/zod/schemas/rewards.ts | head -50

echo -e "\n=== UI Components: Check for accessibility patterns ==="
rg "aria-|role=|tabIndex" apps/web/ui/partners/rewards/ -A 1 -B 1

echo -e "\n=== Error Handling: Check error patterns ==="
rg "throw new" apps/web/lib/actions/partners/ -A 2 -B 2

Length of output: 86552


🏁 Script executed:

#!/bin/bash
# Deep dive into potential security and data validation issues
echo "=== Check Input Sanitization in Actions ==="
rg "modifiers" apps/web/lib/actions/partners/ -A 5 -B 5

echo -e "\n=== Check Plan Validation Logic ==="
rg "canUseAdvancedRewardLogic" apps/web/ -A 3 -B 3

echo -e "\n=== Check Database Transaction Safety ==="
rg "prisma\.(reward|partner)" apps/web/lib/actions/partners/ -A 3

echo -e "\n=== Check Type Safety in UI Components ==="
rg "any\|unknown" apps/web/ui/partners/rewards/ -A 2 -B 2

echo -e "\n=== Check for Potential XSS in Dynamic Content ==="
rg "dangerouslySetInnerHTML\|innerHTML" apps/web/ui/partners/rewards/ -A 2 -B 2

echo -e "\n=== Check Form Validation Integration ==="
rg "useForm\|zodResolver" apps/web/ui/partners/rewards/ -A 3 -B 1

Length of output: 13486


🏁 Script executed:

#!/bin/bash
# Analyze the comprehensive reward conditions and validation logic
echo "=== Reward Conditions Schema Deep Dive ==="
cat apps/web/lib/zod/schemas/rewards.ts

echo -e "\n=== Check for Proper Error Boundaries ==="
rg "try.*catch\|ErrorBoundary" apps/web/ui/partners/rewards/ -A 3 -B 1

echo -e "\n=== Performance: Check for unnecessary re-renders ==="
rg "useMemo\|useCallback\|React\.memo" apps/web/ui/partners/rewards/ -A 2 -B 1

echo -e "\n=== Accessibility: Form labels and ARIA ==="
rg "htmlFor\|aria-label\|aria-describedby" apps/web/ui/partners/rewards/ -A 1 -B 1

echo -e "\n=== Data Fetching Patterns ==="
rg "useSWR\|useQuery" apps/web/ui/partners/rewards/ -A 2 -B 1

Length of output: 4067


@steven-tey Based on my exhaustive analysis of this reward logic implementation, here's my final comprehensive review:

🔴 CRITICAL SECURITY FINDINGS

1. JSON Storage Validation Gap

Files: create-reward.ts, update-reward.ts
Risk Level: HIGH

// Current implementation stores raw JSON
modifiers: modifiers || Prisma.JsonNull,
modifiers: modifiers === null ? Prisma.JsonNull : modifiers,

Issue: While Zod validates the structure client-side, there's no server-side JSON sanitization before database storage.

Recommendation: Add server-side validation:

if (modifiers) {
  // Validate against rewardConditionsArraySchema server-side
  const validatedModifiers = rewardConditionsArraySchema.parse(modifiers);
  // Then store validatedModifiers
}

2. Plan Capability Race Condition

Risk Level: MEDIUM

The plan validation occurs after input parsing but workspace data could be stale. Consider adding a workspace refresh check for critical plan-dependent features.

🟡 CODE QUALITY ISSUES

3. Missing Error Boundaries

Files: All UI components in apps/web/ui/partners/rewards/
Impact: Potential app crashes from complex form interactions

// Recommended wrapper for complex components
<ErrorBoundary fallback={<RewardErrorFallback />}>
  <RewardsLogic />
</ErrorBoundary>

4. Performance Concerns

Files: rewards-logic.tsx, add-edit-reward-sheet.tsx
Issue: No performance optimizations found in complex form components

  • Missing useMemo for expensive calculations
  • No useCallback for event handlers
  • Large forms without optimization could cause render performance issues

5. Accessibility Gaps

Critical Issue: No accessibility patterns found:

  • Missing aria-label attributes
  • No htmlFor associations
  • No screen reader support for complex form interactions

POSITIVE ASPECTS

  1. Excellent Schema Design: The Zod schemas are comprehensive and well-structured
  2. Proper Plan Enforcement: Multi-layer plan capability validation
  3. Clean Architecture: Good separation between UI, actions, and validation
  4. Comprehensive Error Handling: Descriptive error messages throughout
  5. Type Safety: Strong TypeScript usage with proper interfaces

📊 DETAILED SCHEMA ANALYSIS

The rewardConditionsArraySchema is well-designed:

export const rewardConditionSchema = z.object({
  entity: z.enum(CONDITION_ENTITIES),
  attribute: z.enum(CONDITION_ATTRIBUTES),
  operator: z.enum(CONDITION_OPERATORS),
  value: z.union([z.string(), z.number(), z.array(z.string()), z.array(z.number())]),
});

Strengths:

  • Strict entity/attribute/operator validation
  • Flexible value types
  • Proper nesting with conditions arrays

Concern: The TODO comment in RewardSchema indicates incomplete typing that should be addressed.

🛠 IMMEDIATE ACTION ITEMS

  1. 🔥 HIGH PRIORITY:

    • Add server-side JSON validation for modifiers
    • Implement error boundaries for complex UI components
    • Add basic accessibility attributes
  2. 📈 MEDIUM PRIORITY:

    • Add performance optimizations (useMemo, useCallback)
    • Implement proper loading states
    • Add comprehensive error handling in UI
  3. 🔧 LOW PRIORITY:

    • Complete the TODO in RewardSchema typing
    • Add unit tests for complex validation logic
    • Consider rate limiting for reward operations

🎯 OVERALL ASSESSMENT

Architecture Quality: EXCELLENT ⭐⭐⭐⭐⭐
Security Posture: GOOD ⭐⭐⭐⭐
Code Quality: GOOD ⭐⭐⭐⭐
Accessibility: POOR ⭐⭐
Performance: UNKNOWN ⭐⭐⭐

Final Recommendation: This is a solid, well-architected feature that demonstrates excellent planning and implementation. The main concerns are around accessibility and some security hardening, but the core logic is sound and production-ready with the suggested security improvements.

Risk Assessment: MEDIUM - Addressable issues that don't block deployment but should be prioritized for the next iteration.

🎯 This reward logic system is ready for production with the critical security fixes applied.

✅ Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
apps/web/lib/zod/schemas/rewards.ts (1)

67-67: Address the TODO comment

There's a TODO comment indicating that the modifiers type needs to be fixed. Since you've already improved the modifiers validation in line 85, this comment should be updated or removed.

-  modifiers: z.any().nullish(), // TODO: Fix this
+  modifiers: rewardConditionsArraySchema.nullish(),
apps/web/ui/partners/rewards/inline-badge-popover.tsx (1)

216-221: Consider the implications of JSON.stringify comparison

The JSON.stringify comparison works well for string arrays but could be problematic if this component is reused with complex objects or circular references. Consider adding a comment to document this limitation or use a more robust comparison method if needed in the future.

apps/web/ui/partners/rewards/rewards-logic.tsx (1)

380-384: Consider reliability of external CDN for country flags

The country flags are loaded from an external CDN. Consider hosting these assets locally or implementing a fallback mechanism to ensure reliability.

 icon: (
   <img
     alt={`${key} flag`}
-    src={`https://hatscripts.github.io/circle-flags/flags/${key.toLowerCase()}.svg`}
+    src={`https://hatscripts.github.io/circle-flags/flags/${key.toLowerCase()}.svg`}
+    onError={(e) => {
+      e.currentTarget.style.display = 'none';
+    }}
     className="size-3 shrink-0"
   />
 ),
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fd690d and dee0263.

📒 Files selected for processing (17)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/rewards.tsx (1 hunks)
  • apps/web/lib/actions/partners/create-reward.ts (4 hunks)
  • apps/web/lib/actions/partners/update-reward.ts (4 hunks)
  • apps/web/lib/plan-capabilities.ts (1 hunks)
  • apps/web/lib/zod/schemas/rewards.ts (2 hunks)
  • apps/web/ui/partners/add-edit-reward-sheet.tsx (0 hunks)
  • apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (1 hunks)
  • apps/web/ui/partners/rewards/inline-badge-popover.tsx (1 hunks)
  • apps/web/ui/partners/rewards/reward-icon-square.tsx (1 hunks)
  • apps/web/ui/partners/rewards/reward-partners-card.tsx (1 hunks)
  • apps/web/ui/partners/rewards/reward-partners-table.tsx (5 hunks)
  • apps/web/ui/partners/rewards/rewards-logic.tsx (1 hunks)
  • apps/web/ui/partners/rewards/rewards-upgrade-modal.tsx (1 hunks)
  • packages/ui/src/icons/nucleo/index.ts (1 hunks)
  • packages/ui/src/icons/nucleo/user-minus.tsx (1 hunks)
  • packages/ui/src/table/table.tsx (2 hunks)
  • packages/ui/src/table/types.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/web/ui/partners/add-edit-reward-sheet.tsx
🧰 Additional context used
🧠 Learnings (14)
📓 Common learnings
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.906Z
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.
apps/web/ui/partners/rewards/reward-icon-square.tsx (3)

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.

Learnt from: devkiran
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.

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.906Z
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.

apps/web/lib/actions/partners/create-reward.ts (2)

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.906Z
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.

apps/web/lib/actions/partners/update-reward.ts (2)

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.906Z
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.

apps/web/ui/partners/rewards/reward-partners-table.tsx (4)

Learnt from: devkiran
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.

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.906Z
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.

Learnt from: TWilson023
PR: #2614
File: apps/web/ui/partners/design/previews/lander-preview.tsx:181-181
Timestamp: 2025-07-09T20:52:56.592Z
Learning: In apps/web/ui/partners/design/previews/lander-preview.tsx, the ellipsis wave animation delay calculation 3 - i * -0.15 is intentionally designed to create negative delays that offset each dot's animation cycle. This pattern works correctly for the intended ellipsis effect and should not be changed to positive incremental delays.

apps/web/lib/zod/schemas/rewards.ts (2)

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.906Z
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.

apps/web/ui/partners/rewards/rewards-upgrade-modal.tsx (4)

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.906Z
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.

Learnt from: devkiran
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.

Learnt from: TWilson023
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.

apps/web/ui/partners/rewards/rewards-logic.tsx (3)

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.906Z
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.

Learnt from: TWilson023
PR: #2519
File: apps/web/ui/analytics/utils.ts:35-37
Timestamp: 2025-06-16T19:21:23.506Z
Learning: In the useAnalyticsFilterOption function in apps/web/ui/analytics/utils.ts, the pattern options?.context ?? useContext(AnalyticsContext) is intentionally designed as a complete replacement strategy, not a merge. When options.context is provided, it should contain all required fields (baseApiPath, queryString, selectedTab, requiresUpgrade) and completely replace the React context, not be merged with it. This is used for dependency injection or testing scenarios.

apps/web/ui/partners/rewards/reward-partners-card.tsx (5)

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.906Z
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.

Learnt from: TWilson023
PR: #2614
File: apps/web/ui/partners/design/previews/lander-preview.tsx:181-181
Timestamp: 2025-07-09T20:52:56.592Z
Learning: In apps/web/ui/partners/design/previews/lander-preview.tsx, the ellipsis wave animation delay calculation 3 - i * -0.15 is intentionally designed to create negative delays that offset each dot's animation cycle. This pattern works correctly for the intended ellipsis effect and should not be changed to positive incremental delays.

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/reward-partners-card.tsx:99-101
Timestamp: 2025-07-30T15:28:50.203Z
Learning: In the Dub codebase, the inert attribute is intentionally set to an empty string ("") instead of a boolean value to avoid TypeScript issues. This pattern is used consistently across the codebase and works correctly despite the HTML specification expecting a boolean value.

Learnt from: TWilson023
PR: #2484
File: apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/layout.tsx:9-44
Timestamp: 2025-06-25T18:07:53.860Z
Learning: In the dub codebase onboarding layouts, avoid suggesting premature abstraction of shared background components when the implementations are still dynamic and might diverge. The team prefers to wait until patterns stabilize before creating shared components.

apps/web/lib/plan-capabilities.ts (1)

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.

apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (5)

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.906Z
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.

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.

Learnt from: TWilson023
PR: #2519
File: apps/web/ui/analytics/utils.ts:35-37
Timestamp: 2025-06-16T19:21:23.506Z
Learning: In the useAnalyticsFilterOption function in apps/web/ui/analytics/utils.ts, the pattern options?.context ?? useContext(AnalyticsContext) is intentionally designed as a complete replacement strategy, not a merge. When options.context is provided, it should contain all required fields (baseApiPath, queryString, selectedTab, requiresUpgrade) and completely replace the React context, not be merged with it. This is used for dependency injection or testing scenarios.

Learnt from: TWilson023
PR: #2614
File: apps/web/ui/partners/design/previews/lander-preview.tsx:181-181
Timestamp: 2025-07-09T20:52:56.592Z
Learning: In apps/web/ui/partners/design/previews/lander-preview.tsx, the ellipsis wave animation delay calculation 3 - i * -0.15 is intentionally designed to create negative delays that offset each dot's animation cycle. This pattern works correctly for the intended ellipsis effect and should not be changed to positive incremental delays.

Learnt from: TWilson023
PR: #2471
File: apps/web/ui/auth/reset-password-form.tsx:52-65
Timestamp: 2025-06-04T15:09:51.562Z
Learning: In the Dub codebase, server-side validation errors for password fields are handled via toast notifications rather than using react-hook-form's setError method to display field-specific errors. This architectural pattern separates client-side validation feedback (inline) from server-side error handling (toast).

apps/web/ui/partners/rewards/inline-badge-popover.tsx (1)

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.

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

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.120Z
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.

Learnt from: TWilson023
PR: #2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.906Z
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.

packages/ui/src/icons/nucleo/user-minus.tsx (1)

Learnt from: TWilson023
PR: #2614
File: apps/web/ui/partners/design/previews/lander-preview.tsx:181-181
Timestamp: 2025-07-09T20:52:56.592Z
Learning: In apps/web/ui/partners/design/previews/lander-preview.tsx, the ellipsis wave animation delay calculation 3 - i * -0.15 is intentionally designed to create negative delays that offset each dot's animation cycle. This pattern works correctly for the intended ellipsis effect and should not be changed to positive incremental delays.

🔇 Additional comments (25)
packages/ui/src/table/types.ts (1)

66-66: LGTM! Clean type addition for styling customization.

The emptyWrapperClassName property follows naming conventions and is properly placed with other className properties.

packages/ui/src/icons/nucleo/user-minus.tsx (1)

1-45: LGTM! Well-implemented SVG icon component.

The UserMinus component follows established patterns for icon components, uses proper TypeScript typing with SVGProps<SVGSVGElement>, and the SVG markup is well-formed for representing a user with minus sign.

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

9-9: LGTM! Import path correctly updated for refactored component.

The import path change aligns with the reward sheet component relocation and refactoring.

packages/ui/src/icons/nucleo/index.ts (1)

213-213: LGTM! Properly ordered export addition.

The export for user-minus is correctly placed alphabetically and follows the established pattern.

apps/web/lib/plan-capabilities.ts (1)

14-15: Plan capability enforcement validated

I’ve confirmed that the new canUseAdvancedRewardLogic flag is properly enforced both server- and client-side:

  • apps/web/lib/actions/partners/create-reward.ts
    • Throws if !getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic before applying modifiers
  • apps/web/lib/actions/partners/update-reward.ts
    • Same guard in reward-update flow
  • apps/web/ui/partners/rewards/rewards-logic.tsx
    • UI gating via getPlanCapabilities(plan).canUseAdvancedRewardLogic (falls back to upgrade modal)

As long as workspace.plan is fetched fresh from the database within these actions (as it is today), there are no gaps for bypass. No changes required here.

apps/web/ui/partners/rewards/reward-icon-square.tsx (1)

3-7: LGTM!

Clean and well-structured utility component. The destructuring pattern with type annotation is correct, and the styling follows the design system consistently.

packages/ui/src/table/table.tsx (1)

291-291: LGTM!

Clean implementation of the emptyWrapperClassName prop that follows existing patterns in the component. The optional prop provides good customization capability without breaking existing usage.

Also applies to: 521-526

apps/web/lib/actions/partners/update-reward.ts (3)

6-6: LGTM!

Appropriate imports for the new advanced reward logic functionality.

Also applies to: 12-12


28-28: LGTM!

Proper parameter extraction following the existing pattern.


80-80: Critical: JSON injection vulnerability in modifiers

Raw modifiers are stored as Prisma.JsonValue without sanitization, posing a risk of JSON injection attacks and potential database corruption.

Implement JSON schema validation and sanitization before database storage:

+ // Validate and sanitize modifiers before storage
+ const sanitizedModifiers = modifiers ? 
+   JSON.parse(JSON.stringify(modifiers)) : // Basic sanitization
+   null;
+ 
- modifiers: modifiers === null ? Prisma.JsonNull : modifiers,
+ modifiers: sanitizedModifiers === null ? Prisma.JsonNull : sanitizedModifiers,

Additionally, consider implementing comprehensive JSON schema validation to ensure modifiers conform to expected structure.

Likely an incorrect or invalid review comment.

apps/web/ui/partners/rewards/reward-partners-table.tsx (4)

12-13: LGTM!

Appropriate icon imports for the include/exclude mode visual indicators.


38-38: LGTM!

Improved label clarity with descriptive text that explicitly indicates the mode purpose.


236-236: LGTM!

Good utilization of the new Table component styling capabilities. The rounded container and fixed height for empty state improve visual consistency.

Also applies to: 238-238


247-254: LGTM!

Excellent UI improvement with clear visual indicators. The conditional icon rendering and semantic label structure enhance user experience and accessibility.

apps/web/lib/actions/partners/create-reward.ts (2)

6-6: LGTM!

Consistent imports for advanced reward logic functionality, matching the update action implementation.

Also applies to: 12-12


97-97: Critical: JSON injection vulnerability in modifiers

Same security concern as in update-reward.ts - raw modifiers stored without sanitization pose JSON injection risks.

Apply the same sanitization approach:

+ // Validate and sanitize modifiers before storage
+ const sanitizedModifiers = modifiers ? 
+   JSON.parse(JSON.stringify(modifiers)) : // Basic sanitization
+   null;
+ 
- modifiers: modifiers || Prisma.JsonNull,
+ modifiers: sanitizedModifiers || Prisma.JsonNull,

Likely an incorrect or invalid review comment.

apps/web/ui/partners/rewards/rewards-upgrade-modal.tsx (1)

179-191: Well-structured modal management hook

The useRewardsUpgradeModal hook provides a clean API for managing the upgrade modal state and rendering.

apps/web/lib/zod/schemas/rewards.ts (2)

18-56: Excellent schema refactoring for type safety

The extraction of constants and explicit schema definitions significantly improves type safety and maintainability. The structured approach to defining conditions, operators, and attributes makes the validation logic more robust.


85-85: Great improvement to modifiers validation

Replacing z.any().nullish() with rewardConditionsArraySchema.nullish() provides proper type safety and validation for the modifiers field.

apps/web/ui/partners/rewards/reward-partners-card.tsx (1)

126-205: Well-optimized partner preview component

The PartnerPreviewOrCount component demonstrates excellent performance optimizations:

  • Efficiently fetches only missing partner data
  • Proper memoization of preview partners
  • Smooth transitions between count and avatar views
  • Robust fallback handling for missing data
apps/web/ui/partners/rewards/inline-badge-popover.tsx (2)

35-72: Clean popover implementation with good UX considerations

The InlineBadgePopover component provides a well-structured wrapper with:

  • Context-based state sharing for nested components
  • Proper wheel event handling for modal compatibility
  • Clear visual feedback for invalid states

81-175: Excellent menu component with thoughtful features

The InlineBadgePopoverMenu component demonstrates excellent UX design:

  • Smart sorting that moves selected items to top when closed
  • Smooth scroll progress indicator
  • Support for both single and multi-select modes
  • Generic typing for flexibility
apps/web/ui/partners/rewards/rewards-logic.tsx (2)

40-96: Excellent plan capability enforcement

The component properly checks workspace plan capabilities before allowing access to advanced reward logic, providing a smooth upgrade path via the modal.


184-211: Well-structured constants and type definitions

The constants for entities, operators, and labels provide excellent type safety and maintainability. The mapping between events and allowed entities is particularly well-designed.

apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (1)

184-227: Plan capability enforcement verified
Both createRewardAction and updateRewardAction import getPlanCapabilities and immediately throw if workspace.plan lacks canUseAdvancedRewardLogic, before any modifiers are processed. No further action needed.

Comment on lines +53 to +59
if (
modifiers &&
!getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
)
throw new Error(
"Advanced reward structures are only available on the Advanced plan and above.",
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: Plan capability bypass risk

Same security concern as in update-reward.ts - the plan capability check occurs after extracting modifiers and could be bypassed if workspace data is stale.

Apply the same fix as recommended for update-reward.ts:

+ // Refresh workspace to ensure latest plan data
+ const refreshedWorkspace = await prisma.workspace.findUnique({
+   where: { id: workspace.id },
+   select: { plan: true }
+ });
+ 
  if (
    modifiers &&
-   !getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
+   !getPlanCapabilities(refreshedWorkspace?.plan || workspace.plan).canUseAdvancedRewardLogic
  )
📝 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.

Suggested change
if (
modifiers &&
!getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
)
throw new Error(
"Advanced reward structures are only available on the Advanced plan and above.",
);
// Refresh workspace to ensure latest plan data
const refreshedWorkspace = await prisma.workspace.findUnique({
where: { id: workspace.id },
select: { plan: true },
});
if (
modifiers &&
!getPlanCapabilities(refreshedWorkspace?.plan || workspace.plan).canUseAdvancedRewardLogic
)
throw new Error(
"Advanced reward structures are only available on the Advanced plan and above.",
);
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/create-reward.ts around lines 53 to 59, the
plan capability check happens after extracting modifiers, which risks bypass if
workspace data is stale. Move the plan capability check to occur before any
extraction or processing of modifiers to ensure the validation happens first and
prevents unauthorized access to advanced reward logic.

Comment on lines +41 to +47
if (
modifiers &&
!getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
)
throw new Error(
"Advanced reward structures are only available on the Advanced plan and above.",
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: Plan capability bypass risk

The plan capability check occurs after extracting modifiers from user input. If workspace data is stale or manipulated, this validation could be bypassed, allowing unauthorized access to advanced features.

Consider refreshing workspace plan validation and adding server-side double-checking:

+ // Refresh workspace to ensure latest plan data
+ const refreshedWorkspace = await prisma.workspace.findUnique({
+   where: { id: workspace.id },
+   select: { plan: true }
+ });
+ 
  if (
    modifiers &&
-   !getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
+   !getPlanCapabilities(refreshedWorkspace?.plan || workspace.plan).canUseAdvancedRewardLogic
  )
📝 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.

Suggested change
if (
modifiers &&
!getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
)
throw new Error(
"Advanced reward structures are only available on the Advanced plan and above.",
);
// Refresh workspace to ensure latest plan data
const refreshedWorkspace = await prisma.workspace.findUnique({
where: { id: workspace.id },
select: { plan: true }
});
if (
modifiers &&
!getPlanCapabilities(refreshedWorkspace?.plan || workspace.plan).canUseAdvancedRewardLogic
)
throw new Error(
"Advanced reward structures are only available on the Advanced plan and above.",
);
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/update-reward.ts around lines 41 to 47, the
plan capability check happens after extracting modifiers, risking bypass if
workspace data is stale or manipulated. To fix this, ensure the workspace plan
is freshly validated from a trusted source before checking capabilities, and add
a server-side double-check to enforce plan restrictions strictly before
processing modifiers or applying advanced reward logic.

Comment on lines +189 to +217
let modifiers: RewardConditionsArray | null = null;
if (data.modifiers?.length) {
try {
modifiers = rewardConditionsArraySchema.parse(
data.modifiers.map((m) => ({
...m,
amount: type === "flat" ? m.amount * 100 : m.amount,
})),
);
} catch (error) {
console.error(error);
setError("root.logic", { message: "Invalid reward logic" });
toast.error(
"Invalid reward logic. Please fix the errors and try again.",
);
return;
}
}

const payload = {
...data,
workspaceId,
includedPartnerIds: isDefault ? null : includedPartnerIds,
excludedPartnerIds: isDefault ? excludedPartnerIds : null,
amount: type === "flat" ? data.amount * 100 : data.amount,
maxDuration:
Infinity === Number(data.maxDuration) ? null : data.maxDuration,
modifiers,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add JSON sanitization for modifiers before database storage

The modifiers are validated for structure but not sanitized before being passed to the create/update actions. As highlighted in the PR security review, storing raw modifiers as Prisma.JsonValue poses a JSON injection risk.

Consider adding sanitization after validation:

 let modifiers: RewardConditionsArray | null = null;
 if (data.modifiers?.length) {
   try {
     modifiers = rewardConditionsArraySchema.parse(
       data.modifiers.map((m) => ({
         ...m,
         amount: type === "flat" ? m.amount * 100 : m.amount,
       })),
     );
+    // Sanitize the validated modifiers to prevent JSON injection
+    modifiers = JSON.parse(JSON.stringify(modifiers));
   } catch (error) {
     console.error(error);
     setError("root.logic", { message: "Invalid reward logic" });
     toast.error(
       "Invalid reward logic. Please fix the errors and try again.",
     );
     return;
   }
 }

Additionally, ensure the server-side actions also implement sanitization before database operations.

📝 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.

Suggested change
let modifiers: RewardConditionsArray | null = null;
if (data.modifiers?.length) {
try {
modifiers = rewardConditionsArraySchema.parse(
data.modifiers.map((m) => ({
...m,
amount: type === "flat" ? m.amount * 100 : m.amount,
})),
);
} catch (error) {
console.error(error);
setError("root.logic", { message: "Invalid reward logic" });
toast.error(
"Invalid reward logic. Please fix the errors and try again.",
);
return;
}
}
const payload = {
...data,
workspaceId,
includedPartnerIds: isDefault ? null : includedPartnerIds,
excludedPartnerIds: isDefault ? excludedPartnerIds : null,
amount: type === "flat" ? data.amount * 100 : data.amount,
maxDuration:
Infinity === Number(data.maxDuration) ? null : data.maxDuration,
modifiers,
};
let modifiers: RewardConditionsArray | null = null;
if (data.modifiers?.length) {
try {
modifiers = rewardConditionsArraySchema.parse(
data.modifiers.map((m) => ({
...m,
amount: type === "flat" ? m.amount * 100 : m.amount,
})),
);
// Sanitize the validated modifiers to prevent JSON injection
modifiers = JSON.parse(JSON.stringify(modifiers));
} catch (error) {
console.error(error);
setError("root.logic", { message: "Invalid reward logic" });
toast.error(
"Invalid reward logic. Please fix the errors and try again.",
);
return;
}
}
const payload = {
...data,
workspaceId,
includedPartnerIds: isDefault ? null : includedPartnerIds,
excludedPartnerIds: isDefault ? excludedPartnerIds : null,
amount: type === "flat" ? data.amount * 100 : data.amount,
maxDuration:
Infinity === Number(data.maxDuration) ? null : data.maxDuration,
modifiers,
};
🤖 Prompt for AI Agents
In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx around lines 189 to
217, the modifiers are validated but not sanitized before being included in the
payload for database storage, which risks JSON injection. After successfully
parsing modifiers with rewardConditionsArraySchema, apply a JSON sanitization
step to clean the data before assigning it to the payload. Also, verify that
server-side create/update actions perform similar sanitization before writing to
the database.

@steven-tey steven-tey merged commit d0f663b into main Jul 30, 2025
8 checks passed
@steven-tey steven-tey deleted the reward-logic branch July 30, 2025 15:40
This was referenced Aug 8, 2025
@coderabbitai coderabbitai bot mentioned this pull request Sep 10, 2025
@coderabbitai coderabbitai bot mentioned this pull request Oct 4, 2025
@coderabbitai coderabbitai bot mentioned this pull request Nov 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants