Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions projects/keepkey-vault/src/bun/reports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/

import type { ReportData, ReportSection, ChainBalance } from '../shared/types'
import { getLatestDeviceSnapshot, getCachedPubkeys } from './db'
import { getLatestDeviceSnapshot, getCachedPubkeys, getSetting } from './db'
import { getPioneer } from './pioneer'

/** Section title prefixes — shared with tax-export.ts for reliable extraction. */
Expand Down Expand Up @@ -767,6 +767,9 @@ function sanitize(text: string): string {

export async function reportToPdfBuffer(data: ReportData): Promise<Buffer> {
const { PDFDocument, StandardFonts, rgb, degrees } = await import('pdf-lib')
const reportLocale = getSetting('number_locale') || 'en-US'
// Stored values are always USD — use USD currency label with user's number locale for separators
const reportCurrency = 'USD'

console.log('[reports] Starting PDF generation...')

Expand Down Expand Up @@ -897,7 +900,7 @@ export async function reportToPdfBuffer(data: ReportData): Promise<Buffer> {
y -= 30

// ── Total Portfolio Value (big number) ──
const totalStr = `$${totalPortfolioUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
const totalStr = new Intl.NumberFormat(reportLocale, { style: 'currency', currency: reportCurrency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(totalPortfolioUsd)
const totalLabel = 'Total Portfolio Value'
const totalLabelW = font.widthOfTextAtSize(totalLabel, 11)
const totalValW = bold.widthOfTextAtSize(totalStr, 28)
Expand Down Expand Up @@ -958,7 +961,7 @@ export async function reportToPdfBuffer(data: ReportData): Promise<Buffer> {

for (const slice of slices.slice(0, 12)) {
const pct = ((slice.usd / totalPortfolioUsd) * 100).toFixed(1)
const usdStr = `$${slice.usd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
const usdStr = new Intl.NumberFormat(reportLocale, { style: 'currency', currency: reportCurrency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(slice.usd)

// Color swatch
page.drawRectangle({
Expand Down
4 changes: 3 additions & 1 deletion projects/keepkey-vault/src/bun/swap-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* CSV is plain-text, compatible with spreadsheet apps and tax tools.
*/
import type { SwapHistoryRecord } from '../shared/types'
import { getSetting } from './db'

// ── CSV Export ────────────────────────────────────────────────────────

Expand Down Expand Up @@ -189,7 +190,8 @@ export async function generateSwapPdf(records: SwapHistoryRecord[]): Promise<Buf
colX = ML + 4

// Date
const dateStr = new Date(r.createdAt).toLocaleString('en-US', {
const swapLocale = getSetting('number_locale') || 'en-US'
const dateStr = new Date(r.createdAt).toLocaleString(swapLocale, {
month: '2-digit', day: '2-digit', year: '2-digit',
hour: '2-digit', minute: '2-digit', hour12: false,
})
Expand Down
39 changes: 33 additions & 6 deletions projects/keepkey-vault/src/mainview/components/AnimatedUsd.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
import { useMemo } from "react"
import CountUp from "react-countup"
import { Text, type TextProps } from "@chakra-ui/react"
import { useFiat } from "../lib/fiat-context"
import { getFiatConfig } from "../../shared/fiat"

interface AnimatedUsdProps extends TextProps {
value: number
/** Wrapper text before the formatted value (e.g. "(" for parenthesized display) */
prefix?: string
/** Wrapper text after the formatted value (e.g. ")") */
suffix?: string
duration?: number
/** Override decimal places (defaults to the currency's configured decimals) */
decimals?: number
}

/** Animated USD counter with CountUp animation. */
export function AnimatedUsd({ value, prefix = "$", suffix, duration = 1.2, decimals = 2, color = "#23DCC8", ...textProps }: AnimatedUsdProps) {
/** Animated fiat counter. Delegates all number+symbol formatting to Intl.NumberFormat. */
export function AnimatedUsd({ value, prefix = "", suffix = "", duration = 1.2, decimals, color = "#23DCC8", ...textProps }: AnimatedUsdProps) {
const { currency, locale } = useFiat()
const cfg = getFiatConfig(currency)
const dec = decimals ?? cfg.decimals

const formatter = useMemo(() => {
try {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
minimumFractionDigits: dec,
maximumFractionDigits: dec,
currencyDisplay: 'narrowSymbol',
})
} catch {
return null
}
}, [locale, currency, dec])

const formatValue = (n: number) => {
const formatted = formatter ? formatter.format(n) : `${cfg.symbol}${n.toFixed(dec)}`
return `${prefix}${formatted}${suffix}`
}

if (!isFinite(value) || value <= 0) {
return <Text as="span" color={color} {...textProps}>{prefix}0.{'0'.repeat(decimals)}{suffix}</Text>
return <Text as="span" color={color} {...textProps}>{formatValue(0)}</Text>
}
return (
<Text as="span" color={color} {...textProps}>
{prefix}
<CountUp key={value} start={0} end={value} decimals={decimals} duration={duration} separator="," preserveValue={false} />
{suffix}
<CountUp key={value} start={0} end={value} decimals={dec} duration={duration} formattingFn={formatValue} preserveValue={false} />
</Text>
)
}
12 changes: 7 additions & 5 deletions projects/keepkey-vault/src/mainview/components/AssetPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { CHAINS, BTC_SCRIPT_TYPES, btcAccountPath, isChainSupported } from "../.
import type { ChainBalance, TokenBalance, TokenVisibilityStatus, AppSettings } from "../../shared/types"
import { getAssetIcon, caipToIcon } from "../../shared/assetLookup"
import { AnimatedUsd } from "./AnimatedUsd"
import { formatBalance, formatUsd } from "../lib/formatting"
import { formatBalance } from "../lib/formatting"
import { useFiat } from "../lib/fiat-context"
import { ReceiveView } from "./ReceiveView"
import { SendForm } from "./SendForm"

Expand Down Expand Up @@ -48,6 +49,7 @@ interface AssetPageProps {

export function AssetPage({ chain, balance, onBack, firmwareVersion }: AssetPageProps) {
const { t } = useTranslation("asset")
const { fmtCompact, symbol: fiatSymbol } = useFiat()
const [view, setView] = useState<AssetView>("receive")
const [selectedToken, setSelectedToken] = useState<TokenBalance | null>(null)
const [address, setAddress] = useState<string | null>(balance?.address || null)
Expand Down Expand Up @@ -381,7 +383,7 @@ export function AssetPage({ chain, balance, onBack, firmwareVersion }: AssetPage
</Text>
{tok.balanceUsd > 0 && (
<Text fontSize="11px" color="kk.textMuted" lineHeight="1.2">
${formatUsd(tok.balanceUsd)}
{fmtCompact(tok.balanceUsd)}
</Text>
)}
</Box>
Expand Down Expand Up @@ -476,7 +478,7 @@ export function AssetPage({ chain, balance, onBack, firmwareVersion }: AssetPage
{activeBalance.balance} {chain.symbol}
</Text>
{cleanBalanceUsd > 0 && (
<AnimatedUsd value={cleanBalanceUsd} prefix="($" suffix=")" fontSize="xs" fontWeight="500" display={{ base: "none", sm: "block" }} />
<AnimatedUsd value={cleanBalanceUsd} prefix="(" suffix=")" fontSize="xs" fontWeight="500" display={{ base: "none", sm: "block" }} />
)}
<Box
as="button"
Expand Down Expand Up @@ -651,7 +653,7 @@ export function AssetPage({ chain, balance, onBack, firmwareVersion }: AssetPage
</Text>
)}
{tokenTotalUsd > 0 && (
<Text fontSize="xs" color="kk.gold" fontWeight="500">${formatUsd(tokenTotalUsd)}</Text>
<Text fontSize="xs" color="kk.gold" fontWeight="500">{fmtCompact(tokenTotalUsd)}</Text>
)}
{isEvmChain && (
<IconButton
Expand Down Expand Up @@ -694,7 +696,7 @@ export function AssetPage({ chain, balance, onBack, firmwareVersion }: AssetPage
{zeroValueTokens.length > 0 && (
<>
<Text fontSize="10px" color="kk.textMuted" w="100%" px="1" mt="1">
{t("zeroValueTokens", { count: zeroValueTokens.length })}
{t("zeroValueTokens", { count: zeroValueTokens.length, zeroValue: fmtCompact(0) || `${fiatSymbol}0` })}
</Text>
{zeroValueTokens.map((tok) => renderTokenRow(tok))}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Box, Flex, Text, Button } from "@chakra-ui/react"
import { FaPlus } from "react-icons/fa"
import { useTranslation } from "react-i18next"
import { BTC_SCRIPT_TYPES } from "../../shared/chains"
import { formatBalance, formatUsd } from "../lib/formatting"
import { formatBalance } from "../lib/formatting"
import { useFiat } from "../lib/fiat-context"
import type { BtcAccountSet, BtcScriptType } from "../../shared/types"

interface BtcXpubSelectorProps {
Expand All @@ -15,6 +16,7 @@ interface BtcXpubSelectorProps {
export function BtcXpubSelector({ btcAccounts, onSelectXpub, onAddAccount, addingAccount }: BtcXpubSelectorProps) {
const { accounts, selectedXpub } = btcAccounts
const { t } = useTranslation("receive")
const { fmtCompact } = useFiat()
if (accounts.length === 0) return null

const selAcct = selectedXpub?.accountIndex ?? 0
Expand Down Expand Up @@ -95,7 +97,7 @@ export function BtcXpubSelector({ btcAccounts, onSelectXpub, onAddAccount, addin
)}
{xpubData && xpubData.balanceUsd > 0 && (
<Text fontSize="9px" color="kk.textMuted" lineHeight="1.2">
${formatUsd(xpubData.balanceUsd)}
{fmtCompact(xpubData.balanceUsd)}
</Text>
)}
</Flex>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, Flex, Text, Button } from "@chakra-ui/react"
import { FaPlus, FaTimes } from "react-icons/fa"
import { formatUsd } from "../lib/formatting"
import { useFiat } from "../lib/fiat-context"
import type { EvmAddressSet } from "../../shared/types"

interface EvmAddressSelectorProps {
Expand All @@ -12,6 +12,7 @@ interface EvmAddressSelectorProps {
}

export function EvmAddressSelector({ evmAddresses, onSelectIndex, onAddIndex, onRemoveIndex, adding }: EvmAddressSelectorProps) {
const { fmtCompact } = useFiat()
const { addresses, selectedIndex } = evmAddresses

// Don't render if only one address tracked
Expand Down Expand Up @@ -56,7 +57,7 @@ export function EvmAddressSelector({ evmAddresses, onSelectIndex, onAddIndex, on
</Text>
{addr.balanceUsd > 0 && (
<Text fontSize="9px" color="kk.textMuted" lineHeight="1.2">
${formatUsd(addr.balanceUsd)}
{fmtCompact(addr.balanceUsd)}
</Text>
)}
</Flex>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useRef } from "react"
import { Box, Flex, Text, Spinner, Image } from "@chakra-ui/react"
import { rpcRequest, onRpcMessage } from "../lib/rpc"
import { useFiat } from "../lib/fiat-context"
import { Z } from "../lib/z-index"
import type { ReportMeta } from "../../shared/types"

Expand All @@ -22,6 +23,7 @@ interface ReportDialogProps {
}

export function ReportDialog({ onClose }: ReportDialogProps) {
const { locale: fiatLocale, fmtCompact } = useFiat()
const [generating, setGenerating] = useState(false)
const [progress, setProgress] = useState<{ message: string; percent: number } | null>(null)
const [reports, setReports] = useState<ReportMeta[]>([])
Expand Down Expand Up @@ -250,11 +252,11 @@ export function ReportDialog({ onClose }: ReportDialogProps) {
Full Detail Report
</Text>
<Text fontSize="10px" color={r.status === "error" ? "#DC3545" : "kk.textMuted"}>
{r.status === "error" ? "Failed" : `$${r.totalUsd.toFixed(2)}`}
{r.status === "error" ? "Failed" : fmtCompact(r.totalUsd)}
</Text>
</Flex>
<Text fontSize="10px" color="kk.textMuted" mb="3">
{new Date(r.createdAt).toLocaleString()}
{new Date(r.createdAt).toLocaleString(fiatLocale)}
</Text>
{r.error && (
<Text fontSize="10px" color="#DC3545" mb="3">{r.error}</Text>
Expand Down
14 changes: 8 additions & 6 deletions projects/keepkey-vault/src/mainview/components/SendForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { useState, useEffect, useCallback, useMemo, Fragment } from "react"
import { useTranslation } from "react-i18next"
import { Box, Flex, Text, VStack, Button, Input } from "@chakra-ui/react"
import { rpcRequest } from "../lib/rpc"
import { formatBalance, formatUsd } from "../lib/formatting"
import { formatBalance } from "../lib/formatting"
import { useFiat } from "../lib/fiat-context"
import { getAsset } from "../../shared/assetLookup"
import { QrScannerOverlay } from "./QrScannerOverlay"
import type { ChainDef } from "../../shared/chains"
Expand Down Expand Up @@ -41,6 +42,7 @@ interface SendFormProps {

export function SendForm({ chain, address, balance, token, onClearToken, xpubOverride, scriptTypeOverride, evmAddressIndex }: SendFormProps) {
const { t } = useTranslation("send")
const { fmt, fmtCompact } = useFiat()
const [recipient, setRecipient] = useState("")
const [amount, setAmount] = useState("")
const [usdAmount, setUsdAmount] = useState("")
Expand Down Expand Up @@ -301,7 +303,7 @@ export function SendForm({ chain, address, balance, token, onClearToken, xpubOve
</Text>
{hasPrice && (
<Text fontSize="10px" fontFamily="mono" color="kk.textMuted">
${formatUsd(parseFloat(displayBalance) * pricePerUnit)}
{fmtCompact(parseFloat(displayBalance) * pricePerUnit)}
</Text>
)}
</Flex>
Expand Down Expand Up @@ -408,7 +410,7 @@ export function SendForm({ chain, address, balance, token, onClearToken, xpubOve
<Flex align="center" gap="1">
{inputMode === 'crypto' ? (
<Text fontSize="11px" color="kk.textMuted" fontFamily="mono">
{amountUsdPreview !== null ? `$${formatUsd(amountUsdPreview)}` : '$0.00'}
{amountUsdPreview !== null ? (fmtCompact(amountUsdPreview) || fmt(0)) : fmt(0)}
</Text>
) : (
<Text fontSize="11px" color="kk.textMuted" fontFamily="mono">
Expand All @@ -421,7 +423,7 @@ export function SendForm({ chain, address, balance, token, onClearToken, xpubOve
</Flex>
)}
{pricePerUnit > 0 && (
<Text fontSize="10px" color="kk.textMuted">1 {displaySymbol} = ${formatUsd(pricePerUnit)}</Text>
<Text fontSize="10px" color="kk.textMuted">1 {displaySymbol} = {fmtCompact(pricePerUnit)}</Text>
)}
</Flex>
)}
Expand Down Expand Up @@ -492,7 +494,7 @@ export function SendForm({ chain, address, balance, token, onClearToken, xpubOve
<Flex direction="column" align="flex-end">
<Text fontSize="xs" fontFamily="mono" color="kk.textPrimary">{isMax ? 'MAX' : amount} {displaySymbol}</Text>
{!isMax && amountUsdPreview !== null && (
<Text fontSize="10px" fontFamily="mono" color="kk.textMuted">${formatUsd(amountUsdPreview)}</Text>
<Text fontSize="10px" fontFamily="mono" color="kk.textMuted">{fmtCompact(amountUsdPreview)}</Text>
)}
</Flex>
</Flex>
Expand All @@ -501,7 +503,7 @@ export function SendForm({ chain, address, balance, token, onClearToken, xpubOve
<Flex direction="column" align="flex-end">
<Text fontSize="xs" fontFamily="mono" color="kk.textPrimary">{formatBalance(buildResult.fee)} {chain.symbol}</Text>
{buildResult.feeUsd != null && buildResult.feeUsd > 0 && (
<Text fontSize="10px" fontFamily="mono" color="kk.textMuted">${formatUsd(buildResult.feeUsd)}</Text>
<Text fontSize="10px" fontFamily="mono" color="kk.textMuted">{fmtCompact(buildResult.feeUsd)}</Text>
)}
</Flex>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"
import type { ChainDef } from "../../shared/chains"
import type { BuildTxResult, BroadcastResult, StakingPosition } from "../../shared/types"
import { rpcRequest } from "../lib/rpc"
import { useFiat } from "../lib/fiat-context"
import { Z } from "../lib/z-index"

interface StakingPanelProps {
Expand Down Expand Up @@ -53,6 +54,7 @@ interface DelegateDialogProps {

function DelegateDialog({ isOpen, onClose, chain, availableBalance, rewardAmount, rewardUsd, onSuccess, watchOnly }: DelegateDialogProps) {
const { t } = useTranslation("staking")
const { fmtCompact } = useFiat()
const [validatorAddress, setValidatorAddress] = useState("")
const [amount, setAmount] = useState("")
const [memo, setMemo] = useState(t('defaultDelegationMemo'))
Expand Down Expand Up @@ -287,7 +289,7 @@ function DelegateDialog({ isOpen, onClose, chain, availableBalance, rewardAmount
{rewardAmount && (
<Text fontSize="10px" color="kk.textMuted" mt="2">
{rewardUsd && rewardUsd > 0
? t('rewardsAvailableWithUsd', { amount: rewardAmount, symbol: chain.symbol, usd: rewardUsd.toFixed(2) })
? t('rewardsAvailableWithUsd', { amount: rewardAmount, symbol: chain.symbol, fiatValue: fmtCompact(rewardUsd) })
: t('rewardsAvailable', { amount: rewardAmount, symbol: chain.symbol })}
</Text>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Box, Flex, Text, VStack, Button, Input, Image, HStack } from "@chakra-u
import CountUp from "react-countup"
import { rpcRequest, onRpcMessage } from "../lib/rpc"
import { formatBalance } from "../lib/formatting"
import { formatUsd } from "../lib/formatting"
import { useFiat } from "../lib/fiat-context"
import { getAssetIcon } from "../../shared/assetLookup"
import { CHAINS, getExplorerTxUrl } from "../../shared/chains"
import type { ChainDef } from "../../shared/chains"
Expand Down Expand Up @@ -240,7 +240,7 @@ interface AssetSelectorProps {

function AssetSelector({ label, selected, assets, onSelect, balances, exclude, disabled, nativeOnly }: AssetSelectorProps) {
const { t } = useTranslation("swap")
const fmtCompact = (v: number | string | null | undefined) => { const n = typeof v === 'string' ? parseFloat(v) : (v ?? 0); return !isFinite(n) || n === 0 ? '' : `$${formatUsd(n)}` }
const { fmtCompact } = useFiat()
const [open, setOpen] = useState(false)
const [search, setSearch] = useState("")
const inputRef = useRef<HTMLInputElement>(null)
Expand Down Expand Up @@ -445,8 +445,7 @@ interface SwapDialogProps {
// ── Main SwapDialog ─────────────────────────────────────────────────
export function SwapDialog({ open, onClose, chain, balance, address, resumeSwap }: SwapDialogProps) {
const { t } = useTranslation("swap")
const fmtCompact = (v: number | string | null | undefined) => { const n = typeof v === 'string' ? parseFloat(v) : (v ?? 0); return !isFinite(n) || n === 0 ? '' : `$${formatUsd(n)}` }
const fiatSymbol = '$'
const { fmtCompact, symbol: fiatSymbol } = useFiat()

// ── State ─────────────────────────────────────────────────────────
const [phase, setPhase] = useState<SwapPhase>('input')
Expand Down
Loading
Loading