Skip to content

[TASK-14416] Feat/profile exchange rate#1206

Merged
Zishan-7 merged 5 commits intopeanut-wallet-devfrom
feat/profile-exchange-rate
Sep 12, 2025
Merged

[TASK-14416] Feat/profile exchange rate#1206
Zishan-7 merged 5 commits intopeanut-wallet-devfrom
feat/profile-exchange-rate

Conversation

@Zishan-7
Copy link
Contributor

@Zishan-7 Zishan-7 commented Sep 11, 2025

Contributes to TASK-14414 as well

image image

@Zishan-7 Zishan-7 requested a review from Hugo0 September 11, 2025 17:24
@vercel
Copy link

vercel bot commented Sep 11, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
peanut-wallet Ready Ready Preview Comment Sep 11, 2025 5:55pm

@notion-workspace
Copy link

@notion-workspace
Copy link

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 11, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Exchange rate widget component
src/components/Global/ExchangeRateWidget/index.tsx
New ExchangeRateWidget component: reads from, to, amount from URL, uses useExchangeRate, debounces source input, syncs URL via router.replace, derives flag codes, renders send/receive inputs, rate line, fee card, delivery time, and a configurable CTA.
Profile exchange-rate page
src/app/(mobile-ui)/profile/exchange-rate/page.tsx
New client page ExchangeRatePage (uses 'use client') that renders ExchangeRateWidget inside PageContainer with NavHeader; back navigates to /profile; widget CTA navigates to /add-money.
Landing page integration
src/components/LandingPage/noFees.tsx
Replaces previous inlined URL-driven exchange UI with ExchangeRateWidget; removes URL/state/exchange logic from this file; wires widget CTA to /setup; retains decorative visuals and cloud animation helper.
Profile menu updates
src/components/Profile/index.tsx
Updates menu entry label to “Exchange rates and fees” and href to /profile/exchange-rate, removes comingSoon; comments out Invite friends menu item.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Hugo0
  • jjramirezn

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "[TASK-14416] Feat/profile exchange rate" is concise and directly reflects the primary change in the diff (adding a profile exchange-rate page and related widget and updating the Profile menu), and it includes the task reference for traceability; it therefore relates to the changeset and is readable by reviewers scanning PR history.
Description Check ✅ Passed The PR description is minimal but on-topic: it references the related task (TASK-14414) and includes two screenshot attachments that appear to document the UI changes, so it is related to the changeset and satisfies this lenient check.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/profile-exchange-rate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 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, flagCode is 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 excludeCurrencies to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0d88d and 68bcbe5.

📒 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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 68bcbe5 and 9ac02d4.

📒 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.

@Zishan-7 Zishan-7 merged commit b37c3cc into peanut-wallet-dev Sep 12, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants