feat: Add custom reward display configuration for embed#3779
feat: Add custom reward display configuration for embed#3779tarai-dl wants to merge 1 commit intodubinc:mainfrom
Conversation
Implement rewardDisplay option in program embed configuration to allow programs to customize how reward amounts are rendered in the embed. Closes dubinc#3775
|
@tarai-dl is attempting to deploy a commit to the Dub Team on Vercel. A member of the Team first needs to authorize it. |
|
|
📝 WalkthroughWalkthroughThe changes introduce a customizable reward display system for referral embeds. A new Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 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 |
There was a problem hiding this comment.
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/embed/referrals/bounties/performance-section.tsx (1)
412-484:⚠️ Potential issue | 🟠 MajorMissing
rewardDisplayOptionsincolumnsmemo dependencies (stale-closure risk).The
columnsuseMemoat lines 412-484 capturesrewardDisplayOptionsinside theamountcell renderer (line 479) but only lists[isCurrency]as a dependency. IfprogramEmbedData.rewardDisplaychanges after mount (e.g., config hot-reload, SWR refresh of embed data, or provider re-render with new options), the cell will keep formatting with the stale options untilisCurrencyflips.Same issue in
EmbedBountyCommissionsTableat line 659 ([]deps, usesrewardDisplayOptionsat line 656).Proposed fix
- }, [isCurrency]); + }, [isCurrency, rewardDisplayOptions]);- [], + [rewardDisplayOptions],Additionally, note the
EmbedBountyEventsTable"Amount" column rendersrow.original.amountwhich istotalSaleAmount(revenue) — see the cross-file semantic note onearnings.tsx.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(ee)/app.dub.co/embed/referrals/bounties/performance-section.tsx around lines 412 - 484, The useMemo that builds columns (the columns variable in performance-section.tsx) captures rewardDisplayOptions inside the "amount" cell via rewardFormatter but only lists [isCurrency] as dependencies, causing stale closures; update the memo dependency array to include rewardDisplayOptions (or the specific value like programEmbedData.rewardDisplay) so the columns regenerate when display options change, and apply the same fix to EmbedBountyCommissionsTable where its columns memo currently has [] deps but uses rewardDisplayOptions; ensure any cell renderers that call rewardFormatter reference the up-to-date prop in the dependency list so formatting updates correctly.apps/web/app/(ee)/app.dub.co/embed/referrals/page-client.tsx (1)
159-230:⚠️ Potential issue | 🟠 Major
programEmbedDataidentity churns every render, defeating theembedDatamemo.
programEmbedSchema.parse(program.embedData)at line 159 produces a new object on every render. Since it's now both a member of the memoizedembedDatavalue (line 214) and a dependency of theuseMemo(line 228), the memo is effectively a no-op —embedDatagets a new reference each render, which in turn re-renders every consumer ofReferralsEmbedDataContext(activity, earnings, leaderboard, links-list, performance-section, etc.).Memoize the parse:
Proposed fix
- const programEmbedData = programEmbedSchema.parse(program.embedData); + const programEmbedData = useMemo( + () => programEmbedSchema.parse(program.embedData), + [program.embedData], + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(ee)/app.dub.co/embed/referrals/page-client.tsx around lines 159 - 230, programEmbedSchema.parse(program.embedData) is creating a new object each render and busting the embedData memo; replace the direct parse with a memoized value (e.g. const programEmbedData = useMemo(() => programEmbedSchema.parse(program.embedData), [program.embedData])) so programEmbedData identity is stable across renders and embedData useMemo actually caches; update imports if needed and keep programEmbedData in the embedData dependency list.
♻️ Duplicate comments (2)
apps/web/app/(ee)/app.dub.co/embed/referrals/links-list.tsx (1)
124-129:⚠️ Potential issue | 🟡 MinorSales column uses
saleAmount(revenue), not rewards — see cross-file note inearnings.tsx.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(ee)/app.dub.co/embed/referrals/links-list.tsx around lines 124 - 129, The "sales" column is formatting revenue (row.original.saleAmount) as a reward; update the column to use the actual reward value and label: replace row.original.saleAmount with the reward field (e.g., row.original.rewardAmount or row.original.reward) passed into rewardFormatter, and adjust the column id/header from "sales"/"Sales" to "rewards"/"Rewards" if the column should represent rewards rather than revenue (see rewardFormatter usage and earnings.tsx for the correct reward property name).apps/web/app/(ee)/app.dub.co/embed/referrals/activity.tsx (1)
81-85:⚠️ Potential issue | 🟡 Minor
subValueissaleAmount(revenue), not a reward — see cross-file note inearnings.tsx.Flagging for consistency; root-cause comment is on
earnings.tsxline 69-82.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(ee)/app.dub.co/embed/referrals/activity.tsx around lines 81 - 85, subValue in activity.tsx is actually saleAmount (revenue) and should not be formatted with rewardFormatter/rewardDisplayOptions; replace the call to rewardFormatter(subValue, rewardDisplayOptions) with the revenue/currency formatter used in earnings.tsx (or import the shared saleAmount/currency formatter) and update the variable/prop name to saleAmount where possible to avoid confusion (ensure you import the correct formatter function from the same module/utility used in earnings.tsx and remove rewardDisplayOptions usage).
🧹 Nitpick comments (2)
packages/utils/src/functions/reward-formatter.ts (2)
23-26: Currency mode silently dropsprefix/suffix/compact/divisor.If a caller sets
mode: "currency"together with any ofprefix,suffix,compact, ordivisor, those fields are silently ignored. This is a reasonable design choice (currency formatting owns its own symbol/locale), but it can surprise users configuringrewardDisplayin the embed admin UI (e.g. setting a prefix then switching mode back to currency). Consider either:
- documenting in the
RewardDisplayOptionsJSDoc thatprefix/suffix/compact/divisoronly apply in"custom"mode, or- honoring
compactin currency mode too viacurrencyFormatter(valueInCents, { notation: "compact" })if that's desired.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/utils/src/functions/reward-formatter.ts` around lines 23 - 26, Update the API docs to make the behavior explicit: in RewardDisplayOptions JSDoc (in packages/utils/src/functions/reward-formatter.ts) add a sentence that prefix, suffix, compact, and divisor only apply when mode === "custom" and are ignored when options.mode === "currency"; leave the existing early-return that calls currencyFormatter(valueInCents) unchanged (i.e., keep the check of options.mode === "currency" and the call to currencyFormatter) so callers know the fields are intentionally ignored in currency mode.
29-31: Minor: bigint → Number coercion can silently lose precision, and unvalidateddivisorcan blow up.Two small robustness points for the custom-mode path:
Number(valueInCents)for abigintlarger thanNumber.MAX_SAFE_INTEGERwill silently lose precision. In practice reward amounts in cents won't hit 2^53, so this is low risk, but worth a comment.- This utility is exported and typed as
divisor?: numberon the public interface — it's only the zod schema (.positive()) that guarantees non-zero/positive. A TS caller passing{ divisor: 0 }or a negative value will produceInfinity/NaN/negative output. A cheap guard keeps the utility safe independently of the schema:🛡️ Proposed guard
- const cents = typeof valueInCents === "bigint" ? Number(valueInCents) : valueInCents; - const divisor = options.divisor ?? 100; - const value = cents / divisor; + const cents = + typeof valueInCents === "bigint" ? Number(valueInCents) : valueInCents; + const divisor = + options.divisor && options.divisor > 0 ? options.divisor : 100; + const value = cents / divisor;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/utils/src/functions/reward-formatter.ts` around lines 29 - 31, The custom-mode path currently coerces bigint with Number(valueInCents) (risking silent precision loss) and trusts options.divisor (which can be 0 or negative). In reward-formatter.ts around the valueInCents/divisor logic, add a small guard: if valueInCents is a bigint, check against Number.MAX_SAFE_INTEGER and either handle via BigInt arithmetic or throw/assert with a clear message (or at least add a comment explaining the safe-assumption), and validate options.divisor to ensure it's a finite positive non-zero number (fallback to 100 or throw) before dividing; update the variables named valueInCents, cents, divisor, and value accordingly so the function is robust regardless of zod validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In
`@apps/web/app/`(ee)/app.dub.co/embed/referrals/bounties/performance-section.tsx:
- Around line 412-484: The useMemo that builds columns (the columns variable in
performance-section.tsx) captures rewardDisplayOptions inside the "amount" cell
via rewardFormatter but only lists [isCurrency] as dependencies, causing stale
closures; update the memo dependency array to include rewardDisplayOptions (or
the specific value like programEmbedData.rewardDisplay) so the columns
regenerate when display options change, and apply the same fix to
EmbedBountyCommissionsTable where its columns memo currently has [] deps but
uses rewardDisplayOptions; ensure any cell renderers that call rewardFormatter
reference the up-to-date prop in the dependency list so formatting updates
correctly.
In `@apps/web/app/`(ee)/app.dub.co/embed/referrals/page-client.tsx:
- Around line 159-230: programEmbedSchema.parse(program.embedData) is creating a
new object each render and busting the embedData memo; replace the direct parse
with a memoized value (e.g. const programEmbedData = useMemo(() =>
programEmbedSchema.parse(program.embedData), [program.embedData])) so
programEmbedData identity is stable across renders and embedData useMemo
actually caches; update imports if needed and keep programEmbedData in the
embedData dependency list.
---
Duplicate comments:
In `@apps/web/app/`(ee)/app.dub.co/embed/referrals/activity.tsx:
- Around line 81-85: subValue in activity.tsx is actually saleAmount (revenue)
and should not be formatted with rewardFormatter/rewardDisplayOptions; replace
the call to rewardFormatter(subValue, rewardDisplayOptions) with the
revenue/currency formatter used in earnings.tsx (or import the shared
saleAmount/currency formatter) and update the variable/prop name to saleAmount
where possible to avoid confusion (ensure you import the correct formatter
function from the same module/utility used in earnings.tsx and remove
rewardDisplayOptions usage).
In `@apps/web/app/`(ee)/app.dub.co/embed/referrals/links-list.tsx:
- Around line 124-129: The "sales" column is formatting revenue
(row.original.saleAmount) as a reward; update the column to use the actual
reward value and label: replace row.original.saleAmount with the reward field
(e.g., row.original.rewardAmount or row.original.reward) passed into
rewardFormatter, and adjust the column id/header from "sales"/"Sales" to
"rewards"/"Rewards" if the column should represent rewards rather than revenue
(see rewardFormatter usage and earnings.tsx for the correct reward property
name).
---
Nitpick comments:
In `@packages/utils/src/functions/reward-formatter.ts`:
- Around line 23-26: Update the API docs to make the behavior explicit: in
RewardDisplayOptions JSDoc (in packages/utils/src/functions/reward-formatter.ts)
add a sentence that prefix, suffix, compact, and divisor only apply when mode
=== "custom" and are ignored when options.mode === "currency"; leave the
existing early-return that calls currencyFormatter(valueInCents) unchanged
(i.e., keep the check of options.mode === "currency" and the call to
currencyFormatter) so callers know the fields are intentionally ignored in
currency mode.
- Around line 29-31: The custom-mode path currently coerces bigint with
Number(valueInCents) (risking silent precision loss) and trusts options.divisor
(which can be 0 or negative). In reward-formatter.ts around the
valueInCents/divisor logic, add a small guard: if valueInCents is a bigint,
check against Number.MAX_SAFE_INTEGER and either handle via BigInt arithmetic or
throw/assert with a clear message (or at least add a comment explaining the
safe-assumption), and validate options.divisor to ensure it's a finite positive
non-zero number (fallback to 100 or throw) before dividing; update the variables
named valueInCents, cents, divisor, and value accordingly so the function is
robust regardless of zod validation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0da72d2a-b440-4a09-8215-06584339128c
📒 Files selected for processing (10)
apps/web/app/(ee)/app.dub.co/embed/referrals/activity.tsxapps/web/app/(ee)/app.dub.co/embed/referrals/bounties/performance-section.tsxapps/web/app/(ee)/app.dub.co/embed/referrals/earnings-summary.tsxapps/web/app/(ee)/app.dub.co/embed/referrals/earnings.tsxapps/web/app/(ee)/app.dub.co/embed/referrals/leaderboard.tsxapps/web/app/(ee)/app.dub.co/embed/referrals/links-list.tsxapps/web/app/(ee)/app.dub.co/embed/referrals/page-client.tsxapps/web/lib/zod/schemas/program-embed.tspackages/utils/src/functions/index.tspackages/utils/src/functions/reward-formatter.ts
Summary
This PR adds a
rewardDisplayoption to the program embed configuration, allowing programs to customize how reward amounts are rendered in the embed UI.Changes
Added
rewardDisplayconfiguration toprogramEmbedSchemawith options:mode: "currency" (default) | "custom"prefix: string (e.g., "")suffix: string (e.g., " credits")divisor: number (default: 100, for cents to units conversion)compact: boolean (e.g., "22k" vs "22000")Created
rewardFormatterutility function that respects the display configurationUpdated all embed components to use
rewardFormatterinstead ofcurrencyFormatter:Usage Example
{ "rewardDisplay": { "mode": "custom", "prefix": "", "suffix": " credits", "divisor": 100, "compact": true } }This would display
$220.00as "2.2k credits" instead.Closes #3775
Summary by CodeRabbit
New Features
Documentation
rewardDisplayconfiguration options.