[TASK-14416] Feat/profile exchange rate#1206
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds a new ExchangeRateWidget component, a client-side profile page at src/app/(mobile-ui)/profile/exchange-rate/page.tsx that uses it, replaces the landing page exchange UI with the widget, and updates profile menu entries and invites comment-out. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks (2 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
src/components/LandingPage/noFees.tsx (1)
9-11: Naming consistency with widget.If you adopt the “Widget” rename, update this import too (see earlier comment).
Apply this diff once the component is renamed:
-import ExchangeRateWiget from '../Global/ExchangeRateWidget' +import ExchangeRateWidget from '../Global/ExchangeRateWidget' @@ - <ExchangeRateWiget + <ExchangeRateWidget
🧹 Nitpick comments (7)
src/components/Profile/index.tsx (2)
47-55: Keep the Invite entry comment actionable (add a TODO with ticket/feature flag).Add a TODO with the Invites project ticket/flag so this doesn’t get lost.
95-97: Unify copy with page title; update to “Exchange rate & fees”.The page header uses “Exchange rate & fees” while this label says “Exchange rates and fees”. Suggest aligning here.
Apply this diff:
- label="Exchange rates and fees" + label="Exchange rate & fees"src/app/(mobile-ui)/profile/exchange-rate/page.tsx (1)
13-13: Copy consistency with Profile menu.Title “Exchange rate & fees” is good; ensure the Profile menu item uses the same phrasing (see suggested change there).
src/components/Global/ExchangeRateWidget/index.tsx (4)
80-88: Provide a safe fallback for flags to avoid broken images.If a mapping isn’t found,
flagCodeis undefined and the CDN URL breaks.Apply this diff:
- const sourceCurrencyFlag = useMemo( - () => countryCurrencyMappings.find((currency) => currency.currencyCode === sourceCurrency)?.flagCode, - [sourceCurrency] - ) + const sourceCurrencyFlag = useMemo(() => { + return ( + countryCurrencyMappings.find((c) => c.currencyCode === sourceCurrency)?.flagCode || null + ) + }, [sourceCurrency]) @@ - const destinationCurrencyFlag = useMemo( - () => countryCurrencyMappings.find((currency) => currency.currencyCode === destinationCurrency)?.flagCode, - [destinationCurrency] - ) + const destinationCurrencyFlag = useMemo(() => { + return ( + countryCurrencyMappings.find((c) => c.currencyCode === destinationCurrency)?.flagCode || null + ) + }, [destinationCurrency])And render flags conditionally (see next comment).
131-139: Guard flag image rendering to prevent 404s; add mobile-friendly input attributes.Render the flag only when available; also help mobile numeric keypad.
Apply this diff:
- <Image - src={`https://flagcdn.com/w320/${sourceCurrencyFlag}.png`} - alt={`${sourceCurrencyFlag} flag`} - width={160} - height={160} - className="size-4 rounded-full object-cover" - /> + {sourceCurrencyFlag && ( + <Image + src={`https://flagcdn.com/w320/${sourceCurrencyFlag}.png`} + alt={`${sourceCurrency} flag`} + width={160} + height={160} + className="size-4 rounded-full object-cover" + /> + )}Also consider on the input (Line 120) adding:
- type="number" + type="number" + inputMode="decimal" + step="any"
175-183: Apply the same flag guard for destination.Mirror the conditional rendering for destination currency.
Apply this diff:
- <Image - src={`https://flagcdn.com/w320/${destinationCurrencyFlag}.png`} - alt={`${destinationCurrencyFlag} flag`} - width={160} - height={160} - className="size-4 rounded-full object-cover" - /> + {destinationCurrencyFlag && ( + <Image + src={`https://flagcdn.com/w320/${destinationCurrencyFlag}.png`} + alt={`${destinationCurrency} flag`} + width={160} + height={160} + className="size-4 rounded-full object-cover" + /> + )}And add on the input (Line 166):
- type="number" + type="number" + inputMode="decimal" + step="any"
124-141: Prevent selecting identical currencies (optional UX).Enable
excludeCurrenciesto avoid picking the same currency on both sides.Apply this diff:
- <CurrencySelect + <CurrencySelect selectedCurrency={sourceCurrency} setSelectedCurrency={setSourceCurrency} - // excludeCurrencies={[destinationCurrency]} + excludeCurrencies={[destinationCurrency]} @@ - <CurrencySelect + <CurrencySelect selectedCurrency={destinationCurrency} setSelectedCurrency={setDestinationCurrency} + excludeCurrencies={[sourceCurrency]}Also applies to: 169-185
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/app/(mobile-ui)/profile/exchange-rate/page.tsx(1 hunks)src/components/Global/ExchangeRateWidget/index.tsx(1 hunks)src/components/LandingPage/noFees.tsx(2 hunks)src/components/Profile/index.tsx(2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-07T15:25:45.170Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#422
File: src/components/Request/Pay/Views/Initial.view.tsx:76-78
Timestamp: 2024-10-07T15:25:45.170Z
Learning: In `src/components/Request/Pay/Views/Initial.view.tsx`, both `txFee` and `utils.formatTokenAmount(...)` return strings, ensuring that `calculatedFee` consistently returns a string without the need for additional type conversion.
Applied to files:
src/components/LandingPage/noFees.tsx
📚 Learning: 2024-10-07T15:28:25.280Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#422
File: src/components/Request/Pay/Views/Initial.view.tsx:76-78
Timestamp: 2024-10-07T15:28:25.280Z
Learning: In `src/components/Request/Pay/Views/Initial.view.tsx`, both `txFee` and `utils.formatTokenAmount(estimatedGasCost, 3)` return strings, ensuring consistent return types for `calculatedFee`.
Applied to files:
src/components/LandingPage/noFees.tsx
🧬 Code graph analysis (1)
src/components/Global/ExchangeRateWidget/index.tsx (5)
src/components/Global/Icons/Icon.tsx (2)
IconName(64-124)Icon(195-204)src/hooks/useExchangeRate.ts (1)
useExchangeRate(26-150)src/hooks/useDebounce.ts (1)
useDebounce(9-23)src/constants/countryCurrencyMapping.ts (1)
countryCurrencyMappings(10-48)src/components/0_Bruddle/Button.tsx (1)
Button(76-267)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy-Preview
🔇 Additional comments (2)
src/components/LandingPage/noFees.tsx (1)
127-131: LGTM: Clean integration and CTA wiring.Good reuse of the widget with a clear CTA to “/setup”.
src/components/Global/ExchangeRateWidget/index.tsx (1)
131-137: No action required — next.config.js already allows flagcdn.com.images.remotePatterns in next.config.js includes hostname: '*' for both http and https, so the flagcdn Image will load in production.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/components/Global/ExchangeRateWidget/index.tsx (1)
236-236: Rename typo fixed: ExchangeRateWiget → ExchangeRateWidget. LGTM.
🧹 Nitpick comments (8)
src/app/(mobile-ui)/profile/exchange-rate/page.tsx (2)
13-13: Align copy with menu label (“Exchange rates and fees”).Minor text consistency nit; matches the profile menu item.
Apply:
- <NavHeader title="Exchange rate & fees" onPrev={() => router.replace('/profile')} /> + <NavHeader title="Exchange rates and fees" onPrev={() => router.replace('/profile')} />
13-13: Back UX: consider router.back() to preserve deep-link history.If users arrive via a direct link, replace('/profile') may feel jarring. Consider falling back to /profile only when no history.
Example:
- <NavHeader title="Exchange rates and fees" onPrev={() => router.replace('/profile')} /> + <NavHeader + title="Exchange rates and fees" + onPrev={() => (window.history.length > 1 ? router.back() : router.replace('/profile'))} + />src/components/Global/ExchangeRateWidget/index.tsx (6)
132-141: Guard flag image rendering and improve alt text.Avoid broken images when a mapping is missing; use currency code in alt.
Apply:
- <Image - src={`https://flagcdn.com/w320/${sourceCurrencyFlag}.png`} - alt={`${sourceCurrencyFlag} flag`} - width={160} - height={160} - className="size-4 rounded-full object-cover" - /> + {sourceCurrencyFlag ? ( + <Image + src={`https://flagcdn.com/w320/${sourceCurrencyFlag}.png`} + alt={`${sourceCurrency} flag`} + width={160} + height={160} + className="size-4 rounded-full object-cover" + /> + ) : ( + <div className="size-4 rounded-full bg-grey-2" aria-hidden /> + )} @@ - <Image - src={`https://flagcdn.com/w320/${destinationCurrencyFlag}.png`} - alt={`${destinationCurrencyFlag} flag`} - width={160} - height={160} - className="size-4 rounded-full object-cover" - /> + {destinationCurrencyFlag ? ( + <Image + src={`https://flagcdn.com/w320/${destinationCurrencyFlag}.png`} + alt={`${destinationCurrency} flag`} + width={160} + height={160} + className="size-4 rounded-full object-cover" + /> + ) : ( + <div className="size-4 rounded-full bg-grey-2" aria-hidden /> + )}Also applies to: 176-186
122-124: Better numeric input UX on mobile.Allow decimals without spinners.
Apply:
- type="number" + type="number" + inputMode="decimal" + step="any" @@ - type="number" + type="number" + inputMode="decimal" + step="any"Also applies to: 167-169
191-199: Hide misleading “0.0000” when rate is unavailable.Treat non-positive rates as unavailable.
Apply:
- ) : ( - <> - 1 {sourceCurrency} = {exchangeRate.toFixed(4)} {destinationCurrency} - </> - )} + ) : isError || exchangeRate <= 0 ? ( + <span>Rate currently unavailable</span> + ) : ( + <>1 {sourceCurrency} = {exchangeRate.toFixed(4)} {destinationCurrency}</> + )}
129-131: Prevent selecting identical currencies (if supported by CurrencySelect).Pass excludeCurrencies to each selector to avoid same-from/to.
Apply (verify prop name exists):
- // excludeCurrencies={[destinationCurrency]} + excludeCurrencies={[destinationCurrency]} @@ - setSelectedCurrency={setDestinationCurrency} + setSelectedCurrency={setDestinationCurrency} + excludeCurrencies={[sourceCurrency]}Also applies to: 171-174
11-15: Make URL syncing optional for embed contexts (landing pages).Avoid querystring churn where it’s undesirable; default remains on.
Apply:
interface IExchangeRateWidgetProps { ctaLabel: string ctaIcon: IconName ctaAction: () => void + syncUrl?: boolean } @@ -const ExchangeRateWidget: FC<IExchangeRateWidgetProps> = ({ ctaLabel, ctaIcon, ctaAction }) => { +const ExchangeRateWidget: FC<IExchangeRateWidgetProps> = ({ ctaLabel, ctaIcon, ctaAction, syncUrl = true }) => { @@ - const updateUrlParams = useCallback( + const updateUrlParams = useCallback( (params: { from?: string; to?: string; amount?: number }) => { + if (!syncUrl) return const newSearchParams = new URLSearchParams(searchParams.toString()) @@ - router.replace(`?${newSearchParams.toString()}`, { scroll: false }) + router.replace(`?${newSearchParams.toString()}`, { scroll: false }) }, - [searchParams, router] + [searchParams, router, syncUrl] ) @@ - useEffect(() => { + useEffect(() => { + if (!syncUrl) return if (typeof debouncedSourceAmount === 'number' && debouncedSourceAmount !== urlSourceAmount) { updateUrlParams({ amount: debouncedSourceAmount }) } - }, [debouncedSourceAmount, urlSourceAmount, updateUrlParams]) + }, [debouncedSourceAmount, urlSourceAmount, updateUrlParams, syncUrl])Then, in NoFees usage:
-<ExchangeRateWidget +<ExchangeRateWidget + syncUrl={false}Also applies to: 17-20, 47-58, 75-80
100-115: UI polish (optional): add labels htmlFor and aria attributes.Improves accessibility for the two inputs.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/app/(mobile-ui)/profile/exchange-rate/page.tsx(1 hunks)src/components/Global/ExchangeRateWidget/index.tsx(1 hunks)src/components/LandingPage/noFees.tsx(2 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2024-10-07T15:25:45.170Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#422
File: src/components/Request/Pay/Views/Initial.view.tsx:76-78
Timestamp: 2024-10-07T15:25:45.170Z
Learning: In `src/components/Request/Pay/Views/Initial.view.tsx`, both `txFee` and `utils.formatTokenAmount(...)` return strings, ensuring that `calculatedFee` consistently returns a string without the need for additional type conversion.
Applied to files:
src/components/LandingPage/noFees.tsx
📚 Learning: 2025-08-14T14:42:54.411Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1094
File: src/utils/withdraw.utils.ts:181-191
Timestamp: 2025-08-14T14:42:54.411Z
Learning: The countryCodeMap in src/components/AddMoney/consts/index.ts uses uppercase 3-letter country codes as keys (like 'AUT', 'BEL', 'CZE') that map to 2-letter country codes, requiring input normalization to uppercase for proper lookups.
Applied to files:
src/components/Global/ExchangeRateWidget/index.tsx
📚 Learning: 2025-08-15T08:04:31.171Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1098
File: src/components/LandingPage/noFees.tsx:26-26
Timestamp: 2025-08-15T08:04:31.171Z
Learning: In the peanut-ui codebase, URL parameter parsing with parseFloat() for numeric values is considered safe due to: 1) parseFloat() only returns numbers/NaN (no executable code), 2) comprehensive validation exists elsewhere in the codebase, 3) values flow through type-safe React components, and 4) no direct DOM manipulation occurs that could expose XSS vectors.
Applied to files:
src/components/Global/ExchangeRateWidget/index.tsx
📚 Learning: 2025-08-22T07:25:59.304Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1104
File: src/components/Payment/PaymentForm/index.tsx:596-600
Timestamp: 2025-08-22T07:25:59.304Z
Learning: The `TokenAmountInput` component in `src/components/Global/TokenAmountInput/` always returns decimal strings (e.g., "1,234.56"), not base units. When passing these values to external APIs like Daimo's `toUnits` prop, simply stripping commas with `.replace(/,/g, '')` is sufficient.
Applied to files:
src/components/Global/ExchangeRateWidget/index.tsx
📚 Learning: 2024-10-29T12:20:47.207Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#495
File: src/components/Create/Link/Input.view.tsx:244-248
Timestamp: 2024-10-29T12:20:47.207Z
Learning: In the `TokenAmountInput` component within `src/components/Global/TokenAmountInput/index.tsx`, when `balance` is undefined, the `maxValue` prop should be set to an empty string `''`.
Applied to files:
src/components/Global/ExchangeRateWidget/index.tsx
📚 Learning: 2024-12-11T10:13:22.806Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#564
File: src/components/Request/Pay/Views/Initial.view.tsx:430-430
Timestamp: 2024-12-11T10:13:22.806Z
Learning: In the React TypeScript file `src/components/Request/Pay/Views/Initial.view.tsx`, when reviewing the `InitialView` component, do not flag potential issues with using non-null assertion `!` on the `slippagePercentage` variable, as handling undefined values in this context is considered out of scope.
Applied to files:
src/components/Global/ExchangeRateWidget/index.tsx
🧬 Code graph analysis (1)
src/components/Global/ExchangeRateWidget/index.tsx (5)
src/components/Global/Icons/Icon.tsx (2)
IconName(64-124)Icon(195-204)src/hooks/useExchangeRate.ts (1)
useExchangeRate(26-150)src/hooks/useDebounce.ts (1)
useDebounce(9-23)src/constants/countryCurrencyMapping.ts (1)
countryCurrencyMappings(10-48)src/components/0_Bruddle/Button.tsx (1)
Button(76-267)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy-Preview
🔇 Additional comments (5)
src/app/(mobile-ui)/profile/exchange-rate/page.tsx (1)
15-19: Resolved — /add-money route exists.
Found route pages:
- src/app/(mobile-ui)/add-money/page.tsx
- src/app/(mobile-ui)/add-money/[country]/page.tsx
- src/app/(mobile-ui)/add-money/[country]/bank/page.tsx
- src/app/(mobile-ui)/add-money/crypto/page.tsx
- src/app/(mobile-ui)/add-money/crypto/direct/page.tsx
- src/app/(mobile-ui)/add-money/us/bank/page.tsx
CTA target is present; no change required.
src/components/LandingPage/noFees.tsx (2)
9-15: Nice simplification—delegating to ExchangeRateWidget reduces duplication.
127-131: /setup route present — no action required.
Found at src/app/(setup)/setup/page.tsx; router.push('/setup') will navigate to that route.src/components/Global/ExchangeRateWidget/index.tsx (2)
1-10: Good fixes: named import for countryCurrencyMappings and clean props typing.
132-139: No action required — Next/Image already allows flagcdn.com. next.config.js defines images.remotePatterns with hostname '*' for both http and https, so flagcdn.com is permitted.
Contributes to TASK-14414 as well