Skip to content

feat: affiliate program#6793

Open
kernelwhisperer wants to merge 83 commits intodevelopfrom
feat/affiliate
Open

feat: affiliate program#6793
kernelwhisperer wants to merge 83 commits intodevelopfrom
feat/affiliate

Conversation

@kernelwhisperer
Copy link
Contributor

@kernelwhisperer kernelwhisperer commented Jan 7, 2026

Summary

Fully implement the Affiliate program v1:

  • partner code creation
  • trader code consumption
  • rewards tracking UI

Other changes

  • Refactor LinkStyledButton and move it from theme to @cowprotocol/ui
  • Rename RateInfoWrapper to FooterBox
  • Add RowRewards to Swap, Limit, TWAP and Bridge

Affiliate program (partners + traders)

Feature README file from apps/cowswap-frontend/src/modules/affiliate/README.md

1) Purpose

The affiliate program will amplify word-of-mouth marketing for CoW Swap by incentivizing referrals. Its mechanic will also facilitate a slew of other marketing tactics including KOL (influencer) and publisher activation, low funnel offers (e.g. with social and display ads), and high funnel measurement (e.g. with podcast and OOH ads).

2) Actors

  • Traders
  • Partners or Affiliates (KOLs/influencers, publishers)
  • Program managers (Marketing Squad)
  • Accountants (Finance Squad)
  • Maintainers (Web Squad, DevOps Squad)

3) Feature flag

  • Gated by a LaunchDarkly feature flag called isAffiliateProgramEnabled
  • When enabled:
    1. two new pages show up in Account, i.e. My Rewards and Affiliate
    2. the referral code input row shows up in the trade form alongside a Modal to input the code

4) Data flow

  • CMS is source of truth for codes
  • Dune is the source of truth for volumes, payouts and eligibility
  • BFF checks CMS every 5 minutes for new/updated codes
  • Dune is used for accounting
  • Frontend relies on BFF and does not call Dune/CMS directly
flowchart TB
  CMS[Strapi CMS]
  BFF[BFF service]
  FE[Frontend]
  Dune

  BFF -->|5-min polling| CMS
  CMS -->|codes & params| BFF

  BFF -->|codes & params| Dune
  Dune -->|stats| BFF

  BFF -->|/affiliate/:address| FE
  BFF -->|/ref-codes/:code| FE
  BFF -->|/affiliate/affiliate-stats/:address| FE
  BFF -->|/affiliate/trader-stats/:address| FE

Loading

5) Codes

  • Immutable (except for the enabled flag) to keep accounting simple
  • Unique code per partner (1:1 mapping)
  • If a partner needs multiple codes, they must use multiple wallets
  • You can bypass the immutability by deleting and recreating the code, please avoid doing this!!

5.1) Disabling codes

  • Program managers can soft-disable a code using the CMS Admin dashboard
  • This stops new sign-ups that go through our frontend
  • This does not affect historical payouts
  • It can be bypassed by traders that do not use our frontend

5.2) Default params + updates

  • Maintainers can change the program defaults by updating the CMS environment variables:

    1. Tweak params in /workspaces/infrastructure/cms/index.ts
    2. Run pulumi up in /workspaces/infrastructure/cms (after ssologin, pulumi stack select)
  • CMS env defaults:

    • AFFILIATE_REWARD_AMOUNT = 20
    • AFFILIATE_TRIGGER_VOLUME = 250000
    • AFFILIATE_TIME_CAP_DAYS = 90
    • AFFILIATE_VOLUME_CAP = 0 (unlimited)
    • AFFILIATE_REVENUE_SPLIT_AFFILIATE_PCT = 50
    • AFFILIATE_REVENUE_SPLIT_TRADER_PCT = 50
    • AFFILIATE_REVENUE_SPLIT_DAO_PCT = 0

5.3) Special codes

  • Program managers can create a new code from the CMS dashboard with special parameters by specifying any of the following:
    REWARD_AMOUNT, TRIGGER_VOLUME, TIME_CAP_DAYS, VOLUME_CAP, REVENUE_SPLIT_AFFILIATE_PCT, REVENUE_SPLIT_TRADER_PCT, REVENUE_SPLIT_DAO_PCT
  • Program managers can also a new code from the CMS dashboard with default parameters by leaving the above fields empty.
  • Important: if you create a code in CMS it might take up to 5 minutes to show up in the BFF due to polling interval.

6) Partner privacy

Goal: protect partner privacy by not leaking wallet addresses

Audit:

  • Ensure FE uses a CMS key that cannot read the affiliate collection
  • Ensure BFF strips partner address from /ref-codes/:code (called by traders)
  • Ensure CMS disallows reading the affiliate collection without API key

6.1) Revenue splits privacy

Goal: protect revenue splits by not leaking them to traders

Audit:

  • BFF strips revenue split fields from /ref-codes/:code, instead it only returns traderRewardAmount
  • This is calculated as rewardAmount * revenueSplitTraderPct / 100

7) Eligibility (hard requirement)

  • New traders only; allowing existing traders is non-trivial
  • FE checks prior orders and informs users if they are eligible
  • Dune filters out ineligible traders

8) Environments

Staging:

curl -s "https://bff.barn.cow.fi/ref-codes/FOOBAR"
curl -s "https://bff.barn.cow.fi/affiliate/0x6fc1Fb2e17DFf120fa8F838af139aF443070Fd0E"
curl -s "https://bff.barn.cow.fi/affiliate/affiliate-stats/0x6fc1Fb2e17DFf120fa8F838af139aF443070Fd0E"
curl -s "https://bff.barn.cow.fi/affiliate/trader-stats/0x6fc1Fb2e17DFf120fa8F838af139aF443070Fd0E"

Production:

9) Payouts

  • Payouts happen on Mainnet only, using USDC
  • Very very important: payouts must be done from 2 different wallets. One for partners and one for traders. Otherwise the math breaks.
  • Using a SafeWallet you can created Nested Safes and label them accordingly: affiliate payouts and trader payouts.
  • After having the payout wallet addresses you need to paste them in the Dune dashboards, both Affiliate Overview and Traders Overview
  • The dune dashboard will check USDC transfers from those wallets and compute what was already paid out and what is pending payout.
  • From two other dashboards, Next payouts for affiliates and Next payouts for traders, you can export a CSV file with the pending payouts with you can drop into Safe's CSV Airdrop app. (it takes a few minutes for the dashboards to reflect new payouts)

10) File structure

  • Besides this README, there is also a /apps/cowswap-frontend/src/modules/affiliate/SPEC.md file with use-cases (useful for AGENTS).
  • The Dune queries are version controlled in /apps/cowswap-frontend/src/modules/affiliate/misc.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added affiliate program with referral code generation and sharing functionality
    • Launched rewards dashboard to track referral earnings and upcoming payouts
    • Implemented referral code modal with validation and QR code generation for easy sharing
    • Added rewards row to trading interface showing referral code status
    • Introduced network-aware referral code entry with wallet connection requirements
  • Chores

    • Added QR code library dependency

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: 15

