Skip to content

refactor(affiliate-dashboard): migrate to Chakra UI + React Query#12287

Merged
kaladinlight merged 4 commits intodevelopfrom
feat/affiliate-dashboard-chakra-refactor
Apr 20, 2026
Merged

refactor(affiliate-dashboard): migrate to Chakra UI + React Query#12287
kaladinlight merged 4 commits intodevelopfrom
feat/affiliate-dashboard-chakra-refactor

Conversation

@kaladinlight
Copy link
Copy Markdown
Contributor

@kaladinlight kaladinlight commented Apr 20, 2026

Description

Refactor of the affiliate dashboard — foundational cleanup ahead of the new affiliate swaps API shape, period selector, and polish that follow on separate branches.

  • Split the 1270-line App.tsx into components/, hooks/, and lib/ folders
  • Migrate inline styles to Chakra UI with the shapeshift theme palette
  • Replace custom useState/useEffect fetchers with React Query (useQuery + useMutation)
  • Add Zod validation on all API responses with a shared parseResponse helper
  • Eliminate period-change flicker via stale-while-revalidate (keepPreviousData)
  • Make the dashboard responsive with a mobile-card fallback for the swaps table
  • Strip workspace-shared deps from package.json (hoisted from root)

No backend changes — zero DB migrations, zero API changes. Dashboard is a standalone package under packages/affiliate-dashboard.

Issue (if applicable)

closes #

Risk

Low. Isolated to packages/affiliate-dashboard; no changes to web app, chain adapters, wallets, or on-chain tx paths. Worst case is a regression in the affiliate dashboard UI, which is an internal-facing tool.

Testing

Engineering

  • pnpm --filter @shapeshiftoss/affiliate-dashboard dev boots the dashboard without errors
  • SIWE auth, register, receive-address update, and affiliate-BPS update all still work end-to-end against the public-api
  • Period selector, overview stats, and swaps table render correctly at mobile, tablet, and desktop breakpoints
  • React Query devtools show cached/stale transitions on period changes (no flicker)

Operations

  • 🏁 My feature is behind a flag and doesn't require operations testing (yet)

Internal affiliate dashboard only — not user-facing.

Screenshots (if applicable)

Summary by CodeRabbit

  • New Features

    • Dashboard reorganized with tabbed interface for Overview, Swaps, and Settings management.
    • Settings tab now enables affiliate registration, configuration updates, and authentication flows.
    • Enhanced display of affiliate statistics, swap history with pagination, and commission details.
  • Styling

    • Implemented comprehensive theme system for improved visual consistency.

- Split 1270-line App.tsx into components, hooks, and lib folders
- Migrate inline styles to Chakra UI with shapeshift theme palette
- Replace custom useState/useEffect fetchers with React Query (useQuery + useMutation)
- Add Zod validation for all API responses with shared parseResponse helper
- Add stale-while-revalidate via keepPreviousData to eliminate period-change flicker
- Make dashboard responsive with mobile-card fallback for swaps table
- Strip workspace-shared deps from package.json (hoisted from root)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kaladinlight kaladinlight requested a review from a team as a code owner April 20, 2026 16:39
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 20, 2026

Warning

Rate limit exceeded

@kaladinlight has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 25 minutes and 23 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 25 minutes and 23 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 42fe8f76-b415-4a6c-a977-fb23180196ba

📥 Commits

Reviewing files that changed from the base of the PR and between 146380d and 9fb70f7.

📒 Files selected for processing (16)
  • .claude/guidelines/pr-rules.md
  • packages/affiliate-dashboard/src/components/settings/AffiliateBpsCard.tsx
  • packages/affiliate-dashboard/src/components/settings/ClaimCodeCard.tsx
  • packages/affiliate-dashboard/src/components/settings/ReceiveAddressCard.tsx
  • packages/affiliate-dashboard/src/components/settings/RegisterCard.tsx
  • packages/affiliate-dashboard/src/components/settings/SettingsTab.tsx
  • packages/affiliate-dashboard/src/components/swaps/AssetPill.tsx
  • packages/affiliate-dashboard/src/components/swaps/SwapsTable.tsx
  • packages/affiliate-dashboard/src/hooks/useAffiliateActions.ts
  • packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts
  • packages/affiliate-dashboard/src/hooks/useAffiliateSwaps.ts
  • packages/affiliate-dashboard/src/hooks/useSiweAuth.ts
  • packages/affiliate-dashboard/src/lib/api.ts
  • packages/affiliate-dashboard/src/lib/constants.ts
  • packages/affiliate-dashboard/src/lib/format.ts
  • packages/affiliate-dashboard/src/types/appkit.d.ts
