From 1b3a21d5d8496c42f4dec4116fdcf59ad6f038cd Mon Sep 17 00:00:00 2001 From: Marco Toniut Date: Mon, 7 Oct 2024 16:24:20 +0100 Subject: [PATCH] CN-508: stake filters for eth staking modal (#7870) * feat: Select staking provider modal desktop v2 * feat: Select staking provider modal mobile v2 * chore: update V1 modal's folder to _deprecated * chore: add KelpDAO svg icon to mobile * chore: add P2P and RocketPool logos to mobile * chore: minor fixes * chore: changeset * feat: guard new modal behind feature flag * fix: broken types import * chore: cleanup * fix: size prop for native ChipTabs * fix: braze transform ignore on jest config * chore: update copy and add disabled filter to deprecated banner * chore: final copy * fix: icon sizes and outline for light mode * fix: visual updates to the modals * fix: visual updates to the gradient effect * feat: tracking interactions * chore: replace Braze feature flag with Firebase's * chore: address PR comments pt 1 * chore: use camelCase for translation keys * chore: extract styled-components from StakeFlowModal * chore: revert modal radius change * chore: re-arrange paddings of modal * chore: remove unnecessary check * chore: remove autoredirect with one provider on mobile version of modal --- .changeset/curvy-hornets-change.md | 13 + apps/ledger-live-desktop/src/config/urls.ts | 5 + .../StakeFlowModal/EthStakingModalBody.tsx | 118 +------- .../StakeFlowModal/component/ProviderItem.tsx | 141 +++++----- .../families/evm/StakeFlowModal/index.tsx | 248 +++++++++++++++-- .../families/evm/StakeFlowModal/styles.tsx | 97 +++++++ .../EthStakingModalBody.tsx | 146 ++++++++++ .../StakingIcon.tsx | 0 .../component/ProviderItem.tsx | 94 +++++++ .../evm/StakeFlowModal_deprecated/index.tsx | 53 ++++ .../types.ts | 0 .../utils/getTrackProperties.ts | 16 ++ .../families/evm/StakeModalVersionWrapper.tsx | 13 + .../src/renderer/families/evm/modals.ts | 2 +- .../src/types/featureFlags.ts | 1 + .../static/i18n/en/app.json | 68 ++++- .../src/components/RootDrawer/RootDrawer.tsx | 23 +- .../StakingDrawer/EvmStakingDrawerBody.tsx | 58 +--- .../EvmStakingDrawerProvider.tsx | 93 ++++--- .../EvmStakingDrawerProviderIcon.tsx | 46 ++-- .../src/families/evm/StakingDrawer/index.tsx | 147 ++++++++-- .../src/families/evm/StakingDrawer/types.ts | 16 +- .../EvmStakingDrawerBody.tsx | 85 ++++++ .../EvmStakingDrawerProvider.tsx | 81 ++++++ .../EvmStakingDrawerProviderIcon.tsx | 28 ++ .../evm/StakingDrawer_deprecated/index.tsx | 59 ++++ .../evm/StakingDrawer_deprecated/types.ts | 13 + apps/ledger-live-mobile/src/icons/Figment.tsx | 23 +- apps/ledger-live-mobile/src/icons/KelpDAO.tsx | 26 ++ apps/ledger-live-mobile/src/icons/Kiln.tsx | 13 +- apps/ledger-live-mobile/src/icons/Lido.tsx | 10 +- .../src/icons/MissingIcon.tsx | 4 +- apps/ledger-live-mobile/src/icons/P2P.tsx | 54 ++++ .../src/icons/RocketPool.tsx | 259 ++++++++++++++++++ apps/ledger-live-mobile/src/icons/Stader.tsx | 23 +- .../src/locales/en/common.json | 68 ++++- apps/ledger-live-mobile/src/utils/urls.tsx | 5 + .../src/featureFlags/defaultFeatures.ts | 1 + .../packages/types-live/src/feature.ts | 43 ++- .../icons/src/svg/book-graduation.svg | 9 + .../native/src/components/Tabs/Chip/index.tsx | 57 ++-- .../src/components/Tabs/Graph/index.tsx | 16 +- .../components/Tabs/TemplateTabs/index.tsx | 37 ++- .../react/src/components/cta/Button/index.tsx | 6 +- 44 files changed, 1903 insertions(+), 415 deletions(-) create mode 100644 .changeset/curvy-hornets-change.md create mode 100644 apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/styles.tsx create mode 100644 apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal_deprecated/EthStakingModalBody.tsx rename apps/ledger-live-desktop/src/renderer/families/evm/{StakeFlowModal => StakeFlowModal_deprecated}/StakingIcon.tsx (100%) create mode 100644 apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal_deprecated/component/ProviderItem.tsx create mode 100644 apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal_deprecated/index.tsx rename apps/ledger-live-desktop/src/renderer/families/evm/{StakeFlowModal => StakeFlowModal_deprecated}/types.ts (100%) create mode 100644 apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal_deprecated/utils/getTrackProperties.ts create mode 100644 apps/ledger-live-desktop/src/renderer/families/evm/StakeModalVersionWrapper.tsx create mode 100644 apps/ledger-live-mobile/src/families/evm/StakingDrawer_deprecated/EvmStakingDrawerBody.tsx create mode 100644 apps/ledger-live-mobile/src/families/evm/StakingDrawer_deprecated/EvmStakingDrawerProvider.tsx create mode 100644 apps/ledger-live-mobile/src/families/evm/StakingDrawer_deprecated/EvmStakingDrawerProviderIcon.tsx create mode 100644 apps/ledger-live-mobile/src/families/evm/StakingDrawer_deprecated/index.tsx create mode 100644 apps/ledger-live-mobile/src/families/evm/StakingDrawer_deprecated/types.ts create mode 100644 apps/ledger-live-mobile/src/icons/KelpDAO.tsx create mode 100644 apps/ledger-live-mobile/src/icons/P2P.tsx create mode 100644 apps/ledger-live-mobile/src/icons/RocketPool.tsx create mode 100644 libs/ui/packages/icons/src/svg/book-graduation.svg diff --git a/.changeset/curvy-hornets-change.md b/.changeset/curvy-hornets-change.md new file mode 100644 index 000000000000..f98a1c818595 --- /dev/null +++ b/.changeset/curvy-hornets-change.md @@ -0,0 +1,13 @@ +--- +"ledger-live-desktop": minor +"live-mobile": minor +"@ledgerhq/icons-ui": minor +"@ledgerhq/types-live": patch +"@ledgerhq/native-ui": patch +--- + +ledger-live-desktop: Updated staking modal. Filtering per category. New copy and design +live-mobile: Updated staking modal. Filtering per category. New copy and design +@ledgerhq/icons-ui: Add book-graduation icon +@ledgerhq/types-live: Update schema of ethStakingProviders flag +@ledgerhq/native-ui: Add `xs` size to Button diff --git a/apps/ledger-live-desktop/src/config/urls.ts b/apps/ledger-live-desktop/src/config/urls.ts index ee2bf428d1b3..083c32dc3c43 100644 --- a/apps/ledger-live-desktop/src/config/urls.ts +++ b/apps/ledger-live-desktop/src/config/urls.ts @@ -168,6 +168,11 @@ export const urls = { editEvmTx: { learnMore: "https://support.ledger.com/article/9756122596765-zd", }, + ledgerAcademy: { + whatIsEthereumRestaking: "https://www.ledger.com/academy/what-is-ethereum-restaking", + ethereumStakingHowToStakeEth: + "https://www.ledger.com/academy/ethereum-staking-how-to-stake-eth", + }, ledgerByFigmentTC: "https://cdn.figment.io/legal/Current%20Ledger_Online%20Staking%20Delgation%20Services%20Agreement.pdf", ens: "https://support.ledger.com/article/9710787581469-zd", diff --git a/apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/EthStakingModalBody.tsx b/apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/EthStakingModalBody.tsx index 1ec6c8d1f475..4138e3230941 100644 --- a/apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/EthStakingModalBody.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/EthStakingModalBody.tsx @@ -1,59 +1,26 @@ -import { Flex, Text } from "@ledgerhq/react-ui"; -import { Account } from "@ledgerhq/types-live"; -import React, { useCallback, useState } from "react"; -import { useHistory } from "react-router-dom"; -import BigNumber from "bignumber.js"; -import { useTranslation } from "react-i18next"; - -import { appendQueryParamsToDappURL } from "@ledgerhq/live-common/platform/utils/appendQueryParamsToDappURL"; -import { getCryptoCurrencyById } from "@ledgerhq/live-common/currencies/index"; import { LiveAppManifest } from "@ledgerhq/live-common/platform/types"; +import { appendQueryParamsToDappURL } from "@ledgerhq/live-common/platform/utils/appendQueryParamsToDappURL"; +import { Flex } from "@ledgerhq/react-ui"; +import { Account, EthStakingProvider } from "@ledgerhq/types-live"; +import React, { useCallback } from "react"; +import { useHistory } from "react-router-dom"; import { track } from "~/renderer/analytics/segment"; -import CheckBox from "~/renderer/components/CheckBox"; -import EthStakeIllustration from "~/renderer/icons/EthStakeIllustration"; - -import { - CheckBoxContainer, - LOCAL_STORAGE_KEY_PREFIX, -} from "~/renderer/modals/Receive/steps/StepReceiveStakingFlow"; -import { ListProvider, ListProviders } from "./types"; +import { ProviderItem } from "./component/ProviderItem"; import { getTrackProperties } from "./utils/getTrackProperties"; -import ProviderItem from "./component/ProviderItem"; - -const ethMagnitude = getCryptoCurrencyById("ethereum").units[0].magnitude; - -const ETH_LIMIT = BigNumber(32).times(BigNumber(10).pow(ethMagnitude)); - -// Comparison fns for sorting providers by minimum ETH required -const ascending = (a: ListProvider, b: ListProvider) => (a?.min || 0) - (b?.min || 0); -const descending = (a: ListProvider, b: ListProvider) => (b?.min || 0) - (a?.min || 0); - type Props = { account: Account; - singleProviderRedirectMode?: boolean; onClose?: () => void; - hasCheckbox?: boolean; source?: string; - listProviders?: ListProviders; + providers: EthStakingProvider[]; }; export type StakeOnClickProps = { - provider: ListProvider; + provider: EthStakingProvider; manifest: LiveAppManifest; }; - -export function EthStakingModalBody({ - hasCheckbox = false, - singleProviderRedirectMode = true, - source, - onClose, - account, - listProviders = [], -}: Props) { - const { t } = useTranslation(); +export function EthStakingModalBody({ source, onClose, account, providers }: Props) { const history = useHistory(); - const [doNotShowAgain, setDoNotShowAgain] = useState(false); const stakeOnClick = useCallback( ({ @@ -66,6 +33,7 @@ export function EthStakingModalBody({ button: providerConfigID, ...getTrackProperties({ value, modal: source }), }); + history.push({ pathname: value, ...(customDappUrl ? { customDappUrl } : {}), @@ -78,69 +46,13 @@ export function EthStakingModalBody({ [history, account.id, onClose, source], ); - const redirectIfOnlyProvider = useCallback( - (stakeOnClickProps: StakeOnClickProps) => { - if (singleProviderRedirectMode && listProviders.length === 1) { - stakeOnClick(stakeOnClickProps); - } - }, - [singleProviderRedirectMode, listProviders.length, stakeOnClick], - ); - - const checkBoxOnChange = useCallback(() => { - const value = !doNotShowAgain; - global.localStorage.setItem(`${LOCAL_STORAGE_KEY_PREFIX}${account?.currency?.id}`, `${value}`); - setDoNotShowAgain(value); - track("button_clicked2", { - button: "not_show", - ...getTrackProperties({ value, modal: source }), - }); - }, [doNotShowAgain, account?.currency?.id, source]); - - const hasMinValidatorEth = account.spendableBalance.isGreaterThan(ETH_LIMIT); - - const listProvidersSorted = listProviders.sort(hasMinValidatorEth ? descending : ascending); - return ( - - - - {t("ethereum.stake.title")} - - {listProviders.length <= 1 && ( - - - - )} - - {t("ethereum.stake.subTitle")} - - - - - {listProvidersSorted.map(item => ( - - - - ))} + + {providers.map(x => ( + + - {hasCheckbox && ( - - - - )} - + ))} ); } diff --git a/apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/component/ProviderItem.tsx b/apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/component/ProviderItem.tsx index 24dead9da680..0009e312fee5 100644 --- a/apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/component/ProviderItem.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/component/ProviderItem.tsx @@ -1,94 +1,113 @@ import { useRemoteLiveAppManifest } from "@ledgerhq/live-common/platform/providers/RemoteLiveAppProvider/index"; -import { Flex, Icon, Tag as TagCore, Text } from "@ledgerhq/react-ui"; -import React, { useCallback, useEffect, useMemo } from "react"; +import { useLocalLiveAppManifest } from "@ledgerhq/live-common/wallet-api/LocalLiveAppProvider/index"; +import { CryptoIcon, Flex, Icon, Text } from "@ledgerhq/react-ui"; +import { EthStakingProvider } from "@ledgerhq/types-live"; +import React, { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import styled, { DefaultTheme, StyledComponent } from "styled-components"; +import ProviderIcon from "~/renderer/components/ProviderIcon"; import { StakeOnClickProps } from "../EthStakingModalBody"; -import { StakingIcon } from "../StakingIcon"; -import { ListProvider } from "../types"; -import { useLocalLiveAppManifest } from "@ledgerhq/live-common/wallet-api/LocalLiveAppProvider/index"; -export const Container: StyledComponent< - "div", - DefaultTheme, - Record, - never -> = styled(Flex)` +const IconContainer = styled.div( + ({ theme }) => ` + display: flex; + justify-content: center; + align-items: center; + width: ${theme.space[6]}px; + height: ${theme.space[6]}px; + border-radius: 100%; + background-color: ${theme.colors.opacityDefault.c05}; + margin-top: ${theme.space[3]}px; +`, +); + +function StakingIcon({ icon }: { icon?: string }) { + if (!icon) { + return null; + } + + const [iconName, iconType] = icon.split(":"); + + // if no icon type then treat as "normal" icon. + if (!iconType) { + return ( + + + + ); + } + if (iconType === "crypto") { + return ; + } + if (iconType === "provider") { + return ( + + + + ); + } + + return null; +} + +const Container: StyledComponent<"div", DefaultTheme, Record, never> = styled( + Flex, +)` cursor: pointer; - border-radius: 8px; + background-color: ${p => p.theme.colors.opacityDefault.c05}; :hover { background-color: ${p => p.theme.colors.primary.c10}; } `; -export const Tag = styled(TagCore)` - padding: 3px 6px; - > span { - font-size: 11px; - text-transform: none; - font-weight: bold; - line-height: 11.66px; - } -`; - -type Props = { - provider: ListProvider; +interface Props { + provider: EthStakingProvider; stakeOnClick(_: StakeOnClickProps): void; - redirectIfOnlyProvider(_: StakeOnClickProps): void; -}; +} -const ProviderItem = ({ provider, stakeOnClick, redirectIfOnlyProvider }: Props) => { - const { t, i18n } = useTranslation(); +export const ProviderItem = ({ provider, stakeOnClick }: Props) => { + const { t } = useTranslation(); const localManifest = useLocalLiveAppManifest(provider.liveAppId); const remoteManifest = useRemoteLiveAppManifest(provider.liveAppId); const manifest = useMemo(() => remoteManifest || localManifest, [localManifest, remoteManifest]); - const hasTag = !!provider?.min && i18n.exists(`ethereum.stake.${provider.id}.tag`); - - useEffect(() => { - if (manifest) redirectIfOnlyProvider({ provider, manifest }); - }, [redirectIfOnlyProvider, provider, manifest]); - - const stakeLink = useCallback(() => { - if (manifest) stakeOnClick({ provider, manifest }); + const handleClick = useCallback(() => { + if (manifest) { + stakeOnClick({ provider, manifest }); + } }, [provider, stakeOnClick, manifest]); return ( - - - - {t(`ethereum.stake.${provider.id}.title`)} - - {hasTag && ( - - {t(`ethereum.stake.${provider.id}.tag`)} - - )} - - + + + {t(`ethereum.stake.provider.${provider.id}.title`)} + - {t(`ethereum.stake.${provider.id}.description`)} + {provider.lst + ? t("ethereum.stake.lst") + : provider.min + ? t("ethereum.stake.requiredMinimum", { + min: provider.min, + }) + : t("ethereum.stake.noMinimum")} - - + + + {t(`ethereum.stake.rewardsStrategy.${provider.rewardsStrategy}`)} + ); }; - -export default ProviderItem; diff --git a/apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/index.tsx b/apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/index.tsx index 8d258c038ff1..652bd05dc35b 100644 --- a/apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/evm/StakeFlowModal/index.tsx @@ -1,23 +1,73 @@ -import React from "react"; -import { Account } from "@ledgerhq/types-live"; +import { getCryptoCurrencyById } from "@ledgerhq/live-common/currencies/index"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import Modal, { ModalBody } from "~/renderer/components/Modal"; -import { Flex } from "@ledgerhq/react-ui"; +import { Box, Button, Flex, Icons, Text } from "@ledgerhq/react-ui"; +import { Account, EthStakingProvider, EthStakingProviderCategory } from "@ledgerhq/types-live"; +import BigNumber from "bignumber.js"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "styled-components"; +import { urls } from "~/config/urls"; +import { track } from "~/renderer/analytics/segment"; import TrackPage from "~/renderer/analytics/TrackPage"; +import Modal from "~/renderer/components/Modal"; +import EthStakeIllustration from "~/renderer/icons/EthStakeIllustration"; +import { openURL } from "~/renderer/linking"; import { EthStakingModalBody } from "./EthStakingModalBody"; +import { Footer, Header, IconButton, ScrollableContainer, SHADOW_HEIGHT } from "./styles"; -type Props = { +const ethMagnitude = getCryptoCurrencyById("ethereum").units[0].magnitude; + +const BUTTON_CLICKED_TRACK_EVENT = "button_clicked"; + +const ETH_LIMIT = BigNumber(32).times(BigNumber(10).pow(ethMagnitude)); + +// Comparison fns for sorting providers by minimum ETH required +const ascending = (a: EthStakingProvider, b: EthStakingProvider) => (a?.min ?? 0) - (b?.min ?? 0); +const descending = (a: EthStakingProvider, b: EthStakingProvider) => (b?.min ?? 0) - (a?.min ?? 0); + +type Option = EthStakingProviderCategory | "all"; +const OPTION_VALUES: Option[] = ["all", "liquid", "protocol", "pooling", "restaking"] as const; + +export interface Props { account: Account; - singleProviderRedirectMode?: boolean; /** Analytics source */ source?: string; hasCheckbox?: boolean; -}; +} + +const MODAL_WIDTH = 500; + +export const StakeModal = ({ account, source }: Props) => { + const { t } = useTranslation(); + const { colors } = useTheme(); + + const hasMinValidatorEth = account.spendableBalance.isGreaterThan(ETH_LIMIT); -const StakingModal = ({ account, hasCheckbox, singleProviderRedirectMode, source }: Props) => { const ethStakingProviders = useFeature("ethStakingProviders"); const providers = ethStakingProviders?.params?.listProvider; + const [selected, setSelected] = useState