-
Notifications
You must be signed in to change notification settings - Fork 13
Better UX for token selection #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughDependency updates add Radix UI components and react-window. New UI primitives (Avatar, Dialog, Separator, Badge, CopyButton) are introduced. A new TokenPicker and hooks (useTokenList, useTokenSearch) replace static TOKEN_PRESETS across pages. Token verification via on-chain metadata is added. TOKEN_PRESETS file is removed. A TokenCrousel import was commented causing a runtime error. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Page as Page (Create/Generate)
participant TokenPicker
participant useTokenList
participant useTokenSearch
User->>TokenPicker: Open picker
TokenPicker->>useTokenList: useTokenList(chainId)
useTokenList-->>TokenPicker: { tokens, loading, error }
alt loading
TokenPicker-->>User: Show loading
else error
TokenPicker-->>User: Show error
else tokens loaded
User->>TokenPicker: Type query
TokenPicker->>useTokenSearch: setQuery(query)
useTokenSearch-->>TokenPicker: filtered tokens (debounced)
User->>TokenPicker: Select token
TokenPicker-->>Page: onSelect(token)
end
sequenceDiagram
autonumber
actor User
participant Page as Page (Create/Generate)
participant Verify as verifyToken()
participant Chain as Blockchain (ERC20)
User->>Page: Enter contract address
Page->>Verify: verifyToken(address)
Verify->>Chain: read symbol/name/decimals
alt success
Chain-->>Verify: {symbol,name,decimals}
Verify-->>Page: verified token data
Page-->>User: Show Verified + details
else failure
Chain-->>Verify: error
Verify-->>Page: error
Page-->>User: Show error guidance
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ 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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (9)
frontend/src/components/TokenCrousel.jsx (3)
4-9: ReferenceError at runtime: TOKEN_PRESETS is undefined.The import was commented out, but the code still spreads TOKEN_PRESETS to build duplicatedTokens. This will crash on first render.
Apply this minimal, future-proof change to accept tokens from props (works with your new hooks), and avoid any dependency on the removed presets:
-import { useEffect, useRef } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { motion } from "framer-motion"; import { SiEthereum } from "react-icons/si"; -// import { TOKEN_PRESETS } from "@/utils/erc20_token"; -const TokenCarousel = () => { - const carouselRef = useRef(); - const duplicatedTokens = [...TOKEN_PRESETS, ...TOKEN_PRESETS]; // Double the tokens for seamless loop +const TokenCarousel = ({ tokens = [] }) => { + const carouselRef = useRef(null); + const duplicatedTokens = useMemo(() => tokens.length ? [...tokens, ...tokens] : [], [tokens]);Follow-up: Update the calling site(s) to pass the token array from your new useTokenList hook once it resolves.
10-22: Guard against null refs and empty lists to avoid exceptions and wasted RAF cycles.If the ref is still null or the list is empty, the effect should bail. Also ensure we don’t read scrollWidth from an undefined node.
useEffect(() => { - const carousel = carouselRef.current; + const carousel = carouselRef.current; + if (!carousel || duplicatedTokens.length === 0) return; let animationFrame; let speed = 1; // Pixels per frame let position = 0; const animate = () => { position -= speed; - if (position <= -carousel.scrollWidth / 2) { + const half = carousel.scrollWidth / 2; + if (!Number.isFinite(half) || half <= 0) { + return; // nothing to animate yet + } + if (position <= -half) { position = 0; } carousel.style.transform = `translateX(${position}px)`; animationFrame = requestAnimationFrame(animate); }; - animate(); + animationFrame = requestAnimationFrame(animate);
84-85: RenameTokenCrousel.jsxtoTokenCarousel.jsxand update its importVerification shows the component file is misspelled and there’s a single import site that must be updated:
- File to rename:
•frontend/src/components/TokenCrousel.jsx→frontend/src/components/TokenCarousel.jsx- Import to update (Landing.jsx, line 14):
• Changetoimport TokenCarousel from "@/components/TokenCrousel";import TokenCarousel from "@/components/TokenCarousel";I can provide a simple jscodeshift or
sedcodemod to automate renaming the file and fixing all import paths—just let me know.frontend/src/page/SentInvoice.jsx (2)
155-161: Fix BigInt/Number mismatch when checking network chainIdEthers v6 returns
chainIdas a BigInt. Comparing to a Number throws or miscompares. Cast before comparing.Apply this diff:
- if (network.chainId != 11155111) { + if (Number(network.chainId) !== 11155111) { setError( `You're connected to ${network.name}. Please switch to Sepolia network to view your invoices.` ); setLoading(false); return; }
422-426: Guard address formatter against undefined/null
substringonundefinedwill throw when client address is missing. Add a safe fallback.Apply this diff:
- const formatAddress = (address) => { - return `${address.substring(0, 10)}...${address.substring( - address.length - 10 - )}`; - }; + const formatAddress = (addr) => { + if (!addr || typeof addr !== "string") return "—"; + const start = addr.slice(0, 10); + const end = addr.slice(-10); + return `${start}...${end}`; + };frontend/src/page/CreateInvoice.jsx (4)
20-27: Fix incorrect Badge import (will crash at runtime).Badge is imported from lucide-react, but it's a UI primitive defined in components/ui/badge. This will throw a module export error at build/runtime.
Apply this diff:
-import { - Badge, - CalendarIcon, - CheckCircle2, - Coins, - Loader2, - PlusIcon, - XCircle, -} from "lucide-react"; +import { + CalendarIcon, + CheckCircle2, + Coins, + Loader2, + PlusIcon, + XCircle, +} from "lucide-react"; @@ -import TokenPicker, { ToggleSwitch } from "@/components/TokenPicker"; +import TokenPicker, { ToggleSwitch } from "@/components/TokenPicker"; +import { Badge } from "@/components/ui/badge";Also applies to: 46-48
97-116: Remove legacy TOKEN_PRESETS usage; preselect using dynamic token list.TOKEN_PRESETS no longer exists. This reference will throw ReferenceError when a tokenAddress is present in the URL and customToken !== "true".
Apply this diff to resolve via the new token list:
- } else { - const preselectedToken = TOKEN_PRESETS.find( - (token) => - token.address.toLowerCase() === urlTokenAddress.toLowerCase() - ); - if (preselectedToken) { - setSelectedToken(preselectedToken); - setUseCustomToken(false); - } else { - setUseCustomToken(true); - setCustomTokenAddress(urlTokenAddress); - verifyToken(urlTokenAddress); - } - } + } else { + // Try to resolve from dynamic token list + const found = (tokens || []).find((t) => { + const ca = (t.contract_address || t.address || "").toLowerCase(); + return ca === urlTokenAddress.toLowerCase(); + }); + if (found) { + setSelectedToken({ + address: found.contract_address || found.address, + symbol: found.symbol, + name: found.name, + logo: found.image, + decimals: Number(found.decimals ?? 18), + }); + setUseCustomToken(false); + } else { + setUseCustomToken(true); + setCustomTokenAddress(urlTokenAddress); + verifyToken(urlTokenAddress); + } + }Add these supporting lines (outside the shown hunk):
// imports import { useTokenList } from "../hooks/useTokenList"; // inside component const { tokens } = useTokenList(account?.chainId || 1);
233-244: Validate payment token before building invoice payload.If no token is selected/verified, accessing paymentToken fields will throw. Also guard custom flow until verification succeeds.
const paymentToken = useCustomToken ? verifiedToken : selectedToken; - const invoicePayload = { + if (!paymentToken) { + alert("Please select a token or verify your custom token before creating the invoice."); + setLoading(false); + return; + } + if (useCustomToken && tokenVerificationState !== "success") { + alert("Please wait for token verification to complete."); + setLoading(false); + return; + } + + const invoicePayload = {
339-351: Enforce dynamic token decimals across all payment flowsWe’ve identified multiple instances where
18is still hard-coded for token decimals. To correctly support tokens with varying decimals, replace every18with the selected token’spaymentToken.decimals. Specifically:
- frontend/src/page/ReceivedInvoice.jsx (≈ line 402)
- const amountDueInWei = ethers.parseUnits(String(amountDue), 18);
- const amountDueInWei = ethers.parseUnits(String(amountDue), paymentToken.decimals);
- frontend/src/page/CreateInvoice.jsx
• Initial total calculation (≈ lines 121–125)- const qty = parseUnits(item.qty || "0", 18);
- const unitPrice = parseUnits(item.unitPrice|| "0", 18);
- const discount = parseUnits(item.discount || "0", 18);
- const tax = parseUnits(item.tax || "0", 18);
- const lineTotal = (qty * unitPrice) / parseUnits("1", 18);
- const qty = parseUnits(item.qty || "0", paymentToken.decimals);
- const unitPrice = parseUnits(item.unitPrice|| "0", paymentToken.decimals);
- const discount = parseUnits(item.discount || "0", paymentToken.decimals);
- const tax = parseUnits(item.tax || "0", paymentToken.decimals);
- const lineTotal = (qty * unitPrice) / parseUnits("1", paymentToken.decimals);
• **On-change recalculation** (≈ lines 165–170) ```diff
- const qty = parseUnits(updatedItem.qty || "0", 18);
- const unitPrice = parseUnits(updatedItem.unitPrice|| "0", 18);
- const discount = parseUnits(updatedItem.discount || "0", 18);
- const tax = parseUnits(updatedItem.tax || "0", 18);
- const lineTotal = (qty * unitPrice) / parseUnits("1", 18);
- const qty = parseUnits(updatedItem.qty || "0", paymentToken.decimals);
- const unitPrice = parseUnits(updatedItem.unitPrice|| "0", paymentToken.decimals);
- const discount = parseUnits(updatedItem.discount || "0", paymentToken.decimals);
- const tax = parseUnits(updatedItem.tax || "0", paymentToken.decimals);
- const lineTotal = (qty * unitPrice) / parseUnits("1", paymentToken.decimals);
- Token metadata definitions
• frontend/src/utils/CitreaTestnet.jsx (line 9) &
• frontend/src/page/GenerateLink.jsx (line 226)// Ensure these “decimals: 18” entries match each token’s actual decimals, // or load decimals from a shared config/token list instead of hard-coding.By refactoring these spots to use
paymentToken.decimals, the app will correctly handle tokens beyond the 18-decimal default.
🧹 Nitpick comments (26)
frontend/src/components/TokenCrousel.jsx (3)
34-41: Cleanup safety: remove listeners only if node exists.Tiny guard to avoid errors during unmount in edge cases.
return () => { - cancelAnimationFrame(animationFrame); - carousel.removeEventListener("mouseenter", pause); - carousel.removeEventListener("mouseleave", resume); + if (animationFrame) cancelAnimationFrame(animationFrame); + if (carousel) { + carousel.removeEventListener("mouseenter", pause); + carousel.removeEventListener("mouseleave", resume); + } };
48-51: Key stability: some tokens may lack an address; provide a robust key.Use a fallback based on chainId/symbol to avoid duplicate keys and React churn.
- {duplicatedTokens.map((token, index) => ( + {duplicatedTokens.map((token, index) => { + const key = token.address ?? `${token.chainId ?? 'na'}-${token.symbol ?? 'token'}-${index}`; + return ( <motion.div - key={`${token.address}-${index}`} + key={key}…and close the returned JSX accordingly.
64-69: Zero-address heuristic for native token is brittle.Relying on "0x000...000" to detect native currency will mislabel tokens across chains that encode natives differently. Prefer a semantic flag (e.g., token.isNative) from your token hook, or a central helper.
- {token.address === "0x0000000000000000000000000000000000000000" && ( + {(token.isNative === true) && ( <div className="absolute -bottom-1 -right-1 bg-green-500 rounded-full p-0.5"> <SiEthereum className="text-white text-xs" /> </div> )}If your data model doesn’t include isNative, add a small mapper in the hook layer to set it consistently per chain.
frontend/src/components/ui/copyButton.jsx (1)
1-39: Solid UX; add fallback + a11y polish + guard for empty text.Current behavior is fine. A few low-effort improvements:
- Guard when textToCopy is empty.
- Graceful fallback if navigator.clipboard is unavailable (HTTP, older browsers).
- Slightly longer success state improves perceived feedback.
- Expose aria-label for screen readers; avoid hard-coded “Copy address”.
-import { Check, Copy } from "lucide-react"; -import { useState } from "react"; +import { Check, Copy } from "lucide-react"; +import { useState } from "react"; -const CopyButton = ({ textToCopy, className = "" }) => { +const CopyButton = ({ textToCopy, className = "", ariaLabel = "Copy to clipboard" }) => { const [copied, setCopied] = useState(false); const handleCopy = async (e) => { e.stopPropagation(); try { - await navigator.clipboard.writeText(textToCopy); + if (!textToCopy) return; + if (navigator?.clipboard?.writeText) { + await navigator.clipboard.writeText(textToCopy); + } else { + // Fallback: use a temporary textarea + const ta = document.createElement("textarea"); + ta.value = textToCopy; + ta.setAttribute("readonly", ""); + ta.style.position = "absolute"; + ta.style.left = "-9999px"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + document.body.removeChild(ta); + } setCopied(true); - setTimeout(() => setCopied(false), 500); + setTimeout(() => setCopied(false), 1500); } catch (err) { console.error("Failed to copy:", err); } }; return ( <button type="button" onClick={handleCopy} - className={`inline-flex items-center gap-1 px-2 py-1 text-xs rounded hover:bg-gray-100 transition-colors ${className}`} - title="Copy address" + className={`inline-flex items-center gap-1 px-2 py-1 text-xs rounded hover:bg-gray-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${className}`} + title={ariaLabel} + aria-label={ariaLabel} + disabled={!textToCopy} > {copied ? ( <> <Check className="w-3 h-3 text-green-600" /> <span className="text-green-600">Copied!</span> </> ) : ( <> <Copy className="w-3 h-3 text-gray-500" /> <span className="text-gray-500">Copy</span> </> )} </button> ); };If you prefer toast-based feedback across the app, I can wire it to react-hot-toast instead of inline state.
frontend/src/components/ui/avatar.jsx (1)
1-18: Broken fallback on image error; consider Radix Avatar for a11y and consistency.As written, if the img fails to load, children won’t render because the presence of src keeps the
path active. Also, we added @radix-ui/react-avatar—using it gives us semantics and fallbacks for free.
Two options:
- Option A (minimal fix): Track error state and show children when load fails.
- Option B (preferred): Wrap Radix Avatar primitives for consistency with other UI components.
Minimal fix:
-const Avatar = ({ src, alt, className = "", children, onError }) => ( - <div - className={`inline-flex items-center justify-center rounded-full bg-gray-100 overflow-hidden ${className}`} - > - {src ? ( - <img - src={src} - alt={alt} - className="w-full h-full object-cover rounded-full" - onError={onError} - /> - ) : ( - <div className="flex items-center justify-center w-full h-full text-xs font-medium text-gray-600"> - {children} - </div> - )} - </div> -); +import * as React from "react"; + +const Avatar = ({ src, alt = "", className = "", children }) => { + const [errored, setErrored] = React.useState(false); + const showImage = !!src && !errored; + return ( + <div className={`inline-flex items-center justify-center rounded-full bg-gray-100 overflow-hidden ${className}`}> + {showImage ? ( + <img + src={src} + alt={alt} + loading="lazy" + decoding="async" + className="w-full h-full object-cover rounded-full" + onError={() => setErrored(true)} + /> + ) : ( + <div className="flex items-center justify-center w-full h-full text-xs font-medium text-gray-600"> + {children} + </div> + )} + </div> + ); +};If you want Option B, I can drop in a Radix-based wrapper to match the Separator pattern.
frontend/src/components/ui/separator.jsx (1)
21-23: Minor a11y/naming polish (optional).
- Consider exposing a role when decorative={false} so screen readers treat it as a separator.
- Exporting a default alongside named export can reduce import verbosity in consumers, but up to style.
-<no code change required> +# If you flip decorative to false at call sites, Radix will set role="separator". +# Document that in a JSDoc comment above the component for discoverability.frontend/src/hooks/useTokenSearch.js (3)
21-37: Prefix indexing builds O(N·L) entries; cap prefix length to control memoryIndexing every prefix of name/symbol for long strings grows maps quickly. Cap to a reasonable length (e.g., 10) to keep memory predictable.
Apply this diff:
- for (let i = 1; i <= symbol.length; i++) { + for (let i = 1; i <= Math.min(symbol.length, 10); i++) { const prefix = symbol.slice(0, i); ... } ... - for (let i = 1; i <= name.length; i++) { + for (let i = 1; i <= Math.min(name.length, 10); i++) { const prefix = name.slice(0, i); ... }
90-99: Address search is O(N) per keystroke; optionally use prefix-only index for 0x… inputsCurrent substring search over all addresses may stutter on large lists. If you restrict to prefix search (most natural for addresses), you can index by prefixes like you do for symbol/name.
If you choose this route, add an address-prefix index and replace the
includes()loop with a set lookup.
45-50: Either wire up loading state for the debounce, or remove it from the API
loading/errorare never updated. This can mislead consumers relying on them.Option A: remove them from the hook’s return. Option B: toggle
loadingaround the debounce:const [debouncedQuery, setDebouncedQuery] = useState(""); useEffect(() => { - const timer = setTimeout(() => { + setLoading(true); + const timer = setTimeout(() => { setDebouncedQuery(query); setPage(1); // Reset pagination when query changes + setLoading(false); }, 250); return () => clearTimeout(timer); }, [query]);Also applies to: 51-59
frontend/src/components/ui/dialog.jsx (1)
31-35: Optional: consider mobile overflow handlingOn small screens with tall content, adding
max-h-[90vh] overflow-y-autotoDialogContentimproves UX.Apply this diff:
- className={cn( - "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", - className - )} + className={cn( + "fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 sm:rounded-lg max-h-[90vh] overflow-y-auto data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]", + className + )}frontend/src/page/SentInvoice.jsx (2)
163-167: Replace alert with user-friendly error state and toastAvoid
alert()in-app. Use the existing error/toast patterns and stop the loading spinner.Apply this diff:
- if (!litNodeClient) { - alert("Lit client not initialized"); - return; - } + if (!litNodeClient) { + setError("Encryption client not initialized. Please refresh and try again."); + toast.error("Unable to initialize secure session"); + setLoading(false); + return; + }
151-153: Verify provider construction with wagmi’s WalletClientPassing
walletClientdirectly toBrowserProvidermay not be EIP-1193 compatible in all wagmi versions. Preferwindow.ethereumorwalletClient.transportwhere available.If needed, switch to:
- const provider = new BrowserProvider(walletClient); + const provider = new BrowserProvider( + walletClient?.transport?.value || window.ethereum + );Do the same in
handleCancelInvoice. Please confirm your wagmi/ethers versions and adjust accordingly.Also applies to: 381-387
frontend/src/hooks/useTokenList.js (2)
60-62: Remove debug loggingLeftover
console.log(data)adds noise.Apply this diff:
- const data = await response.json(); - console.log(data); + const data = await response.json();
48-55: Consider making the token-list URL configurableHardcoding a GitHub URL is brittle. Prefer an env/config value so you can swap hosts without redeploying.
Example:
- const dataUrl = `https://raw.githubusercontent.com/StabilityNexus/TokenList/main/${ChainIdToName[chainId]}-tokens.json`; + const base = import.meta.env.VITE_TOKENLIST_BASE_URL + ?? "https://raw.githubusercontent.com/StabilityNexus/TokenList/main"; + const dataUrl = `${base}/${ChainIdToName[chainId]}-tokens.json`;frontend/src/page/CreateInvoice.jsx (4)
119-132: Tax labeled as percentage is added as absolute amount.Headers/UI say “TAX(%)”, but calculations add tax as a flat amount. This will miscompute line totals and the invoice.
If tax is a percentage, compute it relative to (qty*unitPrice - discount). Example fix within handleItemData:
- const tax = parseUnits(updatedItem.tax || "0", 18); - const lineTotal = (qty * unitPrice) / parseUnits("1", 18); - const finalAmount = lineTotal - discount + tax; + const taxPct = parseUnits(updatedItem.tax || "0", 18); // e.g., "10" => 10% + const one = parseUnits("1", 18); + const hundred = parseUnits("100", 18); + const lineTotal = (qty * unitPrice) / one; + const base = lineTotal - discount; + const tax = (base * taxPct) / hundred; + const finalAmount = base + tax;Adjust the read-only UI calculation similarly to avoid display drift.
Also applies to: 154-174, 892-899
196-216: Use walletClient as primary provider for verification; window.ethereum as fallback.Keeps behavior consistent with the rest of the page and allows verification when injected provider is unavailable.
- try { - if (typeof window !== "undefined" && window.ethereum) { - const provider = new BrowserProvider(window.ethereum); + try { + const provider = walletClient + ? new BrowserProvider(walletClient) + : (typeof window !== "undefined" && window.ethereum) + ? new BrowserProvider(window.ethereum) + : null; + if (provider) { const contract = new ethers.Contract(address, ERC20_ABI, provider); const [symbol, name, decimals] = await Promise.all([ contract.symbol().catch(() => "UNKNOWN"), contract.name().catch(() => "Unknown Token"), contract.decimals().catch(() => 18), ]); setVerifiedToken({ address, symbol, name, decimals }); setTokenVerificationState("success"); } else { console.error("No Ethereum provider found"); setTokenVerificationState("error"); }
50-53: Avoid double useAccount calls; destructure once.Minor cleanup: get address/isConnected/chainId from a single useAccount to prevent mismatched states across hooks.
- const { data: walletClient } = useWalletClient(); - const { isConnected } = useAccount(); - const account = useAccount(); + const { data: walletClient } = useWalletClient(); + const account = useAccount(); // if you need the full object only + // or better: + // const { address: accountAddress, isConnected, chainId } = useAccount();
70-73: Remove dead constant.TESTNET_TOKEN is unused.
- const TESTNET_TOKEN = ["0xB5E9C6e57C9d312937A059089B547d0036c155C7"]; //sepolia based chainvoice test token (CIN)frontend/src/components/TokenPicker.jsx (2)
1-11: Remove unused icons.ToggleLeft and ToggleRight are imported but never used.
-import { - Search, - X, - AlertCircle, - Loader2, - ChevronDown, - Coins, - ToggleLeft, - ToggleRight, -} from "lucide-react"; +import { Search, X, AlertCircle, Loader2, ChevronDown, Coins } from "lucide-react";
90-111: Fix regex highlighting logic to avoid stateful .test() bug.Using a global regex with .test() within map is stateful and can miss matches. Since split() uses a capturing group, matched parts are at odd indices.
function HighlightMatch({ text, query }) { if (!query.trim()) return <>{text}</>; - const regex = new RegExp( - `(${query.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")})`, - "gi" - ); - const parts = text.split(regex); + const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")})`, "gi"); + const parts = text.split(regex); return ( <> - {parts.map((part, index) => - regex.test(part) ? ( + {parts.map((part, index) => + index % 2 === 1 ? ( <span key={index} className="bg-blue-100 text-blue-600 rounded px-0.5 font-medium" > {part} </span> ) : ( part ) )} </> ); }frontend/src/page/GenerateLink.jsx (4)
17-27: Clean up unused imports.cn, CopyButton, Badge, and PlusIcon are imported but not used.
-import { cn } from "@/lib/utils"; @@ -import TokenPicker, { ToggleSwitch } from "@/components/TokenPicker"; -import { CopyButton } from "@/components/ui/copyButton"; -import { Badge } from "@/components/ui/badge"; +import TokenPicker, { ToggleSwitch } from "@/components/TokenPicker"; @@ -import { Copy, Link, Check, Wallet, PlusIcon, Loader2, CheckCircle2, XCircle, Coins, } from "lucide-react"; +import { Copy, Link, Check, Wallet, Loader2, CheckCircle2, XCircle, Coins } from "lucide-react";
62-63: Remove unused constant.TESTNET_TOKEN is not referenced.
- const TESTNET_TOKEN = ["0xB5E9C6e57C9d312937A059089B547d0036c155C7"];
218-233: Avoid hardcoding decimals=18 for selected tokens (future proofing).While decimals aren’t used in link generation, keeping the accurate value prevents propagation of incorrect metadata if this object is reused.
- setSelectedToken({ - address: token.contract_address, - symbol: token.symbol, - name: token.name, - logo: token.image, - decimals: 18, - }); + setSelectedToken({ + address: token.contract_address, + symbol: token.symbol, + name: token.name, + logo: token.image, + decimals: Number(token.decimals ?? 18), + });Also consider extending useTokenList to include decimals if the upstream JSON has it (see note below).
104-119: Permit verification without walletClient by falling back to window.ethereum.This keeps the “auto-verify” promise even if wagmi isn’t connected yet.
- if (!walletClient) return; - setTokenVerificationState("verifying"); try { - const provider = new BrowserProvider(walletClient); + const provider = walletClient + ? new BrowserProvider(walletClient) + : (typeof window !== "undefined" && window.ethereum) + ? new BrowserProvider(window.ethereum) + : null; + if (!provider) { + setTokenVerificationState("error"); + return; + }frontend/src/page/ReceivedInvoice.jsx (2)
652-663: Use e.currentTarget for image fallback.Minor: prefer currentTarget over target to avoid surprises with event bubbling.
- onError={(e) => { - e.target.src = "/tokenImages/generic.png"; - }} + onError={(e) => { + e.currentTarget.src = "/tokenImages/generic.png"; + }}Also applies to: 951-955
43-44: Consider enriching token list with decimals to reduce chain calls.getTokenDecimals relies on tokenInfo.decimals, but useTokenList currently drops decimals during transform. If your TokenList JSON includes decimals, carry it through to avoid extra RPC calls later.
Outside this file, adjust useTokenList transformation:
// in useTokenList.js transform const transformedData = data.map((token) => ({ contract_address: token.contract_address || token.address, symbol: token.symbol, name: token.name, image: token.image || token.logo || "/tokenImages/generic.png", decimals: token.decimals, // preserve if present }));Also applies to: 70-72, 340-342
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (15)
frontend/package.json(3 hunks)frontend/src/components/TokenCrousel.jsx(1 hunks)frontend/src/components/TokenPicker.jsx(1 hunks)frontend/src/components/ui/avatar.jsx(1 hunks)frontend/src/components/ui/badge.jsx(1 hunks)frontend/src/components/ui/copyButton.jsx(1 hunks)frontend/src/components/ui/dialog.jsx(1 hunks)frontend/src/components/ui/separator.jsx(1 hunks)frontend/src/hooks/useTokenList.js(1 hunks)frontend/src/hooks/useTokenSearch.js(1 hunks)frontend/src/page/CreateInvoice.jsx(7 hunks)frontend/src/page/GenerateLink.jsx(8 hunks)frontend/src/page/ReceivedInvoice.jsx(14 hunks)frontend/src/page/SentInvoice.jsx(12 hunks)frontend/src/utils/erc20_token.js(0 hunks)
💤 Files with no reviewable changes (1)
- frontend/src/utils/erc20_token.js
🧰 Additional context used
🧬 Code graph analysis (9)
frontend/src/components/TokenPicker.jsx (8)
frontend/src/lib/utils.js (1)
cn(4-6)frontend/src/hooks/useTokenSearch.js (3)
query(48-48)filteredTokens(64-101)useTokenSearch(45-118)frontend/src/components/ui/avatar.jsx (1)
Avatar(1-18)frontend/src/components/ui/badge.jsx (1)
Badge(1-7)frontend/src/components/ui/copyButton.jsx (1)
CopyButton(5-39)frontend/src/hooks/useTokenList.js (2)
useTokenList(18-86)tokens(19-19)frontend/src/components/ui/button.jsx (1)
Button(37-45)frontend/src/components/ui/input.jsx (1)
Input(5-16)
frontend/src/page/SentInvoice.jsx (4)
frontend/src/page/GenerateLink.jsx (3)
useAccount(30-30)useTokenList(38-38)walletClient(31-31)frontend/src/page/ReceivedInvoice.jsx (6)
useAccount(59-59)useTokenList(71-71)getTokenInfo(83-91)getTokenLogo(94-102)getTokenDecimals(105-108)walletClient(58-58)frontend/src/hooks/useTokenList.js (1)
tokens(19-19)frontend/src/contractsABI/ERC20_ABI.js (2)
ERC20_ABI(1-10)ERC20_ABI(1-10)
frontend/src/components/ui/dialog.jsx (1)
frontend/src/lib/utils.js (1)
cn(4-6)
frontend/src/hooks/useTokenSearch.js (2)
frontend/src/hooks/useTokenList.js (1)
tokens(19-19)frontend/src/components/TokenPicker.jsx (1)
filteredTokens(193-193)
frontend/src/hooks/useTokenList.js (2)
frontend/src/page/GenerateLink.jsx (1)
useTokenList(38-38)frontend/src/page/ReceivedInvoice.jsx (1)
useTokenList(71-71)
frontend/src/components/ui/separator.jsx (1)
frontend/src/lib/utils.js (1)
cn(4-6)
frontend/src/page/GenerateLink.jsx (4)
frontend/src/components/ui/copyButton.jsx (2)
copied(6-6)CopyButton(5-39)frontend/src/hooks/useTokenList.js (3)
useTokenList(18-86)loading(20-20)tokens(19-19)frontend/src/components/TokenPicker.jsx (3)
ToggleSwitch(50-84)ToggleSwitch(50-84)TokenPicker(175-382)frontend/src/components/ui/badge.jsx (1)
Badge(1-7)
frontend/src/page/CreateInvoice.jsx (4)
frontend/src/page/GenerateLink.jsx (2)
loading(46-46)verifyToken(104-124)frontend/src/components/TokenPicker.jsx (3)
ToggleSwitch(50-84)ToggleSwitch(50-84)TokenPicker(175-382)frontend/src/components/ui/badge.jsx (1)
Badge(1-7)frontend/src/components/ui/copyButton.jsx (1)
CopyButton(5-39)
frontend/src/page/ReceivedInvoice.jsx (3)
frontend/src/page/SentInvoice.jsx (5)
useTokenList(79-79)getTokenInfo(82-90)getTokenLogo(93-101)getTokenDecimals(104-107)error(69-69)frontend/src/hooks/useTokenList.js (3)
useTokenList(18-86)tokens(19-19)error(21-21)frontend/src/contractsABI/ERC20_ABI.js (2)
ERC20_ABI(1-10)ERC20_ABI(1-10)
🔇 Additional comments (14)
frontend/package.json (1)
21-22: All specified versions validated & peer dependencies are compatible
- Confirmed that the following packages exist on npm and match the versions in frontend/package.json:
• framer-motion@^12.23.12
• @radix-ui/react-avatar@^1.1.10
• @radix-ui/react-dialog@^1.1.15
• @radix-ui/react-separator@^1.1.7
• @radix-ui/react-slot@^1.2.3
• react-window@^1.8.11- Verified peerDependencies:
• wagmi requires react >=18 (covers 18.3.x), viem@2.x, @tanstack/react-query@>=5.0.0
• viem only requires TypeScript@>=5.0.4
• @radix-ui/react-dialog supports react ^16.8 || ^17 || ^18 || ^19
• framer-motion supports react ^18 || ^19
• ethers declares no peerDependencies- Project’s React (18.3.x) and Vite 6 usage remain fully compatible—none of these libraries impose Vite peer constraints.
Optional recommendations (non–blocking):
- Add an “engines” field, e.g.
"engines": { "node": ">=18.17" }- Add a “packageManager” lock, e.g.
"packageManager": "pnpm@LATEST" | "npm@LATEST"Applies to lines 21–22, 26–27, 37, 50 in frontend/package.json.
frontend/src/components/ui/copyButton.jsx (1)
41-41: Named export is consistent.Keeping a named export here is fine and aligns with other UI primitives.
frontend/src/components/ui/avatar.jsx (1)
19-19: Export style is fine.Named export matches how you’re composing other primitives.
frontend/src/components/ui/separator.jsx (1)
6-20: LGTM; idiomatic Radix wrapper with good defaults.
- Correctly forwards ref and props.
- Orientation-based sizing is clean; cn usage aligns with project utils.
- Decorative defaults are sensible.
frontend/src/components/ui/badge.jsx (2)
1-7: LGTM — simple, consistent primitiveCompact, accessible, and Tailwind-friendly. No functional concerns.
1-9: Badge export verification complete – no breaking references found
- No occurrences of
badgeVariantsdetected across.js,.jsx,.ts, or.tsxfiles.- No default-import usages of
Badgefound in the codebase.- The component is safely consumed via its named export; adding
remains an optional convenience rather than a necessity.+export default Badgefrontend/src/components/ui/dialog.jsx (1)
15-25: LGTM — well-structured Radix wrappers with sensible defaultsForwarded refs, class merging, overlay/content layering, and accessible close control are all set up correctly.
Also applies to: 26-45, 67-81
frontend/src/page/CreateInvoice.jsx (1)
1011-1016: Display fallback symbol is OK.Nice touch using verifiedToken?.symbol / selectedToken?.symbol with a neutral "TOKEN" fallback to keep totals readable before selection.
frontend/src/components/TokenPicker.jsx (2)
136-146: Nice avatar fallbacks and truncation.Good use of a generic token image fallback and symbol initials; the truncation keeps rows tight.
Also applies to: 247-255
340-351: Selection predicate covers both shapes.Accounting for selected.contract_address vs selected.address is correct given caller variability.
frontend/src/page/GenerateLink.jsx (2)
186-211: Great UX for toggleable token selection vs custom address.The two-mode flow, verification states, and contextual notes are solid and consistent with CreateInvoice.
Also applies to: 356-401
77-84: Link param composition looks correct.tokenAddress resolves to either address or contract_address; chain param fallback to "1". Good.
frontend/src/page/ReceivedInvoice.jsx (2)
272-321: Good fallback strategy for unknown tokens.Attempting on-chain symbol/name/decimals when the token isn’t in the list keeps invoices readable. The generic logo fallback is sensible.
343-412: ERC-20 vs native payment branches make sense.
- For ERC-20: ensure allowance ≥ amountDue, approve if needed, pay with ETH fee only.
- For native: include fee + amount in value.
Nice UX feedback via toasts/alerts.
Also applies to: 1056-1074
Updated the UI/UX for token selection.
Then
Now
Summary by CodeRabbit
New Features
Bug Fixes
Chores