diff --git a/.changeset/curly-bats-work.md b/.changeset/curly-bats-work.md new file mode 100644 index 000000000000..54a35775c599 --- /dev/null +++ b/.changeset/curly-bats-work.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/coin-tezos": patch +--- + +Tezos send full balance 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/.changeset/fifty-rats-perform.md b/.changeset/fifty-rats-perform.md new file mode 100644 index 000000000000..f8689af4d1c0 --- /dev/null +++ b/.changeset/fifty-rats-perform.md @@ -0,0 +1,6 @@ +--- +"@ledgerhq/coin-algorand": patch +"@ledgerhq/coin-tezos": patch +--- + +Tezos send full balance and fix algorand format issue diff --git a/.changeset/heavy-peas-cover.md b/.changeset/heavy-peas-cover.md new file mode 100644 index 000000000000..9e3031a32fa1 --- /dev/null +++ b/.changeset/heavy-peas-cover.md @@ -0,0 +1,6 @@ +--- +"ledger-live-desktop": patch +"live-mobile": patch +--- + +Add the platform OS to the params passed to earn to enable the correct braze environment diff --git a/.changeset/nice-plants-complain.md b/.changeset/nice-plants-complain.md new file mode 100644 index 000000000000..b412b51fd442 --- /dev/null +++ b/.changeset/nice-plants-complain.md @@ -0,0 +1,6 @@ +--- +"live-mobile": minor +"@ledgerhq/native-ui": minor +--- + +Add the receive flow `NeedMemoTagModal` 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/bitcoin/AccountHeaderManageActions.ts b/apps/ledger-live-desktop/src/renderer/families/bitcoin/AccountHeaderManageActions.ts new file mode 100644 index 000000000000..bc5ffc14b5ee --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/families/bitcoin/AccountHeaderManageActions.ts @@ -0,0 +1,60 @@ +import { getMainAccount } from "@ledgerhq/live-common/account/index"; + +import { useTranslation } from "react-i18next"; +import { useHistory } from "react-router"; +import { track } from "~/renderer/analytics/segment"; +import { stakeDefaultTrack } from "~/renderer/screens/stake/constants"; +import { BitcoinAccount } from "@ledgerhq/coin-bitcoin/lib/types"; +import { TokenAccount } from "@ledgerhq/types-live"; +import IconCoins from "~/renderer/icons/Coins"; + +type Props = { + account: BitcoinAccount | TokenAccount; + parentAccount: BitcoinAccount | undefined | null; +}; + +const AccountHeaderActions = ({ account, parentAccount }: Props) => { + const history = useHistory(); + const { t } = useTranslation(); + const mainAccount = getMainAccount(account, parentAccount); + const { + bitcoinResources, + currency: { id: currencyId }, + } = mainAccount; + if (!bitcoinResources || parentAccount || currencyId !== "bitcoin") return null; + + const stakeOnClick = () => { + const value = "/platform/acre"; + + track("button_clicked2", { + ...stakeDefaultTrack, + delegation: "stake", + page: "Page Account", + button: "delegate", + provider: "Acre", + currency: "BTC", + }); + history.push({ + pathname: value, + state: { + accountId: account.id, + returnTo: `/account/${account.id}`, + }, + }); + }; + + return [ + { + key: "Stake", + icon: IconCoins, + label: t("accounts.contextMenu.yield"), + event: "button_clicked2", + eventProperties: { + button: "stake", + }, + onClick: stakeOnClick, + }, + ]; +}; + +export default AccountHeaderActions; diff --git a/apps/ledger-live-desktop/src/renderer/families/bitcoin/index.ts b/apps/ledger-live-desktop/src/renderer/families/bitcoin/index.ts index d65009a89e20..321e5b73ff1f 100644 --- a/apps/ledger-live-desktop/src/renderer/families/bitcoin/index.ts +++ b/apps/ledger-live-desktop/src/renderer/families/bitcoin/index.ts @@ -2,12 +2,14 @@ import "./live-common-setup"; import sendAmountFields from "./SendAmountFields"; import sendRecipientFields from "./SendRecipientFields"; import StepReceiveFundsPostAlert from "./StepReceiveFundsPostAlert"; +import accountHeaderManageActions from "./AccountHeaderManageActions"; import { BitcoinFamily } from "./types"; const family: BitcoinFamily = { sendAmountFields, sendRecipientFields, StepReceiveFundsPostAlert, + accountHeaderManageActions, }; export default family; 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