📝 Walkthrough

Walkthrough

Comprehensive refactoring of the affiliate-dashboard from a monolithic React component to a modular, component-driven architecture. Removed runtime dependencies while introducing React Query for data fetching, Chakra UI for styling, Zod for validation, and creating 40+ new UI components and utilities to support tab-based settings, overview, and swaps pages.

Changes

Cohort / File(s) Summary
Package & Build Configuration
package.json, src/main.tsx, src/theme/index.ts, src/types/appkit.d.ts
Removed runtime dependencies (react, react-dom, viem, wagmi, @tanstack/react-query) while adding @reown/appkit-adapter-wagmi. Integrated Chakra UI providers and theme bootstrapping. Added TypeScript declarations for appkit-button custom element.
Core Layout Components
src/components/Layout.tsx, src/components/Header.tsx, src/components/ConfigBar.tsx, src/components/EmptyState.tsx, src/components/ErrorBanner.tsx, src/components/TabBar.tsx, src/components/PeriodSelector.tsx, src/components/ShapeShiftLogo.tsx
Created foundational UI elements for viewport structure, header branding, configuration display, empty/error states, tab navigation, and period selection. All are stateless presentation components with Chakra styling.
Overview Tab
src/components/overview/OverviewTab.tsx, src/components/overview/StatCard.tsx
Renders affiliate statistics dashboard with period selection and three summary cards (Total Swaps, Total Volume, Fees Earned). Includes loading, error, and empty states.
Settings Tab
src/components/settings/SettingsTab.tsx, src/components/settings/SettingsCard.tsx, src/components/settings/ActionMessage.tsx, src/components/settings/AuthBanner.tsx, src/components/settings/AuthStatusBar.tsx
Main settings tab orchestrating authentication flow and conditional UI display. SettingsCard provides a reusable card wrapper. ActionMessage displays mutation results. Auth components handle sign-in/out.
Settings Operations Cards
src/components/settings/RegisterCard.tsx, src/components/settings/AffiliateBpsCard.tsx, src/components/settings/ReceiveAddressCard.tsx, src/components/settings/ClaimCodeCard.tsx, src/components/settings/ConfigSummaryCard.tsx
Specialized settings UI for registration, BPS updates, receive address changes, code claims, and configuration summary. Each includes input validation, loading states, and error handling.
Swaps Tab
src/components/swaps/SwapsTab.tsx, src/components/swaps/SwapsTable.tsx, src/components/swaps/AssetPill.tsx, src/components/swaps/StatusBadge.tsx, src/components/swaps/Pagination.tsx
Swaps display with period selection and responsive table/card layouts. SwapsTable switches between mobile and desktop rendering. AssetPill and StatusBadge are reusable swap-related badges. Pagination controls multi-page data.
Data-Fetching Hooks
src/hooks/useAffiliateStats.ts, src/hooks/useAffiliateConfig.ts, src/hooks/useAffiliateSwaps.ts, src/hooks/useAffiliateActions.ts, src/hooks/useSiweAuth.ts
Migrated from manual useState/useCallback to React Query-based hooks. useAffiliateStats, useAffiliateConfig, useAffiliateSwaps now accept parameters and return UseQueryResult. useAffiliateActions centralizes mutations (register, claimCode, updateBps, updateReceiveAddress). useSiweAuth validates responses with Zod.
Utility Libraries
src/lib/api.ts, src/lib/format.ts, src/lib/periods.ts, src/lib/constants.ts
New shared utilities: api.ts provides parseResponse and postJson with Zod validation; format.ts exports formatters (formatUsd, bpsToPercent, shortenAddress, formatDate, formatNumber); periods.ts generates reporting periods relative to current date; constants.ts defines SWAPS_PER_PAGE and AFFILIATE_URL.
Main Application
src/App.tsx
Refactored from 1245 lines of monolithic logic to 74 lines of component composition. Removed manual data fetching, formatters, and inline styles. Now orchestrates tab switching, passes query results and action handlers to child tab components, and delegates all UI rendering.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Hops through the refactor, what a sight!
Monolith split into components bright,
React Query hooks now fetch with grace,
Chakra UI styles the entire place,
Settings, swaps, and stats aligned—
A modular dashboard, redesigned!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title 'refactor(affiliate-dashboard): migrate to Chakra UI + React Query' clearly and concisely summarizes the main changes: migrating to Chakra UI and React Query, which are the primary refactoring objectives evident throughout the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/affiliate-dashboard-chakra-refactor

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.

