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

Skip to content

Conversation

@steven-tey
Copy link
Collaborator

@steven-tey steven-tey commented Oct 5, 2025

Summary by CodeRabbit

  • New Features

    • Import now supports selecting and importing multiple Rewardful campaigns; UI shows a two-step flow with toggle for “all” vs “selected”, searchable/sortable campaign selection, and import counts/validation.
    • New shared scrollable container with a subtle bottom fade for improved list browsing.
  • Refactor

    • Import flows (partners, customers, commissions, etc.) and payloads updated to handle multiple campaign selections and map affiliates to their correct groups for consistent imports.

@vercel
Copy link
Contributor

vercel bot commented Oct 5, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Oct 5, 2025 8:51pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 5, 2025

Walkthrough

Converts Rewardful imports from a single-campaign flow to multi-campaign: replaces importCampaign with importCampaigns, changes payloads/schemas to use campaignIds[], updates importers (partners/customers/commissions), adjusts API and UI modal for multi-select, and adds a ScrollContainer component.

Changes

Cohort / File(s) Summary of changes
API route action switch
apps/web/app/(ee)/api/cron/import/rewardful/route.ts
Replaces action "import-campaign" with "import-campaigns" and calls importCampaigns(payload).
Start import action input
apps/web/lib/actions/partners/start-rewardful-import.ts
Replaces campaignId (string) with campaignIds (string[]), updates parsed input, and queues action "import-campaigns".
Rewardful API adjustments
apps/web/lib/rewardful/api.ts
Removes retrieveCampaign; updates listPartners signature to drop campaignId and add expand[]=campaign to queries.
Single-campaign importer removal
apps/web/lib/rewardful/import-campaign.ts
Deletes the single-campaign importer implementation and its export.
Multi-campaign importer added
apps/web/lib/rewardful/import-campaigns.ts
Adds importCampaigns(payload): lists campaigns, filters by campaignIds, upserts partner groups, creates sale rewards if missing, updates program defaults for default campaigns, and queues import-partners.
Commissions import update
apps/web/lib/rewardful/import-commissions.ts
Switches to campaignIds array everywhere; updates createCommission signature and validation/logging to handle multiple campaigns.
Customers import update
apps/web/lib/rewardful/import-customers.ts
Switches to campaignIds array, updates createCustomer parameter and filtering logic, and adjusts logs.
Partners import refactor
apps/web/lib/rewardful/import-partners.ts
Replaces single-default-group logic with a per-campaign group mapping; filters affiliates by campaignIds; validates group existence per affiliate; removes DEFAULT_PARTNER_GROUP usage; updates partner creation to use mapped group fields.
Schemas and payload shape
apps/web/lib/rewardful/schemas.ts
Replaces enum value "import-campaign" with "import-campaigns"; removes groupId and campaignId from payload schema and adds campaignIds: string[].
Types updates
apps/web/lib/rewardful/types.ts
Adds default: boolean to RewardfulCampaign; changes RewardfulAffiliate.campaign from optional to required.
Import modal UX overhaul
apps/web/ui/modals/import-rewardful-modal.tsx
Replaces single selection with selectedCampaignIds multi-select, adds "all vs select" mode, searchable list, step flow changes, and validation for multi-campaign import.
Partners group multi-select UI
apps/web/ui/partners/groups/groups-multi-select.tsx
Replaces custom Scroll wrapper with new ScrollContainer and removes scroll-progress logic/useScrollProgress usage in this component.
UI package export
packages/ui/src/index.tsx
Re-exports ScrollContainer in the UI package public exports.
New UI component
packages/ui/src/scroll-container.tsx
Adds ScrollContainer component with scroll tracking and bottom gradient fade driven by useScrollProgress.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Admin as Admin User
  participant Modal as ImportRewardfulModal
  participant Actions as start-rewardful-import
  participant Queue as Cron Job Queue (/api/cron/import/rewardful)
  participant Importer as rewardfulImporter
  participant RWAPI as RewardfulApi
  participant DB as Prisma

  Admin->>Modal: open modal & select campaigns
  Modal->>Actions: call start-rewardful-import (action: import-campaigns, campaignIds)
  Actions->>Queue: enqueue job (import-campaigns, campaignIds)
  Queue->>Importer: process job -> importCampaigns(payload)
  Importer->>RWAPI: list campaigns
  RWAPI-->>Importer: campaigns[]
  Importer->>Importer: filter campaigns by campaignIds
  loop per campaign
    Importer->>DB: upsert partner group
    DB-->>Importer: group upserted
    alt no saleRewardId
      Importer->>DB: create sale reward
      DB-->>Importer: reward created
    end
    alt campaign.default == true
      Importer->>DB: update program payout defaults
      DB-->>Importer: program updated
    end
  end
  Importer->>Importer: queue import-partners (campaignIds)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • devkiran
  • TWilson023

