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

Skip to content

Conversation

@TWilson023
Copy link
Collaborator

@TWilson023 TWilson023 commented Aug 8, 2025

Summary by CodeRabbit

  • New Features

    • Added support for displaying reward modifiers in reward descriptions and lists, including a tooltip that details modifier conditions.
    • Introduced a new tooltip component to show detailed information about reward modifiers.
    • Added human-readable labels for reward condition operators.
  • Enhancements

    • Reward amount displays now support ranges when modifiers are present, showing both base and modifier amounts.
    • Centralized and improved operator label handling in condition logic and dropdowns.
  • Bug Fixes

    • Corrected inline style property for user selection in drawer content to use proper React syntax.
  • Configuration

    • Added options to hide reward modifiers in specific contexts, such as emails and customer dashboards.
    • Improved link behavior by disabling scroll on navigation in reward items.

@TWilson023 TWilson023 requested a review from steven-tey August 8, 2025 21:32
@TWilson023 TWilson023 marked this pull request as ready for review August 8, 2025 21:32
@vercel
Copy link
Contributor

vercel bot commented Aug 8, 2025

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

Name Status Preview Updated (UTC)
dub ✅ Ready (Inspect) Visit Preview Aug 8, 2025 11:31pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 8, 2025

Walkthrough

This set of changes introduces support for reward modifiers throughout the application, enhancing components and logic to handle and display multiple reward amounts and their conditions. New props and types are added to relevant components, a tooltip for modifier details is introduced, and operator label mappings are centralized. Minor style and prop formatting adjustments are also included.

Changes

Cohort / File(s) Change Summary
Reward Modifiers Support in UI Components
apps/web/ui/partners/program-reward-description.tsx, apps/web/ui/partners/program-reward-list.tsx, apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
Enhanced reward description and list components to handle and display modifiers, including new props, conditional logic for multiple amounts, and a new tooltip component for modifier details.
Reward Amount Construction Logic
apps/web/lib/api/sales/construct-reward-amount.ts, apps/web/app/(ee)/app.dub.co/embed/referrals/faq.tsx, apps/web/app/api/og/program/route.tsx
Updated logic to pass arrays of amounts to reward amount construction when modifiers exist, and refactored function signatures and internal logic to support this.
Modifier Display Control
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx, apps/web/lib/partners/approve-partner-enrollment.ts, apps/web/lib/partners/bulk-approve-partners.ts
Added or updated props and arguments to control the display of reward modifiers in customer and partner-related UIs and notifications.
Operator Label Centralization
apps/web/lib/zod/schemas/rewards.ts, apps/web/ui/partners/rewards/rewards-logic.tsx
Introduced a centralized constant for operator labels and updated logic to use this mapping instead of local definitions.
Minor Style Update
packages/ui/src/sheet.tsx
Changed inline style property from "user-select" to userSelect for React compatibility.
Miscellaneous UI Adjustment
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/rewards.tsx
Added scroll={false} attribute to a conditional Link component to modify scroll behavior without changing other logic.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ProgramRewardList
    participant ProgramRewardDescription
    participant ProgramRewardModifiersTooltip

    User->>ProgramRewardList: View reward list
    ProgramRewardList->>ProgramRewardDescription: Render each reward (with showModifiers)
    ProgramRewardDescription->>ProgramRewardModifiersTooltip: (If showModifiers & modifiers exist) Render tooltip
    ProgramRewardModifiersTooltip-->>ProgramRewardDescription: Tooltip with modifier details
    ProgramRewardDescription-->>ProgramRewardList: Render reward row with modifiers info
    ProgramRewardList-->>User: Display rewards with modifier tooltips
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15–20 minutes

Possibly related PRs

  • Reward modifiers #2645: Introduced a context-aware reward modifiers system with backend schema, evaluation logic, and database changes, related to this PR's UI and formatting enhancements for reward modifiers.

Poem

In the garden of code where rewards now bloom,
Modifiers hop in, dispelling the gloom.
Tooltips appear with a magical flair,
Descriptions and logic now handle with care.
With carrots of clarity and labels anew,
This bunny’s delighted—great work from the crew! 🥕✨

Note

