From 864c5743481883a7854ca9d42f61fc4c1ef5dbdc Mon Sep 17 00:00:00 2001 From: Corey Martin Date: Sun, 8 Dec 2024 17:34:41 -0800 Subject: [PATCH] [core, ui, uma-bridge] Allow passing appendUnits arg to formatCurrencyStr (#14083) https://github.com/user-attachments/assets/95e267f9-145d-4697-b2a8-6b3a1d24e8f5 GitOrigin-RevId: af2ae9335289978c50d31e7e56f2ae1276105859 --- packages/core/src/utils/currency.ts | 38 ++++++++-- .../core/src/utils/tests/currency.test.ts | 75 +++++++++++++++++++ packages/ui/src/components/CurrencyAmount.tsx | 44 +++++------ 3 files changed, 127 insertions(+), 30 deletions(-) diff --git a/packages/core/src/utils/currency.ts b/packages/core/src/utils/currency.ts index d589049b..0ecc4a14 100644 --- a/packages/core/src/utils/currency.ts +++ b/packages/core/src/utils/currency.ts @@ -534,6 +534,10 @@ export const abbrCurrencyUnit = (unit: CurrencyUnitType) => { return "SAT"; case CurrencyUnit.MILLISATOSHI: return "MSAT"; + case CurrencyUnit.MILLIBITCOIN: + return "mBTC"; + case CurrencyUnit.MICROBITCOIN: + return "μBTC"; case CurrencyUnit.USD: return "USD"; case CurrencyUnit.MXN: @@ -547,6 +551,12 @@ const defaultOptions = { precision: undefined, compact: false, showBtcSymbol: false, + append: undefined, +}; + +export type AppendUnitsOptions = { + plural?: boolean | undefined; + lowercase?: boolean | undefined; }; type FormatCurrencyStrOptions = { @@ -554,6 +564,7 @@ type FormatCurrencyStrOptions = { precision?: number | "full" | undefined; compact?: boolean | undefined; showBtcSymbol?: boolean | undefined; + appendUnits?: AppendUnitsOptions | undefined; }; export function formatCurrencyStr( @@ -570,7 +581,7 @@ export function formatCurrencyStr( const { unit } = currencyAmount; const centCurrencies = [CurrencyUnit.USD, CurrencyUnit.MXN] as string[]; - /* Currencies are always provided in the smallest unit, e.g. cents for USD. These should be + /* centCurrencies are always provided in the smallest unit, e.g. cents for USD. These should be * divided by 100 for proper display format: */ if (centCurrencies.includes(unit)) { num = num / 100; @@ -602,40 +613,57 @@ export function formatCurrencyStr( const currentLocale = getCurrentLocale(); + let formattedStr = ""; switch (unit) { case CurrencyUnit.MXN: case CurrencyUnit.USD: - return num.toLocaleString(currentLocale, { + formattedStr = num.toLocaleString(currentLocale, { style: "currency", currency: defaultCurrencyCode, notation: compact ? ("compact" as const) : undefined, maximumFractionDigits: getDefaultMaxFractionDigits(2, 2), }); + break; case CurrencyUnit.BITCOIN: /* In most cases product prefers 4 precision digtis for BTC. In a few places full precision (8 digits) are preferred, e.g. for a transaction details page: */ - return `${symbol}${num.toLocaleString(currentLocale, { + formattedStr = `${symbol}${num.toLocaleString(currentLocale, { notation: compact ? ("compact" as const) : undefined, maximumFractionDigits: getDefaultMaxFractionDigits(4, 8), })}`; + break; case CurrencyUnit.SATOSHI: /* In most cases product prefers hiding sub sat precision (msats). In a few places full precision (3 digits) are preferred, e.g. for Lightning fees paid on a transaction details page: */ - return `${symbol}${num.toLocaleString(currentLocale, { + formattedStr = `${symbol}${num.toLocaleString(currentLocale, { notation: compact ? ("compact" as const) : undefined, maximumFractionDigits: getDefaultMaxFractionDigits(0, 3), })}`; + break; case CurrencyUnit.MILLISATOSHI: case CurrencyUnit.MICROBITCOIN: case CurrencyUnit.MILLIBITCOIN: case CurrencyUnit.NANOBITCOIN: default: - return `${symbol}${num.toLocaleString(currentLocale, { + formattedStr = `${symbol}${num.toLocaleString(currentLocale, { notation: compact ? ("compact" as const) : undefined, maximumFractionDigits: getDefaultMaxFractionDigits(0, 0), })}`; } + + if (options?.appendUnits) { + const unitStr = abbrCurrencyUnit(unit); + const unitSuffix = options.appendUnits.plural && num > 1 ? "s" : ""; + const unitStrWithSuffix = `${unitStr}${unitSuffix}`; + formattedStr += ` ${ + options.appendUnits.lowercase + ? unitStrWithSuffix.toLowerCase() + : unitStrWithSuffix + }`; + } + + return formattedStr; } export function separateCurrencyStrParts(currencyStr: string) { diff --git a/packages/core/src/utils/tests/currency.test.ts b/packages/core/src/utils/tests/currency.test.ts index cb54f11b..25f590e5 100644 --- a/packages/core/src/utils/tests/currency.test.ts +++ b/packages/core/src/utils/tests/currency.test.ts @@ -3,6 +3,7 @@ import { convertCurrencyAmountValue, CurrencyUnit, + formatCurrencyStr, localeToCurrencySymbol, mapCurrencyAmount, separateCurrencyStrParts, @@ -361,3 +362,77 @@ describe("separateCurrencyStrParts", () => { symbol: "$", }); }); + +describe("formatCurrencyStr", () => { + it("should return the expected currency string", () => { + expect( + formatCurrencyStr({ + value: 5000, + unit: CurrencyUnit.USD, + }), + ).toBe("$50.00"); + }); + + it("should return the expected currency string with precision 1", () => { + expect( + formatCurrencyStr( + { value: 5000.245235323, unit: CurrencyUnit.Bitcoin }, + { precision: 1 }, + ), + ).toBe("5,000.2"); + }); + + it("should return the expected currency string with precision full", () => { + expect( + formatCurrencyStr( + { value: 5000.245235323, unit: CurrencyUnit.Bitcoin }, + { precision: "full" }, + ), + ).toBe("5,000.24523532"); + }); + + it("should return the expected currency string with compact", () => { + expect( + formatCurrencyStr( + { value: 5000.245235323, unit: CurrencyUnit.Bitcoin }, + { compact: true }, + ), + ).toBe("5K"); + }); + + it("should return the expected currency string with appendUnits", () => { + expect( + formatCurrencyStr( + { value: 100000, unit: CurrencyUnit.Satoshi }, + { appendUnits: { plural: true, lowercase: true } }, + ), + ).toBe("100,000 sats"); + }); + + it("should return the expected currency string with appendUnits plural and lowercase", () => { + expect( + formatCurrencyStr( + { value: 100000, unit: CurrencyUnit.Satoshi }, + { appendUnits: { plural: true, lowercase: true } }, + ), + ).toBe("100,000 sats"); + }); + + it("should return the expected currency string with appendUnits plural and lowercase with value < 2", () => { + expect( + formatCurrencyStr( + { value: 1, unit: CurrencyUnit.Satoshi }, + { appendUnits: { plural: true, lowercase: true } }, + ), + ).toBe("1 sat"); + }); + + it("should return the expected currency string with appendUnits plural and lowercase with value < 2", () => { + expect( + formatCurrencyStr( + { value: 100012, unit: CurrencyUnit.Mxn }, + { appendUnits: { plural: false } }, + ), + ).toBe("$1,000.12 MXN"); + }); +}); diff --git a/packages/ui/src/components/CurrencyAmount.tsx b/packages/ui/src/components/CurrencyAmount.tsx index ae6c10f0..cbc1b726 100644 --- a/packages/ui/src/components/CurrencyAmount.tsx +++ b/packages/ui/src/components/CurrencyAmount.tsx @@ -1,6 +1,7 @@ // Copyright ©, 2022, Lightspark Group, Inc. - All Rights Reserved import styled from "@emotion/styled"; import type { + AppendUnitsOptions, CurrencyAmountArg, CurrencyMap, CurrencyUnitType, @@ -21,7 +22,7 @@ type CurrencyAmountProps = { amount: CurrencyAmountArg | CurrencyMap; displayUnit?: CurrencyUnitType; shortNumber?: boolean; - showUnits?: boolean; + showUnits?: boolean | AppendUnitsOptions | undefined; ml?: number; id?: string; includeEstimatedIndicator?: boolean; @@ -57,23 +58,34 @@ export function CurrencyAmount({ const value = amountMap[displayUnit]; const defaultFormattedNumber = amountMap.formatted[displayUnit]; + const appendUnits = + showUnits === false + ? undefined + : showUnits === true + ? ({ + plural: true, + lowercase: true, + } as const) + : showUnits; + /* There are just a few ways that CurrencyAmounts need to be formatted throughout the UI. In general the default should always be used: */ let formattedNumber = defaultFormattedNumber; if (shortNumber) { formattedNumber = formatCurrencyStr( { value: Number(value), unit: displayUnit }, - { precision: 1, compact: true }, + { precision: 1, compact: true, appendUnits }, ); } else if (fullPrecision) { formattedNumber = formatCurrencyStr( { value: Number(value), unit: displayUnit }, - { precision: "full" }, + { precision: "full", appendUnits }, + ); + } else if (appendUnits) { + formattedNumber = formatCurrencyStr( + { value: Number(value), unit: displayUnit }, + { appendUnits }, ); - } - - if (showUnits) { - formattedNumber += ` ${shorttext(displayUnit, value)}`; } let content: string | ReactNode = formattedNumber; @@ -105,24 +117,6 @@ export const CurrencyIcon = ({ unit }: { unit: CurrencyUnitType }) => { } }; -const shorttext = (unit: CurrencyUnitType, value: number) => { - const pl = value !== 1; - switch (unit) { - case CurrencyUnit.BITCOIN: - return "BTC"; - case CurrencyUnit.MILLIBITCOIN: - return "mBTC"; - case CurrencyUnit.MICROBITCOIN: - return "μBTC"; - case CurrencyUnit.SATOSHI: - return `sat${pl ? "s" : ""}`; - case CurrencyUnit.MILLISATOSHI: - return `msat${pl ? "s" : ""}`; - default: - return unit; - } -}; - const StyledCurrencyAmount = styled.span<{ ml: number }>` color: inherit !important; white-space: nowrap;