Poem

A rabbit taps keys with delighted thumps,
Bundling campaigns in joyful clumps.
Groups pop up, rewards align,
Partners hop in a tidy line.
Scrolls now fade, imports take flight—carrots queued, everything’s right. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely describes the primary change in this pull request—enhancing the Rewardful importer to support importing multiple campaigns—and directly aligns with the code modifications that transition from single-campaign to multi-campaign support.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch improve-rewardful

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@steven-tey
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 5, 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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/lib/rewardful/import-partners.ts (1)

62-115: Stop logging every non-selected affiliate as an error

Because listPartners now pulls every affiliate across all campaigns, the else branch fires for affiliates that simply aren’t in the requested campaignIds, and we record an INACTIVE_PARTNER error for each of them. That regresses the previous behaviour (where the API filtered by campaign) and will flood the importer error log with false failures whenever someone imports only a subset of campaigns.

Filter down to the selected campaign IDs before splitting into “active” versus “skipped” affiliates so we only log genuine in-campaign skips, and update the message accordingly.

-    const activeAffiliates: typeof affiliates = [];
-    const notImportedAffiliates: typeof affiliates = [];
-
-    for (const affiliate of affiliates) {
-      if (
-        affiliate.state === "active" &&
-        affiliate.leads > 0 &&
-        campaignIds.includes(affiliate.campaign.id)
-      ) {
-        activeAffiliates.push(affiliate);
-      } else {
-        notImportedAffiliates.push(affiliate);
-      }
-    }
+    const affiliatesForSelectedCampaigns = affiliates.filter((affiliate) =>
+      campaignIds.includes(affiliate.campaign.id),
+    );
+
+    const activeAffiliates = affiliatesForSelectedCampaigns.filter(
+      (affiliate) => affiliate.state === "active" && affiliate.leads > 0,
+    );
+
+    const notImportedAffiliates = affiliatesForSelectedCampaigns.filter(
+      (affiliate) => affiliate.state !== "active" || affiliate.leads <= 0,
+    );
@@
-          message: `Partner ${affiliate.email} not imported because it is not active or has no leads or is not in selected campaignIds (${campaignIds.join(", ")}).`,
+          message: `Partner ${affiliate.email} not imported because it is not active or has no leads.`,
apps/web/lib/rewardful/import-commissions.ts (1)

29-85: Guard against missing campaignIds in legacy payloads.

Line 29 introduces campaignIds from the payload, and Line 159 immediately calls campaignIds.includes(...). Jobs queued before this deploy (or any fallback caller that still sends the single campaignId payload) will arrive without campaignIds; at runtime that turns into undefined.includes(...), throwing and killing the whole import batch. Please make the filter tolerant of campaignIds being absent by defaulting to “import everything” unless the array is present, e.g. only run the includes check when campaignIds?.length is truthy and type the argument accordingly.

