Conversation
…actor RowRewards button styling
…referral program constants
There was a problem hiding this comment.
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 forformatDateWithTimezone.The
formatShortDatetests cover nullish values and invalid dates, butformatDateWithTimezoneonly 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 sharedLabelRowandLabel.The
LabelRowandLabelcomponents in this file closely duplicate those inapps/cowswap-frontend/src/modules/affiliate/ui/shared.tsx. The differences are:
LabelRow: addspaddingandwidth: 100%Label: uses design token for font-size and addsfont-weight: 600If 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 exportingLinkStyledButtonPropsfor consumer type safety.Since
LinkStyledButtonis 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
ReactNodeto 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.
QuoteSwapContentlives undermodules/**/pure/**but now depends onuseIsRowRewardsVisible(). Consider passingisRowRewardsVisibleas 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
sanitizeReferralCodeandisReferralCodeLengthValid. Consider adding edge case tests for:
sanitizeReferralCode(undefined)orsanitizeReferralCode(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 defaultingsupportedNetworktotrue.When
chainIdisundefined(wallet not connected),supportedNetworkdefaults totrue. 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: UseRoutesconstants instead of hardcoded URL strings.The affiliate menu items use hardcoded URL strings (
'/account/my-rewards','/account/affiliate'), whilemenuConsts.tsxusesRoutes.ACCOUNT_MY_REWARDSandRoutes.ACCOUNT_AFFILIATEfor 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: HoistrenderActionout 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 splittingAccountAffiliate.
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: Avoidletreassignments for cancellation flags.
UseuseRefor anAbortControllerinstead oflet cancelled/activeto 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 tohttps://swap.cow.fican 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 arrayThe
traderReferralCode.actionsobject 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
setShouldAutoVerifydirectly from the context or verifying thatactionsis 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 PendingVerificationParamswould 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 127The condition
walletStatus === 'unsupported' && hasCodeinresolveVerificationStatewill never be true becausederiveUiStatereturns'unsupported'early at lines 95-98 before callingresolveVerificationState.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
atomWithStoragefor 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
savedCodeandwalletState(lines 119-122) based on the API response. This couples the fetching concern with state reconciliation.Consider:
- Keep the fetch effect pure (only set
traderStats)- Use a separate effect or the
useTraderReferralCodeWalletSynchook to reconcile state from statsapps/cowswap-frontend/src/modules/affiliate/model/state/traderReferralCodeStorage.ts (2)
14-47: PreferatomWithStorageover manual localStorage hooks.Per coding guidelines, Jotai's
atomWithStorageis preferred over manualuseState+useEffectwithlocalStorage. 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
atomWithStoragewith a custom adapter for sanitization.
36-41: Redundant assignment in catch block.
shouldHydrate = trueon line 37 is redundant since it's already initialized totrueon line 24 and never set tofalse.♻️ 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-disableindicates 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-disablesuggests 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.warnso diagnostics are consistent and centrally managed.As per coding guidelines, "Replace stray
console.log/debug/infowith 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
buildPartnerTypedDataand 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 therender*helper with a small component.To align with the JSX helper guidance, convert
renderRejectionReasoninto 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/uishould not depend onpages/Account/styled; moveCard/ExtLinkinto 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".
| #: 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." |
There was a problem hiding this comment.
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.
| #: 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.
| 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." |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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.
| 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.
| // TODO: replace placeholder URL once the referral docs are provisioned | ||
| export const AFFILIATE_HOW_IT_WORKS_URL = 'https://docs.cow.fi' |
There was a problem hiding this comment.
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.
| 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, |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| // eslint-disable-next-line @typescript-eslint/explicit-function-return-type | ||
| const ACCOUNT_ITEM = (chainId: SupportedChainId) => ({ | ||
| const ACCOUNT_ITEM = (chainId: SupportedChainId, isAffiliateProgramEnabled: boolean) => ({ |
There was a problem hiding this comment.
🛠️ 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.
| <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> |
There was a problem hiding this comment.
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.
| // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, max-lines-per-function, complexity | ||
| export default function AccountMyRewards() { |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Remove eslint-disable comments by refactoring.
The component exceeds guidelines for complexity and line count. Consider extracting:
- A
useTraderStatshook for the data fetching logic (lines 98-139) - A
useRewardsMetricshook 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).
| 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 |
There was a problem hiding this comment.
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'.
Summary
Fully implement the Affiliate program v1:
Other changes
LinkStyledButtonand move it fromthemeto@cowprotocol/uiRateInfoWrappertoFooterBoxRowRewardsto Swap, Limit, TWAP and BridgeAffiliate program (partners + traders)
Feature README file from
apps/cowswap-frontend/src/modules/affiliate/README.md1) 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
3) Feature flag
isAffiliateProgramEnabledAccount, i.e.My RewardsandAffiliate4) Data flow
5) Codes
enabledflag) to keep accounting simple5.1) Disabling codes
5.2) Default params + updates
Maintainers can change the program defaults by updating the CMS environment variables:
/workspaces/infrastructure/cms/index.tspulumi upin/workspaces/infrastructure/cms(after ssologin, pulumi stack select)CMS env defaults:
AFFILIATE_REWARD_AMOUNT=20AFFILIATE_TRIGGER_VOLUME=250000AFFILIATE_TIME_CAP_DAYS=90AFFILIATE_VOLUME_CAP=0(unlimited)AFFILIATE_REVENUE_SPLIT_AFFILIATE_PCT=50AFFILIATE_REVENUE_SPLIT_TRADER_PCT=50AFFILIATE_REVENUE_SPLIT_DAO_PCT=05.3) Special codes
REWARD_AMOUNT, TRIGGER_VOLUME, TIME_CAP_DAYS, VOLUME_CAP, REVENUE_SPLIT_AFFILIATE_PCT, REVENUE_SPLIT_TRADER_PCT, REVENUE_SPLIT_DAO_PCT6) Partner privacy
Goal: protect partner privacy by not leaking wallet addresses
Audit:
/ref-codes/:code(called by traders)6.1) Revenue splits privacy
Goal: protect revenue splits by not leaking them to traders
Audit:
/ref-codes/:code, instead it only returnstraderRewardAmountrewardAmount * revenueSplitTraderPct / 1007) Eligibility (hard requirement)
8) Environments
Staging:
66486796648689affiliate_program_data_stagingProduction:
65608536560325affiliate_program_data9) Payouts
affiliate payoutsandtrader payouts.Affiliate OverviewandTraders OverviewNext payouts for affiliatesandNext 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
/apps/cowswap-frontend/src/modules/affiliate/SPEC.mdfile with use-cases (useful for AGENTS)./apps/cowswap-frontend/src/modules/affiliate/misc.Summary by CodeRabbit
Release Notes
New Features
Chores