diff --git a/.changeset/beige-rings-move.md b/.changeset/beige-rings-move.md new file mode 100644 index 000000000000..eac530b6611a --- /dev/null +++ b/.changeset/beige-rings-move.md @@ -0,0 +1,5 @@ +--- +"live-mobile": minor +--- + +LLM - Ledger Sync improve error message when deleting backup on multiple instances at the same time diff --git a/.changeset/eight-ducks-rescue.md b/.changeset/eight-ducks-rescue.md new file mode 100644 index 000000000000..fcccba8e9e4b --- /dev/null +++ b/.changeset/eight-ducks-rescue.md @@ -0,0 +1,5 @@ +--- +"live-mobile": minor +--- + +Prepare add account v2 reusable screens diff --git a/.changeset/few-toes-drive.md b/.changeset/few-toes-drive.md new file mode 100644 index 000000000000..63879489940e --- /dev/null +++ b/.changeset/few-toes-drive.md @@ -0,0 +1,9 @@ +--- +"@ledgerhq/types-live": minor +"@ledgerhq/coin-evm": minor +"ledger-live-desktop": minor +"live-mobile": minor +"@ledgerhq/live-common": minor +--- + +add mev protection diff --git a/.changeset/heavy-starfishes-fix.md b/.changeset/heavy-starfishes-fix.md new file mode 100644 index 000000000000..25eb83adf3ff --- /dev/null +++ b/.changeset/heavy-starfishes-fix.md @@ -0,0 +1,5 @@ +--- +"live-mobile": minor +--- + +LLM - Ledger Sync improved error message when a removed member tries to remove another member diff --git a/.changeset/honest-feet-trade.md b/.changeset/honest-feet-trade.md new file mode 100644 index 000000000000..77a76d3615a3 --- /dev/null +++ b/.changeset/honest-feet-trade.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": minor +--- + +fix swap Id in history diff --git a/.changeset/odd-cooks-glow.md b/.changeset/odd-cooks-glow.md new file mode 100644 index 000000000000..2878cc369e16 --- /dev/null +++ b/.changeset/odd-cooks-glow.md @@ -0,0 +1,7 @@ +--- +"ledger-live-desktop": minor +"@ledgerhq/live-nft-react": minor +"@ledgerhq/live-nft": minor +--- + +Rework Hiddencollections diff --git a/.changeset/shaggy-days-flash.md b/.changeset/shaggy-days-flash.md new file mode 100644 index 000000000000..a9bd76b37757 --- /dev/null +++ b/.changeset/shaggy-days-flash.md @@ -0,0 +1,8 @@ +--- +"@ledgerhq/types-live": patch +"ledger-live-desktop": patch +"live-mobile": patch +"@ledgerhq/live-common": patch +--- + +Remove `feature_recover_upsell_redirection` feature flag and unused components diff --git a/.changeset/silly-cougars-rescue.md b/.changeset/silly-cougars-rescue.md new file mode 100644 index 000000000000..95b714377d0f --- /dev/null +++ b/.changeset/silly-cougars-rescue.md @@ -0,0 +1,5 @@ +--- +"live-mobile": minor +--- + +LLM - Ledger Sync improved error message when an unauthorized member tries to delete a backup diff --git a/.changeset/spicy-schools-look.md b/.changeset/spicy-schools-look.md new file mode 100644 index 000000000000..3efbec70043b --- /dev/null +++ b/.changeset/spicy-schools-look.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": patch +--- + +Fix speculos transport diff --git a/apps/ledger-live-desktop/src/newArch/components/ContextMenu/createContextMenuItems.ts b/apps/ledger-live-desktop/src/newArch/components/ContextMenu/createContextMenuItems.ts index ff1d8206cd96..5c546c1422be 100644 --- a/apps/ledger-live-desktop/src/newArch/components/ContextMenu/createContextMenuItems.ts +++ b/apps/ledger-live-desktop/src/newArch/components/ContextMenu/createContextMenuItems.ts @@ -45,6 +45,7 @@ export function createContextMenuItems({ openModal("MODAL_HIDE_NFT_COLLECTION", { collectionName: collectionName ?? collectionAddress, collectionId: `${account.id}|${collectionAddress}`, + blockchain: account.currency.id, onClose: () => { if (goBackToAccount) { setDrawer(); diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/hooks/useOpenHideCollectionModal.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/hooks/useOpenHideCollectionModal.ts index 2d4febd2f122..802efd357d1f 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/hooks/useOpenHideCollectionModal.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/hooks/useOpenHideCollectionModal.ts @@ -29,8 +29,9 @@ export const useOpenHideCollectionModal = ( collectionName: collectionName as string, collectionId: `${account.id}|${nft.contract}`, onClose, + blockchain: account.currency.id, }), ), }; - }, [account.id, dispatch, metadata, nft.contract, onClose, t]); + }, [account.currency.id, account.id, dispatch, metadata, nft.contract, onClose, t]); }; diff --git a/apps/ledger-live-desktop/src/renderer/actions/constants.ts b/apps/ledger-live-desktop/src/renderer/actions/constants.ts index ad79c2d67822..09cf07655e5a 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/constants.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/constants.ts @@ -1,4 +1,7 @@ // Action types + +/** Settings --------- */ export const TOGGLE_MEMOTAG_INFO = "settings/toggleShouldDisplayMemoTagInfo"; export const TOGGLE_MEV = "settings/toggleMEV"; export const TOGGLE_MARKET_WIDGET = "settings/toggleMarketWidget"; +export const UPDATE_NFT_COLLECTION_STATUS = "settings/updateNftCollectionStatus"; diff --git a/apps/ledger-live-desktop/src/renderer/actions/settings.ts b/apps/ledger-live-desktop/src/renderer/actions/settings.ts index 16ed922b65d5..be1df1bef74c 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/settings.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/settings.ts @@ -25,7 +25,14 @@ import { import { useRefreshAccountsOrdering } from "~/renderer/actions/general"; import { Language, Locale } from "~/config/languages"; import { Layout } from "LLD/features/Collectibles/types/Layouts"; -import { TOGGLE_MARKET_WIDGET, TOGGLE_MEMOTAG_INFO, TOGGLE_MEV } from "./constants"; +import { + TOGGLE_MARKET_WIDGET, + TOGGLE_MEMOTAG_INFO, + TOGGLE_MEV, + UPDATE_NFT_COLLECTION_STATUS, +} from "./constants"; +import { BlockchainsType } from "@ledgerhq/live-nft/supported"; +import { NftStatus } from "@ledgerhq/live-nft/types"; export type SaveSettings = (a: Partial) => { type: string; payload: Partial; @@ -215,15 +222,6 @@ export const blacklistToken = (tokenId: string) => ({ type: "BLACKLIST_TOKEN", payload: tokenId, }); -export const hideNftCollection = (collectionId: string) => ({ - type: "HIDE_NFT_COLLECTION", - payload: collectionId, -}); - -export const whitelistNftCollection = (collectionId: string) => ({ - type: "WHITELIST_NFT_COLLECTION", - payload: collectionId, -}); export const hideOrdinalsAsset = (inscriptionId: string) => ({ type: "HIDE_ORDINALS_ASSET", @@ -255,14 +253,16 @@ export const showToken = (tokenId: string) => ({ type: "SHOW_TOKEN", payload: tokenId, }); -export const unhideNftCollection = (collectionId: string) => ({ - type: "UNHIDE_NFT_COLLECTION", - payload: collectionId, -}); -export const unwhitelistNftCollection = (collectionId: string) => ({ - type: "UNWHITELIST_NFT_COLLECTION", - payload: collectionId, + +export const updateNftStatus = ( + blockchain: BlockchainsType, + collectionId: string, + status: NftStatus, +) => ({ + type: UPDATE_NFT_COLLECTION_STATUS, + payload: { blockchain, collectionId, status }, }); + export const unhideOrdinalsAsset = (inscriptionId: string) => ({ type: "UNHIDE_ORDINALS_ASSET", payload: inscriptionId, diff --git a/apps/ledger-live-desktop/src/renderer/components/ContextMenu/NFTCollectionContextMenu.tsx b/apps/ledger-live-desktop/src/renderer/components/ContextMenu/NFTCollectionContextMenu.tsx index d0633a17ed5a..eee1c99ccbcd 100644 --- a/apps/ledger-live-desktop/src/renderer/components/ContextMenu/NFTCollectionContextMenu.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/ContextMenu/NFTCollectionContextMenu.tsx @@ -42,6 +42,7 @@ export default function NFTCollectionContextMenu({ history.replace(`account/${account.id}`); } }, + blockchain: account.currency.id, }), ), }, diff --git a/apps/ledger-live-desktop/src/renderer/components/SyncOnboarding/Manual/BackupStep.tsx b/apps/ledger-live-desktop/src/renderer/components/SyncOnboarding/Manual/BackupStep.tsx deleted file mode 100644 index 71b4692e4304..000000000000 --- a/apps/ledger-live-desktop/src/renderer/components/SyncOnboarding/Manual/BackupStep.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { Device } from "@ledgerhq/live-common/hw/actions/types"; -import { Flex, Icons, IconsLegacy, Link, Tag, Text } from "@ledgerhq/react-ui"; -import { StorylyInstanceID } from "@ledgerhq/types-live"; -import React, { ComponentProps, useCallback, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import styled, { useTheme } from "styled-components"; -import { track } from "~/renderer/analytics/segment"; -import ButtonV3 from "../../ButtonV3"; -import TrackPage from "~/renderer/analytics/TrackPage"; -import { useStoryly } from "~/storyly/useStoryly"; - -import { StepText, StepSubtitleText } from "./shared"; -import { useCustomPath } from "@ledgerhq/live-common/hooks/recoverFeatureFlag"; -import { useDispatch } from "react-redux"; -import { useHistory } from "react-router"; -import { saveSettings } from "~/renderer/actions/settings"; - -type BackupStepProps = { - device: Device; - onPressKeepManualBackup(): void; -}; - -type ChoiceBodyProps = { - isOpened: boolean; - device: Device; - onPressKeepManualBackup(): void; -}; - -type Choice = { - id: "backup" | "keep_on_paper"; - icon: React.ReactNode; - getTitle: (selected?: boolean) => string; - tag?: React.ReactNode; - body: React.ComponentType; -}; - -const ChoiceText = styled(Text).attrs({ - variant: "paragraph", - color: "neutral.c70", -})``; - -const VideoLink: React.FC<{}> = () => { - const { t } = useTranslation(); - const { ref: storylyRef, dataRef } = useStoryly(StorylyInstanceID.backupRecoverySeed); - return ( - -
- {/* @ts-expect-error the `storyly-web` package doesn't provide any typings yet. */} - -
- { - track("button_clicked", { button: "How it works", flow: "Device onboarding" }); - storylyRef.current?.openStory({ - group: dataRef.current?.at(0)?.id.toString(10), - }); - }} - > - {t("syncOnboarding.manual.backup.backupChoice.howItWorksCta")} - -
- ); -}; - -const BackupBody: React.FC = ({ isOpened }) => { - const { t } = useTranslation(); - - const dispatch = useDispatch(); - const history = useHistory(); - - const servicesConfig = useFeature("protectServicesDesktop"); - - const recoverActivatePath = useCustomPath(servicesConfig, "activate", "lld-onboarding-24") || ""; - - const navigateToRecover = useCallback(() => { - console.log("recoverActivatePath", recoverActivatePath); - const [pathname, search] = recoverActivatePath.split("?"); - dispatch( - saveSettings({ - hasCompletedOnboarding: true, - }), - ); - history.push({ - pathname, - search: search ? `?${search}` : undefined, - state: { fromOnboarding: false }, - }); - }, [dispatch, history, recoverActivatePath]); - - return isOpened ? ( - - {t("syncOnboarding.manual.backup.backupChoice.description")} - - {t("syncOnboarding.manual.backup.backupChoice.tryCta")} - - - {t("syncOnboarding.manual.backup.backupChoice.redeemCodeCta")} - - - - ) : null; -}; - -const KeepOnPaperBody: React.FC = ({ isOpened, onPressKeepManualBackup }) => { - const { t } = useTranslation(); - return isOpened ? ( - - {t("syncOnboarding.manual.backup.manualBackup.description")} - - {t("syncOnboarding.manual.backup.manualBackup.cta")} - - - ) : null; -}; - -const WrappedTag: React.FC<{ text: string } & ComponentProps> = ({ - text, - ...props -}) => { - const { t } = useTranslation(); - return ( - - {t(text)} - - ); -}; - -const choices: Choice[] = [ - { - id: "backup", - getTitle: selected => - selected - ? "syncOnboarding.manual.backup.backupChoice.titleSelected" - : "syncOnboarding.manual.backup.backupChoice.title", - icon: , - tag: ( - - ), - body: BackupBody, - }, - { - id: "keep_on_paper", - getTitle: () => "syncOnboarding.manual.backup.manualBackup.title", - icon: , - body: KeepOnPaperBody, - }, -]; - -const BackupStep: React.FC = props => { - const { device, onPressKeepManualBackup } = props; - const [choice, setChoice] = useState(null); - const { radii } = useTheme(); - - const { t } = useTranslation(); - - useEffect(() => { - if (choice === "backup") { - track("button_clicked", { button: "Ledger Recover", flow: "Device onboarding" }); - } else if (choice === "keep_on_paper") { - track("button_clicked", { button: "Manual Backup", flow: "Device onboarding" }); - } - }, [choice]); - - return ( - - - {/* @ts-expect-error weird props issue with React 18 */} - {t("syncOnboarding.manual.backup.description")} - {choices.map(({ id, getTitle, icon, tag, body: Body }) => ( -
setChoice(id)}> - - - - {icon} - {/* @ts-expect-error weird props issue with React 18 */} - - {t(getTitle(choice === id))} - - - {tag ?? null} - - - -
- ))} -
- ); -}; - -export default BackupStep; diff --git a/apps/ledger-live-desktop/src/renderer/components/SyncOnboarding/Manual/SyncOnboardingCompanion.tsx b/apps/ledger-live-desktop/src/renderer/components/SyncOnboarding/Manual/SyncOnboardingCompanion.tsx index f7aff61d21af..4584d5ae3a42 100644 --- a/apps/ledger-live-desktop/src/renderer/components/SyncOnboarding/Manual/SyncOnboardingCompanion.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/SyncOnboarding/Manual/SyncOnboardingCompanion.tsx @@ -35,7 +35,6 @@ import { setDrawer } from "~/renderer/drawers/Provider"; import LockedDeviceDrawer from "./LockedDeviceDrawer"; import { LockedDeviceError } from "@ledgerhq/errors"; import { useRecoverRestoreOnboarding } from "~/renderer/hooks/useRecoverRestoreOnboarding"; -import BackupStep from "./BackupStep"; const READY_REDIRECT_DELAY_MS = 2000; const POLLING_PERIOD_MS = 1000; @@ -117,8 +116,6 @@ const SyncOnboardingCompanion: React.FC = ({ const [seedPathStatus, setSeedPathStatus] = useState("choice_new_or_restore"); const servicesConfig = useFeature("protectServicesDesktop"); - const recoverUpsellRedirection = useFeature("recoverUpsellRedirection"); - const hasBackupStep = !recoverUpsellRedirection?.enabled; const recoverRestoreStaxPath = useCustomPath(servicesConfig, "restore", "lld-onboarding-24"); @@ -192,22 +189,6 @@ const SyncOnboardingCompanion: React.FC = ({ ), }, - ...(hasBackupStep - ? [ - { - key: StepKey.Backup, - status: "inactive" as StepStatus, - title: t("syncOnboarding.manual.backup.title"), - titleCompleted: t("syncOnboarding.manual.backup.title"), - renderBody: () => ( - setStepKey(StepKey.Apps)} - /> - ), - }, - ] - : []), { key: StepKey.Apps, status: "inactive", @@ -237,7 +218,6 @@ const SyncOnboardingCompanion: React.FC = ({ [ t, deviceName, - hasBackupStep, hasAppLoader, productName, device, @@ -359,24 +339,16 @@ const SyncOnboardingCompanion: React.FC = ({ // When the device is seeded, there are 2 cases before triggering the application install step: // - the user came to the sync onboarding with an non-seeded device and did a full onboarding: onboarding flag `Ready` // - the user came to the sync onboarding with an already seeded device: onboarding flag `WelcomeScreen1` - if (deviceOnboardingState?.isOnboarded && !seededDeviceHandled.current) { - if (deviceOnboardingState?.currentOnboardingStep === DeviceOnboardingStep.Ready) { - // device was just seeded - setStepKey(hasBackupStep ? StepKey.Backup : StepKey.Apps); - seededDeviceHandled.current = true; - return; - } else if ( - deviceOnboardingState?.currentOnboardingStep === DeviceOnboardingStep.WelcomeScreen1 - ) { - // device was already seeded, switch to the apps step - if (hasBackupStep) { - __DEV__ ? setStepKey(StepKey.Backup) : setStepKey(StepKey.Apps); // for ease of testing in dev mode without having to reset the device - } else { - setStepKey(StepKey.Apps); - } - seededDeviceHandled.current = true; - return; - } + if ( + deviceOnboardingState?.isOnboarded && + !seededDeviceHandled.current && + [DeviceOnboardingStep.Ready, DeviceOnboardingStep.WelcomeScreen1].includes( + deviceOnboardingState.currentOnboardingStep, + ) + ) { + setStepKey(StepKey.Apps); + seededDeviceHandled.current = true; + return; } // case DeviceOnboardingStep.SafetyWarning not handled so the previous step (new seed, restore, recover) is kept @@ -427,7 +399,7 @@ const SyncOnboardingCompanion: React.FC = ({ default: break; } - }, [deviceOnboardingState, hasBackupStep, notifySyncOnboardingShouldReset]); + }, [deviceOnboardingState, notifySyncOnboardingShouldReset]); // When the user gets close to the seed generation step, sets the lost synchronization delay // and timers to a higher value. It avoids having a warning message while the connection is lost diff --git a/apps/ledger-live-desktop/src/renderer/drawers/SwapOperationDetails/index.tsx b/apps/ledger-live-desktop/src/renderer/drawers/SwapOperationDetails/index.tsx index e917713c05ec..59424e618989 100644 --- a/apps/ledger-live-desktop/src/renderer/drawers/SwapOperationDetails/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/drawers/SwapOperationDetails/index.tsx @@ -255,9 +255,9 @@ const SwapOperationDetails = ({ - + - {swapId} + diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts index f6406bd162c4..5c6da00ff3ec 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts @@ -1,8 +1,10 @@ -import { hideNftCollection } from "~/renderer/actions/settings"; +import { updateNftStatus } from "~/renderer/actions/settings"; import { useHideSpamCollection } from "../useHideSpamCollection"; import { renderHook } from "tests/testUtils"; import { INITIAL_STATE } from "~/renderer/reducers/settings"; import { useDispatch } from "react-redux"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; +import { NftStatus } from "@ledgerhq/live-nft/types"; jest.mock("react-redux", () => ({ ...jest.requireActual("react-redux"), @@ -17,31 +19,41 @@ describe("useHideSpamCollection", () => { mockDispatch.mockClear(); }); - it("should dispatch hideNftCollection action if collection is not whitelisted", () => { + it("should dispatch updateNftStatus action if collection is not already marked with a status", () => { const { result } = renderHook(() => useHideSpamCollection(), { initialState: { settings: { ...INITIAL_STATE, - whitelistedNftCollections: ["collectionA", "collectionB"], - hiddenNftCollections: [], + nftCollectionsStatusByNetwork: {}, }, }, }); - result.current.hideSpamCollection("collectionC"); + result.current.hideSpamCollection("collectionC", BlockchainEVM.Ethereum); - expect(mockDispatch).toHaveBeenCalledWith(hideNftCollection("collectionC")); + expect(mockDispatch).toHaveBeenCalledWith( + updateNftStatus(BlockchainEVM.Ethereum, "collectionC", NftStatus.spam), + ); }); - it("should not dispatch hideNftCollection action if collection is whitelisted", () => { + it("should not dispatch hideNftCollection action if collection is already marked with a status", () => { const { result } = renderHook(() => useHideSpamCollection(), { initialState: { settings: { - hiddenNftCollections: [], + nftCollectionsStatusByNetwork: { + [BlockchainEVM.Ethereum]: { + collectionA: NftStatus.spam, + }, + [BlockchainEVM.Avalanche]: { + collectionB: NftStatus.spam, + }, + }, whitelistedNftCollections: ["collectionA", "collectionB"], }, }, }); - result.current.hideSpamCollection("collectionA"); + + result.current.hideSpamCollection("collectionA", BlockchainEVM.Ethereum); + result.current.hideSpamCollection("collectionB", BlockchainEVM.Avalanche); expect(mockDispatch).not.toHaveBeenCalled(); }); diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useNftCollectionsStatus.test.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useNftCollectionsStatus.test.ts new file mode 100644 index 000000000000..3150504beac7 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useNftCollectionsStatus.test.ts @@ -0,0 +1,88 @@ +import { renderHook } from "tests/testUtils"; +import { INITIAL_STATE } from "~/renderer/reducers/settings"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; +import { NftStatus } from "@ledgerhq/live-nft/types"; +import { useNftCollectionsStatus } from "../useNftCollectionsStatus"; + +describe("useNftCollectionsStatus", () => { + it("should returns only NFTs (contract) with NftStatus !== whitelisted when FF is ON", () => { + const { result } = renderHook(() => useNftCollectionsStatus(), { + initialState: { + settings: { + ...INITIAL_STATE, + overriddenFeatureFlags: { + nftsFromSimplehash: { + enabled: true, + }, + }, + nftCollectionsStatusByNetwork: { + [BlockchainEVM.Ethereum]: { + collectionETHA: NftStatus.whitelisted, + collectionETHB: NftStatus.blacklisted, + collectionETHC: NftStatus.spam, + collectionETHD: NftStatus.spam, + }, + [BlockchainEVM.Avalanche]: { + collectionAVAX1: NftStatus.blacklisted, + collectionAVAX2: NftStatus.spam, + collectionAVAX3: NftStatus.blacklisted, + }, + [BlockchainEVM.Polygon]: { + collectionP1: NftStatus.blacklisted, + collectionP2: NftStatus.whitelisted, + }, + }, + }, + }, + }); + + expect(result.current.hiddenNftCollections).toEqual([ + "collectionETHB", + "collectionETHC", + "collectionETHD", + "collectionAVAX1", + "collectionAVAX2", + "collectionAVAX3", + "collectionP1", + ]); + }); + + it("should returns only NFTs (contract) with NftStatus.blacklisted when FF is oFF ", () => { + const { result } = renderHook(() => useNftCollectionsStatus(), { + initialState: { + settings: { + ...INITIAL_STATE, + overriddenFeatureFlags: { + nftsFromSimplehash: { + enabled: false, + }, + }, + nftCollectionsStatusByNetwork: { + [BlockchainEVM.Ethereum]: { + collectionETHA: NftStatus.whitelisted, + collectionETHB: NftStatus.blacklisted, + collectionETHC: NftStatus.spam, + collectionETHD: NftStatus.spam, + }, + [BlockchainEVM.Avalanche]: { + collectionAVAX1: NftStatus.blacklisted, + collectionAVAX2: NftStatus.spam, + collectionAVAX3: NftStatus.blacklisted, + }, + [BlockchainEVM.Polygon]: { + collectionP1: NftStatus.blacklisted, + collectionP2: NftStatus.whitelisted, + }, + }, + }, + }, + }); + + expect(result.current.hiddenNftCollections).toEqual([ + "collectionETHB", + "collectionAVAX1", + "collectionAVAX3", + "collectionP1", + ]); + }); +}); diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts index 7772d51dddee..d185050f4681 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts @@ -1,25 +1,34 @@ import { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { whitelistedNftCollectionsSelector } from "~/renderer/reducers/settings"; -import { hideNftCollection } from "~/renderer/actions/settings"; +import { nftCollectionsStatusByNetworkSelector } from "~/renderer/reducers/settings"; +import { updateNftStatus } from "~/renderer/actions/settings"; +import { BlockchainsType } from "@ledgerhq/live-nft/supported"; +import { NftStatus } from "@ledgerhq/live-nft/types"; export function useHideSpamCollection() { const spamFilteringTxFeature = useFeature("spamFilteringTx"); - const whitelistedNftCollections = useSelector(whitelistedNftCollectionsSelector); + const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); + + const nftCollectionsStatusByNetwork = useSelector(nftCollectionsStatusByNetworkSelector); const dispatch = useDispatch(); + const hideSpamCollection = useCallback( - (collection: string) => { - if (!whitelistedNftCollections.includes(collection)) { - dispatch(hideNftCollection(collection)); + (collection: string, blockchain: BlockchainsType) => { + const elem = Object.entries(nftCollectionsStatusByNetwork).find( + ([key]) => key === blockchain, + )?.[1]; + + if (!elem) { + dispatch(updateNftStatus(blockchain, collection, NftStatus.spam)); } }, - [dispatch, whitelistedNftCollections], + [dispatch, nftCollectionsStatusByNetwork], ); return { hideSpamCollection, - enabled: spamFilteringTxFeature?.enabled, + enabled: spamFilteringTxFeature?.enabled && nftsFromSimplehashFeature?.enabled, }; } diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollections.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollections.ts index 16832100f6e5..253ca3c4a8dc 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollections.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollections.ts @@ -4,11 +4,7 @@ import { nftsByCollections } from "@ledgerhq/live-nft/index"; import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; import { Account, ProtoNFT } from "@ledgerhq/types-live"; import { useMemo } from "react"; -import { useSelector } from "react-redux"; -import { - hiddenNftCollectionsSelector, - whitelistedNftCollectionsSelector, -} from "~/renderer/reducers/settings"; +import { useNftCollectionsStatus } from "./useNftCollectionsStatus"; export function useNftCollections({ account, @@ -25,19 +21,18 @@ export function useNftCollections({ const threshold = nftsFromSimplehashFeature?.params?.threshold; const simplehashEnabled = nftsFromSimplehashFeature?.enabled; - const whitelistNft = useSelector(whitelistedNftCollectionsSelector); - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); + const { hiddenNftCollections, whitelistedNftCollections } = useNftCollectionsStatus(); const nftsOwnedToCheck = useMemo(() => account?.nfts ?? nftsOwned, [account?.nfts, nftsOwned]); const whitelistedNfts = useMemo( () => nftsOwnedToCheck?.filter(nft => - whitelistNft + whitelistedNftCollections .map(collection => decodeCollectionId(collection).contractAddress) .includes(nft.contract), ) ?? [], - [nftsOwnedToCheck, whitelistNft], + [nftsOwnedToCheck, whitelistedNftCollections], ); const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollectionsStatus.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollectionsStatus.ts new file mode 100644 index 000000000000..72876c2d43c1 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollectionsStatus.ts @@ -0,0 +1,41 @@ +import { useMemo } from "react"; +import { useSelector } from "react-redux"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { nftCollectionsStatusByNetworkSelector } from "~/renderer/reducers/settings"; +import { NftStatus } from "@ledgerhq/live-nft/types"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; + +export function useNftCollectionsStatus() { + const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); + const nftCollectionsStatusByNetwork = useSelector(nftCollectionsStatusByNetworkSelector); + + const shouldDisplaySpams = !nftsFromSimplehashFeature?.enabled; + + const nftCollectionParser = ( + nftCollection: Record>, + applyFilterFn: (arg0: [string, NftStatus]) => boolean, + ) => + Object.values(nftCollection).flatMap(contracts => + Object.entries(contracts) + .filter(applyFilterFn) + .map(([contract]) => contract), + ); + + const list = useMemo(() => { + return nftCollectionParser(nftCollectionsStatusByNetwork, ([_, status]) => + !shouldDisplaySpams ? status !== NftStatus.whitelisted : status === NftStatus.blacklisted, + ); + }, [nftCollectionsStatusByNetwork, shouldDisplaySpams]); + + const whitelisted = useMemo(() => { + return nftCollectionParser( + nftCollectionsStatusByNetwork, + ([_, status]) => status === NftStatus.whitelisted, + ); + }, [nftCollectionsStatusByNetwork]); + + return { + hiddenNftCollections: list, + whitelistedNftCollections: whitelisted, + }; +} diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftLinks.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftLinks.ts index f3ef4a4977e0..3c9801fb601e 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftLinks.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftLinks.ts @@ -116,11 +116,12 @@ export default ( collectionName: metadata?.tokenName ?? nft.contract, collectionId: `${account.id}|${nft.contract}`, onClose, + blockchain: account.currency.id, }), ); }, }), - [account.id, dispatch, metadata?.tokenName, nft.contract, onClose, t], + [account.currency.id, account.id, dispatch, metadata?.tokenName, nft.contract, onClose, t], ); const customImageUri = useMemo(() => { const mediaTypes = metadata ? getMetadataMediaTypes(metadata) : null; diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.ts b/apps/ledger-live-desktop/src/renderer/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.ts index d1f9bba4c659..125d2f7831b3 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.ts @@ -1,4 +1,3 @@ -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; import { useSelector } from "react-redux"; import { shouldRedirectToPostOnboardingOrRecoverUpsell } from "@ledgerhq/live-common/postOnboarding/logic/shouldRedirectToPostOnboardingOrRecoverUpsell"; import { DeviceModelId } from "@ledgerhq/types-devices"; @@ -17,12 +16,10 @@ export function useShouldRedirect(): { } { const hasBeenUpsoldRecover = useSelector(hasBeenUpsoldRecoverSelector); const hasRedirectedToPostOnboarding = useSelector(hasBeenRedirectedToPostOnboardingSelector); - const recoverUpsellRedirection = useFeature("recoverUpsellRedirection"); const lastOnboardedDevice = useSelector(lastOnboardedDeviceSelector); return shouldRedirectToPostOnboardingOrRecoverUpsell({ hasBeenUpsoldRecover, hasRedirectedToPostOnboarding, - upsellForTouchScreenDevices: Boolean(recoverUpsellRedirection?.enabled), lastConnectedDevice: lastOnboardedDevice, supportedDeviceModels: [ DeviceModelId.nanoSP, diff --git a/apps/ledger-live-desktop/src/renderer/live-common-setup.ts b/apps/ledger-live-desktop/src/renderer/live-common-setup.ts index b0130ba46690..22addafa6fe4 100644 --- a/apps/ledger-live-desktop/src/renderer/live-common-setup.ts +++ b/apps/ledger-live-desktop/src/renderer/live-common-setup.ts @@ -13,7 +13,6 @@ import logger from "./logger"; import { currentMode, setDeviceMode } from "@ledgerhq/live-common/hw/actions/app"; import { getFeature } from "@ledgerhq/live-common/featureFlags/index"; import { FeatureId } from "@ledgerhq/types-live"; -import { getEnv } from "@ledgerhq/live-env"; import { overriddenFeatureFlagsSelector } from "~/renderer/reducers/settings"; import { State } from "./reducers"; import { DeviceManagementKitTransport } from "@ledgerhq/live-dmk"; @@ -31,8 +30,6 @@ const getFeatureWithOverrides = (key: FeatureId, store: Store) => { export function registerTransportModules(store: Store) { setEnvOnAllThreads("USER_ID", getUserId()); const vaultTransportPrefixID = "vault-transport:"; - const isSpeculosEnabled = !!getEnv("SPECULOS_API_PORT"); - const isProxyEnabled = !!getEnv("DEVICE_PROXY_URL"); const ldmkFeatureFlag = getFeatureWithOverrides("ldmkTransport", store); listenLogs(({ id, date, ...log }) => { @@ -44,7 +41,6 @@ export function registerTransportModules(store: Store) { registerTransportModule({ id: "sdk", open: (_id: string, timeoutMs?: number, context?: TraceContext) => { - if (isSpeculosEnabled && isProxyEnabled) return null; trace({ type: "renderer-setup", message: "Open called on registered module", @@ -66,7 +62,6 @@ export function registerTransportModules(store: Store) { registerTransportModule({ id: "ipc", open: (id: string, timeoutMs?: number, context?: TraceContext) => { - if (isSpeculosEnabled || isProxyEnabled) return null; const originalDeviceMode = currentMode; // id could be another type of transport such as vault-transport if (id.startsWith(vaultTransportPrefixID)) return; diff --git a/apps/ledger-live-desktop/src/renderer/modals/HideNftCollection/Body.tsx b/apps/ledger-live-desktop/src/renderer/modals/HideNftCollection/Body.tsx index 572412bb44bc..c7d071242fad 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/HideNftCollection/Body.tsx +++ b/apps/ledger-live-desktop/src/renderer/modals/HideNftCollection/Body.tsx @@ -4,14 +4,17 @@ import Text from "~/renderer/components/Text"; import ModalBody from "~/renderer/components/Modal/ModalBody"; import React from "react"; import Footer from "~/renderer/modals/HideNftCollection/Footer"; +import { BlockchainsType } from "@ledgerhq/live-nft/supported"; const Body = ({ onClose, collectionId, collectionName, + blockchain, }: { onClose: () => void; collectionId: string; collectionName: string; + blockchain: string; }) => { const { t } = useTranslation(); return ( @@ -47,7 +50,13 @@ const Body = ({ )} - renderFooter={() =>