Skip to content

Commit

Permalink
CN-508: stake filters for eth staking modal (#7870)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
marcotoniut-ledger authored Oct 7, 2024
1 parent 191b105 commit 1b3a21d
Show file tree
Hide file tree
Showing 44 changed files with 1,903 additions and 415 deletions.
13 changes: 13 additions & 0 deletions .changeset/curvy-hornets-change.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions apps/ledger-live-desktop/src/config/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<boolean>(false);

const stakeOnClick = useCallback(
({
Expand All @@ -66,6 +33,7 @@ export function EthStakingModalBody({
button: providerConfigID,
...getTrackProperties({ value, modal: source }),
});

history.push({
pathname: value,
...(customDappUrl ? { customDappUrl } : {}),
Expand All @@ -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 (
<Flex flexDirection={"column"} alignItems="center" width={"100%"}>
<Flex flexDirection="column" alignItems="center" rowGap={16}>
<Text ff="Inter|SemiBold" fontSize="24px" lineHeight="32px">
{t("ethereum.stake.title")}
</Text>
{listProviders.length <= 1 && (
<Flex justifyContent="center" py={20} width="100%">
<EthStakeIllustration size={140} />
</Flex>
)}
<Text textAlign="center" color="neutral.c70" fontSize={14} maxWidth={360}>
{t("ethereum.stake.subTitle")}
</Text>
</Flex>
<Flex flexDirection={"column"} mt={5} px={20} width="100%">
<Flex flexDirection={"column"} width="100%">
{listProvidersSorted.map(item => (
<Flex key={item.id} width="100%" flexDirection={"column"}>
<ProviderItem
provider={item}
stakeOnClick={stakeOnClick}
redirectIfOnlyProvider={redirectIfOnlyProvider}
/>
</Flex>
))}
<Flex flexDirection="column" rowGap={2} width="100%">
{providers.map(x => (
<Flex key={x.id} flexDirection="column">
<ProviderItem provider={x} stakeOnClick={stakeOnClick} />
</Flex>
{hasCheckbox && (
<CheckBoxContainer
p={3}
borderRadius={8}
borderWidth={0}
width={"100%"}
onClick={checkBoxOnChange}
mt={15}
>
<CheckBox isChecked={doNotShowAgain} label={t("receive.steps.staking.notShow")} />
</CheckBoxContainer>
)}
</Flex>
))}
</Flex>
);
}
Original file line number Diff line number Diff line change
@@ -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<string, unknown>,
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 (
<IconContainer>
<Icon name={iconName} size={14} />
</IconContainer>
);
}
if (iconType === "crypto") {
return <CryptoIcon name={iconName} size={40} />;
}
if (iconType === "provider") {
return (
<Flex>
<ProviderIcon name={iconName} size="M" />
</Flex>
);
}

return null;
}

const Container: StyledComponent<"div", DefaultTheme, Record<string, unknown>, 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 (
<Container
pl={3}
onClick={stakeLink}
py={4}
alignItems="center"
borderRadius={2}
columnGap={4}
data-testid={`stake-provider-container-${provider.id}`}
onClick={handleClick}
p={3}
>
<StakingIcon icon={provider.icon} />
<Flex flexDirection={"column"} ml={5} flex={"auto"} alignItems="flex-start">
<Flex alignItems="center">
<Text variant="bodyLineHeight" fontSize={14} fontWeight="semiBold" mr={2}>
{t(`ethereum.stake.${provider.id}.title`)}
</Text>
{hasTag && (
<Tag
size="small"
active
type="plain"
style={{ fontFamily: "14px", textTransform: "none" }}
>
{t(`ethereum.stake.${provider.id}.tag`)}
</Tag>
)}
</Flex>

<Flex alignItems="flex-start" flex={2} flexDirection="column">
<Text variant="bodyLineHeight" fontSize={14} fontWeight="semiBold" mr={2}>
{t(`ethereum.stake.provider.${provider.id}.title`)}
</Text>
<Text variant="paragraph" fontSize={13} color="neutral.c70">
{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")}
</Text>
</Flex>
<Flex width={40} justifyContent="center" alignItems="center">
<Icon name="ChevronRight" size={25} />
<Flex flex={1} flexWrap="wrap" justifyContent="right">
<Text variant="paragraph" fontSize={13} color="neutral.c70" textAlign="right">
{t(`ethereum.stake.rewardsStrategy.${provider.rewardsStrategy}`)}
</Text>
</Flex>
</Container>
);
};

export default ProviderItem;
Loading

0 comments on commit 1b3a21d

Please sign in to comment.