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 = () => {