Add program reward education and helper content#3421
Conversation
…tates, and helper blocks Changes: (1) Onboarding reward form - updated descriptions, "Most common" tags, payout model radio cards, dynamic labels; (2) New program reward form - similar updates with custom commission descriptions and page title change; (3) Group reward empty states - structured title + description instead of generic text; (4) Reward creation helper - per-event-type dismissible info blocks with independent localStorage persistence. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughRefactors reward configuration UI and form logic: introduces PAYOUT_MODELS and COMMISSION_STRUCTURE_DESCRIPTIONS, adds "Most common" flags, changes reward/commission selection UIs to card grids, updates duration handling (including Lifetime), adapts amount fields (cents vs percentage), removes Banner usages, and adds a dismissible RewardHelper panel. Changes
Sequence DiagramsequenceDiagram
participant User
participant RewardForm as "Reward Form UI"
participant FormState as "Form State / Hook"
participant Validator as "Validation & Submit"
User->>RewardForm: choose reward event (sale/lead/click)
RewardForm->>FormState: set defaultRewardType
FormState->>FormState: side-effect → set payoutType, adjust maxDuration
User->>RewardForm: choose commission structure (recurring/one-off)
RewardForm->>FormState: set commissionType
FormState->>FormState: side-effect → recurring => maxDuration/null, one-off => maxDuration=0
User->>RewardForm: enter amount
RewardForm->>FormState: update amount (flat vs percentage)
User->>RewardForm: submit
RewardForm->>Validator: validate
Validator->>FormState: compute final payload (amountInCents or amountInPercentage)
Validator->>User: return success / error
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
- Fixed issue that selecting lifetime deselects and defaults to one time.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
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 (1)
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx (1)
296-303:⚠️ Potential issue | 🟡 MinorRemove redundant
selectedattribute on<option>elements.In React controlled components, the
selectedattribute on<option>is ignored and causes a warning. The selection is controlled by the<select>'svalueprop, whichregister()already handles.Proposed fix
{RECURRING_MAX_DURATIONS.filter( (v) => v !== 0 && v !== 1, // filter out one-time and 1-month intervals (we only use 1-month for discounts) ).map((duration) => ( <option key={duration} value={duration} - selected={duration === 12} > {duration} {duration === 1 ? "month" : "months"} </option> ))}If you need a default value of 12 months, set it via the form's
defaultValuesinstead.
🧹 Nitpick comments (2)
apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/program/reward/form.tsx (2)
122-189: Consider extracting the card-based radio selection into a reusable component.The same card-based selection pattern (container div, label with radio, mostCommon badge) is repeated three times for reward type, commission structure, and payout model. Extracting this into a generic
RadioCardGroupcomponent would reduce ~150 lines of duplication and improve maintainability.♻️ Example component extraction
type RadioCardOption<T extends string> = { key: T; label: string; description: string; mostCommon?: boolean; }; function RadioCardGroup<T extends string>({ options, value, onChange, }: { options: readonly RadioCardOption<T>[]; value: T; onChange: (key: T) => void; }) { return ( <div className="grid grid-cols-1 gap-3 lg:grid-cols-2"> {options.map(({ key, label, description, mostCommon }) => { const isSelected = key === value; return ( <div key={key} className={cn( "flex flex-col items-center", mostCommon && "rounded-md border border-neutral-200 bg-neutral-100", )} > <label className={cn( "relative flex w-full cursor-pointer items-start gap-0.5 rounded-md border border-neutral-200 bg-white p-3 text-neutral-600 hover:bg-neutral-50", "transition-all duration-150", mostCommon && "border-transparent shadow-sm", isSelected && "border-black bg-neutral-50 text-neutral-900 ring-1 ring-black", )} > <input type="radio" value={key} className="hidden" checked={isSelected} onChange={() => onChange(key)} /> {/* ... rest of card content */} </label> {mostCommon && ( <span className="py-0.5 text-xs font-medium text-neutral-500"> Most common </span> )} </div> ); })} </div> ); }Also applies to: 206-273, 313-368
297-302: Dead code in duration text rendering.The ternary
duration === 1 ? "month" : "months"at Line 301 is unreachable since1is filtered out at Line 298. The condition will always evaluate to"months".🧹 Simplify to remove dead branch
{RECURRING_MAX_DURATIONS.filter( (v) => v !== 0 && v !== 1, // filter out one-time and 1-month intervals (we only use 1-month for discounts) ).map((duration) => ( <option key={duration} value={duration}> - {duration} {duration === 1 ? "month" : "months"} + {duration} months </option> ))}
Also cleaning up leftover items found by coderabbit
There was a problem hiding this comment.
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/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx (1)
410-415:⚠️ Potential issue | 🟠 MajorAllow resubmission after errors
hasSubmittedis never reset when the action fails, so Line 414 keeps the button disabled and blocks retries after an error. Reset it inonError(or avoid gating withhasSubmitted).🛠️ Suggested fix
const { executeAsync, isPending } = useAction(onboardProgramAction, { onSuccess: () => { router.push(`/${workspaceSlug}/program/new/partners`); mutate(); }, onError: ({ error }) => { toast.error(error.serverError); + setHasSubmitted(false); }, });apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/program/reward/form.tsx (1)
408-413:⚠️ Potential issue | 🟠 MajorAllow resubmission after errors
hasSubmittedisn’t reset on failure, so Line 412 keeps the button disabled after an error and prevents retry. Reset it inonError(or drophasSubmittedgating).🛠️ Suggested fix
const { executeAsync, isPending } = useAction(onboardProgramAction, { onSuccess: () => { continueTo("plan"); mutate(); }, onError: ({ error }) => { toast.error(error.serverError); + setHasSubmitted(false); }, });
|
@coderabbitai full review, any breaking changes |
🧩 Analysis chain
🏁 Script executed: Length of output: 3400 🏁 Script executed: Length of output: 3264 🏁 Script executed: Length of output: 5836 🏁 Script executed: Length of output: 10790 🏁 Script executed: Length of output: 1544 🏁 Script executed: Length of output: 398 🏁 Script executed: Length of output: 2594 🏁 Script executed: Length of output: 5155
Breaking Changes Analysis: ✅ None FoundChanges Reviewed:
Note on AI Summary:The AI summary incorrectly flagged duplicate definitions in Summary:This PR safely adds educational content, improves UX with "Most common" tags, fixes the lifetime selection bug, and updates default values for better onboarding—all without breaking existing functionality. ✅ ✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
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/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx (1)
410-415:⚠️ Potential issue | 🟠 MajorRe-enable submit after errors.
hasSubmittedflips to true on submit and never resets on error, so the CTA stays disabled if onboarding fails. Reset it in the error handler (or afinally) to allow retry.💡 Proposed fix (reset on error)
const { executeAsync, isPending } = useAction(onboardProgramAction, { onSuccess: () => { router.push(`/${workspaceSlug}/program/new/partners`); mutate(); }, onError: ({ error }) => { + setHasSubmitted(false); toast.error(error.serverError); }, });apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/program/reward/form.tsx (1)
408-414:⚠️ Potential issue | 🟠 MajorRe-enable submit after errors.
hasSubmittednever resets on failure, so the button stays disabled after a server error. Reset it in the error handler to allow retry.💡 Proposed fix (reset on error)
const { executeAsync, isPending } = useAction(onboardProgramAction, { onSuccess: () => { continueTo("plan"); mutate(); }, onError: ({ error }) => { + setHasSubmitted(false); toast.error(error.serverError); }, });
🧹 Nitpick comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/discounts/group-discounts.tsx (1)
40-49: Make the clickable card keyboard-accessible.The entire card is clickable but not focusable, which limits keyboard access. If you want the full card to act like a button, add role/tabIndex and keyboard handling (or drop the card-level click and rely solely on the button).
♿ Suggested accessibility tweak
<div + role="button" + tabIndex={0} className={cn( "flex cursor-pointer flex-col gap-4 rounded-lg p-6 transition-all md:flex-row md:items-center", discount && "border border-neutral-200 hover:border-neutral-300", !discount && "bg-neutral-50 hover:bg-neutral-100", )} onClick={(e) => { if (isClickOnInteractiveChild(e)) return; setIsOpen(true); }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setIsOpen(true); + } + }} >
lifetimein the onboarding flow deselects and defaults to one time.most commontags to the onboarding reward builder, and new program setup to reduce decision paralysisOnboarding
CleanShot.2026-02-04.at.13.43.45.mp4
Program creation
CleanShot.2026-02-04.at.14.53.16.mp4
Group rewards
Summary by CodeRabbit
New Features
UI/UX Improvements