@kaladinlight kaladinlight changed the title refactor(affiliate-dashboard): Chakra UI migration and React Query refactor(affiliate-dashboard): migrate to Chakra UI + React Query Apr 20, 2026
Copy link
Copy Markdown
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: 7

🧹 Nitpick comments (7)
packages/affiliate-dashboard/src/lib/api.ts (1)

15-27: Minor: JSON parse failure is swallowed into a generic “Invalid response from server”.

When response.json() rejects (e.g., empty/HTML body on a 200), the null flows into Zod and surfaces as a schema-shape error, losing the actual cause. Consider logging/throwing a distinct message for decode failures to make debugging easier.

♻️ Suggested tweak
-  const json = await response.json().catch(() => null)
-
-  const parsed = schema.safeParse(json)
-  if (!parsed.success) {
-    console.error('Invalid API response shape:', parsed.error.errors)
-    throw new Error('Invalid response from server')
-  }
+  let json: unknown
+  try {
+    json = await response.json()
+  } catch (err) {
+    console.error('Failed to decode JSON response:', err)
+    throw new Error('Invalid response from server (decode failed)')
+  }
+
+  const parsed = schema.safeParse(json)
+  if (!parsed.success) {
+    console.error('Invalid API response shape:', parsed.error.errors)
+    throw new Error('Invalid response from server')
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/affiliate-dashboard/src/lib/api.ts` around lines 15 - 27, The
parseResponse function currently swallows JSON decode failures by mapping them
to Zod shape errors; update parseResponse to catch JSON parsing errors
separately (when calling response.json()) and throw or log a distinct,
informative error before running schema.safeParse — include the original JSON
parsing error message (and optionally the raw response text) in the
thrown/logged message so callers can distinguish decode failures from schema
validation failures; keep existing behavior of calling throwFromResponse when
!response.ok and continue to validate with schema.safeParse only when JSON
decoding succeeded.
packages/affiliate-dashboard/src/lib/periods.ts (1)

10-54: Minor: year-crossing label uses only the end year, and periods is computed once at module load.

Two small things worth considering for the follow-up period-selector work:

  1. formatPeriodLabel only renders end.getUTCFullYear(), so a window like Dec 5 → Jan 5, 2026 reads as “Dec 5 - Jan 5, 2026” and hides that the start was in 2025. Including the start year when it differs would avoid ambiguity.
  2. export const periods: Period[] = generatePeriods() runs once when the module is first imported. If a user keeps the dashboard open across a period boundary (midnight on the 5th UTC), the list will silently drift stale until reload. Recomputing on mount / via a memoized hook would fix it — likely fine to defer to the period-selector polish PR.
♻️ Optional label tweak
 const formatPeriodLabel = (start: Date, end: Date): string => {
   const monthShort = new Intl.DateTimeFormat('en-US', { month: 'short' })
-  const startLabel = `${monthShort.format(start)} ${start.getUTCDate()}`
+  const startYear = start.getUTCFullYear()
+  const endYear = end.getUTCFullYear()
+  const startLabel =
+    startYear === endYear
+      ? `${monthShort.format(start)} ${start.getUTCDate()}`
+      : `${monthShort.format(start)} ${start.getUTCDate()}, ${startYear}`
   const endLabel = `${monthShort.format(end)} ${end.getUTCDate()}`
-  const year = end.getUTCFullYear()
-  return `${startLabel} - ${endLabel}, ${year}`
+  return `${startLabel} - ${endLabel}, ${endYear}`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/affiliate-dashboard/src/lib/periods.ts` around lines 10 - 54,
formatPeriodLabel currently uses only end.getUTCFullYear(), causing misleading
labels for ranges that cross years; update formatPeriodLabel to include the
start year when start.getUTCFullYear() !== end.getUTCFullYear() (e.g., "Dec 5,
2025 - Jan 5, 2026") so the label disambiguates year-crossing periods. Also
avoid computing periods once at module load: replace the module-level export
const periods = generatePeriods() pattern by exporting the generatePeriods
function (or provide a getPeriods helper) and call it from the period selector
component (or on mount/useMemo) so the list is recalculated when the component
mounts or when current time changes rather than staying stale.
packages/affiliate-dashboard/src/main.tsx (1)

28-34: Add a root error boundary around the dashboard.

This is the best place to catch render-time failures from the refactored component tree and show a safe fallback instead of blanking the dashboard.

🛡️ Proposed shape
     <ChakraProvider theme={theme}>
       <WagmiProvider config={wagmiConfig}>
         <QueryClientProvider client={queryClient}>
-          <App />
+          <ErrorBoundary>
+            <App />
+          </ErrorBoundary>
         </QueryClientProvider>
       </WagmiProvider>
     </ChakraProvider>

As per coding guidelines, **/*.{tsx,jsx} should “ALWAYS wrap React components in error boundaries and provide user-friendly fallback components with error logging”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/affiliate-dashboard/src/main.tsx` around lines 28 - 34, Add a root
error boundary around the dashboard by creating a React ErrorBoundary (e.g.,
class RootErrorBoundary with componentDidCatch to log errors) and wrap the
existing top-level tree in main.tsx (wrap ChakraProvider, WagmiProvider,
QueryClientProvider and <App />) with <RootErrorBoundary>; the boundary should
render a simple user-friendly fallback UI (retry/refresh button or message) and
call your logging mechanism (console.error or app logger) with the error and
info inside componentDidCatch to capture render-time failures.
packages/affiliate-dashboard/src/hooks/useAffiliateSwaps.ts (1)

9-9: Consider centralizing this URL in lib/constants alongside AFFILIATE_URL.

Other hooks (useAffiliateConfig, useAffiliateActions) import AFFILIATE_URL from ../lib/constants, but this file builds the swaps URL inline from import.meta.env.VITE_API_URL. If VITE_API_URL is missing at build time this silently becomes "undefined/v1/affiliate/swaps". Colocating with the other endpoints would keep env validation in one place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/affiliate-dashboard/src/hooks/useAffiliateSwaps.ts` at line 9,
AFFILIATE_SWAPS_URL is being built inline in useAffiliateSwaps.ts from
import.meta.env which can produce "undefined/..." if VITE_API_URL is missing;
move the swaps endpoint constant into the shared ../lib/constants (next to
AFFILIATE_URL), perform the same env validation/assembly there, export the new
AFFILIATE_SWAPS_URL, and replace the inline const in useAffiliateSwaps.ts with
an import of AFFILIATE_SWAPS_URL so all affiliate endpoints are centralized and
validated in one place.
packages/affiliate-dashboard/src/hooks/useAffiliateActions.ts (2)

103-124: Returned action wrappers are recreated each render and swallow errors invisibly.

Two concerns with this return shape:

  1. The returned object and its method wrappers are allocated on every render. Since the whole actions object is threaded through App.tsxSettingsTab → each card, nothing downstream can meaningfully benefit from memo/useCallback. Consider wrapping each wrapper in useCallback and the return value in useMemo.

  2. claimCode/updateBps/updateReceiveAddress resolve successfully even on failure (.catch(() => undefined)). onError already sets the message, but callers can't distinguish success from failure — which is exactly why AffiliateBpsCard (flagged separately) clears its input on a failed update. Returning a boolean (or re-throwing) from these wrappers would let cards clear their state only on success.

♻️ Sketch
-    claimCode: async code => {
-      setMessage(null)
-      await claimCodeMutation.mutateAsync(code).catch(() => undefined)
-    },
+    claimCode: useCallback(async (code: string): Promise<boolean> => {
+      setMessage(null)
+      try {
+        await claimCodeMutation.mutateAsync(code)
+        return true
+      } catch {
+        return false
+      }
+    }, [claimCodeMutation]),

…and wrap the full return in useMemo keyed on the mutations + message.

As per coding guidelines: "ALWAYS use useCallback for event handlers and functions passed as props" and "ALWAYS use useMemo for expensive computations, object/array creations".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/affiliate-dashboard/src/hooks/useAffiliateActions.ts` around lines
103 - 124, The returned actions object recreates wrappers each render and
swallows errors; wrap each handler (register, claimCode, updateBps,
updateReceiveAddress) in useCallback (dependencies: their respective mutations
and setMessage) and wrap the final returned object in useMemo (deps: the
callbacks and message/isLoading) to avoid reallocation, and change the async
wrappers (claimCodeMutation.mutateAsync, updateBpsMutation.mutateAsync,
updateReceiveAddressMutation.mutateAsync) to either re-throw errors or return a
boolean success value instead of .catch(() => undefined) so callers can detect
failure (keep onError for messaging via the existing mutation onError handlers);
ensure register still calls registerMutation.mutate inside its useCallback.

29-50: Minor: type authHeaders at the call boundary and validate on mount.

authHeaders: Record<string, string> comes from useSiweAuth. If the user isn't signed in yet and triggers a mutation (shouldn't happen because SettingsTab gates UI on isAuthenticated), the POST will fire without auth and the server response will surface as a generic error message. Nothing to change in this PR, but worth a short-circuit (if (!authHeaders.Authorization) return) in each mutationFn as a defense-in-depth measure against future regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/affiliate-dashboard/src/hooks/useAffiliateActions.ts` around lines
29 - 50, The hook useAffiliateActions should assert the authHeaders shape and
defend against missing Authorization: add a stronger type at the call boundary
for authHeaders (Record<string,string> with Authorization optional/required) and
on mount validate authHeaders.Authorization (e.g., early-return or set an error
message) and, crucially, add a short-circuit inside each mutationFn (e.g., in
registerMutation's mutationFn) that returns/rejects immediately if
!authHeaders?.Authorization to avoid firing POSTs without auth; reference
authHeaders, useAffiliateActions, registerMutation, and mutationFn when applying
these checks.
packages/affiliate-dashboard/src/App.tsx (1)

44-93: Wrap prop-bound handlers in useCallback.

handleSelectPeriod, setSwapPage/setActiveTab (already stable), () => void signIn(), and message => actions.setMessage(message) are passed as props to OverviewTab, SwapsTab, and SettingsTab. The inline lambdas allocate on every render and defeat any downstream memo on those tabs. The last one can just forward actions.setMessage directly.

♻️ Suggested tweak
-  const handleSelectPeriod = (index: number): void => {
-    setSelectedPeriod(index)
-    setSwapPage(0)
-  }
+  const handleSelectPeriod = useCallback((index: number): void => {
+    setSelectedPeriod(index)
+    setSwapPage(0)
+  }, [])
+
+  const handleSignIn = useCallback(() => void signIn(), [signIn])
@@
-              onSignIn={() => void signIn()}
+              onSignIn={handleSignIn}
               onSignOut={signOut}
-              onValidationError={message => actions.setMessage(message)}
+              onValidationError={actions.setMessage}

As per coding guidelines: "ALWAYS use useCallback for event handlers and functions passed as props".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/affiliate-dashboard/src/App.tsx` around lines 44 - 93, The handlers
passed as props (handleSelectPeriod, setSwapPage, setActiveTab, the inline () =>
void signIn() and message => actions.setMessage(message)) are re-created each
render and can break memoization in OverviewTab, SwapsTab and SettingsTab; wrap
handleSelectPeriod in useCallback (dependent on setSelectedPeriod and
setSwapPage), wrap the setters passed as props with useCallback or pass the
stable setters directly (setSwapPage and setActiveTab are already stable so
ensure you pass them unchanged), replace the inline sign-in lambda with a
useCallback that calls signIn (or pass signIn directly if stable), and forward
actions.setMessage directly instead of the inline message =>
actions.setMessage(message) to ensure stable references for child components.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/affiliate-dashboard/src/components/settings/AffiliateBpsCard.tsx`:
- Around line 25-33: The input is being cleared unconditionally in handleUpdate
by calling setValue('') after awaiting onUpdate, which causes the field to be
reset even when the mutation fails; modify the flow so the input is cleared only
on a successful update: either update onUpdate (useAffiliateActions.updateBps)
to return a success boolean or rethrow errors, then in handleUpdate await the
result and only call setValue('') when the mutation indicates success (or move
setValue('') into the mutation's onSuccess callback); reference handleUpdate,
onUpdate (useAffiliateActions.updateBps), and setValue to locate the changes.

In `@packages/affiliate-dashboard/src/components/settings/ReceiveAddressCard.tsx`:
- Around line 28-35: The handleUpdate function awaits onUpdate(trimmed) without
catching rejections which can produce unhandled promise errors; wrap the await
in a try/catch inside handleUpdate, call onValidationError({ type: 'error',
text: <friendly message or error.message> }) when onUpdate rejects, and only
call setAddress('') on success so failures are surfaced in the component message
flow; update the handleUpdate implementation (references: handleUpdate,
onUpdate, onValidationError, ADDRESS_REGEX) accordingly.

In `@packages/affiliate-dashboard/src/components/settings/RegisterCard.tsx`:
- Around line 13-55: The BPS input isn't validated before submit—parseInt
accepts partial/scientific strings and HTML min/max don't stop invalid values—so
update parseBps and RegisterCard: make parseBps strictly parse only whole-number
decimal strings (no exponentals), return NaN for invalid input, and clamp valid
numbers to the allowed range (e.g., 0–1000); in RegisterCard use the strict
parser before calling onRegister, show/track a validation error state (e.g., set
a local error when parseBps returns NaN or out-of-range) and prevent onRegister
from being invoked (early return / disable Button) until the value is valid;
reference parseBps, bps, setBps, onRegister, and the Button/validation state so
you can locate and update the logic and UI.

In `@packages/affiliate-dashboard/src/components/swaps/AssetPill.tsx`:
- Around line 3-8: The AssetPill component computes `symbol` incorrectly by
ignoring `asset.name`; update the logic in AssetPill (where `symbol` is derived)
to use asset.symbol if present, otherwise fall back to asset.name, and only use
the literal 'Unknown' if neither exists so objects like { name: 'Ethereum' }
render that name instead of "Unknown".

In `@packages/affiliate-dashboard/src/components/swaps/SwapsTable.tsx`:
- Around line 24-26: The current parse helpers silently coerce invalid/malformed
financial strings to 0 (parseNum and parseBpsDisplay); change parseNum to
perform strict parsing (e.g., use Number.isFinite on parsed value) and return
null (or undefined) for absent/invalid inputs instead of 0, and update
parseBpsDisplay to validate parseInt results and return null for invalid BPS
(also ensure you don't subtract 10 from invalid values). Then update the UI to
stop relying on implicit zeros and call formatUsdValue(swap.sellAmountUsd) and
formatUsdValue(swap.affiliateFeeUsd) (or equivalent) in the table/card cells so
the formatter can render an explicit fallback for null/invalid values.

In `@packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts`:
- Around line 16-35: The ApiResponseSchema currently accepts any string for
totalVolumeUsd and totalFeesEarnedUsd and then parseFloat(...) || 0 masks
invalid data; update ApiResponseSchema to coerce and validate those fields as
numbers (use z.preprocess to parse the string into a number and then
z.number().refine or .nonnegative() as appropriate) so parseResponse returns
properly typed numeric values, then update fetchStats to use the validated
numeric fields (data.totalVolumeUsd and data.totalFeesEarnedUsd) directly
instead of calling parseFloat(...) || 0 and ensure parseResponse errors early on
validation failures; reference ApiResponseSchema and fetchStats to locate and
change the schema and the mapping logic.

In `@packages/affiliate-dashboard/src/types/appkit.d.ts`:
- Around line 3-9: The current augmentation wrongly targets the removed global
JSX namespace; instead declare a module augmentation for "react/jsx-runtime" and
put the IntrinsicElements entry for 'appkit-button' there. Replace the declare
global { namespace JSX { interface IntrinsicElements { 'appkit-button':
React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> } } }
with a module "react/jsx-runtime" augmentation that exports/augments the JSX
namespace and adds the same IntrinsicElements entry so the custom element type
is recognized under the React 19 / "react-jsx" runtime.

---

Nitpick comments:
In `@packages/affiliate-dashboard/src/App.tsx`:
- Around line 44-93: The handlers passed as props (handleSelectPeriod,
setSwapPage, setActiveTab, the inline () => void signIn() and message =>
actions.setMessage(message)) are re-created each render and can break
memoization in OverviewTab, SwapsTab and SettingsTab; wrap handleSelectPeriod in
useCallback (dependent on setSelectedPeriod and setSwapPage), wrap the setters
passed as props with useCallback or pass the stable setters directly
(setSwapPage and setActiveTab are already stable so ensure you pass them
unchanged), replace the inline sign-in lambda with a useCallback that calls
signIn (or pass signIn directly if stable), and forward actions.setMessage
directly instead of the inline message => actions.setMessage(message) to ensure
stable references for child components.

In `@packages/affiliate-dashboard/src/hooks/useAffiliateActions.ts`:
- Around line 103-124: The returned actions object recreates wrappers each
render and swallows errors; wrap each handler (register, claimCode, updateBps,
updateReceiveAddress) in useCallback (dependencies: their respective mutations
and setMessage) and wrap the final returned object in useMemo (deps: the
callbacks and message/isLoading) to avoid reallocation, and change the async
wrappers (claimCodeMutation.mutateAsync, updateBpsMutation.mutateAsync,
updateReceiveAddressMutation.mutateAsync) to either re-throw errors or return a
boolean success value instead of .catch(() => undefined) so callers can detect
failure (keep onError for messaging via the existing mutation onError handlers);
ensure register still calls registerMutation.mutate inside its useCallback.
- Around line 29-50: The hook useAffiliateActions should assert the authHeaders
shape and defend against missing Authorization: add a stronger type at the call
boundary for authHeaders (Record<string,string> with Authorization
optional/required) and on mount validate authHeaders.Authorization (e.g.,
early-return or set an error message) and, crucially, add a short-circuit inside
each mutationFn (e.g., in registerMutation's mutationFn) that returns/rejects
immediately if !authHeaders?.Authorization to avoid firing POSTs without auth;
reference authHeaders, useAffiliateActions, registerMutation, and mutationFn
when applying these checks.

In `@packages/affiliate-dashboard/src/hooks/useAffiliateSwaps.ts`:
- Line 9: AFFILIATE_SWAPS_URL is being built inline in useAffiliateSwaps.ts from
import.meta.env which can produce "undefined/..." if VITE_API_URL is missing;
move the swaps endpoint constant into the shared ../lib/constants (next to
AFFILIATE_URL), perform the same env validation/assembly there, export the new
AFFILIATE_SWAPS_URL, and replace the inline const in useAffiliateSwaps.ts with
an import of AFFILIATE_SWAPS_URL so all affiliate endpoints are centralized and
validated in one place.

In `@packages/affiliate-dashboard/src/lib/api.ts`:
- Around line 15-27: The parseResponse function currently swallows JSON decode
failures by mapping them to Zod shape errors; update parseResponse to catch JSON
parsing errors separately (when calling response.json()) and throw or log a
distinct, informative error before running schema.safeParse — include the
original JSON parsing error message (and optionally the raw response text) in
the thrown/logged message so callers can distinguish decode failures from schema
validation failures; keep existing behavior of calling throwFromResponse when
!response.ok and continue to validate with schema.safeParse only when JSON
decoding succeeded.

In `@packages/affiliate-dashboard/src/lib/periods.ts`:
- Around line 10-54: formatPeriodLabel currently uses only end.getUTCFullYear(),
causing misleading labels for ranges that cross years; update formatPeriodLabel
to include the start year when start.getUTCFullYear() !== end.getUTCFullYear()
(e.g., "Dec 5, 2025 - Jan 5, 2026") so the label disambiguates year-crossing
periods. Also avoid computing periods once at module load: replace the
module-level export const periods = generatePeriods() pattern by exporting the
generatePeriods function (or provide a getPeriods helper) and call it from the
period selector component (or on mount/useMemo) so the list is recalculated when
the component mounts or when current time changes rather than staying stale.

In `@packages/affiliate-dashboard/src/main.tsx`:
- Around line 28-34: Add a root error boundary around the dashboard by creating
a React ErrorBoundary (e.g., class RootErrorBoundary with componentDidCatch to
log errors) and wrap the existing top-level tree in main.tsx (wrap
ChakraProvider, WagmiProvider, QueryClientProvider and <App />) with
<RootErrorBoundary>; the boundary should render a simple user-friendly fallback
UI (retry/refresh button or message) and call your logging mechanism
(console.error or app logger) with the error and info inside componentDidCatch
to capture render-time failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6dca79a9-b49d-47e8-8116-8f9aa323f333

📥 Commits

Reviewing files that changed from the base of the PR and between aa2e5d5 and 146380d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (39)
  • packages/affiliate-dashboard/package.json
  • packages/affiliate-dashboard/src/App.tsx
  • packages/affiliate-dashboard/src/components/ConfigBar.tsx
  • packages/affiliate-dashboard/src/components/EmptyState.tsx
  • packages/affiliate-dashboard/src/components/ErrorBanner.tsx
  • packages/affiliate-dashboard/src/components/Header.tsx
  • packages/affiliate-dashboard/src/components/Layout.tsx
  • packages/affiliate-dashboard/src/components/PeriodSelector.tsx
  • packages/affiliate-dashboard/src/components/ShapeShiftLogo.tsx
  • packages/affiliate-dashboard/src/components/TabBar.tsx
  • packages/affiliate-dashboard/src/components/overview/OverviewTab.tsx
  • packages/affiliate-dashboard/src/components/overview/StatCard.tsx
  • packages/affiliate-dashboard/src/components/settings/ActionMessage.tsx
  • packages/affiliate-dashboard/src/components/settings/AffiliateBpsCard.tsx
  • packages/affiliate-dashboard/src/components/settings/AuthBanner.tsx
  • packages/affiliate-dashboard/src/components/settings/AuthStatusBar.tsx
  • packages/affiliate-dashboard/src/components/settings/ClaimCodeCard.tsx
  • packages/affiliate-dashboard/src/components/settings/ConfigSummaryCard.tsx
  • packages/affiliate-dashboard/src/components/settings/ReceiveAddressCard.tsx
  • packages/affiliate-dashboard/src/components/settings/RegisterCard.tsx
  • packages/affiliate-dashboard/src/components/settings/SettingsCard.tsx
  • packages/affiliate-dashboard/src/components/settings/SettingsTab.tsx
  • packages/affiliate-dashboard/src/components/swaps/AssetPill.tsx
  • packages/affiliate-dashboard/src/components/swaps/Pagination.tsx
  • packages/affiliate-dashboard/src/components/swaps/StatusBadge.tsx
  • packages/affiliate-dashboard/src/components/swaps/SwapsTab.tsx
  • packages/affiliate-dashboard/src/components/swaps/SwapsTable.tsx
  • packages/affiliate-dashboard/src/hooks/useAffiliateActions.ts
  • packages/affiliate-dashboard/src/hooks/useAffiliateConfig.ts
  • packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts
  • packages/affiliate-dashboard/src/hooks/useAffiliateSwaps.ts
  • packages/affiliate-dashboard/src/hooks/useSiweAuth.ts
  • packages/affiliate-dashboard/src/lib/api.ts
  • packages/affiliate-dashboard/src/lib/constants.ts
  • packages/affiliate-dashboard/src/lib/format.ts
  • packages/affiliate-dashboard/src/lib/periods.ts
  • packages/affiliate-dashboard/src/main.tsx
  • packages/affiliate-dashboard/src/theme/index.ts
  • packages/affiliate-dashboard/src/types/appkit.d.ts

Comment thread packages/affiliate-dashboard/src/components/settings/ReceiveAddressCard.tsx Outdated
Comment thread packages/affiliate-dashboard/src/components/settings/RegisterCard.tsx Outdated
Comment thread packages/affiliate-dashboard/src/components/swaps/AssetPill.tsx Outdated
Comment thread packages/affiliate-dashboard/src/components/swaps/SwapsTable.tsx Outdated
Comment thread packages/affiliate-dashboard/src/hooks/useAffiliateStats.ts
Comment thread packages/affiliate-dashboard/src/types/appkit.d.ts Outdated
kaladinlight and others added 3 commits April 20, 2026 10:44
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…flow

- appkit.d.ts: migrate to react/jsx-runtime module augmentation (React 19 drops global JSX)
- AssetSchema: require symbol, drop defensive string-union and optional fallbacks; AssetPill simplifies accordingly
- settings cards (bps/receive/claim): preserve input on mutation failure via try/catch; useAffiliateActions stops swallowing errors
- register/bps cards: share MIN_BPS/MAX_BPS/DEFAULT_BPS constants and parseBps/bpsToPercent helpers
- parseResponse: generalize to accept any ZodType and infer output (supports .transform())
- NumericString / NullableNumericString in lib/api.ts: strict stats parsing (loud fail) vs per-row soft-fail for swaps (malformed USD -> null -> "—" without killing the page)
- API_BASE_URL in constants; affiliate and auth URLs derive from it

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kaladinlight kaladinlight merged commit 6035be6 into develop Apr 20, 2026
4 checks passed
@kaladinlight kaladinlight deleted the feat/affiliate-dashboard-chakra-refactor branch April 20, 2026 17:46
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.

1 participant