Skip to content

Commit c7851c6

Browse files
fix(market-banner): handle null market change percentages
1 parent c453ea5 commit c7851c6

7 files changed

Lines changed: 51 additions & 26 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"ledger-live-desktop": patch
3+
"@ledgerhq/live-common": patch
4+
---
5+
6+
Fix Market Banner crash when the markets API returns null price-change percentages. Market change fields are now typed as `number | null` to match the API, and the desktop trending tile renders a neutral placeholder instead of calling `.toFixed` on a missing value.

apps/ledger-live-desktop/src/mvvm/features/MarketBanner/__tests__/PerformanceIndicator.test.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ describe("PerformanceIndicator", () => {
1919
expect(container.firstChild).toHaveClass("text-error");
2020
});
2121

22-
it("should render zero percentage with + sign and success color", () => {
22+
it("should render zero percentage with + sign and neutral color", () => {
2323
const value = { priceChangePercentage24h: 0 };
2424
const { container } = render(<PerformanceIndicator value={value} />);
2525

2626
expect(screen.getByText("+0.00%")).toBeInTheDocument();
27-
expect(container.firstChild).toHaveClass("text-success");
27+
expect(container.firstChild).toHaveClass("text-muted");
2828
});
2929

3030
it("should format percentage to 2 decimal places", () => {
@@ -33,4 +33,17 @@ describe("PerformanceIndicator", () => {
3333

3434
expect(screen.getByText("+12.35%")).toBeInTheDocument();
3535
});
36+
37+
it.each([
38+
["null", null],
39+
["undefined", undefined],
40+
["NaN", Number.NaN],
41+
])("should render a neutral placeholder when the value is %s", (_label, change) => {
42+
const value = { priceChangePercentage24h: change };
43+
const { container } = render(<PerformanceIndicator value={value} />);
44+
45+
expect(screen.getByText("—")).toBeInTheDocument();
46+
expect(container.firstChild).toHaveClass("text-muted");
47+
expect(screen.queryByText(/%$/)).not.toBeInTheDocument();
48+
});
3649
});
Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import React from "react";
22
import { MarketItemPerformer } from "@ledgerhq/live-common/market/utils/types";
3+
import { trendPercentageBody3Styles } from "LLD/shared/trendPercentageStyles";
34

45
type PerformanceIndicatorProps = {
56
value: Pick<MarketItemPerformer, "priceChangePercentage24h">;
67
};
78

89
export const PerformanceIndicator = ({ value }: PerformanceIndicatorProps) => {
9-
const textColorClass = value.priceChangePercentage24h >= 0 ? "text-success" : "text-error";
10+
const change = value.priceChangePercentage24h;
11+
12+
if (change == null || !Number.isFinite(change)) {
13+
return <div className={trendPercentageBody3Styles({ variant: "neutral" })}></div>;
14+
}
15+
16+
const variant = change > 0 ? "positive" : change < 0 ? "negative" : "neutral";
1017

1118
return (
12-
<div className={`${textColorClass} body-3`}>
13-
{value.priceChangePercentage24h >= 0 ? "+" : ""}
14-
{value.priceChangePercentage24h.toFixed(2)}%
19+
<div className={trendPercentageBody3Styles({ variant })}>
20+
{change >= 0 ? "+" : ""}
21+
{change.toFixed(2)}%
1522
</div>
1623
);
1724
};

apps/ledger-live-desktop/src/mvvm/features/MarketBanner/hooks/useMarketBannerViewModel.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ function toPerformer(currency: MarketCurrencyData): MarketItemPerformer {
5151
ledgerIds: currency.ledgerIds,
5252
image: currency.image ?? "",
5353
price: currency.price,
54-
priceChangePercentage1h: change[KeysPriceChange.hour] ?? 0,
55-
priceChangePercentage24h: change[KeysPriceChange.day] ?? 0,
56-
priceChangePercentage7d: change[KeysPriceChange.week] ?? 0,
57-
priceChangePercentage30d: change[KeysPriceChange.month] ?? 0,
58-
priceChangePercentage1y: change[KeysPriceChange.year] ?? 0,
54+
priceChangePercentage1h: change[KeysPriceChange.hour] ?? null,
55+
priceChangePercentage24h: change[KeysPriceChange.day] ?? null,
56+
priceChangePercentage7d: change[KeysPriceChange.week] ?? null,
57+
priceChangePercentage30d: change[KeysPriceChange.month] ?? null,
58+
priceChangePercentage1y: change[KeysPriceChange.year] ?? null,
5959
};
6060
}
6161

libs/ledger-live-common/src/market/utils/__tests__/index.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ describe("getChangePercentage", () => {
6464

6565
it("should return 0 when percentage is null", () => {
6666
const dataWithNull = { ...createMockMarketPerformer(), priceChangePercentage24h: null };
67-
// @ts-expect-error testing null value handling
6867
expect(getChangePercentage(dataWithNull, "day")).toBe(0);
6968
});
7069

libs/ledger-live-common/src/market/utils/resolveRangePriceChange.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function resolveRangePriceChange(options: {
3535
selectedRange: string;
3636
chartPrices: readonly number[];
3737
price?: number;
38-
priceChangePercentage?: Record<KeysPriceChange, number>;
38+
priceChangePercentage?: Record<KeysPriceChange, number | null>;
3939
priceChangeKeyExtensions?: Partial<Record<string, KeysPriceChange>>;
4040
}): RangePriceChange {
4141
const { selectedRange, chartPrices, price, priceChangePercentage, priceChangeKeyExtensions } =

libs/ledger-live-common/src/market/utils/types.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export type MarketCurrencyData = {
8989
low24h: number;
9090
ticker: string;
9191
price: number;
92-
priceChangePercentage: Record<KeysPriceChange, number>;
92+
priceChangePercentage: Record<KeysPriceChange, number | null>;
9393
marketCapChangePercentage24h: number;
9494
circulatingSupply: number;
9595
totalSupply?: number;
@@ -137,13 +137,13 @@ export type MarketItemResponse = {
137137
maxSupply: number;
138138
name: string;
139139
price: number;
140-
priceChange24h: number;
141-
priceChangePercentage1h: number;
142-
priceChangePercentage24h: number;
143-
priceChangePercentage30d: number;
144-
priceChangePercentage7d: number;
145-
priceChangePercentage6m?: number;
146-
priceChangePercentage1y: number;
140+
priceChange24h: number | null;
141+
priceChangePercentage1h: number | null;
142+
priceChangePercentage24h: number | null;
143+
priceChangePercentage30d: number | null;
144+
priceChangePercentage7d: number | null;
145+
priceChangePercentage6m?: number | null;
146+
priceChangePercentage1y: number | null;
147147
sparkline: number[];
148148
ticker: string;
149149
totalSupply: number;
@@ -157,11 +157,11 @@ export type MarketItemPerformer = {
157157
id: string;
158158
name: string;
159159
ticker: string;
160-
priceChangePercentage1h: number;
161-
priceChangePercentage24h: number;
162-
priceChangePercentage7d: number;
163-
priceChangePercentage30d: number;
164-
priceChangePercentage1y: number;
160+
priceChangePercentage1h: number | null;
161+
priceChangePercentage24h: number | null;
162+
priceChangePercentage7d: number | null;
163+
priceChangePercentage30d: number | null;
164+
priceChangePercentage1y: number | null;
165165
image: string;
166166
price: number;
167167
ledgerIds: string[];

0 commit comments

Comments
 (0)