From 075a5fda40c6fc24b4d5d7d58bd6c7c353eae79c Mon Sep 17 00:00:00 2001 From: sktbrd Date: Sat, 13 Dec 2025 22:01:12 -0300 Subject: [PATCH 1/3] UI enhancements: Farcaster-inspired redesign - Token-first wallet view with unified token list and chain badges - Global Send/Receive buttons with total balance display - Redesigned Send modal with clean inputs, 50%/Max buttons, USD toggle - Redesigned Receive modal with QR code + token logo, token selector, account selector - Added Ethereum accounts 1 & 2 derivation paths (m/44'/60'/1'/0/0, m/44'/60'/2'/0/0) - Fixed KeepKey logo in popup transaction screens - Changed transfer safety indicator to green checkmark for simple transfers - Improved drawer navigation with back arrow instead of X close button - Deduplicated chain selector to show unique tokens only --- chrome-extension/src/background/keepkey.ts | 25 +- .../src/components/evm/ProjectInfoCard.tsx | 79 +++- .../src/components/evm/RequestMethodCard.tsx | 22 +- .../src/components/other/ProjectInfoCard.tsx | 4 +- .../components/tendermint/ProjectInfoCard.tsx | 4 +- .../src/components/utxo/ProjectInfoCard.tsx | 4 +- pages/side-panel/src/SidePanel.tsx | 240 +++++++++-- pages/side-panel/src/components/Balances.tsx | 156 ++++++-- pages/side-panel/src/components/Receive.tsx | 375 ++++++++++++++---- pages/side-panel/src/components/Transfer.tsx | 223 +++++++---- 10 files changed, 869 insertions(+), 263 deletions(-) diff --git a/chrome-extension/src/background/keepkey.ts b/chrome-extension/src/background/keepkey.ts index d20f6ef..7371794 100644 --- a/chrome-extension/src/background/keepkey.ts +++ b/chrome-extension/src/background/keepkey.ts @@ -1,7 +1,6 @@ /* KeepKey Wallet */ -import { AssetValue } from '@pioneer-platform/helpers'; import { ChainToNetworkId, getChainEnumValue } from '@pioneer-platform/pioneer-caip'; import { getPaths } from '@pioneer-platform/pioneer-coins'; import { keepKeyApiKeyStorage, pioneerKeyStorage } from '@extension/storage'; // Re-import the storage @@ -166,6 +165,30 @@ export const onStartKeepkey = async function () { showDisplay: false, // Not supported by TrezorConnect or Ledger, but KeepKey should do it }); + // Ethereum account 1 - m/44'/60'/1'/0/0 + paths.push({ + note: 'Ethereum account 1', + networks: ['eip155:1'], + script_type: 'ethereum', + type: 'address', + addressNList: [0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 1, 0, 0], + addressNListMaster: [0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 1, 0, 0], + curve: 'secp256k1', + showDisplay: false, + }); + + // Ethereum account 2 - m/44'/60'/2'/0/0 + paths.push({ + note: 'Ethereum account 2', + networks: ['eip155:1'], + script_type: 'ethereum', + type: 'address', + addressNList: [0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 2, 0, 0], + addressNListMaster: [0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 2, 0, 0], + curve: 'secp256k1', + showDisplay: false, + }); + //get username from storage const keepkeyApiKey = (await keepKeyApiKeyStorage.getApiKey()) || 'key:123'; let username = await pioneerKeyStorage.getUsername(); diff --git a/pages/popup/src/components/evm/ProjectInfoCard.tsx b/pages/popup/src/components/evm/ProjectInfoCard.tsx index d3acd5f..4f6f94b 100644 --- a/pages/popup/src/components/evm/ProjectInfoCard.tsx +++ b/pages/popup/src/components/evm/ProjectInfoCard.tsx @@ -1,5 +1,9 @@ import { useMemo, useEffect, useState } from 'react'; -import { Avatar, Box, Text, VStack, Stack, Badge } from '@chakra-ui/react'; +import { Avatar, Box, Text, VStack, Stack, Badge, Image } from '@chakra-ui/react'; + +// KeepKey logo URL with fallback +const KEEPKEY_LOGO = 'https://pioneers.dev/coins/keepkey.png'; +const KEEPKEY_LOGO_FALLBACK = '/icon-128.png'; interface IProps { transaction: any; @@ -7,6 +11,8 @@ interface IProps { export default function ProjectInfoCard({ transaction }: IProps) { const [faviconUrl, setFaviconUrl] = useState(null); + const [logoError, setLogoError] = useState(false); + const isKeepKeyExtension = transaction?.siteUrl === 'KeepKey Browser Extension'; // Clean the URL to extract the domain const cleanUrl = useMemo(() => { @@ -21,38 +27,67 @@ export default function ProjectInfoCard({ transaction }: IProps) { // Attempt to fetch the favicon from the cleaned URL or handle the KeepKey Browser Extension case useEffect(() => { - if (transaction?.siteUrl === 'KeepKey Browser Extension') { - setFaviconUrl('https://api.keepkey.info/coins/keepkey.png'); + if (isKeepKeyExtension) { + setFaviconUrl(KEEPKEY_LOGO); } else if (cleanUrl) { const favicon = `${cleanUrl}/favicon.ico`; setFaviconUrl(favicon); } - }, [cleanUrl, transaction?.siteUrl]); + }, [cleanUrl, isKeepKeyExtension]); + + // Get the appropriate logo src + const getLogoSrc = () => { + if (logoError) return KEEPKEY_LOGO_FALLBACK; + return faviconUrl || KEEPKEY_LOGO; + }; return ( - {/* Main Avatar for the dApp's favicon */} - - - {/* Sub Avatar for KeepKey logo */} - + {/* Logo - square for KeepKey, round avatar for dApps */} + {isKeepKeyExtension ? ( + KeepKey setLogoError(true)} + /> + ) : ( + setLogoError(true)} /> + )} + + {/* Sub Avatar for KeepKey logo - only show if not already KeepKey */} + {!isKeepKeyExtension && ( + + )} - - {transaction?.siteUrl === 'KeepKey Browser Extension' ? 'KeepKey Browser Extension' : cleanUrl}
- - wants to {transaction.type} + {/* For KeepKey extension, just show "wants to" since logo has the name */} + {isKeepKeyExtension ? ( + + wants to{' '} + + {transaction.type} + + + ) : ( + + {cleanUrl}
+ + wants to {transaction.type} +
-
+ )}
); diff --git a/pages/popup/src/components/evm/RequestMethodCard.tsx b/pages/popup/src/components/evm/RequestMethodCard.tsx index 5e7fb8a..5e55641 100644 --- a/pages/popup/src/components/evm/RequestMethodCard.tsx +++ b/pages/popup/src/components/evm/RequestMethodCard.tsx @@ -15,26 +15,30 @@ const getMethodInfo = (txType: string, hasSmartContractExecution: boolean) => { return { title: 'Safe Method', description: 'Does not move funds', - icon: , + icon: , color: 'green.400', }; case 'transfer': return { title: 'Transaction', - description: 'Moves funds only, no smart contract execution', - icon: , - color: 'yellow.400', + description: 'Simple transfer - no smart contract interaction', + icon: , + color: 'green.400', }; case 'eth_sendTransaction': case 'eth_signTransaction': return { - title: 'Transaction', + title: hasSmartContractExecution ? 'Smart Contract' : 'Transaction', description: hasSmartContractExecution - ? 'This transaction has smart contract execution, requires extended validation' - : 'Moves funds only, no smart contract execution', - icon: , - color: 'yellow.400', + ? 'Interacts with smart contract - review carefully' + : 'Simple transfer - no smart contract interaction', + icon: hasSmartContractExecution ? ( + + ) : ( + + ), + color: hasSmartContractExecution ? 'yellow.400' : 'green.400', }; case 'eth_signTypedData': diff --git a/pages/popup/src/components/other/ProjectInfoCard.tsx b/pages/popup/src/components/other/ProjectInfoCard.tsx index c1d202d..7bbd38c 100644 --- a/pages/popup/src/components/other/ProjectInfoCard.tsx +++ b/pages/popup/src/components/other/ProjectInfoCard.tsx @@ -28,13 +28,13 @@ export default function ProjectInfoCard({ transaction }: any) { return ( - + {url}
- wants to {transaction.type} + wants to {transaction.type}
diff --git a/pages/popup/src/components/tendermint/ProjectInfoCard.tsx b/pages/popup/src/components/tendermint/ProjectInfoCard.tsx index c1d202d..7bbd38c 100644 --- a/pages/popup/src/components/tendermint/ProjectInfoCard.tsx +++ b/pages/popup/src/components/tendermint/ProjectInfoCard.tsx @@ -28,13 +28,13 @@ export default function ProjectInfoCard({ transaction }: any) { return ( - + {url}
- wants to {transaction.type} + wants to {transaction.type}
diff --git a/pages/popup/src/components/utxo/ProjectInfoCard.tsx b/pages/popup/src/components/utxo/ProjectInfoCard.tsx index c1d202d..7bbd38c 100644 --- a/pages/popup/src/components/utxo/ProjectInfoCard.tsx +++ b/pages/popup/src/components/utxo/ProjectInfoCard.tsx @@ -28,13 +28,13 @@ export default function ProjectInfoCard({ transaction }: any) { return ( - + {url}
- wants to {transaction.type} + wants to {transaction.type}
diff --git a/pages/side-panel/src/SidePanel.tsx b/pages/side-panel/src/SidePanel.tsx index 9dd51e7..89de10e 100644 --- a/pages/side-panel/src/SidePanel.tsx +++ b/pages/side-panel/src/SidePanel.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useDisclosure, Flex, @@ -13,8 +13,30 @@ import { ModalBody, Spinner, Button, + Tooltip, + Heading, + Icon, + HStack, + VStack, + Drawer, + DrawerOverlay, + DrawerContent, + DrawerHeader, + DrawerBody, + DrawerCloseButton, + Avatar, + Badge, } from '@chakra-ui/react'; -import { ChevronLeftIcon, RepeatIcon, SettingsIcon, CalendarIcon } from '@chakra-ui/icons'; // Using CalendarIcon for Activity +import { + ChevronLeftIcon, + RepeatIcon, + SettingsIcon, + CalendarIcon, + CheckCircleIcon, + WarningIcon, + ArrowUpIcon, + ArrowDownIcon, +} from '@chakra-ui/icons'; import { withErrorBoundary, withSuspense } from '@extension/shared'; import Connect from './components/Connect'; @@ -23,6 +45,8 @@ import Balances from './components/Balances'; import Asset from './components/Asset'; import History from './components/History'; import Settings from './components/Settings'; +import { Transfer } from './components/Transfer'; +import { Receive } from './components/Receive'; const stateNames: { [key: number]: string } = { 0: 'unknown', @@ -35,6 +59,7 @@ const stateNames: { [key: number]: string } = { const SidePanel = () => { const [balances, setBalances] = useState([]); + const [totalUsdBalance, setTotalUsdBalance] = useState(0); const [loading, setLoading] = useState(true); const [keepkeyState, setKeepkeyState] = useState(null); const [assetContext, setAssetContext] = useState(null); @@ -42,8 +67,58 @@ const SidePanel = () => { const [showBack, setShowBack] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); + const [selectedTokenForAction, setSelectedTokenForAction] = useState(null); const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure(); + const { isOpen: isSendOpen, onOpen: onSendOpen, onClose: onSendClose } = useDisclosure(); + const { isOpen: isReceiveOpen, onOpen: onReceiveOpen, onClose: onReceiveClose } = useDisclosure(); + + // Fetch total balance from all tokens + const fetchTotalBalance = useCallback(() => { + chrome.runtime.sendMessage({ type: 'GET_APP_BALANCES' }, response => { + if (response && response.balances) { + setBalances(response.balances); + const total = response.balances.reduce((sum: number, b: any) => sum + parseFloat(b.valueUsd || '0'), 0); + setTotalUsdBalance(total); + } + }); + }, []); + + useEffect(() => { + if (keepkeyState === 5) { + fetchTotalBalance(); + } + }, [keepkeyState, fetchTotalBalance]); + + // Handle global send action + const handleGlobalSend = () => { + if (balances.length > 0) { + // Find the token with highest USD value as default + const sortedBalances = [...balances].sort( + (a, b) => parseFloat(b.valueUsd || '0') - parseFloat(a.valueUsd || '0'), + ); + const defaultToken = sortedBalances[0]; + setSelectedTokenForAction(defaultToken); + // Set asset context and open send + chrome.runtime.sendMessage({ type: 'SET_ASSET_CONTEXT', asset: defaultToken }, () => { + onSendOpen(); + }); + } + }; + + // Handle global receive action + const handleGlobalReceive = () => { + if (balances.length > 0) { + const sortedBalances = [...balances].sort( + (a, b) => parseFloat(b.valueUsd || '0') - parseFloat(a.valueUsd || '0'), + ); + const defaultToken = sortedBalances[0]; + setSelectedTokenForAction(defaultToken); + chrome.runtime.sendMessage({ type: 'SET_ASSET_CONTEXT', asset: defaultToken }, () => { + onReceiveOpen(); + }); + } + }; const refreshBalances = async () => { try { @@ -162,39 +237,100 @@ const SidePanel = () => { } }; + // Format currency for display + const formatCurrency = (value: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value); + }; + return ( - - - KeepKey State: {keepkeyState !== null ? keepkeyState : 'N/A'} -{' '} - {keepkeyState !== null ? stateNames[keepkeyState] : 'unknown'} - - - {/* Row with left-aligned, centered, and right-aligned buttons */} - - {/* Left-aligned button (Settings or Back depending on showBack) */} + {/* Compact Header with Settings and Refresh */} + + : } + icon={showBack ? : } aria-label={showBack ? 'Back' : 'Settings'} + variant="ghost" + size="sm" onClick={handleSettingsClick} /> - - {/* Center-aligned Activity button */} - {/**/} - {/* } // Activity Icon*/} - {/* aria-label="Activity"*/} - {/* onClick={handleTransactionsClick} // Handle transaction context*/} - {/* />*/} - {/**/} - - {/* Right-aligned button (Refresh) */} - } aria-label="Refresh" onClick={() => refreshBalances()} /> + + KeepKey + + + + {keepkeyState === 5 ? ( + + ) : ( + + )} + + - + } + aria-label="Refresh" + variant="ghost" + size="sm" + isLoading={isRefreshing} + onClick={() => refreshBalances()} + /> + + + {/* Total Balance & Global Actions - Only show when paired */} + {keepkeyState === 5 && !showBack && ( + + {/* Total Balance */} + + + Total Balance + + + {formatCurrency(totalUsdBalance)} + + + + {/* Global Action Buttons */} + + + + + + )} {/* Render the appropriate content */} - {renderContent()} + + {renderContent()} + {/* Modal for Settings */} @@ -202,7 +338,7 @@ const SidePanel = () => { - Settings For Your KeepKey + Settings @@ -211,6 +347,56 @@ const SidePanel = () => { + + {/* Send Drawer */} + + + + + + } + variant="ghost" + size="sm" + onClick={onSendClose} + mr={2} + /> + + Send + + + + + + + + + + {/* Receive Drawer */} + + + + + + } + variant="ghost" + size="sm" + onClick={onReceiveClose} + mr={2} + /> + + Receive + + + + + + + + ); }; diff --git a/pages/side-panel/src/components/Balances.tsx b/pages/side-panel/src/components/Balances.tsx index 6badc5e..b487cf0 100644 --- a/pages/side-panel/src/components/Balances.tsx +++ b/pages/side-panel/src/components/Balances.tsx @@ -1,10 +1,43 @@ -import React, { useState, useEffect } from 'react'; -import { Flex, Spinner, Avatar, Box, Text, Badge, Card, Stack, Button } from '@chakra-ui/react'; +import React, { useState, useEffect, useMemo } from 'react'; +import { + Flex, + Spinner, + Avatar, + Box, + Text, + Badge, + Card, + Stack, + Button, + HStack, + IconButton, + Tooltip, + Image, +} from '@chakra-ui/react'; +import { ArrowUpIcon, ArrowDownIcon, ChevronRightIcon } from '@chakra-ui/icons'; import AssetSelect from './AssetSelect'; // Import AssetSelect component import { blockchainDataStorage, blockchainStorage } from '@extension/storage'; import { COIN_MAP_LONG } from '@pioneer-platform/pioneer-coins'; import { NetworkIdToChain } from '@pioneer-platform/pioneer-caip'; +// Chain name mapping for cleaner display +const getChainDisplayName = (networkId: string): string => { + if (networkId?.includes('eip155:1/')) return 'Ethereum'; + if (networkId?.includes('eip155:8453')) return 'Base'; + if (networkId?.includes('eip155:137')) return 'Polygon'; + if (networkId?.includes('eip155:43114')) return 'Avalanche'; + if (networkId?.includes('eip155:56')) return 'BSC'; + if (networkId?.includes('eip155:10')) return 'Optimism'; + if (networkId?.includes('eip155:42161')) return 'Arbitrum'; + if (networkId?.includes('bip122:000000000019d6689c085ae165831e93')) return 'Bitcoin'; + if (networkId?.includes('cosmos:')) return 'Cosmos'; + if (networkId?.includes('cosmos:thorchain')) return 'THORChain'; + if (networkId?.includes('cosmos:mayachain')) return 'Maya'; + // Extract chain name from networkId as fallback + const chain = (NetworkIdToChain as any)[networkId?.split('/')[0]]; + return (COIN_MAP_LONG as any)[chain] || 'Unknown'; +}; + const Balances = ({ setShowBack }: any) => { // Initialize state with cached data if available const getCachedData = (key: string) => { @@ -399,69 +432,104 @@ const Balances = ({ setShowBack }: any) => { } const { integer, largePart, smallPart } = formatBalance(totalBalance); + const chainName = getChainDisplayName(asset.networkId); return ( - + onSelect(asset)} + transition="all 0.2s"> - + {/* Token Icon */} + + + + + {/* Token Info */} - - {asset.name} {asset.manual && Added Asset} - + + + {asset.name} + + {/* Chain Badge - subtle, not dominant */} + + {chainName} + + {asset.manual && ( + + Custom + + )} + {asset.manual ? ( - // For custom added assets - <> - Click to view balance - + + Tap to view balance + ) : ( - // For regular assets with balance - <> - - {integer}. - - {largePart} - + + + {integer}.{largePart} {largePart === '0000' && ( - + {smallPart} )} - + {asset.symbol} - -
- {/*USD {formatUsd(balance?.valueUsd || '0.00')}*/} - USD {formatUsd(totalUsdValue.toString())} - {/*{tokenCount > 1 && Tokens: {tokenCount}}*/} - {/*{tokenCount > 1 && Tokens: {tokenCount}}*/} +
- +
)}
- + + {/* USD Value - Right aligned */} + {!asset.manual && ( + + + ${formatUsd(totalUsdValue.toString())} + + {loadingAssetId === asset.caip && } + + )} + +
); }) )} - {/* Add Blockchain Placeholder */} - - - - - + {/* Add Blockchain - More subtle */} + setShowAssetSelect(true)} + transition="all 0.2s"> + + + Add Blockchain + + )} diff --git a/pages/side-panel/src/components/Receive.tsx b/pages/side-panel/src/components/Receive.tsx index 2eb71cd..6eeaf4a 100644 --- a/pages/side-panel/src/components/Receive.tsx +++ b/pages/side-panel/src/components/Receive.tsx @@ -5,27 +5,35 @@ import { Flex, Text, Badge, - Table, - Tbody, - Tr, - Td, - Select, Spinner, VStack, HStack, useToast, + IconButton, + Image, + Menu, + MenuButton, + MenuList, + MenuItem, } from '@chakra-ui/react'; -import React, { useEffect, useState } from 'react'; -import QRCode from 'qrcode'; // Import the QRCode library +import { CopyIcon, CheckIcon, ChevronDownIcon } from '@chakra-ui/icons'; +import React, { useEffect, useState, useRef } from 'react'; +import QRCode from 'qrcode'; -export function Receive({ onClose }: { onClose: () => void }) { +interface ReceiveProps { + onClose: () => void; + balances?: any[]; +} + +export function Receive({ onClose, balances = [] }: ReceiveProps) { const [walletType, setWalletType] = useState(''); const [selectedAddress, setSelectedAddress] = useState(''); const [pubkeys, setPubkeys] = useState([]); const [assetContext, setAssetContext] = useState(null); const [loading, setLoading] = useState(true); const [hasCopied, setHasCopied] = useState(false); - const [qrCodeDataUrl, setQrCodeDataUrl] = useState(null); // State for QR code image + const [qrCodeDataUrl, setQrCodeDataUrl] = useState(null); + const canvasRef = useRef(null); const toast = useToast(); // Fetch asset context and pubkeys from the backend (extension) @@ -45,7 +53,6 @@ export function Receive({ onClose }: { onClose: () => void }) { if (response.assets.pubkeys && response.assets.pubkeys.length > 0) { const initialAddress = response.assets.pubkeys[0].address || response.assets.pubkeys[0].master; setSelectedAddress(initialAddress); - generateQrCode(initialAddress); // Generate QR code for the initial address } } setLoading(false); @@ -55,10 +62,17 @@ export function Receive({ onClose }: { onClose: () => void }) { fetchAssetContextAndPubkeys(); }, []); - const handleAddressChange = (event: React.ChangeEvent) => { - const address = event.target.value; + // Generate QR code with logo overlay + useEffect(() => { + if (selectedAddress && assetContext?.icon) { + generateQrCodeWithLogo(selectedAddress, assetContext.icon); + } else if (selectedAddress) { + generateQrCode(selectedAddress); + } + }, [selectedAddress, assetContext?.icon]); + + const handleAddressSelect = (address: string) => { setSelectedAddress(address); - generateQrCode(address); // Generate QR code for the selected address }; // Copy to clipboard function @@ -72,20 +86,124 @@ export function Receive({ onClose }: { onClose: () => void }) { duration: 2000, isClosable: true, }); - setTimeout(() => setHasCopied(false), 2000); // Reset the copied status after 2 seconds + setTimeout(() => setHasCopied(false), 2000); }); } }; - // Generate QR code using the QRCode library + // Generate QR code without logo const generateQrCode = (text: string) => { - QRCode.toDataURL(text, { width: 150, margin: 2 }, (err, url) => { - if (err) { - console.error('Error generating QR code:', err); + QRCode.toDataURL( + text, + { + width: 200, + margin: 2, + color: { + dark: '#000000', + light: '#ffffff', + }, + }, + (err, url) => { + if (err) { + console.error('Error generating QR code:', err); + return; + } + setQrCodeDataUrl(url); + }, + ); + }; + + // Generate QR code with logo in center + const generateQrCodeWithLogo = async (text: string, logoUrl: string) => { + try { + // First generate the QR code + const qrDataUrl = await QRCode.toDataURL(text, { + width: 200, + margin: 2, + errorCorrectionLevel: 'H', // High error correction to allow logo overlay + color: { + dark: '#000000', + light: '#ffffff', + }, + }); + + // Create canvas to overlay logo + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) { + setQrCodeDataUrl(qrDataUrl); return; } - setQrCodeDataUrl(url); - }); + + const qrImage = new window.Image(); + qrImage.onload = () => { + canvas.width = qrImage.width; + canvas.height = qrImage.height; + + // Draw QR code + ctx.drawImage(qrImage, 0, 0); + + // Load and draw logo + const logo = new window.Image(); + logo.crossOrigin = 'anonymous'; + logo.onload = () => { + const logoSize = qrImage.width * 0.25; // Logo is 25% of QR code + const logoX = (qrImage.width - logoSize) / 2; + const logoY = (qrImage.height - logoSize) / 2; + + // Draw white background circle for logo + ctx.beginPath(); + ctx.arc(qrImage.width / 2, qrImage.height / 2, logoSize / 2 + 4, 0, Math.PI * 2); + ctx.fillStyle = '#ffffff'; + ctx.fill(); + + // Draw logo + ctx.save(); + ctx.beginPath(); + ctx.arc(qrImage.width / 2, qrImage.height / 2, logoSize / 2, 0, Math.PI * 2); + ctx.closePath(); + ctx.clip(); + ctx.drawImage(logo, logoX, logoY, logoSize, logoSize); + ctx.restore(); + + setQrCodeDataUrl(canvas.toDataURL()); + }; + logo.onerror = () => { + // If logo fails to load, just use QR code without logo + setQrCodeDataUrl(qrDataUrl); + }; + logo.src = logoUrl; + }; + qrImage.src = qrDataUrl; + } catch (err) { + console.error('Error generating QR code with logo:', err); + generateQrCode(text); + } + }; + + // Format address with ellipsis + const formatAddress = (address: string) => { + if (!address) return ''; + if (address.length <= 16) return address; + return `${address.slice(0, 8)}...${address.slice(-8)}`; + }; + + // Get address type label - show Account 0, 1, 2 etc. + const getAddressType = (pubkey: any, index: number) => { + // Try to extract account number from note if available + if (pubkey.note) { + const match = pubkey.note.match(/account\s*(\d+)/i); + if (match) { + return `Account ${match[1]}`; + } + } + // Try to get from addressNList (last element is usually the account index) + if (pubkey.addressNList && pubkey.addressNList.length > 0) { + const lastIndex = pubkey.addressNList[pubkey.addressNList.length - 1]; + return `Account ${lastIndex}`; + } + // Fallback to index in list + return `Account ${index}`; }; if (loading) { @@ -104,71 +222,166 @@ export function Receive({ onClose }: { onClose: () => void }) { ); } - const formatWithEllipsis = (text: string, maxLength: number = 10) => { - if (text.length <= maxLength) return text; - const start = text.slice(0, maxLength / 2); - const end = text.slice(-maxLength / 2); - return `${start}...${end}`; + // Handle token selection from dropdown + const handleTokenSelect = (token: any) => { + setLoading(true); + chrome.runtime.sendMessage({ type: 'SET_ASSET_CONTEXT', asset: token }, () => { + // Re-fetch the asset context + chrome.runtime.sendMessage({ type: 'GET_ASSET_CONTEXT' }, response => { + if (response && response.assets) { + setAssetContext(response.assets); + setPubkeys(response.assets.pubkeys || []); + if (response.assets.pubkeys && response.assets.pubkeys.length > 0) { + const initialAddress = response.assets.pubkeys[0].address || response.assets.pubkeys[0].master; + setSelectedAddress(initialAddress); + } + } + setLoading(false); + }); + }); }; return ( - - {/* Avatar and Title */} - - - Receive {assetContext?.name} - + + {/* Token Selector - at the very top, deduplicated by symbol */} + {balances.length > 0 && + (() => { + // Deduplicate tokens by symbol to avoid showing same chain multiple times + const uniqueTokens = balances.reduce((acc: any[], token) => { + if (!acc.find(t => t.symbol === token.symbol)) { + acc.push(token); + } + return acc; + }, []); - {/* Chain and Address Selector */} - - - - - - - - - - - -
- Network - - {formatWithEllipsis(assetContext?.networkId || '', 20)} -
- Address - - -
- - {/* Address Display Box */} - {selectedAddress && ( - <> - - - {selectedAddress} - - - - {/* QR Code */} - - {qrCodeDataUrl ? QR Code : } - - - {/* Copy Button */} - - - - - )} + return ( + + + } + w="full" + bg="whiteAlpha.100" + _hover={{ bg: 'whiteAlpha.200' }} + _active={{ bg: 'whiteAlpha.200' }} + borderRadius="xl" + py={6}> + + + {assetContext?.name} + + {assetContext?.symbol} + + + + + {uniqueTokens.map((token, index) => ( + handleTokenSelect(token)} + bg={assetContext?.symbol === token.symbol ? 'whiteAlpha.200' : 'transparent'} + _hover={{ bg: 'whiteAlpha.100' }}> + + + + {token.name} + + {token.symbol} + + + + + ))} + + + + ); + })()} + + {/* QR Code with Logo */} + + {qrCodeDataUrl ? ( + QR Code + ) : ( + + + + )} + + + {/* Combined Address Display with Selector and Copy */} + + + {/* Address with optional dropdown */} + + 1 ? 'pointer' : 'default'} + _hover={pubkeys.length > 1 ? { opacity: 0.8 } : {}}> + + + + {getAddressType( + pubkeys.find(p => (p.address || p.master) === selectedAddress) || pubkeys[0], + pubkeys.findIndex(p => (p.address || p.master) === selectedAddress), + )} + + + {selectedAddress} + + + {pubkeys.length > 1 && } + + + {pubkeys.length > 1 && ( + + {pubkeys.map((pubkey, index) => { + const addr = pubkey.address || pubkey.master; + return ( + handleAddressSelect(addr)} + bg={selectedAddress === addr ? 'whiteAlpha.200' : 'transparent'} + _hover={{ bg: 'whiteAlpha.100' }}> + + + {getAddressType(pubkey, index)} + + + {formatAddress(addr)} + + + + ); + })} + + )} + + + {/* Copy button */} + : } + variant="ghost" + colorScheme={hasCopied ? 'green' : 'gray'} + size="lg" + onClick={copyToClipboard} + ml={2} + /> + + + + {/* Warning */} + + Only send {assetContext?.symbol} to this address. Sending other assets may result in permanent loss. +
); } diff --git a/pages/side-panel/src/components/Transfer.tsx b/pages/side-panel/src/components/Transfer.tsx index 4a5d961..ab65ad2 100644 --- a/pages/side-panel/src/components/Transfer.tsx +++ b/pages/side-panel/src/components/Transfer.tsx @@ -11,6 +11,7 @@ import { Heading, Input, VStack, + HStack, useToast, useColorModeValue, Modal, @@ -21,7 +22,11 @@ import { ModalBody, ModalFooter, useDisclosure, + InputGroup, + InputRightElement, + IconButton, } from '@chakra-ui/react'; +import { CloseIcon } from '@chakra-ui/icons'; import React, { useCallback, useEffect, useState } from 'react'; import { NetworkIdToChain } from '@pioneer-platform/pioneer-caip'; import { COIN_MAP_LONG } from '@pioneer-platform/pioneer-coins'; @@ -253,97 +258,169 @@ export function Transfer(): JSX.Element { return ( <> - - - Send {isToken ? tokenStandard : 'Crypto'} - - - - - - - Asset: {assetContext?.name} - {isToken && ( - - {tokenStandard} - - )} - - - Chain: {assetContext?.networkId} - - - Symbol: {assetContext?.symbol} - - - balance: {totalBalance} - - - - - - - Recipient + + {/* Asset Card - Shows what you're sending */} + + + + + + + {assetContext?.name || 'Loading...'} + + + {totalBalance.toFixed(6)} {assetContext?.symbol} + + + + {isToken && ( + + {tokenStandard} + + )} + + + + {/* Recipient Input */} + + + To + + setRecipient(e.target.value)} - placeholder="Enter recipient address" + placeholder="Address, domain or identity" + bg="rgba(255, 255, 255, 0.05)" + border="1px solid" + borderColor="whiteAlpha.200" + borderRadius="xl" + py={6} + _placeholder={{ color: 'whiteAlpha.400' }} + _focus={{ borderColor: 'blue.400', boxShadow: 'none' }} /> - + {recipient && ( + + } + size="xs" + variant="ghost" + onClick={() => setRecipient('')} + /> + + )} + +
+ + {/* Amount Input */} + + + + Amount + + + + + + - - Amount - + + - - {/* Conditionally hide this button if networkId includes "eip155" */} - {!assetContext?.networkId?.includes('eip155') && ( - - )} + + + {useUsdInput ? 'USD' : assetContext?.symbol || '---'} + + - - - + + +
+ + {/* Send Button */} + - - - - Confirm {isToken ? `${tokenStandard} Token ` : ''}Transaction - + {/* Confirmation Modal */} + + + + Confirm Transaction + - Recipient: {recipient} - - Amount: {inputAmount} {assetContext?.symbol} - {isToken && ( - - {tokenStandard} - - )} - - {memo && Memo: {memo}} - {isToken && ( - - This will send {assetContext?.symbol} tokens on {assetContext?.networkId} - - )} + + + + Sending + + + {inputAmount} {assetContext?.symbol} + + + ${inputAmountUsd || '0.00'} + + + + + + To + + + {recipient} + + + - - - From 4df23583c24b56803eaef1444afd6ce909b24cfa Mon Sep 17 00:00:00 2001 From: sktbrd Date: Sat, 13 Dec 2025 22:13:17 -0300 Subject: [PATCH 2/3] fix: enhance injection verification and message handling in KeepKey script From 9eb40339e91dcc90851bb87de89efb7b04a09610 Mon Sep 17 00:00:00 2001 From: sktbrd Date: Sat, 13 Dec 2025 22:13:22 -0300 Subject: [PATCH 3/3] fix: reset logo error state and improve transaction type handling in ProjectInfoCard --- pages/popup/src/components/evm/ProjectInfoCard.tsx | 7 ++++--- pages/popup/src/components/other/ProjectInfoCard.tsx | 2 +- pages/side-panel/src/components/Receive.tsx | 10 ++++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pages/popup/src/components/evm/ProjectInfoCard.tsx b/pages/popup/src/components/evm/ProjectInfoCard.tsx index 4f6f94b..ef60e06 100644 --- a/pages/popup/src/components/evm/ProjectInfoCard.tsx +++ b/pages/popup/src/components/evm/ProjectInfoCard.tsx @@ -27,6 +27,7 @@ export default function ProjectInfoCard({ transaction }: IProps) { // Attempt to fetch the favicon from the cleaned URL or handle the KeepKey Browser Extension case useEffect(() => { + setLogoError(false); // Reset error state for new URL if (isKeepKeyExtension) { setFaviconUrl(KEEPKEY_LOGO); } else if (cleanUrl) { @@ -81,12 +82,12 @@ export default function ProjectInfoCard({ transaction }: IProps) { ) : ( - - {cleanUrl}
+ + {cleanUrl} wants to {transaction.type} -
+ )}
diff --git a/pages/popup/src/components/other/ProjectInfoCard.tsx b/pages/popup/src/components/other/ProjectInfoCard.tsx index 7bbd38c..9d809e3 100644 --- a/pages/popup/src/components/other/ProjectInfoCard.tsx +++ b/pages/popup/src/components/other/ProjectInfoCard.tsx @@ -34,7 +34,7 @@ export default function ProjectInfoCard({ transaction }: any) { {url}
- wants to {transaction.type} + wants to {transaction?.type}
diff --git a/pages/side-panel/src/components/Receive.tsx b/pages/side-panel/src/components/Receive.tsx index 6eeaf4a..6357f28 100644 --- a/pages/side-panel/src/components/Receive.tsx +++ b/pages/side-panel/src/components/Receive.tsx @@ -226,8 +226,18 @@ export function Receive({ onClose, balances = [] }: ReceiveProps) { const handleTokenSelect = (token: any) => { setLoading(true); chrome.runtime.sendMessage({ type: 'SET_ASSET_CONTEXT', asset: token }, () => { + if (chrome.runtime.lastError) { + console.error('Error setting asset context:', chrome.runtime.lastError.message); + setLoading(false); + return; + } // Re-fetch the asset context chrome.runtime.sendMessage({ type: 'GET_ASSET_CONTEXT' }, response => { + if (chrome.runtime.lastError) { + console.error('Error fetching asset context:', chrome.runtime.lastError.message); + setLoading(false); + return; + } if (response && response.assets) { setAssetContext(response.assets); setPubkeys(response.assets.pubkeys || []);