-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Reward Logic #2673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reward Logic #2673
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
WalkthroughThis 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 Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Possibly related PRs
Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
@CodeRabbit final review |
|
@steven-tey I'll conduct a final comprehensive review of all the changes in this PR. ✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🔭 Outside diff range comments (1)
apps/web/lib/zod/schemas/rewards.ts (1)
67-67: Address the TODO: Fix modifiers type in RewardSchemaThe TODO comment indicates that the
modifiersfield should use a proper type instead ofz.any(). Since you've already definedrewardConditionsArraySchema, 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 IDsThe 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 constantThe 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 textThere's a missing word in the description. "get access higher payout" should be "get access to higher payout".
- When you upgrade to Advanced, you’ll get access higher payout + When you upgrade to Advanced, you’ll get access to higher payoutapps/web/ui/partners/rewards/rewards-logic.tsx (2)
388-400: Simplify complex country selection logicThe 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 componentThe 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.tsxfor condition logicOperatorSelector.tsxfor operator dropdownResultTermsEditor.tsxfor result termsThis 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 modalUsing
window.confirmis 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 maintainabilityThis file contains 510 lines with multiple components and hooks. Consider splitting it into:
add-edit-reward-sheet.tsx- Main sheet component and form logicreward-sheet-components.tsx- RewardSheetCard, VerticalLine, AmountInputuse-reward-sheet.tsx- Custom hooksThis 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
textandvaluein 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:
- JSON.stringify comparison could be inefficient for large arrays
- 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
📒 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
emptyWrapperClassNamefollows 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-minusicon 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
Icontype 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
emptyWrapperClassNameprop 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
emptyWrapperClassNameis correctly applied using thecnutility 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
getPlanCapabilitiesimport supports the plan-based access control, and thePrismaimport enables proper JSON null handling for the modifiers field.Also applies to: 12-12
28-28: LGTM! Modifiers field properly destructured.The
modifiersfield is correctly added to the destructured parameters fromparsedInput.
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 : modifiersproperly 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
UserMinusandUserPlusicons 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
UserPlusfor include mode andUserMinusfor exclude mode, providing clear visual indicators that complement the text labels.
236-238: LGTM! Styling improvements implemented correctly.The
containerClassNameadds rounded corners and theemptyWrapperClassNamesets appropriate height for the empty state, leveraging the new Table component functionality.
269-269: LGTM! Improved button styling.The lighter
border-neutral-200color 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 columnsGood use of
Prisma.JsonNullto 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 definitionsThe 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 functionalityThe
PartnerPreviewOrCountcomponent 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 managementThe
RewardsLogiccomponent 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
InlineBadgePopovercomponent are well-implemented with good UX considerations:
- Clean context pattern for state management
- Appropriate visual feedback with the
invalidprop- Smart handling of scroll events in nested scenarios
177-200: Clean input component implementation.The
InlineBadgePopoverInputcomponent is well-implemented with proper forwarding of refs and thoughtful keyboard interaction handling.
|
@CodeRabbit final review, like your life depends on it |
🧩 Analysis chainI'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 ISSUES1. Plan Capability Bypass RiskFile: // 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 ModifiersFile: modifiers: modifiers ?? Prisma.JsonNull, // No sanitization!Recommendation: Implement JSON schema validation and sanitization before database storage. 🔍 CODE QUALITY ISSUES3. Type Safety ConcernsFile: 4. Memory Leaks in Complex FormsFile: 5. Missing Error BoundariesIssue: Complex reward logic UI components lack error boundaries for graceful failure handling. ✅ POSITIVE ASPECTS
🛠 IMMEDIATE ACTION ITEMS
📊 OVERALL ASSESSMENTThis 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) 🏁 Script executed: Length of output: 86552 🏁 Script executed: Length of output: 13486 🏁 Script executed: Length of output: 4067 @steven-tey Based on my exhaustive analysis of this reward logic implementation, here's my final comprehensive review: 🔴 CRITICAL SECURITY FINDINGS1. JSON Storage Validation GapFiles: // 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 ConditionRisk 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 ISSUES3. Missing Error BoundariesFiles: All UI components in // Recommended wrapper for complex components
<ErrorBoundary fallback={<RewardErrorFallback />}>
<RewardsLogic />
</ErrorBoundary>4. Performance ConcernsFiles:
5. Accessibility GapsCritical Issue: No accessibility patterns found:
✅ POSITIVE ASPECTS
📊 DETAILED SCHEMA ANALYSISThe 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:
Concern: The TODO comment in 🛠 IMMEDIATE ACTION ITEMS
🎯 OVERALL ASSESSMENTArchitecture Quality: EXCELLENT ⭐⭐⭐⭐⭐ 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 performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (3)
apps/web/lib/zod/schemas/rewards.ts (1)
67-67: Address the TODO commentThere'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 comparisonThe 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 flagsThe 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
📒 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
emptyWrapperClassNameproperty 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
UserMinuscomponent follows established patterns for icon components, uses proper TypeScript typing withSVGProps<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-minusis correctly placed alphabetically and follows the established pattern.apps/web/lib/plan-capabilities.ts (1)
14-15: Plan capability enforcement validatedI’ve confirmed that the new
canUseAdvancedRewardLogicflag is properly enforced both server- and client-side:
- apps/web/lib/actions/partners/create-reward.ts
• Throws if!getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogicbefore 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 viagetPlanCapabilities(plan).canUseAdvancedRewardLogic(falls back to upgrade modal)As long as
workspace.planis 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
emptyWrapperClassNameprop 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 modifiersRaw modifiers are stored as
Prisma.JsonValuewithout 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 modifiersSame 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 hookThe
useRewardsUpgradeModalhook 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 safetyThe 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 validationReplacing
z.any().nullish()withrewardConditionsArraySchema.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 componentThe
PartnerPreviewOrCountcomponent 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 considerationsThe
InlineBadgePopovercomponent 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 featuresThe
InlineBadgePopoverMenucomponent 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 enforcementThe 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 definitionsThe 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
BothcreateRewardActionandupdateRewardActionimportgetPlanCapabilitiesand immediately throw ifworkspace.planlackscanUseAdvancedRewardLogic, before any modifiers are processed. No further action needed.
| if ( | ||
| modifiers && | ||
| !getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic | ||
| ) | ||
| throw new Error( | ||
| "Advanced reward structures are only available on the Advanced plan and above.", | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| if ( | ||
| modifiers && | ||
| !getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic | ||
| ) | ||
| throw new Error( | ||
| "Advanced reward structures are only available on the Advanced plan and above.", | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
Summary by CodeRabbit
New Features
Enhancements
Refactor
Chores