diff --git a/packages/widget/src/Widget.tsx b/packages/widget/src/Widget.tsx
index 9cacb450..00dfc49a 100644
--- a/packages/widget/src/Widget.tsx
+++ b/packages/widget/src/Widget.tsx
@@ -15,7 +15,6 @@ import { ActivityCompletePage } from "./pages/complete/pages/activity-complete.p
import { PendingCompletePage } from "./pages/complete/pages/pending-complete.page";
import { StakeCompletePage } from "./pages/complete/pages/stake-complete.page";
import { UnstakeCompletePage } from "./pages/complete/pages/unstake-complete.page";
-import { AnimatedFooterContent } from "./pages/components/footer-outlet";
import { Layout } from "./pages/components/layout";
import { headerContainer } from "./pages/components/layout/styles.css";
import { PoweredBy } from "./pages/components/powered-by";
@@ -174,8 +173,6 @@ export const Widget = () => {
-
-
diff --git a/packages/widget/src/components/atoms/token-icon/provider-icon/index.tsx b/packages/widget/src/components/atoms/token-icon/provider-icon/index.tsx
index 19dcd2aa..81996499 100644
--- a/packages/widget/src/components/atoms/token-icon/provider-icon/index.tsx
+++ b/packages/widget/src/components/atoms/token-icon/provider-icon/index.tsx
@@ -1,6 +1,5 @@
import type { TokenDto } from "../../../../domain/types/tokens";
import type { YieldMetadata } from "../../../../domain/types/yields";
-import { useSettings } from "../../../../providers/settings";
import type { Atoms } from "../../../../styles/theme/atoms.css";
import { NetworkLogoImage } from "../network-icon-image";
import { TokenIconContainer } from "../token-icon-container";
@@ -19,25 +18,23 @@ export const ProviderIcon = ({
tokenNetworkLogoHw?: Atoms["hw"];
hideNetwork?: boolean;
}) => {
- const { hideNetworkLogo } = useSettings();
-
return (
- {({ fallbackUrl, mainUrl, name, networkLogoUri, providerIcon }) => (
+ {({ fallbackUrl, mainUrl, name, providerIcon }) => (
<>
- {!hideNetwork && !hideNetworkLogo && (
+ {!hideNetwork && providerIcon && (
diff --git a/packages/widget/src/components/molecules/estimated-reward-amounts/index.tsx b/packages/widget/src/components/molecules/estimated-reward-amounts/index.tsx
new file mode 100644
index 00000000..7d494eb5
--- /dev/null
+++ b/packages/widget/src/components/molecules/estimated-reward-amounts/index.tsx
@@ -0,0 +1,135 @@
+import clsx from "clsx";
+import { useTranslation } from "react-i18next";
+import { selectYieldRewardsText } from "../../../pages/details/earn-page/components/select-yield-section/styles.css";
+import { VerticalDivider } from "../../../pages-dashboard/common/components/divider";
+import { useSettings } from "../../../providers/settings";
+import { combineRecipeWithVariant } from "../../../utils/styles";
+import { Box } from "../../atoms/box";
+import { Text } from "../../atoms/typography/text";
+
+type EstimatedRewardAmountsProps = {
+ earnYearly: string;
+ earnMonthly: string;
+};
+
+export const EstimatedRewardAmounts = ({
+ earnYearly,
+ earnMonthly,
+}: EstimatedRewardAmountsProps) => {
+ const { variant } = useSettings();
+
+ if (variant === "utila" || variant === "porto") {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+const DefaultEarnYearlyOrMonthly = ({
+ earnMonthly,
+ earnYearly,
+}: EstimatedRewardAmountsProps) => {
+ const { t } = useTranslation();
+ const { variant } = useSettings();
+
+ return (
+ <>
+
+
+ {t(variant === "zerion" ? "details.rewards.yearly" : "shared.yearly")}
+
+
+ {earnYearly}
+
+
+
+
+
+ {t("shared.monthly")}
+
+
+ {earnMonthly}
+
+
+ >
+ );
+};
+
+const UtilaEarnYearlyOrMonthly = ({
+ earnMonthly,
+ earnYearly,
+}: EstimatedRewardAmountsProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t("shared.yearly")}
+ {earnYearly}
+
+
+
+
+
+ {t("shared.monthly")}
+ {earnMonthly}
+
+
+ );
+};
diff --git a/packages/widget/src/domain/types/yields.ts b/packages/widget/src/domain/types/yields.ts
index 98590e46..52855af7 100644
--- a/packages/widget/src/domain/types/yields.ts
+++ b/packages/widget/src/domain/types/yields.ts
@@ -71,9 +71,9 @@ export type ValidatorsConfig = Map<
export type DashboardYieldCategory = "stake" | "defi" | "rwa";
export const dashboardYieldCategories = [
- "stake",
- "defi",
"rwa",
+ "defi",
+ "stake",
] as const satisfies ReadonlyArray;
/**
@@ -106,10 +106,6 @@ export const getApiYieldTypesForDashboardCategory = (
.filter(([, mapped]) => mapped === category)
.map(([yieldType]) => yieldType);
-export const getDashboardYieldCategoryForApiYieldType = (
- yieldType: ApiYieldType
-): DashboardYieldCategory => apiYieldTypeToDashboardCategory[yieldType];
-
export const getDashboardYieldCategory = (
yieldDto: YieldBase
): DashboardYieldCategory | null => {
@@ -221,7 +217,12 @@ export const isYieldActionArgRequired = (
export const getYieldRewardTokens = (yieldDto: YieldBase) =>
pipe(
- yieldDto.rewardRate?.components?.map((component) => component.token) ?? [],
+ [
+ ...(yieldDto.outputToken ? [yieldDto.outputToken] : []),
+ ...(yieldDto.rewardRate?.components?.map(
+ (component) => component.token
+ ) ?? []),
+ ],
EArray.dedupeWith((a, b) => tokenString(a) === tokenString(b)),
EArray.filter((token) => tokenString(token) !== tokenString(yieldDto.token))
);
diff --git a/packages/widget/src/hooks/api/use-dashboard-yield-catalog.ts b/packages/widget/src/hooks/api/use-dashboard-yield-catalog.ts
index 6c5e0817..876e0a41 100644
--- a/packages/widget/src/hooks/api/use-dashboard-yield-catalog.ts
+++ b/packages/widget/src/hooks/api/use-dashboard-yield-catalog.ts
@@ -33,21 +33,18 @@ const staleTime = 1000 * 60 * 2;
*/
export const useDashboardYieldCatalog = ({
enabled = true,
- network,
}: {
enabled?: boolean;
- network?: TokenDto["network"] | null;
} = {}) => {
- const { network: walletNetwork } = useSKWallet();
+ const { network, isConnecting } = useSKWallet();
const apiClient = useApiClient();
- const catalogNetwork = network === null ? null : (network ?? walletNetwork);
- const probeEnabled = enabled && (network === null || !!catalogNetwork);
+ const probeEnabled = enabled && !isConnecting;
const results = useQueries({
queries: dashboardYieldCategories.map((category) => {
const params: YieldSummariesParams = {
- ...(catalogNetwork ? { network: catalogNetwork } : {}),
+ ...(network ? { network: network } : {}),
types: getApiYieldTypesForDashboardCategory(category),
sort: "rewardRateDesc",
limit: DEFAULT_YIELD_SUMMARIES_PAGE_LIMIT,
diff --git a/packages/widget/src/hooks/api/use-multi-yields.ts b/packages/widget/src/hooks/api/use-multi-yields.ts
index cfb95121..a00b2fe5 100644
--- a/packages/widget/src/hooks/api/use-multi-yields.ts
+++ b/packages/widget/src/hooks/api/use-multi-yields.ts
@@ -34,6 +34,8 @@ import {
} from "../../domain/types/stake";
import type { SKWallet } from "../../domain/types/wallet";
import {
+ type DashboardYieldCategory,
+ getDashboardYieldCategory,
isNonZeroRewardRateYield,
type ValidatorsConfig,
type Yield,
@@ -152,7 +154,10 @@ export const getFirstEligibleYield = (
) =>
EitherAsync(() =>
params.queryClient.fetchQuery({
- queryKey: getFirstEligibleYieldQueryKey(params.yieldIds),
+ queryKey: getFirstEligibleYieldQueryKey({
+ yieldIds: params.yieldIds,
+ dashboardYieldCategory: params.dashboardYieldCategory,
+ }),
queryFn: () => firstValueFrom(firstEligibleYield$(params)),
})
).mapLeft((e) => {
@@ -235,6 +240,7 @@ const firstEligibleYield$ = (args: {
isConnected: boolean;
network: SKWallet["network"];
yieldIds: ReadonlyArray;
+ dashboardYieldCategory?: DashboardYieldCategory | null;
initParams: InitParams;
positionsData: PositionsData;
tokenBalanceAmount: BigNumber;
@@ -244,6 +250,11 @@ const firstEligibleYield$ = (args: {
let defaultYield: YieldSummaryWithProvider | null = null;
const successStream = multipleYieldSummaries$(args).pipe(
+ filter(
+ (y) =>
+ !args.dashboardYieldCategory ||
+ getDashboardYieldCategory(y) === args.dashboardYieldCategory
+ ),
tap((v) => {
if (isNonZeroRewardRateYield(v) || !defaultYield) {
defaultYield = v;
@@ -332,18 +343,25 @@ const defaultFiltered = createSelector(
})
);
-const getFirstEligibleYieldQueryKey = (yieldIds: ReadonlyArray) => [
- "first-eligible-yield",
+const getFirstEligibleYieldQueryKey = ({
yieldIds,
-];
+ dashboardYieldCategory,
+}: {
+ yieldIds: ReadonlyArray;
+ dashboardYieldCategory?: DashboardYieldCategory | null;
+}) => ["first-eligible-yield", yieldIds, dashboardYieldCategory ?? null];
export const getCachedFirstEligibleYield = ({
queryClient,
yieldIds,
+ dashboardYieldCategory,
}: {
queryClient: QueryClient;
yieldIds: ReadonlyArray;
+ dashboardYieldCategory?: DashboardYieldCategory | null;
}) =>
Maybe.fromNullable(
- queryClient.getQueryData(getFirstEligibleYieldQueryKey(yieldIds))
+ queryClient.getQueryData(
+ getFirstEligibleYieldQueryKey({ yieldIds, dashboardYieldCategory })
+ )
);
diff --git a/packages/widget/src/hooks/api/use-token-list-yields.ts b/packages/widget/src/hooks/api/use-token-list-yields.ts
deleted file mode 100644
index 3f6c9439..00000000
--- a/packages/widget/src/hooks/api/use-token-list-yields.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import type { TokenBalanceScanResponseDto } from "../../domain/types/token-balance";
-import {
- type DashboardYieldCategory,
- getDashboardYieldCategoryForApiYieldType,
-} from "../../domain/types/yields";
-import type { YieldDto } from "../../generated/api/yield";
-import type { useApiClient } from "../../providers/api/api-client-provider";
-import {
- getRewardRateFormatted,
- getRewardTypeFormatted,
-} from "../../utils/formatters";
-import {
- fetchYieldSummariesByIds,
- isVisibleYieldSummary,
-} from "./use-yield-summaries";
-
-type TokenMaxYieldRate = {
- rateFormatted: string;
- rateTypeLabel: string;
-};
-
-const getUniqueYieldIds = (
- tokenBalances: ReadonlyArray
-) => [...new Set(tokenBalances.flatMap((tb) => tb.availableYields))];
-
-export const fetchTokenListYieldSummaries = ({
- apiClient,
- signal,
- tokenBalances,
-}: {
- apiClient: ReturnType;
- signal?: AbortSignal;
- tokenBalances: ReadonlyArray;
-}) =>
- fetchYieldSummariesByIds({
- apiClient,
- signal,
- yieldIds: getUniqueYieldIds(tokenBalances),
- });
-
-export const getDashboardCategoryYieldIdsForToken = (
- availableYieldIds: ReadonlyArray,
- yieldsById: ReadonlyMap,
- category: DashboardYieldCategory
-) =>
- availableYieldIds
- .filter((id) => {
- const yieldDto = yieldsById.get(id);
-
- return (
- !!yieldDto &&
- isVisibleYieldSummary(yieldDto) &&
- getDashboardYieldCategoryForApiYieldType(yieldDto.mechanics.type) ===
- category
- );
- })
- .sort((a, b) => {
- const left = yieldsById.get(a)?.rewardRate?.total ?? 0;
- const right = yieldsById.get(b)?.rewardRate?.total ?? 0;
-
- return right - left;
- });
-
-export const getMaxYieldRateForToken = (
- availableYieldIds: ReadonlyArray,
- yieldsById: ReadonlyMap
-): TokenMaxYieldRate | null => {
- const yields = availableYieldIds
- .map((id) => yieldsById.get(id))
- .filter((yieldDto): yieldDto is YieldDto => !!yieldDto);
-
- if (yields.length === 0) {
- return null;
- }
-
- const maxYield = yields.reduce((max, yieldDto) =>
- (yieldDto.rewardRate?.total ?? 0) > (max.rewardRate?.total ?? 0)
- ? yieldDto
- : max
- );
-
- const rewardType = maxYield.rewardRate?.rateType;
- const rateFormatted = getRewardRateFormatted({
- rewardRate: maxYield.rewardRate?.total,
- });
-
- const rateTypeLabel = getRewardTypeFormatted(rewardType);
-
- if (rateFormatted === "- %" || !rateTypeLabel) {
- return null;
- }
-
- return {
- rateFormatted,
- rateTypeLabel,
- };
-};
diff --git a/packages/widget/src/hooks/api/use-yield-summaries.ts b/packages/widget/src/hooks/api/use-yield-summaries.ts
index 3fa4dc23..eb697dd4 100644
--- a/packages/widget/src/hooks/api/use-yield-summaries.ts
+++ b/packages/widget/src/hooks/api/use-yield-summaries.ts
@@ -170,36 +170,3 @@ export const fetchYieldSummariesWithProvidersByIds = async ({
});
});
};
-
-/**
- * Fetch every page of yield summaries for the given params, looping `offset`
- * until all `total` items have been retrieved.
- */
-export const fetchAllYieldSummaries = async ({
- apiClient,
- params,
- signal,
-}: FetchArgs): Promise => {
- const client = apiClient.withOptions({ signal });
- const limit = params.limit ?? DEFAULT_YIELD_SUMMARIES_PAGE_LIMIT;
-
- const all: YieldSummary[] = [];
- let offset = 0;
-
- while (true) {
- const result = await client.yield.YieldsControllerGetYields({
- params: { ...params, limit, offset },
- });
-
- const items = result.items ?? [];
- all.push(...items);
-
- if (items.length === 0 || all.length >= result.total) {
- break;
- }
-
- offset += items.length;
- }
-
- return all;
-};
diff --git a/packages/widget/src/navigation/containers/animation-layout.tsx b/packages/widget/src/navigation/containers/animation-layout.tsx
index ef27efcc..c5a24098 100644
--- a/packages/widget/src/navigation/containers/animation-layout.tsx
+++ b/packages/widget/src/navigation/containers/animation-layout.tsx
@@ -3,7 +3,6 @@ import { motion } from "motion/react";
import { Just } from "purify-ts";
import type { PropsWithChildren } from "react";
import { useHeaderHeight } from "../../components/molecules/header/use-sync-header-height";
-import { useFooterHeight } from "../../pages/components/footer-outlet/context";
import { useCurrentLayout } from "../../pages/components/layout/layout-context";
import { usePoweredByHeight } from "../../pages/components/powered-by";
import { useMountAnimation } from "../../providers/mount-animation";
@@ -17,7 +16,6 @@ export const [useDisableTransitionDuration, DisableTransitionDurationProvider] =
export const AnimationLayout = ({ children }: PropsWithChildren) => {
const currentLayout = useCurrentLayout();
const [headerHeight] = useHeaderHeight();
- const [footerHeight] = useFooterHeight();
const [poweredByHeight] = usePoweredByHeight();
const { state, dispatch } = useMountAnimation();
@@ -26,10 +24,7 @@ export const AnimationLayout = ({ children }: PropsWithChildren) => {
const containerHeight =
currentLayout.state?.height && headerHeight
- ? currentLayout.state.height +
- headerHeight +
- footerHeight +
- poweredByHeight
+ ? currentLayout.state.height + headerHeight + poweredByHeight
: 0;
const [disableTransitionDuration] = useDisableTransitionDuration();
diff --git a/packages/widget/src/navigation/containers/animation-page.tsx b/packages/widget/src/navigation/containers/animation-page.tsx
index 191cffb0..98bf973a 100644
--- a/packages/widget/src/navigation/containers/animation-page.tsx
+++ b/packages/widget/src/navigation/containers/animation-page.tsx
@@ -1,12 +1,21 @@
import { motion } from "motion/react";
-import type { PropsWithChildren } from "react";
+import type { CSSProperties, PropsWithChildren } from "react";
import { useSettings } from "../../providers/settings";
export const AnimationPage = ({ children }: PropsWithChildren) => {
const { dashboardVariant } = useSettings();
+ const dashboardLayoutStyle: CSSProperties | undefined = dashboardVariant
+ ? {
+ display: "flex",
+ flex: 1,
+ flexDirection: "column",
+ minHeight: 0,
+ }
+ : undefined;
return (
{
return (
) => {
- return (
-
-
-
- );
-};
-
-export const FooterOutlet = () => {
- const [val] = useFooterButton();
-
- const [, setFooterHeight] = useFooterHeight();
-
- const { containerRef } = useSyncFooterHeight();
-
- useEffect(() => {
- !val && setFooterHeight(0);
- }, [setFooterHeight, val]);
-
- if (!val) return null;
-
- return (
-
-
-
- );
-};
diff --git a/packages/widget/src/pages-dashboard/common/components/tabs/index.tsx b/packages/widget/src/pages-dashboard/common/components/tabs/index.tsx
index 58cd8b28..8478e96c 100644
--- a/packages/widget/src/pages-dashboard/common/components/tabs/index.tsx
+++ b/packages/widget/src/pages-dashboard/common/components/tabs/index.tsx
@@ -55,7 +55,11 @@ export const Tabs = () => {
const selectedTab = Match.value(current.pathname).pipe(
Match.when(startsWith("/activity"), () => "activity"),
- Match.when(startsWith("/manage"), () => "manage"),
+ Match.whenOr(
+ startsWith("/manage"),
+ startsWith("/positions"),
+ () => "manage"
+ ),
Match.orElse(() => "earn")
);
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 c86553ab..22236160 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
@@ -53,6 +53,11 @@ export const formatRewardRate = (
return `${APToPercentage(amount.toNumber())}%`;
};
+export const formatMinStakeLabel = (yieldDto: Yield, t: TFunction): string =>
+ getDashboardYieldCategory(yieldDto) === "rwa"
+ ? t("dashboard.earn_details.minimum_subscription")
+ : t("dashboard.earn_details.min_stake");
+
export const formatMinStake = (
yieldDto: Yield,
t: TFunction
@@ -141,6 +146,18 @@ export const formatRewardTokenLabel = (yieldDto: Yield) => {
: symbol;
};
+export const formatPricePerShare = (yieldDto: Yield): string | null => {
+ const price = yieldDto.state?.pricePerShareState?.price;
+
+ if (price === null || price === undefined) return null;
+
+ const amount = BigNumber(price);
+
+ if (!amount.isFinite() || amount.isLessThanOrEqualTo(0)) return null;
+
+ return formatNumber(amount, 8);
+};
+
export const formatCooldownDays = (days: number, t: TFunction): string => {
return days > 0
? t("dashboard.earn_details.cooldown_days", { count: days })
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 d462a161..7fbe5b6a 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
@@ -33,8 +33,10 @@ import {
formatMeaningfulCompactNumber,
formatMeaningfulCompactUsd,
formatMinStake,
+ formatMinStakeLabel,
formatNetworkName,
formatOptionalDays,
+ formatPricePerShare,
formatRequirementStatus,
formatRewardClaiming,
formatRewardRate,
@@ -259,6 +261,7 @@ const getDetailRows = ({
label: t("dashboard.earn_details.reward_token"),
value: formatRewardTokenLabel(yieldDto),
},
+ ...getPricePerShareRows(yieldDto, t),
...facts
.filter((fact) => fact.detailEligible && !promotedFactIds.has(fact.id))
.map((fact) => ({
@@ -268,6 +271,23 @@ const getDetailRows = ({
})),
];
+const getPricePerShareRows = (
+ yieldDto: Yield,
+ t: TFunction
+): EarnDetailRow[] => {
+ const value = formatPricePerShare(yieldDto);
+
+ if (!value) return [];
+
+ return [
+ {
+ id: "price-per-share",
+ label: t("dashboard.earn_details.price_per_share"),
+ value,
+ },
+ ];
+};
+
const getRewardRateFact = ({
effectiveRewardRate,
t,
@@ -325,7 +345,7 @@ const getMinStakeFact = (
id: "min-stake",
kpiEligible: true,
kpiPrimaryEligible: value.kpiPrimaryEligible,
- label: t("dashboard.earn_details.min_stake"),
+ label: formatMinStakeLabel(yieldDto, t),
value: value.value,
};
};
diff --git a/packages/widget/src/pages-dashboard/overview/earn-details/styles.css.ts b/packages/widget/src/pages-dashboard/overview/earn-details/styles.css.ts
index 59d78c63..be34d414 100644
--- a/packages/widget/src/pages-dashboard/overview/earn-details/styles.css.ts
+++ b/packages/widget/src/pages-dashboard/overview/earn-details/styles.css.ts
@@ -5,17 +5,23 @@ import { vars } from "../../../styles/theme/contract.css";
import { OUTLET_PADDING } from "../../common/components/styles.css";
export const container = style({
+ bottom: 0,
boxSizing: "border-box",
- maxHeight: "620px",
- overflowY: "auto",
- scrollbarGutter: "stable",
+ left: 0,
marginRight: `calc(-1 * ${OUTLET_PADDING})`,
+ overflowY: "auto",
paddingRight: OUTLET_PADDING,
+ position: "absolute",
+ right: 0,
+ scrollbarGutter: "stable",
+ top: 0,
});
export const earnDetailsWrapper = style({
- alignSelf: "flex-start",
+ alignSelf: "stretch",
+ minHeight: "620px",
minWidth: 0,
+ position: "relative",
});
export const headerProviderText = style({
diff --git a/packages/widget/src/pages-dashboard/overview/earn-page/index.tsx b/packages/widget/src/pages-dashboard/overview/earn-page/index.tsx
index 88ee0bf1..aa803d9d 100644
--- a/packages/widget/src/pages-dashboard/overview/earn-page/index.tsx
+++ b/packages/widget/src/pages-dashboard/overview/earn-page/index.tsx
@@ -1,6 +1,7 @@
import { Box } from "../../../components/atoms/box";
import { Divider } from "../../../components/atoms/divider";
import { KycGateCard } from "../../../components/molecules/kyc-gate-card";
+import { PageCtaButton } from "../../../pages/components/page-cta";
import { ExtraArgsSelection } from "../../../pages/details/earn-page/components/extra-args-selection";
import { Footer } from "../../../pages/details/earn-page/components/footer";
import { SelectTokenSection } from "../../../pages/details/earn-page/components/select-token-section";
@@ -31,6 +32,7 @@ const EarnKycGateSection = () => {
export const EarnPageContent = () => {
const { variant } = useSettings();
+ const { cta } = useEarnPageContext();
return (
@@ -58,6 +60,8 @@ export const EarnPageContent = () => {
+
+
);
};
diff --git a/packages/widget/src/pages-dashboard/overview/index.tsx b/packages/widget/src/pages-dashboard/overview/index.tsx
index 45d084bd..85a4edac 100644
--- a/packages/widget/src/pages-dashboard/overview/index.tsx
+++ b/packages/widget/src/pages-dashboard/overview/index.tsx
@@ -3,7 +3,6 @@ import { Box } from "../../components/atoms/box";
import { AnimationPage } from "../../navigation/containers/animation-page";
import { BackButtonProvider } from "../common/components/back-button";
import { VerticalDivider } from "../common/components/divider";
-import { FooterOutlet } from "../common/components/footer-outlet";
import { TabPageContainer } from "../common/components/tab-page-container";
import { EarnDetails } from "./earn-details";
import { earnDetailsWrapper } from "./earn-details/styles.css";
@@ -26,8 +25,6 @@ export const OverviewPage = () => {
-
-
diff --git a/packages/widget/src/pages-dashboard/position-details/components/position-details-actions.tsx b/packages/widget/src/pages-dashboard/position-details/components/position-details-actions.tsx
index 0712c80a..151a6b33 100644
--- a/packages/widget/src/pages-dashboard/position-details/components/position-details-actions.tsx
+++ b/packages/widget/src/pages-dashboard/position-details/components/position-details-actions.tsx
@@ -9,9 +9,9 @@ import { SelectValidator } from "../../../components/molecules/select-validator"
import type { YieldPendingActionType } from "../../../domain/types/pending-action";
import { getExtendedYieldType } from "../../../domain/types/yields";
import {
- type FooterButtonVal,
- useRegisterFooterButton,
-} from "../../../pages/components/footer-outlet/context";
+ type PageCta,
+ PageCtaButton,
+} from "../../../pages/components/page-cta";
import {
AmountBlock,
UnstakeInfo,
@@ -83,7 +83,7 @@ export const PositionDetailsActions = () => {
} = usePositionDetails();
const { t } = useTranslation();
- const unstakeFooterButton = useMemo(
+ const unstakeCta = useMemo(
() =>
isLoading
? null
@@ -114,8 +114,6 @@ export const PositionDetailsActions = () => {
]
);
- useRegisterFooterButton(unstakeFooterButton);
-
if (isLoading) {
return (
{
validators={providersDetails.orDefault([])}
yieldDto={v.integrationData}
/>
+
+
>
)
)
diff --git a/packages/widget/src/pages-dashboard/position-details/components/position-details-stake-actions.tsx b/packages/widget/src/pages-dashboard/position-details/components/position-details-stake-actions.tsx
index 4457e130..61f3134c 100644
--- a/packages/widget/src/pages-dashboard/position-details/components/position-details-stake-actions.tsx
+++ b/packages/widget/src/pages-dashboard/position-details/components/position-details-stake-actions.tsx
@@ -6,14 +6,12 @@ import { Spinner } from "../../../components/atoms/spinner";
import { Text } from "../../../components/atoms/typography/text";
import { KycGateCard } from "../../../components/molecules/kyc-gate-card";
import { useUnstakeOrPendingActionParams } from "../../../hooks/navigation/use-unstake-or-pending-action-params";
+import { PageCtaButton } from "../../../pages/components/page-cta";
import { ExtraArgsSelection } from "../../../pages/details/earn-page/components/extra-args-selection";
import { Footer } from "../../../pages/details/earn-page/components/footer";
import { SelectTokenSection } from "../../../pages/details/earn-page/components/select-token-section";
import { useEarnPageContext } from "../../../pages/details/earn-page/state/earn-page-context";
-import {
- useEarnPageDispatch,
- useEarnPageState,
-} from "../../../pages/details/earn-page/state/earn-page-state-context";
+import { useEarnPageDispatch } from "../../../pages/details/earn-page/state/earn-page-state-context";
import { usePositionDetails } from "../../../pages/position-details/hooks/use-position-details";
import { PositionDetailsActionTabs } from "./position-details-action-tabs";
import {
@@ -40,38 +38,21 @@ const StakeKycGateSection = () => {
);
};
-export const shouldInitializePositionDetailsStakeState = ({
- positionYieldId,
- selectedEarnYieldId,
-}: {
- positionYieldId: string | null;
- selectedEarnYieldId: string | null;
-}) => !!positionYieldId && selectedEarnYieldId !== positionYieldId;
-
const PositionDetailsStakeStateInitializer = ({
positionDetails,
}: {
positionDetails: ReturnType;
}) => {
const dispatch = useEarnPageDispatch();
- const { selectedStakeId } = useEarnPageState();
const positionYield = positionDetails.integrationData.extractNullable();
- const selectedEarnYieldId = selectedStakeId.extractNullable();
useEffect(() => {
- if (
- !shouldInitializePositionDetailsStakeState({
- positionYieldId: positionYield?.id ?? null,
- selectedEarnYieldId,
- }) ||
- !positionYield
- ) {
+ if (!positionYield) {
return;
}
- dispatch({ type: "token/select", data: positionYield.token });
- dispatch({ type: "yield/select", data: positionYield });
- }, [dispatch, positionYield, selectedEarnYieldId]);
+ dispatch({ type: "positionDetails/stake/initialize", data: positionYield });
+ }, [dispatch, positionYield]);
return null;
};
@@ -79,6 +60,7 @@ const PositionDetailsStakeStateInitializer = ({
export const PositionDetailsStakeActions = () => {
const positionDetails = usePositionDetails();
const { plain } = useUnstakeOrPendingActionParams();
+ const { cta } = useEarnPageContext();
const { t } = useTranslation();
if (positionDetails.isLoading) {
@@ -147,6 +129,8 @@ export const PositionDetailsStakeActions = () => {
+
+
);
};
diff --git a/packages/widget/src/pages-dashboard/position-details/index.tsx b/packages/widget/src/pages-dashboard/position-details/index.tsx
index 9530fd3f..12d62248 100644
--- a/packages/widget/src/pages-dashboard/position-details/index.tsx
+++ b/packages/widget/src/pages-dashboard/position-details/index.tsx
@@ -10,7 +10,6 @@ import {
BackButtonProvider,
} from "../common/components/back-button";
import { VerticalDivider } from "../common/components/divider";
-import { FooterOutlet } from "../common/components/footer-outlet";
import { TabPageContainer } from "../common/components/tab-page-container";
import {
positionDetailsActionsHasContent,
@@ -85,8 +84,6 @@ const PositionDetailsPageComponent = () => {
justifyContent="space-between"
>
-
-
) : null}
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 1a35b55c..33b15aa7 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
@@ -28,8 +28,10 @@ import {
formatCooldownDays,
formatEnumValue,
formatMinStake,
+ formatMinStakeLabel,
formatNetworkName,
formatOptionalDays,
+ formatPricePerShare,
formatRewardClaiming,
formatRewardRateLabel,
formatRewardTokenLabel,
@@ -488,6 +490,7 @@ const getDetailRows = ({
}): DashboardPositionDetailRow[] => {
const risk = getYieldRiskDisplay(integrationData);
const minStake = formatMinStake(integrationData, t);
+ const pricePerShare = formatPricePerShare(integrationData);
const cooldown = formatCooldownDays(
getYieldCooldownPeriod(integrationData)?.days ?? 0,
t
@@ -519,6 +522,13 @@ const getDetailRows = ({
label: t("dashboard.earn_details.reward_token"),
value: formatRewardTokenLabel(integrationData),
},
+ pricePerShare
+ ? {
+ id: "price-per-share",
+ label: t("dashboard.earn_details.price_per_share"),
+ value: pricePerShare,
+ }
+ : null,
{
id: "type",
label: t("dashboard.earn_details.type"),
@@ -544,7 +554,7 @@ const getDetailRows = ({
minStake
? {
id: "min-stake",
- label: t("dashboard.earn_details.min_stake"),
+ label: formatMinStakeLabel(integrationData, t),
value: minStake.value,
}
: null,
diff --git a/packages/widget/src/pages/complete/hooks/use-complete.hook.ts b/packages/widget/src/pages/complete/hooks/use-complete.hook.ts
index f30093b2..67b9ce94 100644
--- a/packages/widget/src/pages/complete/hooks/use-complete.hook.ts
+++ b/packages/widget/src/pages/complete/hooks/use-complete.hook.ts
@@ -12,7 +12,7 @@ import { useSavedRef } from "../../../hooks/use-saved-ref";
import { useSKWallet } from "../../../providers/sk-wallet";
import { isMobile } from "../../../utils";
import { MaybeWindow } from "../../../utils/maybe-window";
-import { useRegisterFooterButton } from "../../components/footer-outlet/context";
+import type { PageCta } from "../../components/page-cta";
export const useComplete = () => {
const navigate = useNavigate();
@@ -55,19 +55,17 @@ export const useComplete = () => {
const { t } = useTranslation();
- useRegisterFooterButton(
- useMemo(
- () => ({
- disabled: false,
- isLoading: false,
- label: t("complete.continue", {
- context: isLedgerLive ? "ledger" : undefined,
- }),
- onClick: () => onClickRef.current(),
- hide: !!activityReviewMatch,
+ const cta = useMemo(
+ () => ({
+ disabled: false,
+ isLoading: false,
+ label: t("complete.continue", {
+ context: isLedgerLive ? "ledger" : undefined,
}),
- [onClickRef, t, activityReviewMatch, isLedgerLive]
- )
+ onClick: () => onClickRef.current(),
+ hide: !!activityReviewMatch,
+ }),
+ [onClickRef, t, activityReviewMatch, isLedgerLive]
);
return {
@@ -75,5 +73,6 @@ export const useComplete = () => {
unstakeMatch: !!(unstakeMatch || activityUnstakeMatch),
pendingActionMatch: !!(pendingActionMatch || activityPendingMatch),
onViewTransactionClick,
+ cta,
};
};
diff --git a/packages/widget/src/pages/complete/pages/common.page.tsx b/packages/widget/src/pages/complete/pages/common.page.tsx
index cd1e4f9a..39d1d5f1 100644
--- a/packages/widget/src/pages/complete/pages/common.page.tsx
+++ b/packages/widget/src/pages/complete/pages/common.page.tsx
@@ -17,6 +17,7 @@ import {
import { AnimationPage } from "../../../navigation/containers/animation-page";
import { capitalizeFirstLowerRest } from "../../../utils/text";
import { PageContainer } from "../../components/page-container";
+import { PageCtaButton } from "../../components/page-cta";
import { useComplete } from "../hooks/use-complete.hook";
import {
CompleteCommonContextProvider,
@@ -51,8 +52,13 @@ export const CompletePageComponent = ({
}: Props) => {
const { t } = useTranslation();
- const { onViewTransactionClick, unstakeMatch, pendingActionMatch, urls } =
- useCompleteCommonContext();
+ const {
+ cta,
+ onViewTransactionClick,
+ unstakeMatch,
+ pendingActionMatch,
+ urls,
+ } = useCompleteCommonContext();
return (
@@ -211,6 +217,7 @@ export const CompletePageComponent = ({
))}
+
);
diff --git a/packages/widget/src/pages/complete/state/index.tsx b/packages/widget/src/pages/complete/state/index.tsx
index 945e8827..1e985100 100644
--- a/packages/widget/src/pages/complete/state/index.tsx
+++ b/packages/widget/src/pages/complete/state/index.tsx
@@ -1,7 +1,9 @@
import { createContext, type PropsWithChildren, useContext } from "react";
import type { TransactionType } from "../../../domain/types/action";
+import type { PageCta } from "../../components/page-cta";
type CompleteCommonContextType = {
+ cta: PageCta;
urls: {
type: TransactionType;
url: string;
@@ -12,6 +14,7 @@ type CompleteCommonContextType = {
};
const CompleteCommonContext = createContext({
+ cta: null,
urls: [],
unstakeMatch: false,
pendingActionMatch: false,
diff --git a/packages/widget/src/pages/components/footer-outlet/context.ts b/packages/widget/src/pages/components/footer-outlet/context.ts
deleted file mode 100644
index ee540353..00000000
--- a/packages/widget/src/pages/components/footer-outlet/context.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { useIsPresent } from "motion/react";
-import { useEffect } from "react";
-import { useSyncElementHeight } from "../../../hooks/use-sync-element-height";
-import createStateContext from "../../../utils/create-state-context";
-
-export const [useFooterHeight, FooterHeightProvider] = createStateContext(0);
-
-export type FooterButtonVal = {
- onClick: () => void;
- disabled: boolean;
- isLoading: boolean;
- label: string;
- variant?: "primary" | "secondary";
- hide?: boolean;
-} | null;
-
-export const [useFooterButton, FooterButtonProvider] =
- createStateContext(null);
-
-export const useSyncFooterHeight = () =>
- useSyncElementHeight(useFooterHeight()[1]);
-
-export const useRegisterFooterButton = (val: FooterButtonVal) => {
- const [, setFooterButton] = useFooterButton();
-
- const isPresent = useIsPresent();
-
- useEffect(() => {
- if (isPresent) return;
- setFooterButton((prev) => (prev === val ? null : prev));
- }, [isPresent, setFooterButton, val]);
-
- useEffect(() => {
- if (!isPresent || val?.hide) return;
- setFooterButton(val);
-
- return () => {
- setFooterButton((prev) => (prev === val ? null : prev));
- };
- }, [isPresent, setFooterButton, val]);
-};
diff --git a/packages/widget/src/pages/components/footer-outlet/index.tsx b/packages/widget/src/pages/components/footer-outlet/index.tsx
deleted file mode 100644
index 7256529d..00000000
--- a/packages/widget/src/pages/components/footer-outlet/index.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import type { MotionProps, TargetAndTransition } from "motion/react";
-import { motion } from "motion/react";
-import { Just } from "purify-ts";
-import { useEffect, useState } from "react";
-import { Box } from "../../../components/atoms/box";
-import { Button } from "../../../components/atoms/button";
-import { useMountAnimation } from "../../../providers/mount-animation";
-import { useSettings } from "../../../providers/settings";
-import type { FooterButtonVal } from "./context";
-import {
- useFooterButton,
- useFooterHeight,
- useSyncFooterHeight,
-} from "./context";
-import { footerContainer } from "./styles.css";
-
-const FooterButton = ({
- disabled,
- isLoading,
- onClick,
- label,
- variant,
-}: NonNullable) => {
- return (
-
-
-
-
-
- );
-};
-
-const AnimatedFooterButton = (props: NonNullable) => {
- const { containerRef } = useSyncFooterHeight();
-
- const { state } = useMountAnimation();
- const [initAnimationFinished, setInitAnimationFinished] = useState(
- state.layout
- );
-
- const { disableInitLayoutAnimation } = useSettings();
-
- const { animate, initial } = Just({ translateY: 0, opacity: 1 })
- .chain<{ animate: TargetAndTransition; initial: MotionProps["initial"] }>(
- (animateTo) =>
- Just(null).map<{
- animate: TargetAndTransition;
- initial: MotionProps["initial"];
- }>(() => {
- if (disableInitLayoutAnimation && !state.layout) {
- return {
- animate: {},
- initial: { opacity: 1, translateY: 0 },
- };
- }
- if (state.layout) {
- return {
- animate: {
- ...animateTo,
- transition: {
- duration: initAnimationFinished ? 0.3 : 0.6,
- delay: 0.2,
- },
- },
- initial: { opacity: 0, translateY: "-20px" },
- };
- }
-
- return {
- animate: {},
- initial: { opacity: 0, translateY: "-40px" },
- };
- })
- )
- .unsafeCoerce();
-
- return (
- {
- if (def.translateY !== 0 || initAnimationFinished) return;
-
- setInitAnimationFinished(true);
- }}
- >
-
-
- );
-};
-
-export const AnimatedFooterContent = () => {
- const [val] = useFooterButton();
-
- const [, setFooterHeight] = useFooterHeight();
-
- useEffect(() => {
- !val && setFooterHeight(0);
- }, [setFooterHeight, val]);
-
- if (!val) return null;
-
- return ;
-};
diff --git a/packages/widget/src/pages/components/footer-outlet/styles.css.ts b/packages/widget/src/pages/components/footer-outlet/styles.css.ts
deleted file mode 100644
index c19c65b7..00000000
--- a/packages/widget/src/pages/components/footer-outlet/styles.css.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { style } from "@vanilla-extract/css";
-
-export const footerContainer = style({ zIndex: 1 });
diff --git a/packages/widget/src/pages/components/page-container.tsx b/packages/widget/src/pages/components/page-container.tsx
index 00849730..5565a529 100644
--- a/packages/widget/src/pages/components/page-container.tsx
+++ b/packages/widget/src/pages/components/page-container.tsx
@@ -16,7 +16,6 @@ export const PageContainer = ({
display="flex"
flexDirection="column"
px={dashboardVariant ? "0" : "4"}
- marginBottom="4"
flex={1}
paddingTop={dashboardVariant ? "0" : "2"}
gap={dashboardVariant ? "2" : "0"}
diff --git a/packages/widget/src/pages/components/page-cta.tsx b/packages/widget/src/pages/components/page-cta.tsx
new file mode 100644
index 00000000..09f96315
--- /dev/null
+++ b/packages/widget/src/pages/components/page-cta.tsx
@@ -0,0 +1,34 @@
+import { Box } from "../../components/atoms/box";
+import { Button } from "../../components/atoms/button";
+
+export type PageCta = {
+ disabled: boolean;
+ hide?: boolean;
+ isLoading: boolean;
+ label: string;
+ onClick: () => void;
+ variant?: "primary" | "secondary";
+} | null;
+
+export const PageCtaButton = ({ cta }: { cta: PageCta }) => {
+ if (!cta || cta.hide) return null;
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/widget/src/pages/details/activity-page/hooks/use-activity-filters.ts b/packages/widget/src/pages/details/activity-page/hooks/use-activity-filters.ts
index a47f63f7..f04deb3f 100644
--- a/packages/widget/src/pages/details/activity-page/hooks/use-activity-filters.ts
+++ b/packages/widget/src/pages/details/activity-page/hooks/use-activity-filters.ts
@@ -47,9 +47,8 @@ export const useActivityFilters = (
count: counts.get(category) ?? 0,
}));
- // Only surface filters when there is more than one category to switch between.
const options: ActivityFilterOption[] =
- categoryOptions.length > 1
+ categoryOptions.length > 0
? [{ filter: "all", count: totalCount }, ...categoryOptions]
: [];
diff --git a/packages/widget/src/pages/details/earn-page/components/select-yield-section/select-yield-reward-details.tsx b/packages/widget/src/pages/details/earn-page/components/select-yield-section/select-yield-reward-details.tsx
index 4a16e97b..1fab308a 100644
--- a/packages/widget/src/pages/details/earn-page/components/select-yield-section/select-yield-reward-details.tsx
+++ b/packages/widget/src/pages/details/earn-page/components/select-yield-section/select-yield-reward-details.tsx
@@ -1,11 +1,11 @@
import BigNumber from "bignumber.js";
-import clsx from "clsx";
import { Trans, useTranslation } from "react-i18next";
import { Box } from "../../../../../components/atoms/box";
import { Divider } from "../../../../../components/atoms/divider";
import { MorphoStarsIcon } from "../../../../../components/atoms/icons/morpho-stars";
import { Image } from "../../../../../components/atoms/image";
import { Text } from "../../../../../components/atoms/typography/text";
+import { EstimatedRewardAmounts } from "../../../../../components/molecules/estimated-reward-amounts";
import { RewardRateBreakdown } from "../../../../../components/molecules/reward-rate-breakdown";
import { isMorphoProvider } from "../../../../../components/molecules/reward-token-details";
import { getEffectiveYieldRewardRateDetails } from "../../../../../domain/types/reward-rate";
@@ -17,12 +17,10 @@ import {
getYieldOutputToken,
getYieldTypeLabels,
} from "../../../../../domain/types/yields";
-import { VerticalDivider } from "../../../../../pages-dashboard/common/components/divider";
import { useSettings } from "../../../../../providers/settings";
import { formatNumber } from "../../../../../utils";
-import { combineRecipeWithVariant } from "../../../../../utils/styles";
import { useEarnPageContext } from "../../state/earn-page-context";
-import { selectYieldRewardsText } from "./styles.css";
+import { viaProviderImage } from "./styles.css";
export const SelectYieldRewardDetails = () => {
const { variant } = useSettings();
@@ -149,17 +147,10 @@ export const SelectYieldRewardDetails = () => {
))
.extractNullable()}
- {variant === "utila" || variant === "porto" ? (
-
- ) : (
-
- )}
+
{rewardRateDetails
.map((rewardRate) => (
@@ -219,7 +210,7 @@ const YieldStrategyDetails = ({
)}
);
};
-
-const DefaultEarnYearlyOrMonthly = ({
- earnMonthly,
- earnYearly,
-}: {
- earnMonthly: string;
- earnYearly: string;
-}) => {
- const { t } = useTranslation();
-
- const { variant } = useSettings();
-
- return (
- <>
-
-
- {t(variant === "zerion" ? "details.rewards.yearly" : "shared.yearly")}
-
-
- {earnYearly}
-
-
-
-
-
- {t("shared.monthly")}
-
-
- {earnMonthly}
-
-
- >
- );
-};
-
-const UtilaEarnYearlyOrMonthly = ({
- earnMonthly,
- earnYearly,
-}: {
- earnMonthly: string;
- earnYearly: string;
-}) => {
- const { t } = useTranslation();
-
- return (
-
-
- {t("shared.yearly")}
- {earnYearly}
-
-
-
-
-
- {t("shared.monthly")}
- {earnMonthly}
-
-
- );
-};
diff --git a/packages/widget/src/pages/details/earn-page/components/select-yield-section/styles.css.ts b/packages/widget/src/pages/details/earn-page/components/select-yield-section/styles.css.ts
index d6d4632f..506b8066 100644
--- a/packages/widget/src/pages/details/earn-page/components/select-yield-section/styles.css.ts
+++ b/packages/widget/src/pages/details/earn-page/components/select-yield-section/styles.css.ts
@@ -1,3 +1,4 @@
+import { style } from "@vanilla-extract/css";
import { recipe } from "@vanilla-extract/recipes";
import { atoms } from "../../../../../styles/theme/atoms.css";
import { vars } from "../../../../../styles/theme/contract.css";
@@ -61,3 +62,7 @@ export const selectYieldSection = recipe({
},
},
});
+
+export const viaProviderImage = style({
+ minWidth: 0,
+});
diff --git a/packages/widget/src/pages/details/earn-page/earn.page.tsx b/packages/widget/src/pages/details/earn-page/earn.page.tsx
index 4f24698f..9876b9e8 100644
--- a/packages/widget/src/pages/details/earn-page/earn.page.tsx
+++ b/packages/widget/src/pages/details/earn-page/earn.page.tsx
@@ -10,6 +10,7 @@ import { useTrackPage } from "../../../hooks/tracking/use-track-page";
import { useMountAnimation } from "../../../providers/mount-animation";
import { useSettings } from "../../../providers/settings";
import { PageContainer } from "../../components/page-container";
+import { PageCtaButton } from "../../components/page-cta";
import { ExtraArgsSelection } from "./components/extra-args-selection";
import { Footer } from "./components/footer";
import { SelectProvider } from "./components/select-provider";
@@ -48,7 +49,7 @@ const EarnPageComponent = () => {
const { variant } = useSettings();
- const { isError } = useEarnPageContext();
+ const { cta, isError } = useEarnPageContext();
return (
@@ -81,6 +82,8 @@ const EarnPageComponent = () => {
+
+
);
};
diff --git a/packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx b/packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx
index 38467b75..20d1f594 100644
--- a/packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx
+++ b/packages/widget/src/pages/details/earn-page/state/earn-page-context.tsx
@@ -37,23 +37,15 @@ import {
isNonZeroRewardRateYield,
isYieldActionArgRequired,
isYieldValidatorSelectionRequired,
- type Yield,
type YieldBase,
} from "../../../../domain/types/yields";
import type { ValidatorDto } from "../../../../generated/api/yield";
-import { useDashboardYieldCatalog } from "../../../../hooks/api/use-dashboard-yield-catalog";
import { useDefaultTokens } from "../../../../hooks/api/use-default-tokens";
import { useStreamYieldSummaries } from "../../../../hooks/api/use-multi-yields";
import { useTokenBalancesScan } from "../../../../hooks/api/use-token-balances-scan";
-import { getDashboardCategoryYieldIdsForToken } from "../../../../hooks/api/use-token-list-yields";
import { useTokensPrices } from "../../../../hooks/api/use-tokens-prices";
import { useYieldKycGate } from "../../../../hooks/api/use-yield-kyc-gate";
import { useYieldOpportunity } from "../../../../hooks/api/use-yield-opportunity";
-import {
- getYieldOpportunity,
- getYieldOpportunityFromSummary,
-} from "../../../../hooks/api/use-yield-opportunity/get-yield-opportunity";
-import { fetchYieldSummariesByIds } from "../../../../hooks/api/use-yield-summaries";
import { useYieldValidators } from "../../../../hooks/api/use-yield-validators";
import { useNavigateWithScrollToTop } from "../../../../hooks/navigation/use-navigate-with-scroll-to-top";
import {
@@ -71,15 +63,13 @@ import { useProvidersDetails } from "../../../../hooks/use-provider-details";
import { useRewardTokenDetails } from "../../../../hooks/use-reward-token-details";
import { useSavedRef } from "../../../../hooks/use-saved-ref";
import { useYieldType } from "../../../../hooks/use-yield-type";
-import { useApiClient } from "../../../../providers/api/api-client-provider";
import { useEnterStakeStore } from "../../../../providers/enter-stake-store";
import { useMountAnimation } from "../../../../providers/mount-animation";
-import { useSKQueryClient } from "../../../../providers/query-client";
import { useSettings } from "../../../../providers/settings";
import { useSKWallet } from "../../../../providers/sk-wallet";
import { useWagmiConfig } from "../../../../providers/wagmi";
import { defaultFormattedNumber, formatNumber } from "../../../../utils";
-import { useRegisterFooterButton } from "../../../components/footer-outlet/context";
+import type { PageCta } from "../../../components/page-cta";
import type { SelectedStakeData } from "../types";
import {
useEarnPageDispatch,
@@ -115,6 +105,8 @@ export const EarnPageContextProvider = ({
availableYields,
hasNotYieldsForToken,
selectedProviderYieldId,
+ selectedDashboardYieldCategory,
+ availableDashboardYieldCategories,
} = useEarnPageState();
const dispatch = useEarnPageDispatch();
@@ -123,23 +115,18 @@ export const EarnPageContextProvider = ({
const { dashboardVariant, externalProviders, variant } = useSettings();
- const {
- isConnected,
- isConnecting,
- isLedgerLiveAccountPlaceholder,
- isLedgerLive,
- chain,
- } = useSKWallet();
-
- const apiClient = useApiClient();
- const queryClient = useSKQueryClient();
+ const { isConnected, isConnecting, isLedgerLiveAccountPlaceholder, chain } =
+ useSKWallet();
const yieldType = useYieldType(selectedStake).mapOrDefault(
(y) => y.title,
""
);
- const initYieldRes = useInitYield({ selectedToken });
+ const initYieldRes = useInitYield({
+ selectedDashboardYieldCategory,
+ selectedToken,
+ });
const estimatedRewards = useEstimatedRewards({
selectedStake,
@@ -273,60 +260,6 @@ export const EarnPageContextProvider = ({
[defaultTokens.data, deferredTokenSearch, tokenBalancesScan.data]
);
- const dashboardYieldCatalog = useDashboardYieldCatalog({
- enabled: dashboardVariant,
- network: null,
- });
-
- const availableDashboardYieldCategories =
- dashboardYieldCatalog.availableCategories;
-
- const selectedStakeDashboardYieldCategory = selectedStake
- .chainNullable(getDashboardYieldCategory)
- .extractNullable();
- const [
- selectedDashboardYieldCategoryFallback,
- setSelectedDashboardYieldCategoryFallback,
- ] = useState(null);
- const selectedDashboardYieldCategory =
- selectedStakeDashboardYieldCategory ??
- selectedDashboardYieldCategoryFallback;
-
- useEffect(() => {
- if (!selectedStakeDashboardYieldCategory) return;
-
- setSelectedDashboardYieldCategoryFallback(
- selectedStakeDashboardYieldCategory
- );
- }, [selectedStakeDashboardYieldCategory]);
-
- const dashboardSelectionByCategoryRef = useRef(
- new Map<
- DashboardYieldCategory,
- {
- token: TokenBalanceScanResponseDto["token"];
- yieldDto?: YieldBase;
- yieldId: Yield["id"];
- }
- >()
- );
-
- useEffect(() => {
- if (!dashboardVariant) return;
-
- const token = selectedToken.extractNullable();
- const yieldDto = selectedStake.extractNullable();
- const category = yieldDto ? getDashboardYieldCategory(yieldDto) : null;
-
- if (!token || !yieldDto || !category) return;
-
- dashboardSelectionByCategoryRef.current.set(category, {
- token,
- yieldDto,
- yieldId: yieldDto.id,
- });
- }, [dashboardVariant, selectedStake, selectedToken]);
-
const selectedStakeData = useMemo>(
() =>
Maybe.of(yieldSummaries)
@@ -526,170 +459,25 @@ export const EarnPageContextProvider = ({
const onValidatorSearch: SelectModalProps["onSearch"] = (val) =>
setValidatorSearch(val);
- const selectDashboardTokenYield = useCallback(
- ({
- token,
- yieldDto,
- yieldId,
- }: {
- token: TokenBalanceScanResponseDto["token"];
- yieldDto?: YieldBase;
- yieldId: Yield["id"];
- }) => {
- const yieldOpportunity = yieldDto
- ? getYieldOpportunityFromSummary({
- yieldDto,
- isLedgerLive,
- apiClient,
- queryClient,
- })
- : getYieldOpportunity({
- yieldId,
- isLedgerLive,
- apiClient,
- queryClient,
- });
-
- yieldOpportunity
- .map((yieldDto) => {
- dispatch({
- type: "dashboard/token-yield/select",
- data: { token, yieldDto },
- });
- return yieldDto;
- })
- .run();
- },
- [apiClient, dispatch, isLedgerLive, queryClient]
- );
-
- const getDashboardTokenYield = useCallback(
- async ({
- category,
- tokenBalance,
- }: {
- category: DashboardYieldCategory;
- tokenBalance: TokenBalanceScanResponseDto;
- }) => {
- const yieldSummaries = await queryClient.fetchQuery({
- queryKey: [
- "dashboard-token-yield-summaries",
- tokenString(tokenBalance.token),
- tokenBalance.availableYields,
- ],
- staleTime: 1000 * 60 * 2,
- queryFn: ({ signal }) =>
- fetchYieldSummariesByIds({
- apiClient,
- signal,
- yieldIds: tokenBalance.availableYields,
- }),
- });
-
- const yieldsById = new Map(
- yieldSummaries.map((yieldDto) => [yieldDto.id, yieldDto])
- );
- const yieldId = getDashboardCategoryYieldIdsForToken(
- tokenBalance.availableYields,
- yieldsById,
- category
- )[0];
-
- return yieldId
- ? {
- yieldId,
- yieldDto: yieldsById.get(yieldId),
- }
- : null;
- },
- [apiClient, queryClient]
- );
-
- const dashboardTokenSelectionRequestRef = useRef(0);
-
const onTokenBalanceSelect = useCallback(
- async (tokenBalance: TokenBalanceScanResponseDto) => {
- const category = dashboardVariant ? selectedDashboardYieldCategory : null;
-
- if (!category) {
- dispatch({ type: "token/select", data: tokenBalance.token });
- return;
- }
-
- const selectionRequestId = dashboardTokenSelectionRequestRef.current + 1;
- dashboardTokenSelectionRequestRef.current = selectionRequestId;
- dispatch({ type: "token-only/select", data: tokenBalance.token });
-
- const tokenYield = await getDashboardTokenYield({
- category,
- tokenBalance,
- }).catch((error) => {
- console.log(error);
- return null;
- });
-
- if (
- selectionRequestId !== dashboardTokenSelectionRequestRef.current ||
- !tokenYield
- ) {
- return;
- }
-
- selectDashboardTokenYield({
- token: tokenBalance.token,
- yieldDto: tokenYield.yieldDto,
- yieldId: tokenYield.yieldId,
- });
- },
- [
- dashboardVariant,
- dispatch,
- getDashboardTokenYield,
- selectedDashboardYieldCategory,
- selectDashboardTokenYield,
- ]
+ (tokenBalance: TokenBalanceScanResponseDto) =>
+ dispatch({ type: "token/select", data: tokenBalance.token }),
+ [dispatch]
);
const onYieldSelect = (yieldId: string) => {
- const yieldSummary = List.find(
- (summary) => summary.id === yieldId,
- yieldSummaries
- ).extractNullable();
-
- const yieldOpportunity = yieldSummary
- ? getYieldOpportunityFromSummary({
- yieldDto: yieldSummary,
- isLedgerLive,
- apiClient,
- queryClient,
- })
- : getYieldOpportunity({
- yieldId,
- isLedgerLive,
- apiClient,
- queryClient,
- });
-
- yieldOpportunity
- .map((yieldDto) => {
- dispatch({ type: "yield/select", data: yieldDto });
- return yieldDto;
- })
- .run();
+ Maybe.fromNullable(yieldSummaries)
+ .chain((val) => List.find((v) => v.id === yieldId, val))
+ .ifJust((val) => dispatch({ type: "yield/select", data: val }));
};
const onDashboardYieldCategorySelect = (category: DashboardYieldCategory) => {
if (selectedDashboardYieldCategory === category) return;
- setSelectedDashboardYieldCategoryFallback(category);
-
- const target =
- dashboardSelectionByCategoryRef.current.get(category) ??
- dashboardYieldCatalog.initialSelectionByCategory.get(category);
-
- if (!target) return;
-
- selectDashboardTokenYield(target);
+ dispatch({
+ type: "dashboard/yield-category/select",
+ data: category,
+ });
};
const onValidatorSelect = (item: ValidatorDto) =>
@@ -939,50 +727,48 @@ export const EarnPageContextProvider = ({
initYieldRes.isLoading ||
yieldOpportunityLoading;
- useRegisterFooterButton(
- useMemo(
- () =>
- !registerFooterButton || hasNotYieldsForToken
- ? null
- : isConnected && !isLedgerLiveAccountPlaceholder
- ? {
- disabled: buttonDisabled,
- isLoading:
- !buttonCTAText || isFetching || yieldKycGate.isLoading,
- onClick: () => onClickRef.current(),
- label: buttonCTAText,
- }
- : externalProviders
- ? null
- : {
- disabled: appLoading,
- isLoading: appLoading,
- label: t(
- isLedgerLiveAccountPlaceholder
- ? "init.ledger_add_account"
- : "init.connect_wallet"
- ),
- onClick: () => connectClickRef.current(),
- },
- [
- appLoading,
- buttonCTAText,
- buttonDisabled,
- connectClickRef,
- isConnected,
- isLedgerLiveAccountPlaceholder,
- onClickRef,
- externalProviders,
- isFetching,
- yieldKycGate.isLoading,
- t,
- hasNotYieldsForToken,
- registerFooterButton,
- ]
- )
+ const cta = useMemo(
+ () =>
+ !registerFooterButton || hasNotYieldsForToken
+ ? null
+ : isConnected && !isLedgerLiveAccountPlaceholder
+ ? {
+ disabled: buttonDisabled,
+ isLoading: !buttonCTAText || isFetching || yieldKycGate.isLoading,
+ onClick: () => onClickRef.current(),
+ label: buttonCTAText,
+ }
+ : externalProviders
+ ? null
+ : {
+ disabled: appLoading,
+ isLoading: appLoading,
+ label: t(
+ isLedgerLiveAccountPlaceholder
+ ? "init.ledger_add_account"
+ : "init.connect_wallet"
+ ),
+ onClick: () => connectClickRef.current(),
+ },
+ [
+ appLoading,
+ buttonCTAText,
+ buttonDisabled,
+ connectClickRef,
+ isConnected,
+ isLedgerLiveAccountPlaceholder,
+ onClickRef,
+ externalProviders,
+ isFetching,
+ yieldKycGate.isLoading,
+ t,
+ hasNotYieldsForToken,
+ registerFooterButton,
+ ]
);
const value = {
+ cta,
selectedTokenAvailableAmount,
formattedPrice,
symbol,
diff --git a/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx b/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx
index f8642b22..5d9f5956 100644
--- a/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx
+++ b/packages/widget/src/pages/details/earn-page/state/earn-page-state-context.tsx
@@ -13,11 +13,17 @@ import { equalTokens } from "../../../../domain";
import type { Networks } from "../../../../domain/types/chains/networks";
import { isNetworkWithEnterMinBasedOnPosition } from "../../../../domain/types/stake";
import type { TokenDto } from "../../../../domain/types/tokens";
-import type { Yield } from "../../../../domain/types/yields";
+import {
+ type DashboardYieldCategory,
+ getDashboardYieldCategory,
+ type Yield,
+} from "../../../../domain/types/yields";
+import { useDashboardYieldCatalog } from "../../../../hooks/api/use-dashboard-yield-catalog";
import { useYieldOpportunity } from "../../../../hooks/api/use-yield-opportunity";
import { useMaxMinYieldAmount } from "../../../../hooks/use-max-min-yield-amount";
import { usePositionsData } from "../../../../hooks/use-positions-data";
import { useSavedRef } from "../../../../hooks/use-saved-ref";
+import { useSettings } from "../../../../providers/settings";
import { useSKWallet } from "../../../../providers/sk-wallet";
import type { Actions, ExtraData, State } from "./types";
import { useAmountValidation } from "./use-amount-validation";
@@ -55,14 +61,19 @@ const getInitialState = (): State => ({
useMaxAmount: false,
tronResource: Maybe.empty(),
selectedProviderYieldId: Maybe.empty(),
+ selectedDashboardYieldCategory: null,
});
export const EarnPageStateProvider = ({ children }: PropsWithChildren) => {
const { network, isConnected } = useSKWallet();
+ const { dashboardVariant } = useSettings();
const getInitYield = useGetInitYield();
const positionsData = usePositionsData();
+ const dashboardYieldCatalog = useDashboardYieldCatalog({
+ enabled: dashboardVariant,
+ });
const reducer = (state: State, action: Actions): State => {
switch (action.type) {
@@ -73,55 +84,96 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => {
.orDefault(true)
)
.chain(() =>
- getInitYield({ selectedToken: action.data })
- .map | null>((val) =>
- onYieldSelectState({
- yieldDto: val,
+ getInitYield({
+ selectedDashboardYieldCategory:
+ state.selectedDashboardYieldCategory,
+ selectedToken: action.data,
+ })
+ .map<{
+ selectedDashboardYieldCategory: DashboardYieldCategory | null;
+ yieldState: ReturnType | null;
+ }>((yieldDto) => ({
+ selectedDashboardYieldCategory:
+ state.selectedDashboardYieldCategory,
+ yieldState: onYieldSelectState({
+ yieldDto,
positionsData: positionsData.data,
+ }),
+ }))
+ .alt(
+ Maybe.of({
+ selectedDashboardYieldCategory:
+ state.selectedDashboardYieldCategory,
+ yieldState: null,
})
)
- .alt(Maybe.of(null))
)
- .map((val) => ({
+ .map(({ selectedDashboardYieldCategory, yieldState }) => ({
...getInitialState(),
selectedToken: Maybe.of(action.data),
- ...val,
+ selectedDashboardYieldCategory,
+ ...yieldState,
}))
.orDefault(state);
}
- case "token-only/select": {
+ case "dashboard/yield-category/select": {
+ const target = dashboardYieldCatalog.initialSelectionByCategory.get(
+ action.data
+ );
+
+ if (!target) {
+ return {
+ ...state,
+ selectedDashboardYieldCategory: action.data,
+ };
+ }
+
return Maybe.fromFalsy(
- state.selectedToken
- .map((v) => !equalTokens(v, action.data))
- .orDefault(true) || state.selectedStakeId.isJust()
+ state.selectedDashboardYieldCategory !== action.data ||
+ state.selectedToken
+ .map((v) => !equalTokens(v, target.token))
+ .orDefault(true) ||
+ state.selectedStakeId
+ .map((v) => v !== target.yieldDto.id)
+ .orDefault(true)
)
- .map(() => ({
+ .map(() =>
+ onYieldSelectState({
+ yieldDto: target.yieldDto,
+ positionsData: positionsData.data,
+ })
+ )
+ .map((val) => ({
...getInitialState(),
- selectedToken: Maybe.of(action.data),
- autoSelectYield: false,
+ selectedToken: Maybe.of(target.token),
+ selectedDashboardYieldCategory: action.data,
+ ...val,
}))
.orDefault(state);
}
- case "dashboard/token-yield/select": {
+ case "positionDetails/stake/initialize": {
return Maybe.fromFalsy(
state.selectedToken
.map((v) => !equalTokens(v, action.data.token))
.orDefault(true) ||
state.selectedStakeId
- .map((v) => v !== action.data.yieldDto.id)
+ .map((v) => v !== action.data.id)
.orDefault(true)
)
.map(() =>
onYieldSelectState({
- yieldDto: action.data.yieldDto,
+ yieldDto: action.data,
positionsData: positionsData.data,
})
)
- .map((val) => ({
+ .map((val) => ({
...getInitialState(),
selectedToken: Maybe.of(action.data.token),
+ selectedDashboardYieldCategory: getDashboardYieldCategory(
+ action.data
+ ),
...val,
}))
.orDefault(state);
@@ -137,9 +189,12 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => {
positionsData: positionsData.data,
})
)
- .map((val) => ({
+ .map((val) => ({
...getInitialState(),
selectedToken: state.selectedToken,
+ selectedDashboardYieldCategory: getDashboardYieldCategory(
+ action.data
+ ),
...val,
}))
.orDefault(state);
@@ -226,6 +281,7 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => {
useMaxAmount,
tronResource,
selectedProviderYieldId,
+ selectedDashboardYieldCategory: selectedDashboardYieldCategoryFallback,
} = state;
const initTokenRes = useInitToken();
@@ -234,7 +290,10 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => {
[initTokenRes.data]
);
- const initYieldRes = useInitYield({ selectedToken });
+ const initYieldRes = useInitYield({
+ selectedDashboardYieldCategory: selectedDashboardYieldCategoryFallback,
+ selectedToken,
+ });
const initYield = useMemo(
() => Maybe.fromNullable(initYieldRes.data),
[initYieldRes.data]
@@ -258,6 +317,12 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => {
() => Maybe.fromNullable(yieldOpportunity.data),
[yieldOpportunity.data]
);
+ const selectedStakeDashboardYieldCategory = selectedStake
+ .chainNullable(getDashboardYieldCategory)
+ .extractNullable();
+ const selectedDashboardYieldCategory =
+ selectedStakeDashboardYieldCategory ??
+ selectedDashboardYieldCategoryFallback;
/**
* If stake amount is less then min, use min
@@ -412,6 +477,9 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => {
selectedToken,
hasNotYieldsForToken,
selectedProviderYieldId,
+ selectedDashboardYieldCategory,
+ availableDashboardYieldCategories:
+ dashboardYieldCatalog.availableCategories,
}),
[
selectedStakeId,
@@ -431,6 +499,8 @@ export const EarnPageStateProvider = ({ children }: PropsWithChildren) => {
availableYields,
hasNotYieldsForToken,
selectedProviderYieldId,
+ selectedDashboardYieldCategory,
+ dashboardYieldCatalog.availableCategories,
]
);
diff --git a/packages/widget/src/pages/details/earn-page/state/types.ts b/packages/widget/src/pages/details/earn-page/state/types.ts
index 3083b950..47df6847 100644
--- a/packages/widget/src/pages/details/earn-page/state/types.ts
+++ b/packages/widget/src/pages/details/earn-page/state/types.ts
@@ -13,6 +13,7 @@ import type { useEstimatedRewards } from "../../../../hooks/use-estimated-reward
import type { useProvidersDetails } from "../../../../hooks/use-provider-details";
import type { useRewardTokenDetails } from "../../../../hooks/use-reward-token-details";
import type { Action } from "../../../../types/utils";
+import type { PageCta } from "../../../components/page-cta";
import type { SelectedStakeData } from "../types";
export type State = {
@@ -26,13 +27,17 @@ export type State = {
useMaxAmount: boolean;
tronResource: Maybe;
selectedProviderYieldId: Maybe;
+ selectedDashboardYieldCategory: DashboardYieldCategory | null;
};
type TokenBalanceSelectAction = Action<"token/select", TokenDto>;
-type TokenOnlySelectAction = Action<"token-only/select", TokenDto>;
-type DashboardTokenYieldSelectAction = Action<
- "dashboard/token-yield/select",
- { token: TokenDto; yieldDto: Yield }
+type DashboardYieldCategorySelectAction = Action<
+ "dashboard/yield-category/select",
+ DashboardYieldCategory
+>;
+type PositionDetailsStakeInitializeAction = Action<
+ "positionDetails/stake/initialize",
+ Yield
>;
type YieldSelectAction = Action<"yield/select", Yield>;
@@ -53,8 +58,8 @@ type ProviderYieldIdSelectAction = Action<
export type Actions =
| TokenBalanceSelectAction
- | TokenOnlySelectAction
- | DashboardTokenYieldSelectAction
+ | DashboardYieldCategorySelectAction
+ | PositionDetailsStakeInitializeAction
| YieldSelectAction
| StakeAmountChangeAction
| StakeAmountMaxAction
@@ -75,9 +80,11 @@ export type ExtraData = {
availableAmount: Maybe;
availableYields: Maybe;
hasNotYieldsForToken: boolean;
+ availableDashboardYieldCategories: DashboardYieldCategory[];
};
export type EarnPageContextType = {
+ cta: PageCta;
selectedTokenAvailableAmount: Maybe<{
symbol: string;
shortFormattedAmount: string;
diff --git a/packages/widget/src/pages/details/earn-page/state/use-get-init-yield.ts b/packages/widget/src/pages/details/earn-page/state/use-get-init-yield.ts
index 96b29523..56315b5d 100644
--- a/packages/widget/src/pages/details/earn-page/state/use-get-init-yield.ts
+++ b/packages/widget/src/pages/details/earn-page/state/use-get-init-yield.ts
@@ -2,6 +2,7 @@ import { Maybe } from "purify-ts";
import { useCallback } from "react";
import { tokenString } from "../../../../domain";
import type { TokenDto } from "../../../../domain/types/tokens";
+import type { DashboardYieldCategory } from "../../../../domain/types/yields";
import { getCachedFirstEligibleYield } from "../../../../hooks/api/use-multi-yields";
import { useSKQueryClient } from "../../../../providers/query-client";
import { useTokenBalancesMap } from "./use-token-balances-map";
@@ -11,11 +12,18 @@ export const useGetInitYield = () => {
const tokenBalancesMap = useTokenBalancesMap();
return useCallback(
- ({ selectedToken }: { selectedToken: TokenDto }) =>
+ ({
+ selectedDashboardYieldCategory,
+ selectedToken,
+ }: {
+ selectedDashboardYieldCategory?: DashboardYieldCategory | null;
+ selectedToken: TokenDto;
+ }) =>
Maybe.fromNullable(
tokenBalancesMap.get(tokenString(selectedToken))
).chain((val) =>
getCachedFirstEligibleYield({
+ dashboardYieldCategory: selectedDashboardYieldCategory,
queryClient,
yieldIds: val.availableYields,
})
diff --git a/packages/widget/src/pages/details/earn-page/state/use-init-yield.ts b/packages/widget/src/pages/details/earn-page/state/use-init-yield.ts
index 13cb3fcd..2a650a8d 100644
--- a/packages/widget/src/pages/details/earn-page/state/use-init-yield.ts
+++ b/packages/widget/src/pages/details/earn-page/state/use-init-yield.ts
@@ -4,6 +4,7 @@ import { EitherAsync, Maybe } from "purify-ts";
import { getTokenBalances } from "../../../../common/get-token-balances";
import { tokenString } from "../../../../domain";
import type { TokenDto } from "../../../../domain/types/tokens";
+import type { DashboardYieldCategory } from "../../../../domain/types/yields";
import { getFirstEligibleYield } from "../../../../hooks/api/use-multi-yields";
import { getInitParams } from "../../../../hooks/use-init-params";
import { usePositionsData } from "../../../../hooks/use-positions-data";
@@ -15,8 +16,10 @@ import { useSKWallet } from "../../../../providers/sk-wallet";
import { useGetTokenBalancesMap } from "./use-get-token-balances-map";
export const useInitYield = ({
+ selectedDashboardYieldCategory,
selectedToken,
}: {
+ selectedDashboardYieldCategory?: DashboardYieldCategory | null;
selectedToken: Maybe;
}) => {
const getTokenBalancesMap = useGetTokenBalancesMap();
@@ -47,6 +50,7 @@ export const useInitYield = ({
network,
additionalAddresses,
address,
+ selectedDashboardYieldCategory ?? null,
selectedToken.extract(),
],
enabled: !isConnecting,
@@ -84,6 +88,7 @@ export const useInitYield = ({
apiClient,
network,
yieldIds: val.availableYields,
+ dashboardYieldCategory: selectedDashboardYieldCategory,
initParams: initParams,
positionsData: positionsData,
tokenBalanceAmount: new BigNumber(val.amount),
diff --git a/packages/widget/src/pages/review/hooks/use-action-review.hook.ts b/packages/widget/src/pages/review/hooks/use-action-review.hook.ts
index aab56f35..4a2b35a6 100644
--- a/packages/widget/src/pages/review/hooks/use-action-review.hook.ts
+++ b/packages/widget/src/pages/review/hooks/use-action-review.hook.ts
@@ -20,7 +20,7 @@ import { useActivityContext } from "../../../providers/activity-provider";
import { defaultFormattedNumber } from "../../../utils";
import { dateOlderThen7Days } from "../../../utils/date";
import { MaybeWindow } from "../../../utils/maybe-window";
-import { useRegisterFooterButton } from "../../components/footer-outlet/context";
+import type { PageCta } from "../../components/page-cta";
import type { LabelKey } from "../types";
export const useActionReview = () => {
@@ -143,17 +143,15 @@ export const useActionReview = () => {
[selectedAction]
);
- useRegisterFooterButton(
- useMemo(
- () => ({
- label: t(`activity.review.${labelKey}`),
- onClick: () => navigate(`/activity/${path}/steps`),
- disabled: false,
- isLoading: false,
- hide: actionOlderThan7Days,
- }),
- [navigate, path, labelKey, actionOlderThan7Days, t]
- )
+ const cta = useMemo(
+ () => ({
+ label: t(`activity.review.${labelKey}`),
+ onClick: () => navigate(`/activity/${path}/steps`),
+ disabled: false,
+ isLoading: false,
+ hide: actionOlderThan7Days,
+ }),
+ [navigate, path, labelKey, actionOlderThan7Days, t]
);
return {
@@ -166,5 +164,6 @@ export const useActionReview = () => {
inputToken,
actionOlderThan7Days,
labelKey,
+ cta,
};
};
diff --git a/packages/widget/src/pages/review/hooks/use-pending-review.hook.ts b/packages/widget/src/pages/review/hooks/use-pending-review.hook.ts
index a45939ec..014695d3 100644
--- a/packages/widget/src/pages/review/hooks/use-pending-review.hook.ts
+++ b/packages/widget/src/pages/review/hooks/use-pending-review.hook.ts
@@ -17,7 +17,7 @@ import { useApiClient } from "../../../providers/api/api-client-provider";
import { usePendingActionStore } from "../../../providers/pending-action-store";
import { defaultFormattedNumber } from "../../../utils";
import { getGasFeeInUSD } from "../../../utils/formatters";
-import { useRegisterFooterButton } from "../../components/footer-outlet/context";
+import type { PageCta } from "../../components/page-cta";
import type { MetaInfoProps } from "../pages/common-page/common.page";
export const usePendingActionReview = () => {
@@ -148,16 +148,14 @@ export const usePendingActionReview = () => {
const onClickRef = useSavedRef(onClick);
- useRegisterFooterButton(
- useMemo(
- () => ({
- label: t("shared.confirm"),
- onClick: () => onClickRef.current(),
- disabled: false,
- isLoading: actionPendingMutation.isPending,
- }),
- [onClickRef, t, actionPendingMutation.isPending]
- )
+ const cta = useMemo(
+ () => ({
+ label: t("shared.confirm"),
+ onClick: () => onClickRef.current(),
+ disabled: false,
+ isLoading: actionPendingMutation.isPending,
+ }),
+ [onClickRef, t, actionPendingMutation.isPending]
);
const metaInfo: MetaInfoProps = useMemo(() => ({ showMetaInfo: false }), []);
@@ -180,5 +178,6 @@ export const usePendingActionReview = () => {
actionPreviewQuery.isLoading ||
actionPreviewQuery.isFetching ||
gasWarningCheck.isLoading,
+ cta,
};
};
diff --git a/packages/widget/src/pages/review/hooks/use-stake-review.hook.ts b/packages/widget/src/pages/review/hooks/use-stake-review.hook.ts
index e85dfead..80c57036 100644
--- a/packages/widget/src/pages/review/hooks/use-stake-review.hook.ts
+++ b/packages/widget/src/pages/review/hooks/use-stake-review.hook.ts
@@ -1,12 +1,13 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { useSelector } from "@xstate/store/react";
import BigNumber from "bignumber.js";
-import { Maybe } from "purify-ts";
+import { List, Maybe } from "purify-ts";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import { getTransactionGasEstimate } from "../../../domain/types/action";
import { getKycProviderName } from "../../../domain/types/kyc";
+import { isBittensorStaking } from "../../../domain/types/yields";
import { useTokensPrices } from "../../../hooks/api/use-tokens-prices";
import { useYieldKycGate } from "../../../hooks/api/use-yield-kyc-gate";
import { usePositionDetailsStakeMatch } from "../../../hooks/navigation/use-position-details-stake-match";
@@ -20,7 +21,7 @@ import { useEnterStakeStore } from "../../../providers/enter-stake-store";
import { useSettings } from "../../../providers/settings";
import { defaultFormattedNumber } from "../../../utils";
import { getGasFeeInUSD } from "../../../utils/formatters";
-import { useRegisterFooterButton } from "../../components/footer-outlet/context";
+import type { PageCta } from "../../components/page-cta";
import type { MetaInfoProps } from "../pages/common-page/common.page";
import { useFees } from "./use-fees";
@@ -111,6 +112,26 @@ export const useStakeReview = () => {
[estimatedRewards]
);
+ const symbol = selectedToken.mapOrDefault((val) => val.symbol, "");
+ const rewardsTokenSymbol = useMemo(
+ () =>
+ selectedStake
+ .filter((val) => isBittensorStaking(val.id))
+ .chain(() => List.head([...enterRequest.selectedValidators.values()]))
+ .map((validator) => validator.subnet?.tokenSymbol ?? "")
+ .orDefault(symbol),
+ [enterRequest.selectedValidators, selectedStake, symbol]
+ );
+
+ const estimatedRewardAmounts = useMemo(
+ () =>
+ estimatedRewards.map((rewards) => ({
+ earnYearly: `${rewards.yearly} ${rewardsTokenSymbol}`,
+ earnMonthly: `${rewards.monthly} ${rewardsTokenSymbol}`,
+ })),
+ [estimatedRewards, rewardsTokenSymbol]
+ );
+
const pricesState = useTokensPrices({
token: selectedToken,
yieldDto: selectedStake,
@@ -195,22 +216,20 @@ export const useStakeReview = () => {
const { t } = useTranslation();
- useRegisterFooterButton(
- useMemo(
- () => ({
- disabled: kycGateIsBlocking,
- isLoading: enterMutation.isPending || yieldKycGate.isLoading,
- label: t("shared.confirm"),
- onClick: () => onClickRef.current(),
- }),
- [
- enterMutation.isPending,
- kycGateIsBlocking,
- onClickRef,
- t,
- yieldKycGate.isLoading,
- ]
- )
+ const cta = useMemo(
+ () => ({
+ disabled: kycGateIsBlocking,
+ isLoading: enterMutation.isPending || yieldKycGate.isLoading,
+ label: t("shared.confirm"),
+ onClick: () => onClickRef.current(),
+ }),
+ [
+ enterMutation.isPending,
+ kycGateIsBlocking,
+ onClickRef,
+ t,
+ yieldKycGate.isLoading,
+ ]
);
const { variant } = useSettings();
@@ -235,6 +254,7 @@ export const useStakeReview = () => {
amount,
fee,
interestRate,
+ estimatedRewardAmounts,
yieldType,
rewardToken,
metadata,
@@ -256,5 +276,6 @@ export const useStakeReview = () => {
yieldKycGate.isFetching ||
yieldKycGate.isRefetching,
onKycStatusRefresh,
+ cta,
};
};
diff --git a/packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts b/packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts
index d9ebb78e..89025ef4 100644
--- a/packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts
+++ b/packages/widget/src/pages/review/hooks/use-unstake-review.hook.ts
@@ -22,7 +22,7 @@ import { useApiClient } from "../../../providers/api/api-client-provider";
import { useExitStakeStore } from "../../../providers/exit-stake-store";
import { defaultFormattedNumber } from "../../../utils";
import { getGasFeeInUSD } from "../../../utils/formatters";
-import { useRegisterFooterButton } from "../../components/footer-outlet/context";
+import type { PageCta } from "../../components/page-cta";
import { useUnstakeMachine } from "../../position-details/hooks/use-unstake-machine";
import type { MetaInfoProps } from "../pages/common-page/common.page";
@@ -156,22 +156,14 @@ export const useUnstakeActionReview = () => {
const onClickRef = useSavedRef(onClick);
- useRegisterFooterButton(
- useMemo(
- () => ({
- label: t("shared.confirm"),
- onClick: () => onClickRef.current(),
- disabled: kycGateIsBlocking,
- isLoading: unstakeIsLoading || yieldKycGate.isLoading,
- }),
- [
- kycGateIsBlocking,
- onClickRef,
- t,
- unstakeIsLoading,
- yieldKycGate.isLoading,
- ]
- )
+ const cta = useMemo(
+ () => ({
+ label: t("shared.confirm"),
+ onClick: () => onClickRef.current(),
+ disabled: kycGateIsBlocking,
+ isLoading: unstakeIsLoading || yieldKycGate.isLoading,
+ }),
+ [kycGateIsBlocking, onClickRef, t, unstakeIsLoading, yieldKycGate.isLoading]
);
return {
@@ -197,5 +189,6 @@ export const useUnstakeActionReview = () => {
yieldKycGate.isFetching ||
yieldKycGate.isRefetching,
onKycStatusRefresh,
+ cta,
};
};
diff --git a/packages/widget/src/pages/review/pages/action-review.page.tsx b/packages/widget/src/pages/review/pages/action-review.page.tsx
index ceb7a525..5fe84d74 100644
--- a/packages/widget/src/pages/review/pages/action-review.page.tsx
+++ b/packages/widget/src/pages/review/pages/action-review.page.tsx
@@ -10,6 +10,7 @@ import { useTrackEvent } from "../../../hooks/tracking/use-track-event";
import { AnimationPage } from "../../../navigation/containers/animation-page";
import { capitalizeFirstLetters } from "../../../utils/formatters";
import { PageContainer } from "../../components/page-container";
+import { PageCtaButton } from "../../components/page-cta";
import { useActionReview } from "../hooks/use-action-review.hook";
import ReviewTopSection from "./common-page/components/review-top-section";
import { pointerStyles } from "./style.css";
@@ -25,6 +26,7 @@ export const ActionReviewPage = () => {
inputToken,
actionOlderThan7Days,
labelKey,
+ cta,
} = useActionReview();
const info = useMemo(
@@ -90,7 +92,7 @@ export const ActionReviewPage = () => {
.extractNullable()}
{!actionOlderThan7Days && (
-
+
{
)}
+
);
diff --git a/packages/widget/src/pages/review/pages/common-page/common.page.tsx b/packages/widget/src/pages/review/pages/common-page/common.page.tsx
index f9bb157f..ddde2d84 100644
--- a/packages/widget/src/pages/review/pages/common-page/common.page.tsx
+++ b/packages/widget/src/pages/review/pages/common-page/common.page.tsx
@@ -14,6 +14,7 @@ import { useTrackEvent } from "../../../../hooks/tracking/use-track-event";
import { AnimationPage } from "../../../../navigation/containers/animation-page";
import { MetaInfo } from "../../../components/meta-info";
import { PageContainer } from "../../../components/page-container";
+import { type PageCta, PageCtaButton } from "../../../components/page-cta";
import type { FeesBps } from "../../types";
import { feeStyles, pointerStyles } from "../style.css";
import ReviewTopSection from "./components/review-top-section";
@@ -29,6 +30,10 @@ type ReviewPageProps = {
metadata: ComponentProps["metadata"];
info: ReactNode;
rewardTokenDetailsProps: Maybe>;
+ estimatedRewardAmounts?: Maybe<{
+ earnYearly: string;
+ earnMonthly: string;
+ }>;
isGasCheckError: boolean;
loading?: boolean;
depositFee: Maybe;
@@ -37,6 +42,7 @@ type ReviewPageProps = {
commissionFee: Maybe;
notice?: ReactNode;
feeConfigLoading?: boolean;
+ cta: PageCta;
} & MetaInfoProps;
export const ReviewPage = ({
@@ -46,6 +52,7 @@ export const ReviewPage = ({
metadata,
info,
rewardTokenDetailsProps,
+ estimatedRewardAmounts,
isGasCheckError,
loading = false,
depositFee,
@@ -54,6 +61,7 @@ export const ReviewPage = ({
feeConfigLoading = false,
commissionFee,
notice,
+ cta,
...rest
}: ReviewPageProps) => {
const trackEvent = useTrackEvent();
@@ -68,6 +76,7 @@ export const ReviewPage = ({
info={info}
metadata={metadata}
rewardTokenDetailsProps={rewardTokenDetailsProps}
+ estimatedRewardAmounts={estimatedRewardAmounts}
title={title}
token={token}
/>
@@ -133,7 +142,7 @@ export const ReviewPage = ({
>
)}
-
+
+
+
);
diff --git a/packages/widget/src/pages/review/pages/common-page/components/review-top-section.tsx b/packages/widget/src/pages/review/pages/common-page/components/review-top-section.tsx
index 472de350..dacf0b96 100644
--- a/packages/widget/src/pages/review/pages/common-page/components/review-top-section.tsx
+++ b/packages/widget/src/pages/review/pages/common-page/components/review-top-section.tsx
@@ -6,6 +6,7 @@ import { Box } from "../../../../../components/atoms/box";
import { TokenIcon } from "../../../../../components/atoms/token-icon";
import { Heading } from "../../../../../components/atoms/typography/heading";
import { Text } from "../../../../../components/atoms/typography/text";
+import { EstimatedRewardAmounts } from "../../../../../components/molecules/estimated-reward-amounts";
import type { RewardTokenDetails } from "../../../../../components/molecules/reward-token-details";
import type {
TokenDto,
@@ -19,6 +20,10 @@ type Props = {
metadata: Maybe["metadata"]>;
info: ReactNode;
rewardTokenDetailsProps?: Maybe>;
+ estimatedRewardAmounts?: Maybe<{
+ earnYearly: string;
+ earnMonthly: string;
+ }>;
};
const ReviewTopSection = ({
@@ -27,6 +32,7 @@ const ReviewTopSection = ({
metadata,
info,
rewardTokenDetailsProps,
+ estimatedRewardAmounts,
}: Props) => {
const { t } = useTranslation();
@@ -69,10 +75,18 @@ const ReviewTopSection = ({
{rewardTokenDetailsProps
?.filter((v) => v.type === "stake")
.map(() => (
-
+
{t("review.estimated_reward")}
+ {(estimatedRewardAmounts ?? Maybe.empty())
+ .map((amounts) => (
+
+ ))
+ .extractNullable()}
))
.extractNullable()}
diff --git a/packages/widget/src/pages/review/pages/pending-review.page.tsx b/packages/widget/src/pages/review/pages/pending-review.page.tsx
index 98c3a5d1..4361245e 100644
--- a/packages/widget/src/pages/review/pages/pending-review.page.tsx
+++ b/packages/widget/src/pages/review/pages/pending-review.page.tsx
@@ -15,6 +15,7 @@ export const PendingReviewPage = () => {
metaInfo,
gasCheckLoading,
isGasCheckWarning,
+ cta,
} = usePendingActionReview();
useTrackPage("pendingActionReview");
@@ -54,6 +55,7 @@ export const PendingReviewPage = () => {
isGasCheckError={isGasCheckWarning}
loading={gasCheckLoading}
commissionFee={Maybe.empty()}
+ cta={cta}
{...metaInfo}
/>
);
diff --git a/packages/widget/src/pages/review/pages/stake-review.page.tsx b/packages/widget/src/pages/review/pages/stake-review.page.tsx
index 5898c009..de16ada6 100644
--- a/packages/widget/src/pages/review/pages/stake-review.page.tsx
+++ b/packages/widget/src/pages/review/pages/stake-review.page.tsx
@@ -15,6 +15,7 @@ export const StakeReviewPage = () => {
yieldType,
amount,
interestRate,
+ estimatedRewardAmounts,
metadata,
rewardToken,
token,
@@ -30,6 +31,7 @@ export const StakeReviewPage = () => {
kycProviderName,
kycStatusIsChecking,
onKycStatusRefresh,
+ cta,
} = useStakeReview();
const info = useMemo(
@@ -71,9 +73,11 @@ export const StakeReviewPage = () => {
token={token}
info={info}
rewardTokenDetailsProps={rewardTokenDetailsProps}
+ estimatedRewardAmounts={estimatedRewardAmounts}
isGasCheckError={isGasCheckWarning}
loading={gasCheckLoading}
commissionFee={commissionFee}
+ cta={cta}
notice={
kycGate.state !== "pass" || kycStatusIsChecking ? (
{
kycProviderName,
kycStatusIsChecking,
onKycStatusRefresh,
+ cta,
} = useUnstakeActionReview();
useTrackPage("unstakeReview");
@@ -64,6 +65,7 @@ export const UnstakeReviewPage = () => {
isGasCheckError={isGasCheckWarning}
loading={gasCheckLoading}
commissionFee={Maybe.empty()}
+ cta={cta}
notice={
kycGate.state !== "pass" || kycStatusIsChecking ? (
- txStates.length
- ? {
- disabled: false,
- isLoading: false,
- label: t("shared.cancel"),
- onClick: () => onClickRef.current(),
- variant: "secondary",
- }
- : null,
- [txStates.length, t, onClickRef]
- )
+ const cta = useMemo(
+ () =>
+ txStates.length
+ ? {
+ disabled: false,
+ isLoading: false,
+ label: t("shared.cancel"),
+ onClick: () => onClickRef.current(),
+ variant: "secondary",
+ }
+ : null,
+ [txStates.length, t, onClickRef]
);
return {
retry,
txStates,
+ cta,
};
};
diff --git a/packages/widget/src/pages/steps/pages/common.page.tsx b/packages/widget/src/pages/steps/pages/common.page.tsx
index eea856ef..ce722ddb 100644
--- a/packages/widget/src/pages/steps/pages/common.page.tsx
+++ b/packages/widget/src/pages/steps/pages/common.page.tsx
@@ -10,6 +10,7 @@ import type { useProvidersDetails } from "../../../hooks/use-provider-details";
import { AnimationPage } from "../../../navigation/containers/animation-page";
import { useSettings } from "../../../providers/settings";
import { PageContainer } from "../../components/page-container";
+import { PageCtaButton } from "../../components/page-cta";
import { useSteps } from "../hooks/use-steps.hook";
import { utilaPendingApprovalsBanner } from "./styles.css";
import { TxState } from "./tx-state";
@@ -29,7 +30,7 @@ export const StepsPage = ({
}: StepsPageProps) => {
const { variant } = useSettings();
- const { retry, txStates } = useSteps({
+ const { retry, txStates, cta } = useSteps({
inputToken,
session,
onSignSuccess,
@@ -92,6 +93,8 @@ export const StepsPage = ({
)}
+
+
diff --git a/packages/widget/src/providers/index.tsx b/packages/widget/src/providers/index.tsx
index a63bebcf..fbe44d8d 100644
--- a/packages/widget/src/providers/index.tsx
+++ b/packages/widget/src/providers/index.tsx
@@ -4,10 +4,6 @@ import { I18nextProvider } from "react-i18next";
import { HeaderHeightProvider } from "../components/molecules/header/use-sync-header-height";
import { SummaryProvider } from "../hooks/use-summary";
import { DisableTransitionDurationProvider } from "../navigation/containers/animation-layout";
-import {
- FooterButtonProvider,
- FooterHeightProvider,
-} from "../pages/components/footer-outlet/context";
import { CurrentLayoutProvider } from "../pages/components/layout/layout-context";
import { PoweredByHeightProvider } from "../pages/components/powered-by";
import { EarnPageStateProvider } from "../pages/details/earn-page/state/earn-page-state-context";
@@ -52,25 +48,21 @@ export const Providers = ({
-
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
diff --git a/packages/widget/src/translation/English/translations.json b/packages/widget/src/translation/English/translations.json
index a25d22ba..9b337c17 100644
--- a/packages/widget/src/translation/English/translations.json
+++ b/packages/widget/src/translation/English/translations.json
@@ -272,6 +272,7 @@
"tvl": "TVL",
"risk": "Risk",
"min_stake": "Min stake",
+ "minimum_subscription": "Minimum subscription",
"status": "Status",
"active": "Active",
"kyc": "KYC",
@@ -286,9 +287,10 @@
"network": "Network",
"provider": "Provider",
"reward_token": "Reward token",
+ "price_per_share": "Price per share",
"type": "Type",
"reward_schedule": "Reward schedule",
- "cooldown": "Cooldown",
+ "cooldown": "Redemption time",
"cooldown_days_one": "{{count}} day",
"cooldown_days_other": "{{count}} days",
"warmup": "Warmup",
diff --git a/packages/widget/src/translation/French/translations.json b/packages/widget/src/translation/French/translations.json
index 46cb2a4b..9d159a69 100644
--- a/packages/widget/src/translation/French/translations.json
+++ b/packages/widget/src/translation/French/translations.json
@@ -694,6 +694,7 @@
"tvl": "TVL",
"risk": "Risque",
"min_stake": "Mise min.",
+ "minimum_subscription": "Souscription minimale",
"status": "Statut",
"active": "Actif",
"kyc": "KYC",
@@ -708,9 +709,10 @@
"network": "Réseau",
"provider": "Fournisseur",
"reward_token": "Token de récompense",
+ "price_per_share": "Prix par part",
"type": "Type",
"reward_schedule": "Fréquence des récompenses",
- "cooldown": "Délai de retrait",
+ "cooldown": "Temps de rachat",
"cooldown_days_one": "{{count}} jour",
"cooldown_days_other": "{{count}} jours",
"warmup": "Délai d'activation",
diff --git a/packages/widget/tests/domain/token-list-yields.test.ts b/packages/widget/tests/domain/token-list-yields.test.ts
deleted file mode 100644
index 08fe9bc0..00000000
--- a/packages/widget/tests/domain/token-list-yields.test.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-import { describe, expect, it, vi } from "vitest";
-import type { TokenBalanceScanResponseDto } from "../../src/domain/types/token-balance";
-import type { YieldDto } from "../../src/generated/api/yield";
-import {
- fetchTokenListYieldSummaries,
- getDashboardCategoryYieldIdsForToken,
- getMaxYieldRateForToken,
-} from "../../src/hooks/api/use-token-list-yields";
-import type { ApiClient } from "../../src/providers/api/api-client";
-import { yieldApiYieldFixture, yieldRewardRateFixture } from "../fixtures";
-
-const yieldSummary = ({
- id,
- type,
- total = 0.01,
- enter = true,
-}: {
- id: string;
- type: YieldDto["mechanics"]["type"];
- total?: number;
- enter?: boolean;
-}) => {
- const base = yieldApiYieldFixture();
-
- return yieldApiYieldFixture({
- id,
- rewardRate: yieldRewardRateFixture({ total }),
- status: { enter, exit: true },
- mechanics: {
- ...base.mechanics,
- type,
- },
- });
-};
-
-describe("getMaxYieldRateForToken", () => {
- it("formats uppercase reward rate types from the yield API", () => {
- const yieldDto = yieldApiYieldFixture({
- id: "ethereum-eth-native-staking",
- rewardRate: yieldRewardRateFixture({
- total: 0.0349,
- rateType: "APY",
- }),
- });
-
- expect(
- getMaxYieldRateForToken(
- ["ethereum-eth-native-staking"],
- new Map([[yieldDto.id, yieldDto]])
- )
- ).toEqual({
- rateFormatted: "3.49%",
- rateTypeLabel: "APY",
- });
- });
-});
-
-describe("getDashboardCategoryYieldIdsForToken", () => {
- it("filters token yields to the active dashboard category and sorts by reward", () => {
- const stakingYield = yieldSummary({
- id: "ethereum-eth-native-staking",
- type: "staking",
- total: 0.12,
- });
- const vaultYield = yieldSummary({
- id: "ethereum-usdc-vault",
- type: "vault",
- total: 0.04,
- });
- const lendingYield = yieldSummary({
- id: "ethereum-usdc-lending",
- type: "lending",
- total: 0.08,
- });
-
- const yieldsById = new Map(
- [stakingYield, vaultYield, lendingYield].map((yieldDto) => [
- yieldDto.id,
- yieldDto,
- ])
- );
-
- expect(
- getDashboardCategoryYieldIdsForToken(
- [stakingYield.id, vaultYield.id, lendingYield.id],
- yieldsById,
- "defi"
- )
- ).toEqual([lendingYield.id, vaultYield.id]);
- });
-
- it("excludes invisible yields from category counts and selection candidates", () => {
- const visibleYield = yieldSummary({
- id: "ethereum-usdc-vault",
- type: "vault",
- total: 0.04,
- });
- const zeroRewardYield = yieldSummary({
- id: "ethereum-usdc-zero-vault",
- type: "vault",
- total: 0,
- });
- const nonEnterableYield = yieldSummary({
- id: "ethereum-usdc-disabled-lending",
- type: "lending",
- enter: false,
- total: 0.09,
- });
-
- const yieldsById = new Map(
- [visibleYield, zeroRewardYield, nonEnterableYield].map((yieldDto) => [
- yieldDto.id,
- yieldDto,
- ])
- );
-
- expect(
- getDashboardCategoryYieldIdsForToken(
- [visibleYield.id, zeroRewardYield.id, nonEnterableYield.id],
- yieldsById,
- "defi"
- )
- ).toEqual([visibleYield.id]);
- });
-
- it("returns no candidates when a token has no yields in the active category", () => {
- const stakingYield = yieldSummary({
- id: "ethereum-eth-native-staking",
- type: "staking",
- total: 0.12,
- });
-
- expect(
- getDashboardCategoryYieldIdsForToken(
- [stakingYield.id],
- new Map([[stakingYield.id, stakingYield]]),
- "defi"
- )
- ).toEqual([]);
- });
-});
-
-describe("fetchTokenListYieldSummaries", () => {
- it("loads displayed token yield metadata with bounded yield ID chunks", async () => {
- const yieldIds = Array.from(
- { length: 205 },
- (_, index) => `yield-${index}`
- );
- const tokenBalances: TokenBalanceScanResponseDto[] = [
- {
- amount: "0",
- availableYields: yieldIds.slice(0, 120),
- token: {
- decimals: 18,
- name: "Ethereum",
- network: "ethereum",
- symbol: "ETH",
- },
- },
- {
- amount: "0",
- availableYields: [...yieldIds.slice(50, 205), "yield-100"],
- token: {
- decimals: 6,
- name: "USD Coin",
- network: "ethereum",
- symbol: "USDC",
- },
- },
- ];
-
- const getYields = vi.fn(
- async ({ params }: { params: { yieldIds: ReadonlyArray } }) => ({
- total: params.yieldIds.length,
- offset: 0,
- limit: params.yieldIds.length,
- items: params.yieldIds.map((id) => yieldApiYieldFixture({ id })),
- })
- );
-
- const apiClient = {
- withOptions: () => ({ yield: { YieldsControllerGetYields: getYields } }),
- } as unknown as ApiClient;
-
- const result = await fetchTokenListYieldSummaries({
- apiClient,
- tokenBalances,
- });
-
- expect(result).toHaveLength(205);
- expect(result.map((item) => item.id)).toEqual(yieldIds);
- expect(getYields).toHaveBeenCalledTimes(3);
- expect(
- getYields.mock.calls.map(([arg]) => arg.params.yieldIds.length)
- ).toEqual([100, 100, 5]);
- });
-});
diff --git a/packages/widget/tests/domain/yield-summaries.test.ts b/packages/widget/tests/domain/yield-summaries.test.ts
index 693a8319..ae40c3ff 100644
--- a/packages/widget/tests/domain/yield-summaries.test.ts
+++ b/packages/widget/tests/domain/yield-summaries.test.ts
@@ -1,6 +1,5 @@
import { describe, expect, it, vi } from "vitest";
import {
- fetchAllYieldSummaries,
fetchYieldSummariesByIds,
isVisibleYieldSummary,
type YieldSummary,
@@ -48,70 +47,6 @@ describe("isVisibleYieldSummary", () => {
});
});
-describe("fetchAllYieldSummaries", () => {
- it("loops offset until all pages are fetched", async () => {
- const items = Array.from({ length: 5 }, (_, index) =>
- summary({ id: `yield-${index}` })
- );
-
- const getYields = vi.fn(
- async ({ params }: { params: { offset?: number; limit?: number } }) => {
- const offset = params.offset ?? 0;
- const limit = params.limit ?? 2;
-
- return {
- total: items.length,
- offset,
- limit,
- items: items.slice(offset, offset + limit),
- };
- }
- );
-
- const apiClient = {
- withOptions: () => ({ yield: { YieldsControllerGetYields: getYields } }),
- } as unknown as ApiClient;
-
- const result = await fetchAllYieldSummaries({
- apiClient,
- params: { network: "ethereum", limit: 2 },
- });
-
- expect(result.map((item) => item.id)).toEqual([
- "yield-0",
- "yield-1",
- "yield-2",
- "yield-3",
- "yield-4",
- ]);
- expect(getYields).toHaveBeenCalledTimes(3);
- expect(getYields.mock.calls.map(([arg]) => arg.params.offset)).toEqual([
- 0, 2, 4,
- ]);
- });
-
- it("stops when the API returns an empty page", async () => {
- const getYields = vi.fn(async () => ({
- total: 100,
- offset: 0,
- limit: 50,
- items: [],
- }));
-
- const apiClient = {
- withOptions: () => ({ yield: { YieldsControllerGetYields: getYields } }),
- } as unknown as ApiClient;
-
- const result = await fetchAllYieldSummaries({
- apiClient,
- params: { network: "ethereum" },
- });
-
- expect(result).toEqual([]);
- expect(getYields).toHaveBeenCalledTimes(1);
- });
-});
-
describe("fetchYieldSummariesByIds", () => {
it("splits yield IDs into bounded chunks", async () => {
const items = Array.from({ length: 5 }, (_, index) =>
diff --git a/packages/widget/tests/pages-dashboard/earn-details-model.test.tsx b/packages/widget/tests/pages-dashboard/earn-details-model.test.tsx
new file mode 100644
index 00000000..5ff6b141
--- /dev/null
+++ b/packages/widget/tests/pages-dashboard/earn-details-model.test.tsx
@@ -0,0 +1,106 @@
+import type { TFunction } from "i18next";
+import { describe, expect, it } from "vitest";
+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 translations: Record = {
+ "dashboard.earn_details.min_stake": "Min stake",
+ "dashboard.earn_details.minimum_subscription": "Minimum subscription",
+ "dashboard.earn_details.network": "Network",
+ "dashboard.earn_details.price_per_share": "Price per share",
+ "dashboard.earn_details.provider": "Provider",
+ "dashboard.earn_details.reward_token": "Reward token",
+ };
+
+ return translations[key] ?? key;
+};
+
+const minStakeMechanics = {
+ ...yieldApiYieldFixture().mechanics,
+ entryLimits: { minimum: "1", maximum: null },
+};
+
+const makeYield = (overrides?: Partial): Yield =>
+ ({
+ ...yieldApiYieldFixture(),
+ provider: { name: "Midas" },
+ ...overrides,
+ }) as Yield;
+
+describe("getEarnDetailsModel", () => {
+ it("includes price per share in details when yield state provides it", () => {
+ const model = getEarnDetailsModel({
+ t: t as TFunction,
+ yieldDto: makeYield({
+ state: {
+ pricePerShareState: {
+ price: 1.06274537,
+ quoteToken: yieldApiYieldFixture().token,
+ shareToken: yieldApiYieldFixture().token,
+ },
+ },
+ }),
+ });
+
+ expect(
+ model.detailRows.find((row) => row.id === "price-per-share")
+ ).toEqual({
+ id: "price-per-share",
+ label: "Price per share",
+ value: "1.06274537",
+ });
+ });
+
+ it("labels minimum amount as Minimum subscription for RWA yields", () => {
+ const model = getEarnDetailsModel({
+ t: t as TFunction,
+ yieldDto: makeYield({
+ mechanics: {
+ ...minStakeMechanics,
+ type: "real_world_asset",
+ },
+ token: {
+ ...yieldApiYieldFixture().token,
+ symbol: "USDC",
+ },
+ }),
+ });
+
+ expect(model.detailRows.find((row) => row.id === "min-stake")).toEqual(
+ expect.objectContaining({
+ label: "Minimum subscription",
+ })
+ );
+ });
+
+ it("labels minimum amount as Min stake for non-RWA yields", () => {
+ const model = getEarnDetailsModel({
+ t: t as TFunction,
+ yieldDto: makeYield({
+ mechanics: {
+ ...minStakeMechanics,
+ type: "vault",
+ },
+ }),
+ });
+
+ expect(model.detailRows.find((row) => row.id === "min-stake")).toEqual(
+ expect.objectContaining({
+ label: "Min stake",
+ })
+ );
+ });
+
+ it("omits price per share when yield state does not provide it", () => {
+ const model = getEarnDetailsModel({
+ t: t as TFunction,
+ yieldDto: makeYield(),
+ });
+
+ expect(model.detailRows.map((row) => row.id)).not.toContain(
+ "price-per-share"
+ );
+ });
+});
diff --git a/packages/widget/tests/pages-dashboard/position-details-action-tabs.test.tsx b/packages/widget/tests/pages-dashboard/position-details-action-tabs.test.tsx
index 6506feb7..2cb7d1b2 100644
--- a/packages/widget/tests/pages-dashboard/position-details-action-tabs.test.tsx
+++ b/packages/widget/tests/pages-dashboard/position-details-action-tabs.test.tsx
@@ -10,7 +10,6 @@ import { userEvent } from "vitest/browser";
import { shouldRegisterDashboardEarnFooterButton } from "../../src/Dashboard";
import { getPositionDetailsStakeReviewPath } from "../../src/hooks/navigation/use-position-details-stake-match";
import { PositionDetailsActionTabs } from "../../src/pages-dashboard/position-details/components/position-details-action-tabs";
-import { shouldInitializePositionDetailsStakeState } from "../../src/pages-dashboard/position-details/components/position-details-stake-actions";
import { i18nInstance } from "../../src/translation";
import { describe, expect, it } from "../utils/test-extend";
import { render } from "../utils/test-utils";
@@ -69,29 +68,6 @@ const renderTabs = (initialEntries: string | string[]) => {
};
describe("position details action tabs", () => {
- it("detects when stake state must be initialized from the selected position", () => {
- expect(
- shouldInitializePositionDetailsStakeState({
- positionYieldId: "yield-position",
- selectedEarnYieldId: "yield-earn",
- })
- ).toBe(true);
-
- expect(
- shouldInitializePositionDetailsStakeState({
- positionYieldId: "yield-position",
- selectedEarnYieldId: "yield-position",
- })
- ).toBe(false);
-
- expect(
- shouldInitializePositionDetailsStakeState({
- positionYieldId: null,
- selectedEarnYieldId: "yield-earn",
- })
- ).toBe(false);
- });
-
it("builds the nested stake review path from position route params", () => {
expect(
getPositionDetailsStakeReviewPath({
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 04224407..166e42df 100644
--- a/packages/widget/tests/pages-dashboard/position-details-model.test.tsx
+++ b/packages/widget/tests/pages-dashboard/position-details-model.test.tsx
@@ -18,13 +18,14 @@ const t = (key: string, options?: Record): string => {
const translations: Record = {
"dashboard.earn_details.asset": `Asset (${options?.symbol ?? ""})`,
"dashboard.earn_details.auto_compound": "Auto-compound",
- "dashboard.earn_details.cooldown": "Cooldown",
+ "dashboard.earn_details.cooldown": "Redemption time",
"dashboard.earn_details.cooldown_days": `${options?.count ?? ""} days`,
"dashboard.earn_details.instant": "Instant",
"dashboard.earn_details.min_stake": "Min stake",
"dashboard.earn_details.native": "Native",
"dashboard.earn_details.network": "Network",
"dashboard.earn_details.no_minimum": "No minimum",
+ "dashboard.earn_details.price_per_share": "Price per share",
"dashboard.earn_details.provider": "Provider",
"dashboard.earn_details.reward_claiming": "Reward claiming",
"dashboard.earn_details.reward_rate_period": `${options?.rewardType ?? "APY"} (7D)`,
@@ -262,7 +263,7 @@ describe("getDashboardPositionDetailsModel", () => {
expect(
model.metricCards.find((card) => card.id === "unstaking-period")
).toMatchObject({
- label: "Cooldown",
+ label: "Redemption time",
value: "7 days",
});
expect(model.detailRows.map((row) => row.id)).not.toContain("cooldown");
@@ -323,4 +324,34 @@ describe("getDashboardPositionDetailsModel", () => {
},
]);
});
+
+ it("includes price per share in details when yield state provides it", () => {
+ const model = getDashboardPositionDetailsModel({
+ canUnstake: true,
+ integrationData: makeYield({
+ state: {
+ pricePerShareState: {
+ price: 1.06274537,
+ quoteToken: yieldApiYieldFixture().token,
+ shareToken: yieldApiYieldFixture().token,
+ },
+ },
+ }),
+ pendingActions: [],
+ personalizedRewardRate: null,
+ positionBalancesByType: new Map(),
+ providersDetails: [{ name: "Midas", status: "active" }],
+ reducedStakedOrLiquidBalance: null,
+ rewardsSummary: undefined,
+ t: t as TFunction,
+ });
+
+ expect(
+ model.detailRows.find((row) => row.id === "price-per-share")
+ ).toEqual({
+ id: "price-per-share",
+ label: "Price per share",
+ value: "1.06274537",
+ });
+ });
});
diff --git a/packages/widget/tests/use-cases/select-opportunity.test.tsx b/packages/widget/tests/use-cases/select-opportunity.test.tsx
index f7983946..4a0b1da2 100644
--- a/packages/widget/tests/use-cases/select-opportunity.test.tsx
+++ b/packages/widget/tests/use-cases/select-opportunity.test.tsx
@@ -12,7 +12,6 @@ import { renderApp } from "../utils/test-utils";
type LegacyTokenDto = ReturnType["token"];
describe("Select opportunity", () => {
- // This loads cosmos wagmi config, which takes some time, so we need to increase the timeout
it("Works as expected", async ({ worker }) => {
window.history.pushState({}, "", "/");
@@ -141,54 +140,7 @@ describe("Select opportunity", () => {
http.get(legacyApiRoute("/v1/yields/enabled/networks"), async () => {
await delay();
- return HttpResponse.json([
- "ethereum",
- "ethereum-goerli",
- "avalanche-c",
- "celo",
- "akash",
- "cosmos",
- "kava",
- "osmosis",
- "juno",
- "stargaze",
- "persistence",
- "axelar",
- "onomy",
- "quicksilver",
- "agoric",
- "band-protocol",
- "bitsong",
- "chihuahua",
- "comdex",
- "crescent",
- "cronos",
- "cudos",
- "fetch-ai",
- "gravity-bridge",
- "injective",
- "irisnet",
- "ki-network",
- "mars-protocol",
- "regen",
- "secret",
- "sentinel",
- "sommelier",
- "teritori",
- "umee",
- "coreum",
- "desmos",
- "dydx",
- "optimism",
- "fantom",
- "arbitrum",
- "polygon",
- "binance",
- "near",
- "harmony",
- "solana",
- "tezos",
- ]);
+ return HttpResponse.json(["ethereum", "polkadot"]);
}),
http.get(legacyApiRoute("/v1/tokens"), async () => {
await delay();
diff --git a/packages/widget/tests/use-cases/staking-flow/staking-flow.test.tsx b/packages/widget/tests/use-cases/staking-flow/staking-flow.test.tsx
index 3c91564e..70420dea 100644
--- a/packages/widget/tests/use-cases/staking-flow/staking-flow.test.tsx
+++ b/packages/widget/tests/use-cases/staking-flow/staking-flow.test.tsx
@@ -125,6 +125,22 @@ describe("Staking flow", () => {
await expect.element(app.getByText("& earn").first()).toBeInTheDocument();
await expect.element(app.getByText("5.08%").first()).toBeInTheDocument();
+ await expect
+ .element(
+ app
+ .getByTestId("estimated-reward__yearly")
+ .getByText(`0.00508 ${yieldOp.token.symbol}`)
+ )
+ .toBeInTheDocument();
+
+ await expect
+ .element(
+ app
+ .getByTestId("estimated-reward__monthly")
+ .getByText(`0.00042 ${yieldOp.token.symbol}`)
+ )
+ .toBeInTheDocument();
+
await expect.element(app.getByText("Confirm").last()).toBeInTheDocument();
await userEvent.click(app.getByText("Confirm").last());