🔌 MCP (Model Context Protocol) integration is now available in Early Access!

Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context.


📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 50c84ea and 3def006.

📒 Files selected for processing (1)
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
⏰ 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
✨ 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-modifier-display

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

Support

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

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.

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: 6

🔭 Outside diff range comments (2)
apps/web/ui/partners/rewards/rewards-logic.tsx (2)

262-270: Reset dependent fields when the entity changes

Switching entity without clearing attribute, operator, and value can leave the UI in an invalid state (e.g. a sale attribute hanging on a customer entity).
Update the assignment to wipe the dependent fields:

-  setValue(conditionKey, { entity: value as keyof typeof ENTITIES }, { … });
+  setValue(
+    conditionKey,
+    { entity: value as keyof typeof ENTITIES },
+    { shouldDirty: true, shouldValidate: true },
+  );
+  // Immediately clear downstream fields
+  setValue(`${conditionKey}.attribute`, undefined, { shouldDirty: true });
+  setValue(`${conditionKey}.operator`, undefined, { shouldDirty: true });
+  setValue(`${conditionKey}.value`, undefined, { shouldDirty: true });

This mirrors the UX requirement captured in previous PR #2673.


255-256: Guard against undefined operator

operator can be undefined until the user makes a selection, yet operator.toLowerCase() is called unconditionally.
Add a null-safe guard to avoid a run-time crash:

-{conditionIndex === 0 ? "If" : capitalize(operator.toLowerCase())}{" "}
+{conditionIndex === 0
+  ? "If"
+  : operator
+    ? capitalize(operator.toLowerCase())
+    : "—"}{" "}
🧹 Nitpick comments (4)
packages/ui/src/sheet.tsx (1)

39-46: Optional: Add WebkitUserSelect for older Safari; confirm override intent.

  • If you need to support older iOS/Safari/WebView edge cases, consider also setting WebkitUserSelect: "auto".
  • Current spread order lets consumer-provided contentProps.style override userSelect. If the goal is to guarantee selectable content regardless of consumer style, invert the spread.

Proposed diff:

           style={
             // 8px between edge of screen and drawer
             {
               "--initial-transform": "calc(100% + 8px)",
-              userSelect: "auto", // Override default user-select: none from Vaul
+              userSelect: "auto", // Override default user-select: none from Vaul
+              WebkitUserSelect: "auto",
               ...contentProps?.style,
             } as React.CSSProperties
           }
apps/web/lib/zod/schemas/rewards.ts (1)

36-43: Add a compile-time exhaustiveness check for operator labels

CONDITION_OPERATOR_LABELS can silently drift out of sync when new operators are added.
Leverage TypeScript 4.9’s satisfies operator so the compiler flags missing/extraneous keys:

-export const CONDITION_OPERATOR_LABELS = {
+export const CONDITION_OPERATOR_LABELS = {
   equals_to: "is",
   not_equals: "is not",
   starts_with: "starts with",
   ends_with: "ends with",
   in: "is one of",
   not_in: "is not one of",
-} as const;
+} satisfies Record<(typeof CONDITION_OPERATORS)[number], string>;

This keeps the run-time object unchanged while giving full type safety.

apps/web/app/api/og/program/route.tsx (1)

141-151: Use .modifiers?.length to avoid single-element amounts when modifiers is an empty array.

Empty arrays are truthy, producing amounts: [reward.amount]. Prefer checking length.