🤖 Fix all issues with AI agents
In `@apps/cowswap-frontend/src/locales/en-US.po`:
- Around line 3044-3046: Update the translation for the msgid "Affiliate payouts
and registration happens on Ethereum mainnet." by changing the msgstr to use
plural verb agreement: "Affiliate payouts and registration happen on Ethereum
mainnet." so the subject and verb agree.
- Around line 3165-3166: The msgid/msgstr pair containing "You and your
referrals can earn a flat fee <0/> for the eligible volume done through the app.
link." has a stray literal "link." — either remove that trailing text or place
it inside the intended link placeholder; update both the msgid and msgstr so
they match (e.g., remove "link." entirely from both, or change to "…through the
app. <0>link</0>" in both) to fix the localization entry referenced by the
msgid/msgstr.

In `@apps/cowswap-frontend/src/modules/affiliate/config/constants.ts`:
- Around line 28-29: The AFFILIATE_HOW_IT_WORKS_URL constant currently contains
a TODO placeholder; update its production value by either assigning the real
documentation URL or reading it from configuration/env (e.g.
process.env.AFFILIATE_HOW_IT_WORKS_URL) with a safe default, and remove the
hardcoded placeholder string so the export AFFILIATE_HOW_IT_WORKS_URL is
production-ready (or behind a feature gate) instead of pointing to docs.cow.fi.
- Line 19: The storage key constant AFFILIATE_TRADER_STORAGE_KEY currently uses
a dash ("cowswap:affiliate-trader:v2") which violates the camelCase key
guideline; change its string value to use camelCase (for example
"cowswap:affiliateTrader:v2") and update any usages of
AFFILIATE_TRADER_STORAGE_KEY throughout the codebase so all reads/writes use the
renamed key to avoid mismatches.

In `@apps/cowswap-frontend/src/modules/affiliate/misc/affiliates.sql`:
- Around line 129-161: The query performs divisions and a modulo using
affiliate_program_data.trigger_volume (used in expressions computing
total_earned, next_payout, and left_to_next_reward) without guarding against
zero; update the calculations to use
NULLIF(affiliate_program_data.trigger_volume, 0) (or an equivalent CASE that
returns NULL when trigger_volume = 0) wherever trigger_volume is used in a
division or in the modulo (%) and ensure subsequent floor()/round() logic and
the left_to_next_reward CASE handle NULLs (coalesce or conditional) so the query
does not error when trigger_volume is zero or NULL; specifically adjust the
expressions that compute total_earned, next_payout, and the modulo check for
left_to_next_reward, and keep payouts.paid_out and
affiliate_rewards.referral_volume handling as-is.

In `@apps/cowswap-frontend/src/modules/affiliate/misc/traders_debug.sql`:
- Around line 30-39: The CTE bound_ref can emit multiple rows per trader if a
trader has multiple trades with the same block_time because it joins trades to
first_ref_trade on first_ref_trade_time = trades.block_time; modify bound_ref
(or replace its logic) to pick a single row per trader by using ROW_NUMBER()
partitioned by trades.trader ordered by some tie-breaker (e.g.,
trades.block_time, trades.trade_id, or another deterministic column) and then
filter for row_number = 1, or alternatively apply LIMIT 1 with an ORDER BY per
trader to ensure only one referrer_code (referrer_code) / bound_time is returned
per trader for downstream joins.

In
`@apps/cowswap-frontend/src/modules/affiliate/model/hooks/useTraderReferralCodeWalletSync.ts`:
- Around line 34-41: The code casts Number(networkId) to SupportedChainId
unsafely in useTraderReferralCodeWalletSync.ts; replace this with a runtime
guard using isSupportedChainId from `@cowprotocol/common-utils`: import
isSupportedChainId alongside areAddressesEqual, call
isSupportedChainId(Number(networkId)) before treating it as a SupportedChainId,
skip or continue for unsupported IDs, and then call
getDefaultNetworkState(resolvedChainId) / flatOrdersStateNetwork only when the
guard passes so ordersState lookup is safe while still checking owners against
account.

In
`@apps/cowswap-frontend/src/modules/affiliate/model/state/traderReferralCodeReducers.ts`:
- Around line 10-217: Add unit tests covering the new reducer surface: write
focused specs that call reduceOpenModal/reduceCloseModal to verify modalOpen,
modalSource, editMode, incomingCode, inputCode and previousVerification
transitions for linked vs unlinked wallets; tests for
reduceSaveCode/reduceRemoveCode/reduceSetSavedCode to assert savedCode,
inputCode, verification and shouldAutoVerify changes; tests for verification
flow using reduceStartVerification, reduceCompleteVerification and
reduceRequestVerification to exercise idle→pending→checking→completed
transitions and pendingVerificationRequest creation; a cancellation/stale-ID
test for reduceClearPendingVerification to ensure only matching ids clear the
pending request; and wallet-sync test for reduceSetWalletState interacting with
resolveInputCode/resolveVerification via reduceOpenModal to ensure wallet.status
'linked' flips behavior. Use small isolated initial states and assert only
intended fields change.

In `@apps/cowswap-frontend/src/modules/affiliate/README.md`:
- Around line 141-142: Update the payout note text to remove the repeated
intensifier; replace the line starting with "Very very important: payouts must
be done from 2 different wallets." with a crisper phrasing such as "Important:
payouts must be done from two different wallets — one for partners and one for
traders." Keep the following sentence about using a SafeWallet/Nested Safes and
the labels (`affiliate payouts` and `trader payouts`) intact.

In
`@apps/cowswap-frontend/src/modules/affiliate/ui/TraderReferralCodeIneligibleCopy.tsx`:
- Around line 13-17: The external anchor returned by
TraderReferralCodeHowItWorksLink uses target="_blank" and must include
rel="noopener noreferrer" to prevent reverse-tabnabbing; update the
HowItWorksLink element in the TraderReferralCodeHowItWorksLink function to add
rel="noopener noreferrer" alongside the existing href and target attributes.

In
`@apps/cowswap-frontend/src/modules/affiliate/ui/TraderReferralCodeModal/useTraderReferralCodeModalController.ts`:
- Around line 286-296: The analytics label for the verify CTA is wrong: when
account exists the second branch still logs label 'connect_to_verify' and skews
metrics; in the handler where primaryCta.action === 'verify' and you call
actions.requestVerification(displayCode), change the analytics.sendEvent label
to a distinct value (e.g., 'verify' or 'verify_clicked') instead of
'connect_to_verify' so connected-user verification clicks are recorded
separately from connect-to-verify events; update the analytics.sendEvent call in
that branch (the block that calls actions.requestVerification(displayCode)) to
use the new label.

In `@apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx`:
- Around line 17-18: Remove the eslint-disable comment and add an explicit
return type annotation to ACCOUNT_ITEM; declare or reuse a suitable menu item
type (e.g., MenuItem or a specific interface describing the object shape
returned by ACCOUNT_ITEM) and annotate the function as const ACCOUNT_ITEM =
(chainId: SupportedChainId, isAffiliateProgramEnabled: boolean): MenuItem => ({
... }) so the return type is explicit and the eslint suppression can be deleted.

In `@apps/cowswap-frontend/src/pages/Account/Affiliate.tsx`:
- Around line 535-560: Update the user-facing copy in the Affiliate component:
fix the stray word in the HeroSubtitle text (remove "link." so the sentence
reads "You and your referrals can earn a flat fee for the eligible volume done
through the app.") and correct the grammar in the InlineNote (change "Affiliate
payouts and registration happens on Ethereum mainnet." to "Affiliate payouts and
registration happen on Ethereum mainnet."). Locate these strings in
Affiliate.tsx within the HeroSubtitle and InlineNote JSX and replace the text
accordingly.

In `@apps/cowswap-frontend/src/pages/Account/MyRewards.tsx`:
- Around line 152-160: The code is using an unsafe any cast to read
(traderReferralCode as any).wallet.code; update the
TraderWalletReferralCodeState discriminated union so the 'linked' variant
includes the wallet/code shape (e.g., { status: 'linked'; wallet: { code: string
} } or flatten to { status: 'linked'; code: string }) so TypeScript knows the
property exists, then remove the any cast in the traderCode computation and
access the code via the properly typed path (traderReferralCode.wallet.code or
traderReferralCode.code) when traderReferralCode.status === 'linked'.
- Around line 72-73: The AccountMyRewards component is too large and uses
eslint-disable; extract the data fetching block (currently around lines 98-139)
into a new hook named useTraderStats that returns loading, error, and fetched
data (and any query params), and extract the derived calculations block (around
lines 141-186) into a new hook named useRewardsMetrics that accepts the fetched
data and returns computed metrics; then refactor AccountMyRewards to call
useTraderStats and useRewardsMetrics and render only JSX, remove the
eslint-disable comments, and ensure all moved utility functions and state
references are updated to the new hooks (refer to AccountMyRewards,
useTraderStats, and useRewardsMetrics when locating/renaming logic).
🧹 Nitpick comments (32)
libs/common-utils/src/time.test.ts (1)

20-24: Consider adding parity tests for formatDateWithTimezone.

The formatShortDate tests cover nullish values and invalid dates, but formatDateWithTimezone only tests the Unix epoch case. For consistency and confidence in the refactored function, consider adding similar edge case tests.

📝 Suggested additional tests
   describe('formatDateWithTimezone', () => {
     it('treats 0 (unix epoch) as a valid timestamp', () => {
       expect(formatDateWithTimezone(0)).toEqual(expect.any(String))
     })
+
+    it('returns undefined for nullish values', () => {
+      expect(formatDateWithTimezone(undefined)).toBeUndefined()
+      expect(formatDateWithTimezone(null)).toBeUndefined()
+    })
+
+    it('returns undefined for invalid dates', () => {
+      expect(formatDateWithTimezone(new Date('invalid'))).toBeUndefined()
+    })
   })
apps/cowswap-frontend/src/pages/Account/styled.tsx (1)

1-416: Consider splitting this file in the future.

The file is 416 LOC, which exceeds the ~200 LOC guideline for TypeScript files. While styled-components files with many shared exports tend to grow larger, consider splitting into focused files (e.g., Card.styled.tsx, Banner.styled.tsx, Balance.styled.tsx) if additional components are added.

apps/cowswap-frontend/src/modules/affiliate/ui/TraderReferralCodeModal/styles/layout.ts (1)

76-89: Consider reusing or extending shared LabelRow and Label.

The LabelRow and Label components in this file closely duplicate those in apps/cowswap-frontend/src/modules/affiliate/ui/shared.tsx. The differences are:

  • LabelRow: adds padding and width: 100%
  • Label: uses design token for font-size and adds font-weight: 600

If these styling variations are intentional for this modal, consider extending the shared components instead:

♻️ Suggested approach
import { LabelRow as SharedLabelRow, Label as SharedLabel } from '../shared'

export const LabelRow = styled(SharedLabelRow)`
  padding: 0 0 0 8px;
  width: 100%;
`

export const Label = styled(SharedLabel)`
  font-size: var(${UI.FONT_SIZE_NORMAL});
  font-weight: 600;
`
libs/ui/src/pure/LinkStyledButton/index.tsx (1)

3-11: Consider exporting LinkStyledButtonProps for consumer type safety.

Since LinkStyledButton is a shared UI component in @cowprotocol/ui, consumers who need to type wrapper components or spread props will benefit from having access to the props type.

♻️ Suggested change
-type LinkStyledButtonProps = {
+export type LinkStyledButtonProps = {
   disabled?: boolean
   bg?: boolean
   isCopied?: boolean
   color?: string
   margin?: string
   padding?: string
   fontSize?: string
 }
apps/cowswap-frontend/src/common/pure/CancelButton/index.tsx (1)

13-15: Remove linter scaffolding by adding explicit return type.

Per coding guidelines, TODO comments and eslint-disable directives should be resolved by providing explicit types before shipping.

♻️ Suggested fix
-// TODO: Add proper return type annotation
-// eslint-disable-next-line `@typescript-eslint/explicit-function-return-type`
-export function CancelButton({ onClick, children, className }: CancelButtonProps) {
+export function CancelButton({ onClick, children, className }: CancelButtonProps): ReactNode {

Also add ReactNode to the imports:

-import { PropsWithChildren } from 'react'
+import { PropsWithChildren, ReactNode } from 'react'
apps/cowswap-frontend/src/modules/twap/containers/TwapConfirmModal/index.tsx (1)

18-18: Avoid duplicate RowRewards visibility checks.

RowRewards already short-circuits internally, so you can render it directly and drop the extra hook/conditional.

♻️ Proposed simplification
-import { RowRewards, useIsRowRewardsVisible } from 'modules/tradeWidgetAddons'
+import { RowRewards } from 'modules/tradeWidgetAddons'
@@
-  const isRowRewardsVisible = useIsRowRewardsVisible()
@@
-            {isRowRewardsVisible && <RowRewards />}
+            <RowRewards />

Also applies to: 103-103, 166-166

apps/cowswap-frontend/src/modules/swap/containers/SwapConfirmModal/index.tsx (1)

32-32: Consider removing duplicate RowRewards visibility checks.

RowRewards already short-circuits internally, so the local hook/conditional can be dropped.

♻️ Proposed simplification
-import { HighFeeWarning, RowDeadline, RowRewards, useIsRowRewardsVisible } from 'modules/tradeWidgetAddons'
+import { HighFeeWarning, RowDeadline, RowRewards } from 'modules/tradeWidgetAddons'
@@
-  const isRowRewardsVisible = useIsRowRewardsVisible()
@@
-                      {isRowRewardsVisible && <RowRewards />}
+                      <RowRewards />

Also applies to: 81-81, 172-175

apps/cowswap-frontend/src/modules/bridge/pure/contents/QuoteSwapContent/index.tsx (1)

13-140: Keep this pure component hook-free by hoisting visibility to the container.

QuoteSwapContent lives under modules/**/pure/** but now depends on useIsRowRewardsVisible(). Consider passing isRowRewardsVisible as a prop from a container to preserve purity and testability.

♻️ Suggested refactor (update callers accordingly)
-import { RowRewards, RowSlippage, useIsRowRewardsVisible } from 'modules/tradeWidgetAddons'
+import { RowRewards, RowSlippage } from 'modules/tradeWidgetAddons'

 interface QuoteDetailsContentProps {
   context: QuoteSwapContext
   hideRecommendedSlippage?: boolean
+  isRowRewardsVisible?: boolean
 }

-export function QuoteSwapContent({ context, hideRecommendedSlippage }: QuoteDetailsContentProps): ReactNode {
+export function QuoteSwapContent({
+  context,
+  hideRecommendedSlippage,
+  isRowRewardsVisible = false,
+}: QuoteDetailsContentProps): ReactNode {
   const {
     receiveAmountInfo,
     sellAmount,
     expectedReceive,
     slippage,
     recipient,
     bridgeReceiverOverride,
     minReceiveAmount,
     minReceiveUsdValue,
     expectedReceiveUsdValue,
     isSlippageModified,
   } = context
   const isBridgeQuoteRecipient = recipient === BRIDGE_QUOTE_ACCOUNT
-  const isRowRewardsVisible = useIsRowRewardsVisible()
   const contents = [
     createExpectedReceiveContent(expectedReceive, expectedReceiveUsdValue, slippage),
     createSlippageContent(slippage, !!hideRecommendedSlippage, isSlippageModified),
     !isBridgeQuoteRecipient && createRecipientContent(recipient, bridgeReceiverOverride, sellAmount.currency.chainId),
     !isBridgeQuoteRecipient && isRowRewardsVisible && createRewardsContent(),
     createMinReceiveContent(minReceiveAmount, minReceiveUsdValue),
   ]

As per coding guidelines: Pure components can use only built-in hooks, module-state hooks (e.g. useOrdersTableState()), or common state hooks (e.g. useTheme()); hoist other dependencies via props.

apps/cowswap-frontend/src/modules/affiliate/lib/affiliate-program-utils.test.ts (1)

1-34: Good test coverage for core utilities.

The tests appropriately cover the main scenarios for sanitizeReferralCode and isReferralCodeLengthValid. Consider adding edge case tests for:

  • sanitizeReferralCode(undefined) or sanitizeReferralCode(null) if the function is expected to handle these inputs gracefully.
  • isReferralCodeLengthValid('') to document the behavior for empty strings.
💡 Optional: Add edge case tests
 describe('sanitizeReferralCode', () => {
+  it('handles undefined/null input gracefully', () => {
+    expect(sanitizeReferralCode(undefined as unknown as string)).toBe('')
+    expect(sanitizeReferralCode(null as unknown as string)).toBe('')
+  })
+
   it('uppercases and trims whitespace', () => {
 describe('isReferralCodeLengthValid', () => {
+  it('rejects empty string', () => {
+    expect(isReferralCodeLengthValid('')).toBe(false)
+  })
+
   it('accepts lengths between 5 and 20 inclusive', () => {
apps/cowswap-frontend/src/modules/affiliate/ui/TraderReferralCodeModal.tsx (1)

27-27: Clarify the intent of defaulting supportedNetwork to true.

When chainId is undefined (wallet not connected), supportedNetwork defaults to true. This allows the modal to render assuming network support, which may be intentional to show the modal before wallet connection. Consider adding a brief comment to clarify this design decision for future maintainers.

📝 Optional: Add clarifying comment
-  const supportedNetwork = chainId === undefined ? true : isSupportedReferralNetwork(chainId)
+  // Default to true when wallet not connected to allow modal to render pre-connection
+  const supportedNetwork = chainId === undefined ? true : isSupportedReferralNetwork(chainId)
apps/cowswap-frontend/src/modules/affiliate/ui/TraderReferralCodeNetworkBanner.tsx (1)

66-68: Consider using an array for cleaner status checks.

The repeated wallet.status === comparisons could be simplified using an array includes check for better maintainability.

♻️ Optional: Simplify status checks
+const SHOW_BANNER_STATUSES = ['unsupported', 'unknown', 'disconnected'] as const
+
 export function TraderReferralCodeNetworkBanner(props: TraderReferralCodeNetworkBannerProps): ReactNode {
   const { forceVisible = false, onlyWhenUnsupported = false } = props
   const { modalOpen, wallet } = useTraderReferralCode()

   if (!forceVisible && !modalOpen) {
     return null
   }

-  const shouldShow = onlyWhenUnsupported
-    ? wallet.status === 'unsupported'
-    : wallet.status === 'unsupported' || wallet.status === 'unknown' || wallet.status === 'disconnected'
+  const shouldShow = onlyWhenUnsupported
+    ? wallet.status === 'unsupported'
+    : SHOW_BANNER_STATUSES.includes(wallet.status)
apps/cowswap-frontend/src/pages/Account/Menu.tsx (1)

27-32: Use Routes constants instead of hardcoded URL strings.

The affiliate menu items use hardcoded URL strings ('/account/my-rewards', '/account/affiliate'), while menuConsts.tsx uses Routes.ACCOUNT_MY_REWARDS and Routes.ACCOUNT_AFFILIATE for the same URLs. Use the shared constants for consistency and to avoid maintenance issues if routes change.

♻️ Proposed fix
+import { Routes } from 'common/constants/routes'
+
 const ACCOUNT_MENU_LINKS = (chainId: SupportedChainId, isAffiliateProgramEnabled: boolean): MenuItem[] => {
   return [
     { title: msg`Overview`, url: '/account' },
     { title: msg`Tokens`, url: '/account/tokens' },
     { title: ACCOUNT_PROXY_LABEL, url: getProxyAccountUrl(chainId) },
     ...(isAffiliateProgramEnabled
       ? [
-          { title: msg`My rewards`, url: '/account/my-rewards' },
-          { title: msg`Affiliate`, url: '/account/affiliate' },
+          { title: msg`My rewards`, url: Routes.ACCOUNT_MY_REWARDS },
+          { title: msg`Affiliate`, url: Routes.ACCOUNT_AFFILIATE },
         ]
       : []),
   ]
 }

As per coding guidelines: reuse shared constants rather than cloning or hardcoding values.

apps/cowswap-frontend/src/modules/tradeWidgetAddons/pure/Row/RowRewards/index.tsx (1)

22-77: Hoist renderAction out of the component.
Extract the JSX helper to module scope (e.g., RowRewardsAction) and pass props, so the render body stays free of nested JSX helpers.

♻️ Suggested refactor
+type RowRewardsActionProps = {
+  linkedCode?: string
+  onAddCode?: () => void
+  onManageCode?: () => void
+  accountLink?: string
+}
+
+function RowRewardsAction({ linkedCode, onAddCode, onManageCode, accountLink }: RowRewardsActionProps): ReactNode {
+  if (!linkedCode) {
+    return (
+      <LinkStyledButton onClick={onAddCode} padding="0" margin="0" fontSize="inherit" color={`var(${UI.COLOR_PRIMARY_LIGHTER})`}>
+        <Trans>Add code</Trans>
+      </LinkStyledButton>
+    )
+  }
+
+  if (onManageCode) {
+    return (
+      <LinkStyledButton as="button" onClick={onManageCode} type="button" padding="0" margin="0" fontSize="inherit" color={`var(${UI.COLOR_PRIMARY_LIGHTER})`}>
+        {linkedCode}
+      </LinkStyledButton>
+    )
+  }
+
+  return (
+    <LinkStyledButton as="a" href={accountLink ?? '/#/account'} padding="0" margin="0" fontSize="inherit" color={`var(${UI.COLOR_PRIMARY_LIGHTER})`}>
+      {linkedCode}
+    </LinkStyledButton>
+  )
+}
+
 export function RowRewardsContent(props: RowRewardsContentProps): ReactNode {
   const { onAddCode, onManageCode, tooltipContent, linkedCode, accountLink, styleProps } = props
   const tooltip = tooltipContent ?? <Trans>Add a referral code to earn rewards.</Trans>
 
-  const renderAction = (): ReactNode => {
-    ...
-  }
-
   return (
     <StyledRowBetween {...styleProps}>
       ...
-      <TextWrapper textAlign="right">{renderAction()}</TextWrapper>
+      <TextWrapper textAlign="right">
+        <RowRewardsAction linkedCode={linkedCode} onAddCode={onAddCode} onManageCode={onManageCode} accountLink={accountLink} />
+      </TextWrapper>
     </StyledRowBetween>
   )
 }

As per coding guidelines: Never declare components inside render bodies or rely on render* helpers that return JSX; hoist subcomponents to module scope.

apps/cowswap-frontend/src/modules/affiliate/ui/TraderReferralCodeModal/TraderReferralCodeModalContent.tsx (1)

102-147: Extract the repeated subtitle copy to avoid drift.
The “Code binds…” + “Payouts…” copy appears in multiple branches; consider a small helper (or constant) that returns the base sentence and conditionally prefixes “Connect to verify eligibility.”

As per coding guidelines: Hoist repeating strings/tooltips into constants colocated with the feature.

apps/cowswap-frontend/src/pages/Account/Affiliate.tsx (3)

112-113: Remove eslint-disable scaffolding by splitting AccountAffiliate.
The component disables complexity/length lint rules and exceeds the size guideline. Please split into focused hooks/components (and give the export an explicit return type like : JSX.Element) so the disables can be removed.

As per coding guidelines: Remove linter scaffolding (// TODO, eslint-disable) by supplying the correct types before shipping.

Also applies to: 367-368


197-249: Avoid let reassignments for cancellation flags.
Use useRef or an AbortController instead of let cancelled/active to keep to the project’s immutability rule and reduce mutable state in effects.

As per coding guidelines: Never reassign via let; favour const and ternaries/early returns.

Also applies to: 253-290, 322-364


165-172: Avoid hardcoded production origin in referral links.
The fallback to https://swap.cow.fi can leak prod links in non-prod environments (or SSR). Prefer a shared app base-url constant/config for the fallback.

As per coding guidelines: Extend shared constants/enums instead of hardcoding environment-specific lists or toggles.

apps/cowswap-frontend/src/modules/affiliate/ui/TraderReferralCodeModal/TraderReferralCodeForm.tsx (1)

47-48: Remove eslint-disable scaffolding by extracting helpers.
Please refactor the large branches into smaller helpers or a dedicated hook so the max-lines/complexity disables can be removed.

As per coding guidelines: Remove linter scaffolding (// TODO, eslint-disable) by supplying the correct types before shipping; Function complexity <= 15 cyclomatic; length <= 80 lines - extract helpers instead of disabling rules.

Also applies to: 188-189

apps/cowswap-frontend/src/modules/affiliate/model/containers/verificationEffects.ts (2)

113-153: Effect may trigger repeatedly due to object identity in dependency array

The traderReferralCode.actions object is included in the dependency array at line 145. If this object isn't referentially stable (i.e., recreated on each render), the effect will re-run unnecessarily, potentially causing repeated verification attempts.

Consider extracting setShouldAutoVerify directly from the context or verifying that actions is memoized in the provider.

♻️ Suggested improvement
 export function useTraderReferralCodeAutoVerification(params: AutoVerificationParams): void {
   const { traderReferralCode, account, chainId, supportedNetwork, runVerification } = params
-  const { shouldAutoVerify, savedCode, inputCode, incomingCode, verification } = traderReferralCode
+  const { shouldAutoVerify, savedCode, inputCode, incomingCode, verification, actions } = traderReferralCode
+  const { setShouldAutoVerify } = actions

   useEffect(() => {
     // ...
     if (shouldDisable) {
-      traderReferralCode.actions.setShouldAutoVerify(false)
+      setShouldAutoVerify(false)
       return
     }
     // ...
   }, [
     account,
     chainId,
     inputCode,
     incomingCode,
-    traderReferralCode.actions,
+    setShouldAutoVerify,
     traderReferralCode.wallet.status,
     runVerification,
     savedCode,
     shouldAutoVerify,
     supportedNetwork,
     verification.kind,
   ])
 }

212-212: Add blank line before interface definition.

Minor formatting: a blank line before interface PendingVerificationParams would improve readability and consistency with the rest of the file.

apps/cowswap-frontend/src/modules/affiliate/model/hooks/useTraderReferralCodeModalState.ts (1)

120-142: Unreachable condition at line 127

The condition walletStatus === 'unsupported' && hasCode in resolveVerificationState will never be true because deriveUiState returns 'unsupported' early at lines 95-98 before calling resolveVerificationState.

This is dead code that can be safely removed.

♻️ Proposed fix
 function resolveVerificationState(
   verificationKind: TraderReferralCodeVerificationKind,
   walletStatus: TraderReferralCodeWalletStatus,
   hasCode: boolean,
 ): TraderReferralCodeModalUiState | null {
   const orderedConditions: Array<[boolean, TraderReferralCodeModalUiState]> = [
     [verificationKind === 'checking', 'checking'],
-    [walletStatus === 'unsupported' && hasCode, 'unsupported'],
     [verificationKind === 'invalid', 'invalid'],
     [verificationKind === 'valid', 'valid'],
     [verificationKind === 'ineligible' || walletStatus === 'ineligible', 'ineligible'],
     [verificationKind === 'error', 'error'],
     [verificationKind === 'pending', 'pending'],
   ]
apps/cowswap-frontend/src/modules/affiliate/model/state/TraderReferralCodeContext.tsx (1)

81-106: Consider migrating to Jotai for state management.

As per coding guidelines, Jotai is preferred for state memoization and slicing. The current React Context approach works but may cause unnecessary re-renders for consumers that only need a subset of the state.

This could be addressed in a follow-up PR using atomWithStorage for persistence and derived atoms for slices.

apps/cowswap-frontend/src/pages/Account/MyRewards.tsx (1)

98-139: Data fetching mixes with state synchronization.

The effect fetches stats and also updates savedCode and walletState (lines 119-122) based on the API response. This couples the fetching concern with state reconciliation.

Consider:

  1. Keep the fetch effect pure (only set traderStats)
  2. Use a separate effect or the useTraderReferralCodeWalletSync hook to reconcile state from stats
apps/cowswap-frontend/src/modules/affiliate/model/state/traderReferralCodeStorage.ts (2)

14-47: Prefer atomWithStorage over manual localStorage hooks.

Per coding guidelines, Jotai's atomWithStorage is preferred over manual useState + useEffect with localStorage. The atom approach provides:

  • Automatic SSR safety via adapter
  • Built-in cross-tab sync
  • Simpler code without explicit hydration tracking

Since this is a new feature, consider refactoring to use atomWithStorage with a custom adapter for sanitization.


36-41: Redundant assignment in catch block.

shouldHydrate = true on line 37 is redundant since it's already initialized to true on line 24 and never set to false.

♻️ Proposed fix
     } catch (error) {
-      shouldHydrate = true
       if (!isProdLike) {
         console.warn('[ReferralCode] Failed to read saved code from storage', error)
       }
     }
apps/cowswap-frontend/src/modules/affiliate/ui/TraderReferralCodeModal/useTraderReferralCodeModalController.ts (1)

39-128: Remove the lint suppression by splitting the controller hook.

The eslint-disable indicates this hook exceeds complexity/length limits; please extract sub-helpers (e.g., CTA derivation or contentProps assembly) so the hook passes lint without suppression.

As per coding guidelines, "Remove linter scaffolding (// TODO, eslint-disable) by supplying the correct types before shipping" and "Function complexity <= 15 cyclomatic; length <= 80 lines - extract helpers instead of disabling rules".

apps/cowswap-frontend/src/modules/affiliate/model/containers/verificationLogic.ts (2)

42-106: Remove the lint suppression by extracting error/response handling.

The eslint-disable suggests this function exceeds complexity limits; please split into smaller helpers so linting can be re-enabled.

As per coding guidelines, "Remove linter scaffolding (// TODO, eslint-disable) by supplying the correct types before shipping" and "Function complexity <= 15 cyclomatic; length <= 80 lines - extract helpers instead of disabling rules".


102-104: Route warning through the centralized logger.

Please use the shared logger instead of console.warn so diagnostics are consistent and centrally managed.

As per coding guidelines, "Replace stray console.log/debug/info with the centralized logger unless intentionally scoped diagnostics (prefixed) are required".

apps/cowswap-frontend/src/modules/affiliate/lib/affiliate-program-utils.ts (1)

22-35: Remove the eslint-disable and add explicit return types for exports.

Please add an explicit return type for buildPartnerTypedData and apply the same pattern to other exported helpers in this file (e.g., sanitizeReferralCode, formatUsdCompact, isSupportedReferralNetwork).

🔧 Suggested fix (buildPartnerTypedData)
-// eslint-disable-next-line `@typescript-eslint/explicit-function-return-type`
-export function buildPartnerTypedData(params: { walletAddress: string; code: string; chainId: number }) {
+export function buildPartnerTypedData(params: {
+  walletAddress: string
+  code: string
+  chainId: number
+}): {
+  domain: typeof AFFILIATE_TYPED_DATA_DOMAIN
+  types: typeof AFFILIATE_TYPED_DATA_TYPES
+  message: { walletAddress: string; code: string; chainId: number }
+} {
   return {
     domain: {
       ...AFFILIATE_TYPED_DATA_DOMAIN,
     },

As per coding guidelines, "Remove linter scaffolding (// TODO, eslint-disable) by supplying the correct types before shipping" and "Always use optional chaining (?.) and explicit return types for exports".

apps/cowswap-frontend/src/modules/affiliate/ui/TraderReferralCodeModal/traderReferralCodeModal.helpers.tsx (1)

175-224: Replace the render* helper with a small component.

To align with the JSX helper guidance, convert renderRejectionReason into a component and render it directly.

🔧 Suggested refactor
-          {renderRejectionReason(reason)}
+          <RejectionReason reason={reason} />
         </>
       ),
     }
   }, [codeForDisplay, reason])
 }
 
-function renderRejectionReason(reason?: TraderReferralCodeIncomingReason): ReactNode {
+function RejectionReason({ reason }: { reason?: TraderReferralCodeIncomingReason }): ReactNode {
   if (!reason) {
     return null
   }
@@
     default:
       return null
   }
 }

As per coding guidelines, "Never declare components inside render bodies or rely on render*/get* helpers that return JSX; hoist subcomponents to module scope".

apps/cowswap-frontend/src/modules/affiliate/ui/shared.tsx (2)

9-11: Avoid importing from the pages layer in module UI.

modules/affiliate/ui should not depend on pages/Account/styled; move Card/ExtLink into a shared/ui or affiliate-local module to keep FSD layering intact.

As per coding guidelines, "Follow FSD layers (top to bottom): app, pages, widgets, features, entities, shared" and "In FSD, imports may only point to the same or lower layer".


1-728: Split this file to stay within the TSX size limit.

This module is well beyond the 200–250 LOC guideline. Consider splitting into focused files (layout, cards, metrics, donut, links) to keep each piece maintainable.

As per coding guidelines, "Keep TypeScript/TSX source files around 200 LOC; anything over 200 needs active justification, and non-generated files must stay <= 250 LOC".

Comment on lines +3044 to +3046
#: apps/cowswap-frontend/src/pages/Account/Affiliate.tsx
msgid "Affiliate payouts and registration happens on Ethereum mainnet."
msgstr "Affiliate payouts and registration happens on Ethereum mainnet."
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 | 🟡 Minor

Fix subject–verb agreement in user-facing copy.

“Affiliate payouts and registration happens” should be plural.
(Line 3044)

✏️ Suggested copy fix
-msgid "Affiliate payouts and registration happens on Ethereum mainnet."
-msgstr "Affiliate payouts and registration happens on Ethereum mainnet."
+msgid "Affiliate payouts and registration happen on Ethereum mainnet."
+msgstr "Affiliate payouts and registration happen on Ethereum mainnet."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#: apps/cowswap-frontend/src/pages/Account/Affiliate.tsx
msgid "Affiliate payouts and registration happens on Ethereum mainnet."
msgstr "Affiliate payouts and registration happens on Ethereum mainnet."
#: apps/cowswap-frontend/src/pages/Account/Affiliate.tsx
msgid "Affiliate payouts and registration happen on Ethereum mainnet."
msgstr "Affiliate payouts and registration happen on Ethereum mainnet."
🤖 Prompt for AI Agents
In `@apps/cowswap-frontend/src/locales/en-US.po` around lines 3044 - 3046, Update
the translation for the msgid "Affiliate payouts and registration happens on
Ethereum mainnet." by changing the msgstr to use plural verb agreement:
"Affiliate payouts and registration happen on Ethereum mainnet." so the subject
and verb agree.

Comment on lines +3165 to +3166
msgid "You and your referrals can earn a flat fee <0/> for the eligible volume done through the app. link."
msgstr "You and your referrals can earn a flat fee <0/> for the eligible volume done through the app. link."
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 | 🟡 Minor

Remove stray “link.” or wrap it in the intended link placeholder.

The trailing “link.” reads as literal text; if this is meant to be a hyperlink label, it should be inside a placeholder tag, otherwise remove it.
(Line 3165)

✏️ Suggested copy fix (remove stray text)
-msgid "You and your referrals can earn a flat fee <0/> for the eligible volume done through the app. link."
-msgstr "You and your referrals can earn a flat fee <0/> for the eligible volume done through the app. link."
+msgid "You and your referrals can earn a flat fee <0/> for the eligible volume done through the app."
+msgstr "You and your referrals can earn a flat fee <0/> for the eligible volume done through the app."
🤖 Prompt for AI Agents
In `@apps/cowswap-frontend/src/locales/en-US.po` around lines 3165 - 3166, The
msgid/msgstr pair containing "You and your referrals can earn a flat fee <0/>
for the eligible volume done through the app. link." has a stray literal "link."
— either remove that trailing text or place it inside the intended link
placeholder; update both the msgid and msgstr so they match (e.g., remove
"link." entirely from both, or change to "…through the app. <0>link</0>" in
both) to fix the localization entry referenced by the msgid/msgstr.

SupportedChainId.PLASMA,
] as const

export const AFFILIATE_TRADER_STORAGE_KEY = 'cowswap:affiliate-trader:v2'
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 | 🟡 Minor

Storage key base should be camelCase (no dashes).

Current key uses affiliate-trader, which conflicts with the storage key format guideline.

✅ Suggested change
-export const AFFILIATE_TRADER_STORAGE_KEY = 'cowswap:affiliate-trader:v2'
+export const AFFILIATE_TRADER_STORAGE_KEY = 'cowswap:affiliateTrader:v2'

As per coding guidelines: LocalStorage/IndexedDB keys must follow camelCaseBase:v{number} and avoid dashes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const AFFILIATE_TRADER_STORAGE_KEY = 'cowswap:affiliate-trader:v2'
export const AFFILIATE_TRADER_STORAGE_KEY = 'cowswap:affiliateTrader:v2'
🤖 Prompt for AI Agents
In `@apps/cowswap-frontend/src/modules/affiliate/config/constants.ts` at line 19,
The storage key constant AFFILIATE_TRADER_STORAGE_KEY currently uses a dash
("cowswap:affiliate-trader:v2") which violates the camelCase key guideline;
change its string value to use camelCase (for example
"cowswap:affiliateTrader:v2") and update any usages of
AFFILIATE_TRADER_STORAGE_KEY throughout the codebase so all reads/writes use the
renamed key to avoid mismatches.

Comment on lines +28 to +29
// TODO: replace placeholder URL once the referral docs are provisioned
export const AFFILIATE_HOW_IT_WORKS_URL = 'https://docs.cow.fi'
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 | 🟡 Minor

Avoid shipping TODO placeholders in config.

Replace the placeholder URL or move it behind an env/config gate so the constant is production-ready.

🤖 Prompt for AI Agents
In `@apps/cowswap-frontend/src/modules/affiliate/config/constants.ts` around lines
28 - 29, The AFFILIATE_HOW_IT_WORKS_URL constant currently contains a TODO
placeholder; update its production value by either assigning the real
documentation URL or reading it from configuration/env (e.g.
process.env.AFFILIATE_HOW_IT_WORKS_URL) with a safe default, and remove the
hardcoded placeholder string so the export AFFILIATE_HOW_IT_WORKS_URL is
production-ready (or behind a feature gate) instead of pointing to docs.cow.fi.

Comment on lines +129 to +161
cast(
round(
floor(coalesce(affiliate_rewards.referral_volume, 0) / affiliate_program_data.trigger_volume)
* (
cast(affiliate_program_data.reward_amount as decimal(18, 6))
* cast(affiliate_program_data.revenue_split_affiliate_pct as decimal(18, 0))
/ cast(100 as decimal(18, 6))
),
6
)
as decimal(18, 6)
) as total_earned,
cast(coalesce(payouts.paid_out, 0) as decimal(18, 6)) as paid_out,
cast(
round(
(
floor(coalesce(affiliate_rewards.referral_volume, 0) / affiliate_program_data.trigger_volume)
* (
cast(affiliate_program_data.reward_amount as decimal(18, 6))
* cast(affiliate_program_data.revenue_split_affiliate_pct as decimal(18, 0))
/ cast(100 as decimal(18, 6))
)
) - coalesce(payouts.paid_out, 0),
6
)
as decimal(18, 6)
) as next_payout,
case
when (coalesce(affiliate_rewards.referral_volume, 0) % affiliate_program_data.trigger_volume) = 0
then affiliate_program_data.trigger_volume
else affiliate_program_data.trigger_volume -
(coalesce(affiliate_rewards.referral_volume, 0) % affiliate_program_data.trigger_volume)
end as left_to_next_reward,
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

Guard against divide/modulo by zero on trigger_volume.

Several calculations assume trigger_volume > 0. If a code is misconfigured to 0, the query will error. Add NULLIF/CASE guards to keep the query safe.

🛠️ Suggested fix
-  floor(coalesce(affiliate_rewards.referral_volume, 0) / affiliate_program_data.trigger_volume)
+  floor(coalesce(affiliate_rewards.referral_volume, 0) / nullif(affiliate_program_data.trigger_volume, 0))
...
-  floor(coalesce(affiliate_rewards.referral_volume, 0) / affiliate_program_data.trigger_volume)
+  floor(coalesce(affiliate_rewards.referral_volume, 0) / nullif(affiliate_program_data.trigger_volume, 0))
...
-  case
-    when (coalesce(affiliate_rewards.referral_volume, 0) % affiliate_program_data.trigger_volume) = 0
-      then affiliate_program_data.trigger_volume
-    else affiliate_program_data.trigger_volume -
-      (coalesce(affiliate_rewards.referral_volume, 0) % affiliate_program_data.trigger_volume)
-  end as left_to_next_reward,
+  case
+    when affiliate_program_data.trigger_volume = 0 then 0
+    when (coalesce(affiliate_rewards.referral_volume, 0) % affiliate_program_data.trigger_volume) = 0
+      then affiliate_program_data.trigger_volume
+    else affiliate_program_data.trigger_volume -
+      (coalesce(affiliate_rewards.referral_volume, 0) % affiliate_program_data.trigger_volume)
+  end as left_to_next_reward,
🤖 Prompt for AI Agents
In `@apps/cowswap-frontend/src/modules/affiliate/misc/affiliates.sql` around lines
129 - 161, The query performs divisions and a modulo using
affiliate_program_data.trigger_volume (used in expressions computing
total_earned, next_payout, and left_to_next_reward) without guarding against
zero; update the calculations to use
NULLIF(affiliate_program_data.trigger_volume, 0) (or an equivalent CASE that
returns NULL when trigger_volume = 0) wherever trigger_volume is used in a
division or in the modulo (%) and ensure subsequent floor()/round() logic and
the left_to_next_reward CASE handle NULLs (coalesce or conditional) so the query
does not error when trigger_volume is zero or NULL; specifically adjust the
expressions that compute total_earned, next_payout, and the modulo check for
left_to_next_reward, and keep payouts.paid_out and
affiliate_rewards.referral_volume handling as-is.

Comment on lines +286 to +296
if (!account && primaryCta.action === 'verify') {
toggleWalletModal()
analytics.sendEvent({ category: 'referral', action: 'cta_clicked', label: 'connect_to_verify' })
return
}

if (primaryCta.action === 'verify') {
analytics.sendEvent({ category: 'referral', action: 'cta_clicked', label: 'connect_to_verify' })
actions.requestVerification(displayCode)
return
}
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 | 🟡 Minor

Fix the verify CTA analytics label.

When the wallet is already connected, the verify branch still emits connect_to_verify, which skews CTA metrics. Emit a distinct label for actual verification.

🔧 Suggested fix
-    if (primaryCta.action === 'verify') {
-      analytics.sendEvent({ category: 'referral', action: 'cta_clicked', label: 'connect_to_verify' })
-      actions.requestVerification(displayCode)
-      return
-    }
+    if (primaryCta.action === 'verify') {
+      analytics.sendEvent({ category: 'referral', action: 'cta_clicked', label: 'verify_code' })
+      actions.requestVerification(displayCode)
+      return
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!account && primaryCta.action === 'verify') {
toggleWalletModal()
analytics.sendEvent({ category: 'referral', action: 'cta_clicked', label: 'connect_to_verify' })
return
}
if (primaryCta.action === 'verify') {
analytics.sendEvent({ category: 'referral', action: 'cta_clicked', label: 'connect_to_verify' })
actions.requestVerification(displayCode)
return
}
if (!account && primaryCta.action === 'verify') {
toggleWalletModal()
analytics.sendEvent({ category: 'referral', action: 'cta_clicked', label: 'connect_to_verify' })
return
}
if (primaryCta.action === 'verify') {
analytics.sendEvent({ category: 'referral', action: 'cta_clicked', label: 'verify_code' })
actions.requestVerification(displayCode)
return
}
🤖 Prompt for AI Agents
In
`@apps/cowswap-frontend/src/modules/affiliate/ui/TraderReferralCodeModal/useTraderReferralCodeModalController.ts`
around lines 286 - 296, The analytics label for the verify CTA is wrong: when
account exists the second branch still logs label 'connect_to_verify' and skews
metrics; in the handler where primaryCta.action === 'verify' and you call
actions.requestVerification(displayCode), change the analytics.sendEvent label
to a distinct value (e.g., 'verify' or 'verify_clicked') instead of
'connect_to_verify' so connected-user verification clicks are recorded
separately from connect-to-verify events; update the analytics.sendEvent call in
that branch (the block that calls actions.requestVerification(displayCode)) to
use the new label.

Comment on lines 17 to +18
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const ACCOUNT_ITEM = (chainId: SupportedChainId) => ({
const ACCOUNT_ITEM = (chainId: SupportedChainId, isAffiliateProgramEnabled: boolean) => ({
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

Remove eslint-disable and add explicit return type.

As per coding guidelines, shipping eslint-disable scaffolding instead of providing explicit types is a red flag. Add an explicit return type to ACCOUNT_ITEM and remove the suppression.

🔧 Proposed fix
-// eslint-disable-next-line `@typescript-eslint/explicit-function-return-type`
-const ACCOUNT_ITEM = (chainId: SupportedChainId, isAffiliateProgramEnabled: boolean) => ({
+interface AccountMenuItem {
+  label: ReturnType<typeof msg>
+  children: Array<{ href: string; label: ReturnType<typeof msg> }>
+}
+
+const ACCOUNT_ITEM = (chainId: SupportedChainId, isAffiliateProgramEnabled: boolean): AccountMenuItem => ({
🤖 Prompt for AI Agents
In `@apps/cowswap-frontend/src/modules/application/containers/App/menuConsts.tsx`
around lines 17 - 18, Remove the eslint-disable comment and add an explicit
return type annotation to ACCOUNT_ITEM; declare or reuse a suitable menu item
type (e.g., MenuItem or a specific interface describing the object shape
returned by ACCOUNT_ITEM) and annotate the function as const ACCOUNT_ITEM =
(chainId: SupportedChainId, isAffiliateProgramEnabled: boolean): MenuItem => ({
... }) so the return type is explicit and the eslint suppression can be deleted.

Comment on lines +535 to +560
<Trans>
You and your referrals can earn a flat fee <br /> for the eligible volume done through the app. link.
</Trans>
</HeroSubtitle>
<HeroActions>
{!isConnected && (
<ButtonPrimary buttonSize={ButtonSize.BIG} onClick={handleConnect} data-testid="affiliate-connect">
<Trans>Connect wallet</Trans>
</ButtonPrimary>
)}
{isConnected && showUnsupported && (
<ButtonPrimary buttonSize={ButtonSize.BIG} onClick={handleSwitchToMainnet}>
<Trans>Switch to Ethereum</Trans>
</ButtonPrimary>
)}
{isConnected && !showUnsupported && !isSignerAvailable && !showLinkedFlow && (
<ButtonPrimary onClick={handleConnect} data-testid="affiliate-unlock">
<Trans>Become an affiliate</Trans>
</ButtonPrimary>
)}
</HeroActions>
<AffiliateTermsFaqLinks />
{showUnsupported && (
<InlineNote>
<Trans>Affiliate payouts and registration happens on Ethereum mainnet.</Trans>
</InlineNote>
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 | 🟡 Minor

Fix user-facing copy typos.
“You and your referrals can earn a flat fee … through the app. link.” reads like a stray word, and “Affiliate payouts and registration happens” should be “happen.”

🤖 Prompt for AI Agents
In `@apps/cowswap-frontend/src/pages/Account/Affiliate.tsx` around lines 535 -
560, Update the user-facing copy in the Affiliate component: fix the stray word
in the HeroSubtitle text (remove "link." so the sentence reads "You and your
referrals can earn a flat fee for the eligible volume done through the app.")
and correct the grammar in the InlineNote (change "Affiliate payouts and
registration happens on Ethereum mainnet." to "Affiliate payouts and
registration happen on Ethereum mainnet."). Locate these strings in
Affiliate.tsx within the HeroSubtitle and InlineNote JSX and replace the text
accordingly.

Comment on lines +72 to +73
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, max-lines-per-function, complexity
export default function AccountMyRewards() {
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

Remove eslint-disable comments by refactoring.

The component exceeds guidelines for complexity and line count. Consider extracting:

  1. A useTraderStats hook for the data fetching logic (lines 98-139)
  2. A useRewardsMetrics hook for the derived calculations (lines 141-186)

This would reduce the component to pure rendering and eliminate the need for suppressions.

🤖 Prompt for AI Agents
In `@apps/cowswap-frontend/src/pages/Account/MyRewards.tsx` around lines 72 - 73,
The AccountMyRewards component is too large and uses eslint-disable; extract the
data fetching block (currently around lines 98-139) into a new hook named
useTraderStats that returns loading, error, and fetched data (and any query
params), and extract the derived calculations block (around lines 141-186) into
a new hook named useRewardsMetrics that accepts the fetched data and returns
computed metrics; then refactor AccountMyRewards to call useTraderStats and
useRewardsMetrics and render only JSX, remove the eslint-disable comments, and
ensure all moved utility functions and state references are updated to the new
hooks (refer to AccountMyRewards, useTraderStats, and useRewardsMetrics when
locating/renaming logic).

Comment on lines +152 to +160
const traderCode = isConnected
? statsLinkedCode
? statsLinkedCode
: isLinked
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
(traderReferralCode as any).wallet.code
: (traderReferralCode.savedCode ??
(traderReferralCode.verification.kind === 'valid' ? traderReferralCode.verification.code : undefined))
: undefined
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

Remove any cast and fix type definition.

The explicit any cast to access wallet.code suggests the TraderWalletReferralCodeState type is missing the code property when status === 'linked'. Fix the type definition to use a discriminated union:

type TraderWalletReferralCodeState =
  | { status: 'unknown' | 'unsupported' | 'ineligible' }
  | { status: 'linked'; code: string }

Then the access becomes type-safe without casting.

🤖 Prompt for AI Agents
In `@apps/cowswap-frontend/src/pages/Account/MyRewards.tsx` around lines 152 -
160, The code is using an unsafe any cast to read (traderReferralCode as
any).wallet.code; update the TraderWalletReferralCodeState discriminated union
so the 'linked' variant includes the wallet/code shape (e.g., { status:
'linked'; wallet: { code: string } } or flatten to { status: 'linked'; code:
string }) so TypeScript knows the property exists, then remove the any cast in
the traderCode computation and access the code via the properly typed path
(traderReferralCode.wallet.code or traderReferralCode.code) when
traderReferralCode.status === 'linked'.

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