Skip to content

Commit

Permalink
[core, ui, uma-bridge] Allow passing appendUnits arg to formatCurrenc…
Browse files Browse the repository at this point in the history
…yStr (#14083)

https://github.com/user-attachments/assets/95e267f9-145d-4697-b2a8-6b3a1d24e8f5

GitOrigin-RevId: af2ae9335289978c50d31e7e56f2ae1276105859
  • Loading branch information
coreymartin authored and Lightspark Eng committed Dec 9, 2024
1 parent f17fd07 commit 864c574
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 30 deletions.
38 changes: 33 additions & 5 deletions packages/core/src/utils/currency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -547,13 +551,20 @@ const defaultOptions = {
precision: undefined,
compact: false,
showBtcSymbol: false,
append: undefined,
};

export type AppendUnitsOptions = {
plural?: boolean | undefined;
lowercase?: boolean | undefined;
};

type FormatCurrencyStrOptions = {
/* undefined indicates to use default precision for unit defined below */
precision?: number | "full" | undefined;
compact?: boolean | undefined;
showBtcSymbol?: boolean | undefined;
appendUnits?: AppendUnitsOptions | undefined;
};

export function formatCurrencyStr(
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
75 changes: 75 additions & 0 deletions packages/core/src/utils/tests/currency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {
convertCurrencyAmountValue,
CurrencyUnit,
formatCurrencyStr,
localeToCurrencySymbol,
mapCurrencyAmount,
separateCurrencyStrParts,
Expand Down Expand Up @@ -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");
});
});
44 changes: 19 additions & 25 deletions packages/ui/src/components/CurrencyAmount.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright ©, 2022, Lightspark Group, Inc. - All Rights Reserved
import styled from "@emotion/styled";
import type {
AppendUnitsOptions,
CurrencyAmountArg,
CurrencyMap,
CurrencyUnitType,
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 864c574

Please sign in to comment.