-                    ...(rewards[0].modifiers
+                    ...(rewards[0].modifiers?.length
                       ? {
                           amounts: [
                             rewards[0].amount,
                             ...rewards[0].modifiers.map(({ amount }) => amount),
                           ],
                         }
                       : { amount: rewards[0].amount }),
apps/web/app/(ee)/app.dub.co/embed/referrals/faq.tsx (1)

24-33: Prefer .modifiers?.length over a truthy check to avoid single-element amounts.

-        ...(reward.modifiers
+        ...(reward.modifiers?.length
           ? {
               amounts: [
                 reward.amount,
                 ...reward.modifiers.map(({ amount }) => amount),
               ],
             }
           : { amount: reward.amount }),
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between bff2695 and 668be9f.

📒 Files selected for processing (12)
  • apps/web/app/(ee)/app.dub.co/embed/referrals/faq.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx (1 hunks)
  • apps/web/app/api/og/program/route.tsx (1 hunks)
  • apps/web/lib/api/sales/construct-reward-amount.ts (1 hunks)
  • apps/web/lib/partners/approve-partner-enrollment.ts (1 hunks)
  • apps/web/lib/partners/bulk-approve-partners.ts (1 hunks)
  • apps/web/lib/zod/schemas/rewards.ts (1 hunks)
  • apps/web/ui/partners/program-reward-description.tsx (3 hunks)
  • apps/web/ui/partners/program-reward-list.tsx (3 hunks)
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx (1 hunks)
  • apps/web/ui/partners/rewards/rewards-logic.tsx (3 hunks)
  • packages/ui/src/sheet.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 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.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.
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.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.
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
PR: dubinc/dub#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/lib/partners/bulk-approve-partners.ts
  • apps/web/app/api/og/program/route.tsx
  • apps/web/lib/partners/approve-partner-enrollment.ts
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx
  • apps/web/ui/partners/rewards/rewards-logic.tsx
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
  • apps/web/ui/partners/program-reward-description.tsx
  • apps/web/lib/zod/schemas/rewards.ts
  • apps/web/app/(ee)/app.dub.co/embed/referrals/faq.tsx
  • apps/web/ui/partners/program-reward-list.tsx
  • apps/web/lib/api/sales/construct-reward-amount.ts
📚 Learning: 2025-07-30T15:25:13.936Z
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.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/partners/bulk-approve-partners.ts
  • apps/web/app/api/og/program/route.tsx
  • apps/web/lib/partners/approve-partner-enrollment.ts
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx
  • apps/web/ui/partners/rewards/rewards-logic.tsx
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
  • apps/web/ui/partners/program-reward-description.tsx
  • apps/web/lib/zod/schemas/rewards.ts
  • apps/web/app/(ee)/app.dub.co/embed/referrals/faq.tsx
  • apps/web/ui/partners/program-reward-list.tsx
  • apps/web/lib/api/sales/construct-reward-amount.ts
📚 Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
PR: dubinc/dub#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/partners/bulk-approve-partners.ts
  • apps/web/lib/partners/approve-partner-enrollment.ts
  • apps/web/ui/partners/rewards/rewards-logic.tsx
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
📚 Learning: 2025-07-09T20:52:56.592Z
Learnt from: TWilson023
PR: dubinc/dub#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.

Applied to files:

  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
  • apps/web/ui/partners/program-reward-list.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (11)
packages/ui/src/sheet.tsx (1)

43-44: Correct React style key (LGTM).

Switching to userSelect fixes the React.CSSProperties typing and applies as intended. Good catch.

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx (1)

70-75: Prop addition looks good

Explicitly disabling modifier display (showModifiers={false}) keeps the tooltip concise for this view.

apps/web/lib/partners/bulk-approve-partners.ts (1)

84-88: Email copy uses the new flag correctly

Passing showModifiers: false matches the simplified reward description intended for approval emails.

apps/web/lib/partners/approve-partner-enrollment.ts (1)

186-188: Email still may show ranged amounts from modifiers despite showModifiers: false.

showModifiers currently only hides the tooltip; the amount string is still built from modifiers. If the intent is to fully suppress modifier display in emails (amount ranges + tooltip), update ProgramRewardDescription to respect showModifiers when constructing amounts.

See suggested fix in ProgramRewardDescription comment.

Likely an incorrect or invalid review comment.

apps/web/ui/partners/program-reward-list.tsx (3)

7-7: LGTM: Modifier tooltip import added where used.


13-19: LGTM: showModifiers prop (default true) adds needed control in different contexts.


68-73: LGTM: Tooltip correctly gated by showModifiers and non-empty modifiers.

apps/web/ui/partners/program-reward-description.tsx (3)

11-12: LGTM: showModifiers prop surfaces control to callers. Types align with usage.

Also applies to: 20-21


15-16: LGTM: Extending reward pick to include modifiers is correct.


65-71: LGTM: Tooltip rendering is correctly gated by showModifiers and non-empty modifiers.

apps/web/ui/partners/program-reward-modifiers-tooltip.tsx (1)

20-20: Guard clause for empty modifiers — LGTM

Straightforward and prevents rendering unnecessary markup.

@steven-tey
Copy link
Collaborator

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 8, 2025

✅ 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: 1

🔭 Outside diff range comments (2)
apps/web/ui/partners/rewards/rewards-logic.tsx (2)

256-259: Fix potential crash: avoid calling capitalize on undefined

When entity is unset, capitalize(condition.entity) will throw. Use a guard.

-        <InlineBadgePopover
-          text={capitalize(condition.entity) || "Select item"}
-          invalid={!condition.entity}
-        >
+        <InlineBadgePopover
+          text={condition.entity ? capitalize(condition.entity) : "Select item"}
+          invalid={!condition.entity}
+        >

260-271: Reset dependent fields when entity changes to prevent invalid state

Per prior learning, changing entity should clear attribute/operator/value to avoid mismatches with the new entity’s attribute set.

-            onSelect={(value) =>
-              setValue(
-                conditionKey,
-                { entity: value as keyof typeof ENTITIES },
-                {
-                  shouldDirty: true,
-                },
-              )
-            }
+            onSelect={(value) =>
+              setValue(
+                conditionKey,
+                {
+                  entity: value as keyof typeof ENTITIES,
+                  attribute: undefined,
+                  operator: undefined,
+                  value: undefined,
+                },
+                { shouldDirty: true },
+              )
+            }
♻️ Duplicate comments (6)
apps/web/lib/api/sales/construct-reward-amount.ts (1)

18-21: Percentage range string fix is correct (previous issue resolved)

Both ends now include “%” (e.g., 10% - 12%). Thanks for addressing this.

apps/web/ui/partners/program-reward-list.tsx (1)

33-43: Guard on modifiers length to avoid single-element amounts

Use reward.modifiers?.length to only build ranges when there are actual modifiers.

-              {constructRewardAmount({
-                ...(reward.modifiers
+              {constructRewardAmount({
+                ...(reward.modifiers?.length
                   ? {
                       amounts: [
                         reward.amount,
                         ...reward.modifiers.map(({ amount }) => amount),
                       ],
                     }
                   : { amount: reward.amount }),
                 type: reward.type,
               })}{" "}
apps/web/ui/partners/program-reward-description.tsx (1)

30-37: Honor showModifiersTooltip when constructing the amount; also check .modifiers?.length

Without gating by showModifiersTooltip, ranged amounts will show even when the tooltip is hidden (e.g., emails). Gate by both flags to avoid unintended ranged display.

-                {constructRewardAmount({
-                  ...(reward.modifiers?.length
-                    ? {
-                        amounts: [
-                          reward.amount,
-                          ...reward.modifiers.map(({ amount }) => amount),
-                        ],
-                      }
-                    : { amount: reward.amount }),
-                  type: reward.type,
-                })}{" "}
+                {constructRewardAmount({
+                  ...(showModifiersTooltip && reward.modifiers?.length
+                    ? {
+                        amounts: [
+                          reward.amount,
+                          ...reward.modifiers.map(({ amount }) => amount),
+                        ],
+                      }
+                    : { amount: reward.amount }),
+                  type: reward.type,
+                })}{" "}
apps/web/ui/partners/program-reward-modifiers-tooltip.tsx (3)

42-47: Handle undefined maxDuration to avoid “undefined NaN months”

Treat undefined/null/Infinity consistently as lifetime.

-                  {reward.maxDuration === 0
-                    ? "one time"
-                    : reward.maxDuration === Infinity ||
-                        reward.maxDuration === null
-                      ? "for the customer's lifetime"
-                      : `for ${reward.maxDuration} ${pluralize("month", Number(reward.maxDuration))}`}
+                  {reward.maxDuration === 0
+                    ? "one time"
+                    : reward.maxDuration == null || reward.maxDuration === Infinity
+                      ? "for the customer's lifetime"
+                      : `for ${reward.maxDuration} ${pluralize("month", Number(reward.maxDuration))}`}

53-55: Do not use amount as React key

amount isn’t guaranteed unique; use a stable unique id if available or fall back to index.

-            ).map(({ amount, operator, conditions }) => (
-              <Fragment key={amount}>
+            ).map(({ amount, operator, conditions }, i) => (
+              <Fragment key={`${amount}-${i}`}>

71-78: Preserve falsy values and add safe fallbacks for operator and labels

  • Default operator before .toLowerCase().
  • Provide fallback for CONDITION_OPERATOR_LABELS.
  • Don’t drop 0/false in condition.value.
-                            {idx === 0
-                              ? "If"
-                              : capitalize(operator.toLowerCase())}
+                            {idx === 0
+                              ? "If"
+                              : capitalize((operator ?? "and").toLowerCase())}
                             {` ${condition.entity}`}
                             {` ${condition.attribute}`}
-                            {` ${CONDITION_OPERATOR_LABELS[condition.operator]}`}
-                            {` ${condition.value && truncate(Array.isArray(condition.value) ? condition.value.join(", ") : condition.value.toString(), 16)}`}
+                            {` ${CONDITION_OPERATOR_LABELS[condition.operator] ?? condition.operator}`}
+                            {condition.value !== undefined && condition.value !== null
+                              ? ` ${truncate(
+                                  Array.isArray(condition.value)
+                                    ? condition.value.join(", ")
+                                    : String(condition.value),
+                                  16
+                                )}`
+                              : ""}
🧹 Nitpick comments (8)
apps/web/lib/zod/schemas/rewards.ts (1)

36-43: Make operator labels mapping immutable and type-checked against CONDITION_OPERATORS

Prevent accidental runtime mutation and enforce one-to-one coverage at compile-time.

Apply this diff:

-export const CONDITION_OPERATOR_LABELS = {
-  equals_to: "is",
-  not_equals: "is not",
-  starts_with: "starts with",
-  ends_with: "ends with",
-  in: "is one of",
-  not_in: "is not one of",
-} as const;
+export const CONDITION_OPERATOR_LABELS = Object.freeze({
+  equals_to: "is",
+  not_equals: "is not",
+  starts_with: "starts with",
+  ends_with: "ends with",
+  in: "is one of",
+  not_in: "is not one of",
+} satisfies Record<(typeof CONDITION_OPERATORS)[number], string>);
apps/web/lib/api/sales/construct-reward-amount.ts (1)

14-21: Collapse range when all amounts are equal

If multiple amounts are passed but min === max, showing 10% - 10% (or $10 - $10) is noisy; render a single value.

Apply this diff:

   // Range of amounts
   if (amounts && amounts.length > 1) {
     const min = Math.min(...amounts);
     const max = Math.max(...amounts);
+    if (min === max) {
+      return type === "percentage" ? `${min}%` : formatCurrency(min);
+    }
     return type === "percentage"
       ? `${min}% - ${max}%`
       : `${formatCurrency(min)} - ${formatCurrency(max)}`;
   }
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/rewards.tsx (1)

324-327: Avoid passing non-DOM props to a div when As === "div"

href and scroll end up as unknown attributes on a div. Conditionally spread these props only for Link.

Apply this diff:

-      <As
-        href={reward ? `/${slug}/program/rewards?rewardId=${reward.id}` : ""}
-        scroll={false}
-        className="flex cursor-pointer items-center gap-4 rounded-lg border border-neutral-200 p-4 transition-all hover:border-neutral-300"
-      >
+      <As
+        {...(reward
+          ? {
+              href: `/${slug}/program/rewards?rewardId=${reward.id}`,
+              scroll: false,
+            }
+          : {})}
+        className="flex cursor-pointer items-center gap-4 rounded-lg border border-neutral-200 p-4 transition-all hover:border-neutral-300"
+      >
apps/web/ui/partners/rewards/rewards-logic.tsx (1)

293-305: Optional: also reset operator/value when attribute changes

Different attributes may imply different operator/value semantics (e.g., list vs string). Clearing them reduces invalid combos and simplifies validation.

-            onSelect={(value) =>
-              setValue(
-                conditionKey,
-                {
-                  entity: condition.entity,
-                  attribute: value as (typeof CONDITION_ATTRIBUTES)[number],
-                },
-                {
-                  shouldDirty: true,
-                },
-              )
-            }
+            onSelect={(value) =>
+              setValue(
+                conditionKey,
+                {
+                  entity: condition.entity,
+                  attribute: value as (typeof CONDITION_ATTRIBUTES)[number],
+                  operator: undefined,
+                  value: undefined,
+                },
+                { shouldDirty: true },
+              )
+            }
apps/web/app/api/og/program/route.tsx (2)

141-151: Guard on modifiers length to avoid single-element amounts

If modifiers is an empty array, passing amounts: [base] is redundant and may format differently than amount.

-                  {constructRewardAmount({
-                    ...(rewards[0].modifiers
+                  {constructRewardAmount({
+                    ...(rewards[0].modifiers?.length
                       ? {
                           amounts: [
                             rewards[0].amount,
                             ...rewards[0].modifiers.map(({ amount }) => amount),
                           ],
                         }
                       : { amount: rewards[0].amount }),
                     type: rewards[0].type,
                   })}

154-161: Handle Infinity maxDuration to avoid “Infinity months” in OG

Some flows use Infinity to represent lifetime; render a lifetime string instead of “Infinity months”.

-                {rewards[0].maxDuration === null ? (
+                {rewards[0].maxDuration === null ? (
                   "for the customer's lifetime"
-                ) : rewards[0].maxDuration && rewards[0].maxDuration > 1 ? (
+                ) : rewards[0].maxDuration === Infinity ? (
+                  "for the customer's lifetime"
+                ) : rewards[0].maxDuration && rewards[0].maxDuration > 1 ? (
                   <>
                     , and again every month for {rewards[0].maxDuration} months
                   </>
                 ) : null}
apps/web/app/(ee)/app.dub.co/embed/referrals/faq.tsx (1)

24-31: Prefer checking .modifiers?.length to gate passing amounts

Using a truthy check on reward.modifiers will also pass an array when it's empty (harmless but inconsistent). Gate on length for clarity and parity with other call sites.

-        ...(reward.modifiers
+        ...(reward.modifiers?.length
           ? {
               amounts: [
                 reward.amount,
                 ...reward.modifiers.map(({ amount }) => amount),
               ],
             }
           : { amount: reward.amount }),
apps/web/ui/partners/program-reward-description.tsx (1)

11-11: Prop naming/contract clarity

showModifiersTooltip now implicitly controls both tooltip visibility and amount formatting (after applying the above change). Consider either documenting this dual purpose in the prop comment or splitting into showModifiers (amount formatting) and showModifiersTooltip (UI) if you foresee divergent behavior.

Also applies to: 20-20

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 022b7ec and 50c84ea.

📒 Files selected for processing (13)
  • apps/web/app/(ee)/app.dub.co/embed/referrals/faq.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx (1 hunks)
  • apps/web/app/api/og/program/route.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/rewards.tsx (1 hunks)
  • apps/web/lib/api/sales/construct-reward-amount.ts (1 hunks)
  • apps/web/lib/partners/approve-partner-enrollment.ts (1 hunks)
  • apps/web/lib/partners/bulk-approve-partners.ts (1 hunks)
  • apps/web/lib/zod/schemas/rewards.ts (1 hunks)
  • apps/web/ui/partners/program-reward-description.tsx (3 hunks)
  • apps/web/ui/partners/program-reward-list.tsx (3 hunks)
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx (1 hunks)
  • apps/web/ui/partners/rewards/rewards-logic.tsx (3 hunks)
  • packages/ui/src/sheet.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 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.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.
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.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.
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
PR: dubinc/dub#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)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx
  • apps/web/lib/partners/bulk-approve-partners.ts
  • apps/web/lib/partners/approve-partner-enrollment.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/rewards.tsx
  • apps/web/ui/partners/rewards/rewards-logic.tsx
  • apps/web/app/api/og/program/route.tsx
  • apps/web/ui/partners/program-reward-list.tsx
  • apps/web/lib/zod/schemas/rewards.ts
  • apps/web/lib/api/sales/construct-reward-amount.ts
  • apps/web/ui/partners/program-reward-description.tsx
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
  • apps/web/app/(ee)/app.dub.co/embed/referrals/faq.tsx
📚 Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
PR: dubinc/dub#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/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx
  • apps/web/lib/partners/bulk-approve-partners.ts
  • apps/web/lib/partners/approve-partner-enrollment.ts
  • apps/web/ui/partners/rewards/rewards-logic.tsx
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
📚 Learning: 2025-07-30T15:25:13.936Z
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.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)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx
  • apps/web/lib/partners/bulk-approve-partners.ts
  • apps/web/lib/partners/approve-partner-enrollment.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/rewards.tsx
  • apps/web/ui/partners/rewards/rewards-logic.tsx
  • apps/web/app/api/og/program/route.tsx
  • apps/web/ui/partners/program-reward-list.tsx
  • apps/web/lib/zod/schemas/rewards.ts
  • apps/web/lib/api/sales/construct-reward-amount.ts
  • apps/web/ui/partners/program-reward-description.tsx
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
  • apps/web/app/(ee)/app.dub.co/embed/referrals/faq.tsx
📚 Learning: 2025-07-09T20:52:56.592Z
Learnt from: TWilson023
PR: dubinc/dub#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.

Applied to files:

  • apps/web/lib/partners/approve-partner-enrollment.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/rewards/rewards.tsx
  • apps/web/ui/partners/program-reward-list.tsx
  • apps/web/lib/api/sales/construct-reward-amount.ts
  • apps/web/ui/partners/program-reward-description.tsx
  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
PR: dubinc/dub#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/ui/partners/program-reward-modifiers-tooltip.tsx
📚 Learning: 2025-06-18T20:31:51.779Z
Learnt from: TWilson023
PR: dubinc/dub#2538
File: apps/web/ui/partners/overview/blocks/conversion-block.tsx:85-85
Timestamp: 2025-06-18T20:31:51.779Z
Learning: In React components where arrays are defined with useMemo using static array literals (not filtered or dynamically populated), the array will always have the same number of elements regardless of the data values. Using .at(-1)! on such arrays is safe because the array structure is constant, even if individual property values within the array elements may be 0 or falsy.

Applied to files:

  • apps/web/ui/partners/program-reward-modifiers-tooltip.tsx
🔇 Additional comments (8)
packages/ui/src/sheet.tsx (1)

43-45: Correct React style key for user-select; inline override is appropriate

Switching to userSelect fixes the React style key and correctly overrides Vaul’s default. Looks good.

apps/web/lib/api/sales/construct-reward-amount.ts (1)

28-37: Good extraction of currency formatting helper

This removes duplication and preserves formatting semantics (fixed 2 decimals only when needed). LGTM.

apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/customers/[customerId]/page-client.tsx (1)

73-74: Explicitly disabling modifiers tooltip here is sensible

Prevents nested/competing tooltips within the existing Tooltip content. LGTM.

apps/web/lib/partners/bulk-approve-partners.ts (1)

83-89: Email-safe: disable modifiers tooltip in reward description — LGTM

Explicitly passing showModifiersTooltip: false for emails makes sense and matches usage elsewhere. No concerns.

apps/web/lib/partners/approve-partner-enrollment.ts (1)

185-189: Consistent tooltip suppression — LGTM

showModifiersTooltip: false is appropriate for email context and consistent with bulk approval flow.

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

8-13: Centralized operator labels — good move

Switching to CONDITION_OPERATOR_LABELS improves consistency across UI and schema. Usage at both the badge and menu looks correct.

Also applies to: 320-323, 347-351

apps/web/ui/partners/program-reward-list.tsx (1)

68-73: Conditional tooltip rendering — LGTM

Tooltip is correctly gated by showModifiersTooltip and actual modifiers presence.

apps/web/ui/partners/program-reward-description.tsx (1)

65-71: Good conditional rendering of modifiers tooltip

Tooltip is properly gated by showModifiersTooltip and the presence of modifiers.

@steven-tey steven-tey merged commit 5b2dce5 into main Aug 8, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the reward-modifier-display branch August 8, 2025 23:45
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