From c01991987dbea49036ddbbd959a7b4894cc69ca3 Mon Sep 17 00:00:00 2001 From: Petar Todorovic Date: Mon, 15 Jun 2026 12:30:38 +0200 Subject: [PATCH] fix(widget): correct yield-bearing reward token labels Label reward tokens as yield-bearing only when a distinct output token has a valid price-per-share state. This avoids treating automatic reward claiming as a token mechanic and removes the PPS-bearing UI copy. --- packages/widget/src/domain/types/yields.ts | 13 ++++ .../earn-details/earn-details-formatters.ts | 12 ++-- .../earn-details/earn-details-model.tsx | 2 +- .../position-details-model.tsx | 4 +- .../src/translation/English/translations.json | 1 + .../src/translation/French/translations.json | 1 + .../earn-details-model.test.tsx | 55 +++++++++++++++- .../position-details-model.test.tsx | 64 +++++++++++++++++++ 8 files changed, 144 insertions(+), 8 deletions(-) diff --git a/packages/widget/src/domain/types/yields.ts b/packages/widget/src/domain/types/yields.ts index d34df06d..ac286ce5 100644 --- a/packages/widget/src/domain/types/yields.ts +++ b/packages/widget/src/domain/types/yields.ts @@ -325,6 +325,19 @@ export const getYieldOutputToken = (yieldDto: YieldBase) => (outputToken) => !equalTokens(outputToken, yieldDto.token) ); +const hasPositivePricePerShare = (yieldDto: YieldBase) => { + const price = yieldDto.state?.pricePerShareState?.price; + + if (price === null || price === undefined) return false; + + const amount = BigNumber(price); + + return amount.isFinite() && amount.isGreaterThan(0); +}; + +export const hasYieldBearingOutputToken = (yieldDto: YieldBase) => + getYieldOutputToken(yieldDto).isJust() && hasPositivePricePerShare(yieldDto); + const isStakingYieldType = (yieldType: ExtendedYieldType) => yieldType === "staking" || yieldType === "native_staking" || diff --git a/packages/widget/src/pages-dashboard/overview/earn-details/earn-details-formatters.ts b/packages/widget/src/pages-dashboard/overview/earn-details/earn-details-formatters.ts index 22236160..bf6409fa 100644 --- a/packages/widget/src/pages-dashboard/overview/earn-details/earn-details-formatters.ts +++ b/packages/widget/src/pages-dashboard/overview/earn-details/earn-details-formatters.ts @@ -4,6 +4,7 @@ import type { getEffectiveYieldRewardRateDetails } from "../../../domain/types/r import { getDashboardYieldCategory, getYieldActionArg, + hasYieldBearingOutputToken, isNonZeroRewardRateYield, type Yield, } from "../../../domain/types/yields"; @@ -138,11 +139,14 @@ export const formatProviderWebsite = (website: string) => { export const formatProviderWebsiteHref = (website: string) => /^https?:\/\//i.test(website) ? website : `https://${website}`; -export const formatRewardTokenLabel = (yieldDto: Yield) => { - const symbol = yieldDto.token.symbol; +export const formatRewardTokenLabel = ( + yieldDto: Yield, + t: TFunction +): string => { + const symbol = yieldDto.outputToken?.symbol ?? yieldDto.token.symbol; - return yieldDto.mechanics.rewardClaiming === "auto" - ? `${symbol} (PPS-bearing)` + return hasYieldBearingOutputToken(yieldDto) + ? t("dashboard.earn_details.yield_bearing_reward_token", { symbol }) : symbol; }; diff --git a/packages/widget/src/pages-dashboard/overview/earn-details/earn-details-model.tsx b/packages/widget/src/pages-dashboard/overview/earn-details/earn-details-model.tsx index 7fbe5b6a..8a623ef0 100644 --- a/packages/widget/src/pages-dashboard/overview/earn-details/earn-details-model.tsx +++ b/packages/widget/src/pages-dashboard/overview/earn-details/earn-details-model.tsx @@ -259,7 +259,7 @@ const getDetailRows = ({ { id: "reward-token", label: t("dashboard.earn_details.reward_token"), - value: formatRewardTokenLabel(yieldDto), + value: formatRewardTokenLabel(yieldDto, t), }, ...getPricePerShareRows(yieldDto, t), ...facts diff --git a/packages/widget/src/pages-dashboard/position-details/position-details-model.tsx b/packages/widget/src/pages-dashboard/position-details/position-details-model.tsx index 33b15aa7..8895d8ee 100644 --- a/packages/widget/src/pages-dashboard/position-details/position-details-model.tsx +++ b/packages/widget/src/pages-dashboard/position-details/position-details-model.tsx @@ -216,7 +216,7 @@ const getFillerMetric = ({ }; } - const rewardToken = formatRewardTokenLabel(integrationData); + const rewardToken = formatRewardTokenLabel(integrationData, t); if (rewardToken) { promotedFactIds.add("reward-token"); @@ -520,7 +520,7 @@ const getDetailRows = ({ : { id: "reward-token", label: t("dashboard.earn_details.reward_token"), - value: formatRewardTokenLabel(integrationData), + value: formatRewardTokenLabel(integrationData, t), }, pricePerShare ? { diff --git a/packages/widget/src/translation/English/translations.json b/packages/widget/src/translation/English/translations.json index 9b337c17..0acde53c 100644 --- a/packages/widget/src/translation/English/translations.json +++ b/packages/widget/src/translation/English/translations.json @@ -287,6 +287,7 @@ "network": "Network", "provider": "Provider", "reward_token": "Reward token", + "yield_bearing_reward_token": "{{symbol}} (yield-bearing)", "price_per_share": "Price per share", "type": "Type", "reward_schedule": "Reward schedule", diff --git a/packages/widget/src/translation/French/translations.json b/packages/widget/src/translation/French/translations.json index 9d159a69..02182c9f 100644 --- a/packages/widget/src/translation/French/translations.json +++ b/packages/widget/src/translation/French/translations.json @@ -709,6 +709,7 @@ "network": "Réseau", "provider": "Fournisseur", "reward_token": "Token de récompense", + "yield_bearing_reward_token": "{{symbol}} (porteur de rendement)", "price_per_share": "Prix par part", "type": "Type", "reward_schedule": "Fréquence des récompenses", diff --git a/packages/widget/tests/pages-dashboard/earn-details-model.test.tsx b/packages/widget/tests/pages-dashboard/earn-details-model.test.tsx index 5ff6b141..41794182 100644 --- a/packages/widget/tests/pages-dashboard/earn-details-model.test.tsx +++ b/packages/widget/tests/pages-dashboard/earn-details-model.test.tsx @@ -4,7 +4,7 @@ import type { Yield } from "../../src/domain/types/yields"; import { getEarnDetailsModel } from "../../src/pages-dashboard/overview/earn-details/earn-details-model"; import { yieldApiYieldFixture } from "../fixtures"; -const t = (key: string): string => { +const t = (key: string, options?: Record): string => { const translations: Record = { "dashboard.earn_details.min_stake": "Min stake", "dashboard.earn_details.minimum_subscription": "Minimum subscription", @@ -12,6 +12,7 @@ const t = (key: string): string => { "dashboard.earn_details.price_per_share": "Price per share", "dashboard.earn_details.provider": "Provider", "dashboard.earn_details.reward_token": "Reward token", + "dashboard.earn_details.yield_bearing_reward_token": `${options?.symbol ?? ""} (yield-bearing)`, }; return translations[key] ?? key; @@ -103,4 +104,56 @@ describe("getEarnDetailsModel", () => { "price-per-share" ); }); + + it("does not mark auto-claiming rewards as yield-bearing without price per share", () => { + const model = getEarnDetailsModel({ + t: t as TFunction, + yieldDto: makeYield({ + outputToken: { + ...yieldApiYieldFixture().token, + symbol: "stETH", + }, + }), + }); + + expect(model.detailRows.find((row) => row.id === "reward-token")).toEqual({ + id: "reward-token", + label: "Reward token", + value: "stETH", + }); + }); + + it("marks distinct output tokens with price per share as yield-bearing", () => { + const baseYield = yieldApiYieldFixture(); + const model = getEarnDetailsModel({ + t: t as TFunction, + yieldDto: makeYield({ + mechanics: { + ...baseYield.mechanics, + rewardClaiming: "manual", + }, + outputToken: { + ...baseYield.token, + symbol: "mUSDC", + }, + state: { + pricePerShareState: { + price: 1.06274537, + quoteToken: baseYield.token, + shareToken: baseYield.token, + }, + }, + token: { + ...baseYield.token, + symbol: "USDC", + }, + }), + }); + + expect(model.detailRows.find((row) => row.id === "reward-token")).toEqual({ + id: "reward-token", + label: "Reward token", + value: "mUSDC (yield-bearing)", + }); + }); }); diff --git a/packages/widget/tests/pages-dashboard/position-details-model.test.tsx b/packages/widget/tests/pages-dashboard/position-details-model.test.tsx index 166e42df..3cde165a 100644 --- a/packages/widget/tests/pages-dashboard/position-details-model.test.tsx +++ b/packages/widget/tests/pages-dashboard/position-details-model.test.tsx @@ -31,6 +31,7 @@ const t = (key: string, options?: Record): string => { "dashboard.earn_details.reward_rate_period": `${options?.rewardType ?? "APY"} (7D)`, "dashboard.earn_details.reward_schedule": "Reward schedule", "dashboard.earn_details.reward_token": "Reward token", + "dashboard.earn_details.yield_bearing_reward_token": `${options?.symbol ?? ""} (yield-bearing)`, "dashboard.earn_details.risk": "Risk", "dashboard.earn_details.type": "Type", "dashboard.earn_details.vault": "Vault", @@ -325,6 +326,69 @@ describe("getDashboardPositionDetailsModel", () => { ]); }); + it("does not mark auto-claiming position reward tokens as yield-bearing without price per share", () => { + const model = getDashboardPositionDetailsModel({ + canUnstake: true, + integrationData: makeYield(), + pendingActions: [], + personalizedRewardRate: null, + positionBalancesByType: makePositionBalances(), + providersDetails: [{ name: "Rocket Pool", status: "active" }], + reducedStakedOrLiquidBalance: null, + rewardsSummary: undefined, + t: t as TFunction, + }); + + expect(model.detailRows.find((row) => row.id === "reward-token")).toEqual({ + id: "reward-token", + label: "Reward token", + value: "rETH", + }); + }); + + it("marks position output tokens with price per share as yield-bearing", () => { + const baseYield = yieldApiYieldFixture(); + const model = getDashboardPositionDetailsModel({ + canUnstake: true, + integrationData: makeYield({ + mechanics: { + ...makeYield().mechanics, + rewardClaiming: "manual", + }, + outputToken: { + ...baseYield.token, + address: "0x0000000000000000000000000000000000000002", + symbol: "mUSDC", + }, + state: { + pricePerShareState: { + price: 1.06274537, + quoteToken: baseYield.token, + shareToken: baseYield.token, + }, + }, + token: { + ...baseYield.token, + address: "0x0000000000000000000000000000000000000001", + symbol: "USDC", + }, + }), + pendingActions: [], + personalizedRewardRate: null, + positionBalancesByType: makePositionBalances(), + providersDetails: [{ name: "Midas", status: "active" }], + reducedStakedOrLiquidBalance: null, + rewardsSummary: undefined, + t: t as TFunction, + }); + + expect(model.detailRows.find((row) => row.id === "reward-token")).toEqual({ + id: "reward-token", + label: "Reward token", + value: "mUSDC (yield-bearing)", + }); + }); + it("includes price per share in details when yield state provides it", () => { const model = getDashboardPositionDetailsModel({ canUnstake: true,