-        createCommission({
+        createCommission({
           commission,
           program,
-          campaignIds,
+          campaignIds,
-async function createCommission({
-  commission,
-  program,
-  campaignIds,
+async function createCommission({
+  commission,
+  program,
+  campaignIds = [],
-  campaignIds: string[];
+  campaignIds?: string[];
-  if (commission.campaign.id && !campaignIds.includes(commission.campaign.id)) {
+  if (
+    campaignIds.length &&
+    commission.campaign.id &&
+    !campaignIds.includes(commission.campaign.id)
+  ) {

Also applies to: 135-165

🧹 Nitpick comments (1)
packages/ui/src/scroll-container.tsx (1)

5-32: Consider making dimensions and fade color configurable.

The component has several hard-coded values that limit its reusability:

  1. Hard-coded height (h-[190px] at line 17): Different use cases may need different heights
  2. Full viewport width on mobile (w-screen at line 17): This could cause layout issues if the component is nested within a container with padding or constraints
  3. Hard-coded white gradient (from-white at line 27): This won't work with dark mode or non-white backgrounds

Consider accepting height, width, and fadeColor props to make the component more flexible, or allow these styles to be fully controlled via the className prop.

Example refactor to accept a height prop:

 export function ScrollContainer({
   children,
   className,
-}: PropsWithChildren<{ className?: string }>) {
+  height = "190px",
+}: PropsWithChildren<{ className?: string; height?: string }>) {
   const ref = useRef<HTMLDivElement>(null);

   const { scrollProgress, updateScrollProgress } = useScrollProgress(ref);

   return (
     <div className="relative">
       <div
         className={cn(
-          "scrollbar-hide h-[190px] w-screen overflow-y-scroll sm:w-auto",
+          "scrollbar-hide overflow-y-scroll w-screen sm:w-auto",
           className,
         )}
+        style={{ height }}
         ref={ref}
         onScroll={updateScrollProgress}
       >

For the fade color, consider using CSS variables or accepting a prop for dark mode support.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44a5b0a and 0f2dbea.

📒 Files selected for processing (14)
  • apps/web/app/(ee)/api/cron/import/rewardful/route.ts (2 hunks)
  • apps/web/lib/actions/partners/start-rewardful-import.ts (2 hunks)
  • apps/web/lib/rewardful/api.ts (1 hunks)
  • apps/web/lib/rewardful/import-campaign.ts (0 hunks)
  • apps/web/lib/rewardful/import-campaigns.ts (1 hunks)
  • apps/web/lib/rewardful/import-commissions.ts (4 hunks)
  • apps/web/lib/rewardful/import-customers.ts (3 hunks)
  • apps/web/lib/rewardful/import-partners.ts (4 hunks)
  • apps/web/lib/rewardful/schemas.ts (2 hunks)
  • apps/web/lib/rewardful/types.ts (2 hunks)
  • apps/web/ui/modals/import-rewardful-modal.tsx (10 hunks)
  • apps/web/ui/partners/groups/groups-multi-select.tsx (3 hunks)
  • packages/ui/src/index.tsx (1 hunks)
  • packages/ui/src/scroll-container.tsx (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/web/lib/rewardful/import-campaign.ts
🧰 Additional context used
🧬 Code graph analysis (8)
apps/web/lib/rewardful/import-campaigns.ts (4)
apps/web/lib/rewardful/types.ts (1)
  • RewardfulImportPayload (103-105)
apps/web/lib/rewardful/api.ts (1)
  • RewardfulApi (20-96)
apps/web/lib/api/create-id.ts (1)
  • createId (65-70)
apps/web/lib/rewardful/import-campaign.ts (1)
  • importCampaign (11-125)
apps/web/ui/partners/groups/groups-multi-select.tsx (1)
packages/ui/src/scroll-container.tsx (1)
  • ScrollContainer (5-32)
apps/web/lib/rewardful/import-commissions.ts (1)
apps/web/lib/rewardful/types.ts (1)
  • RewardfulCommission (89-101)
apps/web/app/(ee)/api/cron/import/rewardful/route.ts (1)
apps/web/lib/rewardful/import-campaigns.ts (1)
  • importCampaigns (11-118)
apps/web/lib/rewardful/import-customers.ts (1)
apps/web/lib/rewardful/types.ts (1)
  • RewardfulReferral (58-70)
apps/web/ui/modals/import-rewardful-modal.tsx (3)
apps/web/lib/rewardful/types.ts (1)
  • RewardfulCampaign (11-25)
packages/ui/src/toggle-group.tsx (1)
  • ToggleGroup (15-83)
packages/ui/src/scroll-container.tsx (1)
  • ScrollContainer (5-32)
packages/ui/src/scroll-container.tsx (1)
packages/ui/src/hooks/use-scroll-progress.ts (1)
  • useScrollProgress (6-37)
apps/web/lib/actions/partners/start-rewardful-import.ts (1)
apps/web/lib/actions/safe-action.ts (1)
  • authActionClient (33-82)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (3)
packages/ui/src/index.tsx (1)

34-34: LGTM!

The export follows the established pattern and is correctly positioned alphabetically.

apps/web/ui/partners/groups/groups-multi-select.tsx (1)

10-10: LGTM! Clean refactor to shared component.

The transition from the custom Scroll component to the shared ScrollContainer from @dub/ui is well-executed. The import cleanup correctly removes now-unused dependencies (PropsWithChildren, useRef), and the ScrollContainer properly wraps the Command.List while maintaining the existing list behavior.

Also applies to: 16-16, 156-232

packages/ui/src/scroll-container.tsx (1)

1-3: Remove incorrect syntax error concern: All instances of "vertical" in use-scroll-progress.ts use matching quotes, so there’s no compilation issue.

Likely an incorrect or invalid review comment.

@steven-tey steven-tey merged commit 72213ed into main Oct 5, 2025
9 checks passed
@steven-tey steven-tey deleted the improve-rewardful branch October 5, 2025 20:56
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: 4

🧹 Nitpick comments (2)
packages/ui/src/scroll-container.tsx (1)

5-32: Consider making height configurable.

The component works well, but the hardcoded height of 190px (line 17) may not suit all use cases. Consider accepting an optional height prop to allow consumers to override this value when needed.

Example enhancement:

 export function ScrollContainer({
   children,
   className,
+  height = "h-[190px]",
 }: PropsWithChildren<{ className?: string }>) {
+}: PropsWithChildren<{ className?: string; height?: string }>) {
   const ref = useRef<HTMLDivElement>(null);

   const { scrollProgress, updateScrollProgress } = useScrollProgress(ref);

   return (
     <div className="relative">
       <div
         className={cn(
-          "scrollbar-hide h-[190px] w-screen overflow-y-scroll sm:w-auto",
+          "scrollbar-hide w-screen overflow-y-scroll sm:w-auto",
+          height,
           className,
         )}
apps/web/lib/rewardful/import-campaigns.ts (1)

83-90: Consider explicit reward_type handling.

Lines 83-86 map reward_type with an implicit fallback to RewardStructure.percentage for non-"amount" values. While this works for a binary choice, explicitly handling expected values ("amount" and "percentage") with a fallback or error for unexpected values would improve robustness.

Apply this diff for more explicit handling:

           type:
-            reward_type === "amount"
-              ? RewardStructure.flat
-              : RewardStructure.percentage,
+            reward_type === "amount" 
+              ? RewardStructure.flat 
+              : reward_type === "percentage" 
+                ? RewardStructure.percentage 
+                : (() => {
+                    console.warn(`Unknown reward_type: ${reward_type}, defaulting to percentage`);
+                    return RewardStructure.percentage;
+                  })(),

Alternatively, use a mapping object:

+          const rewardTypeMap = {
+            amount: RewardStructure.flat,
+            percentage: RewardStructure.percentage,
+          } as const;
           type:
-            reward_type === "amount"
-              ? RewardStructure.flat
-              : RewardStructure.percentage,
+            rewardTypeMap[reward_type as keyof typeof rewardTypeMap] ?? RewardStructure.percentage,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44a5b0a and 0f2dbea.

📒 Files selected for processing (14)
  • apps/web/app/(ee)/api/cron/import/rewardful/route.ts (2 hunks)
  • apps/web/lib/actions/partners/start-rewardful-import.ts (2 hunks)
  • apps/web/lib/rewardful/api.ts (1 hunks)
  • apps/web/lib/rewardful/import-campaign.ts (0 hunks)
  • apps/web/lib/rewardful/import-campaigns.ts (1 hunks)
  • apps/web/lib/rewardful/import-commissions.ts (4 hunks)
  • apps/web/lib/rewardful/import-customers.ts (3 hunks)
  • apps/web/lib/rewardful/import-partners.ts (4 hunks)
  • apps/web/lib/rewardful/schemas.ts (2 hunks)
  • apps/web/lib/rewardful/types.ts (2 hunks)
  • apps/web/ui/modals/import-rewardful-modal.tsx (10 hunks)
  • apps/web/ui/partners/groups/groups-multi-select.tsx (3 hunks)
  • packages/ui/src/index.tsx (1 hunks)
  • packages/ui/src/scroll-container.tsx (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/web/lib/rewardful/import-campaign.ts
🔇 Additional comments (9)
apps/web/lib/rewardful/import-customers.ts (1)

15-15: LGTM! Clean conversion from single to multi-campaign.

The transition from campaignId to campaignIds is correctly implemented: payload destructuring, parameter typing, filtering logic using includes(), and logging with join() are all appropriate.

Also applies to: 50-50, 72-72, 78-78, 84-87

apps/web/ui/partners/groups/groups-multi-select.tsx (1)

10-10: LGTM! Clean refactor to centralized ScrollContainer.

The transition from custom Scroll wrapper to ScrollContainer simplifies the code while preserving the scrolling behavior and loading/empty states. The tightened React imports and removal of useScrollProgress are appropriate since that logic is now encapsulated in ScrollContainer.

Also applies to: 16-16, 156-232

apps/web/lib/rewardful/api.ts (1)

54-66: LGTM! API simplified for multi-campaign support.

Removing the campaignId parameter and adding expand[] for campaign makes sense for the multi-campaign flow, where filtering is now handled downstream in importPartners based on campaignIds. This simplifies the API surface.

apps/web/lib/rewardful/import-commissions.ts (1)

29-29: LGTM! Consistent multi-campaign conversion.

The transition to campaignIds array is correctly implemented throughout the commission import flow, matching the pattern used in other import modules.

Also applies to: 79-79, 137-137, 145-145, 159-161

apps/web/app/(ee)/api/cron/import/rewardful/route.ts (1)

3-3: LGTM! Route updated for multi-campaign flow.

The import binding, action string, and function call are all correctly updated to use the new importCampaigns module.

Also applies to: 20-21

apps/web/lib/rewardful/import-partners.ts (2)

12-12: LGTM! Multi-campaign filtering correctly implemented.

The conversion to campaignIds-based filtering is correct, and the three-condition check (active state, has leads, campaign in list) properly restricts import scope. The updated logging message accurately reflects the multi-campaign context.

Also applies to: 50-52, 63-67, 113-113


27-32: Slug prefix assumption validated. Verified in import-campaigns.ts that all Rewardful groups are upserted with slug = \rewardful-${campaignId}`, so the mapping in import-partners.ts` is safe.

apps/web/lib/rewardful/import-campaigns.ts (2)

25-27: LGTM! Campaign filtering and group creation are correct.

The filtering logic correctly restricts import to selected campaigns, and the group creation with slug: "rewardful-{campaignId}" format matches the expectation in import-partners.ts (line 29-30), ensuring proper group mapping.

Also applies to: 40-56


98-111: Null-check unnecessary: both fields are non‐nullable. Rewardful TS types (number) and Prisma schema (Int non-nullable) guarantee no null or undefined.

Comment on lines +12 to 15
campaignIds: z
.array(z.string())
.describe("Rewardful campaign IDs to import."),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Harden action input: enforce at least one campaign ID

startRewardfulImportAction used to require a campaign via parsedInput.campaignId. With the array form, we should keep that invariant server-side by adding .min(1) to the schema. That way any misuse (tests, future callers, or UI regressions) is caught before queuing an import job.

campaignIds: z
  .array(z.string())
  .min(1, "Select at least one campaign")
  .describe("Rewardful campaign IDs to import."),
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/start-rewardful-import.ts around lines 12 to
15, the campaignIds schema currently allows an empty array; update the Zod
schema to require at least one campaign by adding .min(1, "Select at least one
campaign") to the array chain so the server enforces that callers provide one or
more campaign IDs and validation fails early.

Comment on lines +70 to +96
if (!createdGroup.saleRewardId) {
const createdReward = await prisma.reward.create({
data: {
id: createId({ prefix: "rw_" }),
programId,
// connect the reward to the group
salePartnerGroup: {
connect: {
id: createdGroup.id,
},
},
event: EventType.sale,
maxDuration: max_commission_period_months,
type:
reward_type === "amount"
? RewardStructure.flat
: RewardStructure.percentage,
amount:
reward_type === "amount"
? commission_amount_cents
: commission_percent,
},
});
console.log(
`Since group was newly created, also created reward ${createdReward.id} with amount ${createdReward.amount} and type ${createdReward.type}`,
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify null handling for reward amounts.

Lines 88-90 assign amount based on reward_type, but don't explicitly handle cases where commission_amount_cents or commission_percent might be null or undefined. While Prisma may enforce non-null constraints, it's safer to validate or provide defaults.

Run the following script to check the Rewardful API response schema and Prisma schema:


🏁 Script executed:

#!/bin/bash
# Description: Verify nullability of commission fields in types and schema

# Check Rewardful type definitions
rg -nP --type=ts -A5 'interface.*RewardfulCampaign' 

# Check if these fields are nullable
rg -nP --type=ts 'commission_amount_cents|commission_percent' 

# Check Prisma schema for Reward.amount nullability
fd -e prisma -x cat {} | grep -A10 "model Reward"

Length of output: 1824


Add runtime checks for commission values
Ensure commission_amount_cents and commission_percent are not null/undefined (Prisma amount is non-nullable) by adding validation or defaulting to 0 before prisma.reward.create.

Comment on lines +14 to 15
campaignIds: z.array(z.string()),
action: rewardfulImportSteps,
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Keep server-side invariant: require at least one campaign ID

The old schema guaranteed a campaign was provided (single campaignId). After switching to campaignIds, the schema now accepts empty arrays. That lets clients queue an import with zero campaigns, which is a regression in server-side validation even if the UI blocks it. Please add .min(1, "Select at least one campaign") so the action continues to reject empty selections.

🤖 Prompt for AI Agents
In apps/web/lib/rewardful/schemas.ts around lines 14 to 15, the campaignIds
array schema currently allows empty arrays which regresses the previous
server-side invariant that at least one campaign must be provided; update the
z.array(z.string()) definition to enforce a minimum length by adding .min(1,
"Select at least one campaign") so the schema rejects empty selections and
preserves server-side validation; ensure the error message matches the reviewer
text and keep the rest of the schema unchanged.

Comment on lines +426 to +443
return (
<Command.Item
key={campaign.id}
value={campaign.name}
onSelect={() =>
setSelectedCampaignIds(
selectedCampaignIds?.includes(campaign.id)
? selectedCampaignIds.length === 1
? null // Revert to null if there will be no campaigns selected
: selectedCampaignIds.filter(
(id) => id !== campaign.id,
)
: [
...(selectedCampaignIds ?? []),
campaign.id,
],
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent deselect-all from reverting to “import everything”

When the last selected campaign is toggled off in “Select campaigns” mode, setSelectedCampaignIds sets the state back to null. handleCampaignsSubmit treats null as “import every campaign,” so submitting the form (e.g., by pressing Enter in the search input) queues every campaign even though the user explicitly deselected all. Please keep “no selection” distinct from “all campaigns.” One fix is to leave the state as an empty array when the last item is removed and only coerce to null when the user switches back to the “All campaigns” toggle.

🤖 Prompt for AI Agents
In apps/web/ui/modals/import-rewardful-modal.tsx around lines 426 to 443, the
current toggle logic sets selectedCampaignIds to null when the last selected
campaign is removed which is later interpreted as “import all”; change the
behavior so removing the last campaign leaves selectedCampaignIds as an empty
array ([]) rather than null, and only set selectedCampaignIds to null when the
user explicitly switches the UI to the “All campaigns” mode; update the onSelect
branch to return [] for the case where selectedCampaignIds.length === 1 (instead
of null) and ensure any mode switch handler coerces [] → null only when the user
chooses “All campaigns.”

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.

2 participants