diff --git a/.changeset/fresh-squids-cheer.md b/.changeset/fresh-squids-cheer.md new file mode 100644 index 000000000000..51bda26bc652 --- /dev/null +++ b/.changeset/fresh-squids-cheer.md @@ -0,0 +1,9 @@ +--- +"@ledgerhq/types-live": patch +"@ledgerhq/live-common": patch +"ledger-live-desktop": patch +"live-mobile": patch +--- + +Feature flag listAppsV2 replaced by listAppsV2minor1 +Fix listApps v2 logic: adapt to breaking changes in the API and fix "polyfilling" logic of data of apps \ No newline at end of file diff --git a/.changeset/poor-cobras-laugh.md b/.changeset/poor-cobras-laugh.md new file mode 100644 index 000000000000..8d8d2e2a045c --- /dev/null +++ b/.changeset/poor-cobras-laugh.md @@ -0,0 +1,8 @@ +--- +"live-mobile": patch +--- + +Fix interaction between "InstalledAppsModal" and "UninstallDependenciesModal", the later one was not getting opened in case an app with dependents was getting uninstalled from the first one, due to a bad usage of drawers (not using QueuedDrawer). +Refactor prop drilling nightmare of setAppInstallWithDependencies/setAppUninstallWithDependencies with a simple React.Context. +Refactor InstalledAppDependenciesModal and UninstallAppDependenciesModal to have no business logic inside +Rename action creator installAppFirstTime to setHasInstalledAnyApp for more clarity diff --git a/apps/ledger-live-desktop/src/renderer/Default.tsx b/apps/ledger-live-desktop/src/renderer/Default.tsx index 8d88c393bc20..ba3f73c0b1ef 100644 --- a/apps/ledger-live-desktop/src/renderer/Default.tsx +++ b/apps/ledger-live-desktop/src/renderer/Default.tsx @@ -190,7 +190,7 @@ export default function Default() { useFetchCurrencyFrom(); const discoverDB = useDiscoverDB(); - const listAppsV2 = useFeature("listAppsV2"); + const listAppsV2 = useFeature("listAppsV2minor1"); useEffect(() => { if (!listAppsV2) return; enableListAppsV2(listAppsV2.enabled); diff --git a/apps/ledger-live-mobile/src/actions/settings.ts b/apps/ledger-live-mobile/src/actions/settings.ts index 0ce451daf7b7..4b0f0aac9b44 100755 --- a/apps/ledger-live-mobile/src/actions/settings.ts +++ b/apps/ledger-live-mobile/src/actions/settings.ts @@ -14,7 +14,7 @@ import { SettingsHideNftCollectionPayload, SettingsImportDesktopPayload, SettingsImportPayload, - SettingsInstallAppFirstTimePayload, + SettingsSetHasInstalledAnyAppPayload, SettingsLastSeenDeviceInfoPayload, SettingsLastSeenDevicePayload, SettingsRemoveStarredMarketcoinsPayload, @@ -115,8 +115,8 @@ export const clearLastSeenCustomImage = () => export const completeOnboarding = createAction( SettingsActionTypes.SETTINGS_COMPLETE_ONBOARDING, ); -export const installAppFirstTime = createAction( - SettingsActionTypes.SETTINGS_INSTALL_APP_FIRST_TIME, +export const setHasInstalledAnyApp = createAction( + SettingsActionTypes.SETTINGS_SET_HAS_INSTALLED_ANY_APP, ); export const switchCountervalueFirst = createAction( SettingsActionTypes.SETTINGS_SWITCH_COUNTERVALUE_FIRST, diff --git a/apps/ledger-live-mobile/src/actions/types.ts b/apps/ledger-live-mobile/src/actions/types.ts index a0065f8a8c84..07124e3a7a73 100644 --- a/apps/ledger-live-mobile/src/actions/types.ts +++ b/apps/ledger-live-mobile/src/actions/types.ts @@ -225,7 +225,7 @@ export enum SettingsActionTypes { SETTINGS_SET_SELECTED_TIME_RANGE = "SETTINGS_SET_SELECTED_TIME_RANGE", SETTINGS_COMPLETE_ONBOARDING = "SETTINGS_COMPLETE_ONBOARDING", SETTINGS_COMPLETE_CUSTOM_IMAGE_FLOW = "SETTINGS_COMPLETE_CUSTOM_IMAGE_FLOW", - SETTINGS_INSTALL_APP_FIRST_TIME = "SETTINGS_INSTALL_APP_FIRST_TIME", + SETTINGS_SET_HAS_INSTALLED_ANY_APP = "SETTINGS_SET_HAS_INSTALLED_ANY_APP", SETTINGS_SET_READONLY_MODE = "SETTINGS_SET_READONLY_MODE", SETTINGS_SET_EXPERIMENTAL_USB_SUPPORT = "SETTINGS_SET_EXPERIMENTAL_USB_SUPPORT", SETTINGS_SWITCH_COUNTERVALUE_FIRST = "SETTINGS_SWITCH_COUNTERVALUE_FIRST", @@ -297,7 +297,7 @@ export type SettingsSetCountervaluePayload = SettingsState["counterValue"]; export type SettingsSetOrderAccountsPayload = SettingsState["orderAccounts"]; export type SettingsSetPairsPayload = { pairs: Array }; export type SettingsSetSelectedTimeRangePayload = SettingsState["selectedTimeRange"]; -export type SettingsInstallAppFirstTimePayload = SettingsState["hasInstalledAnyApp"]; +export type SettingsSetHasInstalledAnyAppPayload = SettingsState["hasInstalledAnyApp"]; export type SettingsSetReadOnlyModePayload = SettingsState["readOnlyModeEnabled"]; export type SettingsHideEmptyTokenAccountsPayload = SettingsState["hideEmptyTokenAccounts"]; export type SettingsFilterTokenOperationsZeroAmountPayload = @@ -388,7 +388,7 @@ export type SettingsPayload = | SettingsSetOrderAccountsPayload | SettingsSetPairsPayload | SettingsSetSelectedTimeRangePayload - | SettingsInstallAppFirstTimePayload + | SettingsSetHasInstalledAnyAppPayload | SettingsSetReadOnlyModePayload | SettingsHideEmptyTokenAccountsPayload | SettingsShowTokenPayload diff --git a/apps/ledger-live-mobile/src/components/RootNavigator/index.tsx b/apps/ledger-live-mobile/src/components/RootNavigator/index.tsx index 8a41519ced8a..c9b689629b00 100644 --- a/apps/ledger-live-mobile/src/components/RootNavigator/index.tsx +++ b/apps/ledger-live-mobile/src/components/RootNavigator/index.tsx @@ -16,7 +16,7 @@ export default function RootNavigator() { const hasCompletedOnboarding = useSelector(hasCompletedOnboardingSelector); const goToOnboarding = !hasCompletedOnboarding && !Config.SKIP_ONBOARDING; - const listAppsV2 = useFeature("listAppsV2"); + const listAppsV2 = useFeature("listAppsV2minor1"); useEffect(() => { if (!listAppsV2) return; enableListAppsV2(listAppsV2.enabled); diff --git a/apps/ledger-live-mobile/src/reducers/settings.ts b/apps/ledger-live-mobile/src/reducers/settings.ts index bf9d08eba8e8..09b222ce15f2 100644 --- a/apps/ledger-live-mobile/src/reducers/settings.ts +++ b/apps/ledger-live-mobile/src/reducers/settings.ts @@ -26,7 +26,7 @@ import type { SettingsHideNftCollectionPayload, SettingsImportDesktopPayload, SettingsImportPayload, - SettingsInstallAppFirstTimePayload, + SettingsSetHasInstalledAnyAppPayload, SettingsLastSeenDeviceInfoPayload, SettingsPayload, SettingsRemoveStarredMarketcoinsPayload, @@ -304,9 +304,9 @@ const handlers: ReducerMap = { }; }, - [SettingsActionTypes.SETTINGS_INSTALL_APP_FIRST_TIME]: (state, action) => ({ + [SettingsActionTypes.SETTINGS_SET_HAS_INSTALLED_ANY_APP]: (state, action) => ({ ...state, - hasInstalledAnyApp: (action as Action).payload, + hasInstalledAnyApp: (action as Action).payload, }), [SettingsActionTypes.SETTINGS_SET_READONLY_MODE]: (state, action) => ({ diff --git a/apps/ledger-live-mobile/src/screens/Manager/AppsInstallUninstallWithDependenciesContext.ts b/apps/ledger-live-mobile/src/screens/Manager/AppsInstallUninstallWithDependenciesContext.ts new file mode 100644 index 000000000000..9e5aa46385f5 --- /dev/null +++ b/apps/ledger-live-mobile/src/screens/Manager/AppsInstallUninstallWithDependenciesContext.ts @@ -0,0 +1,48 @@ +import { App } from "@ledgerhq/types-live"; +import React, { useContext } from "react"; + +/** + * Represents an installed app that depends on other installed apps. + * For instance: + * `{ app: polygonApp, dependents: [ethereumApp] }` + */ +export type AppWithDependencies = { + app: App; + dependencies: App[]; +}; + +/** + * Represents an installed app that has other installed apps depending on it. + * For instance: + * `{ app: ethereumApp, dependents: [polygonApp] }` + */ +export type AppWithDependents = { + app: App; + dependents: App[]; +}; + +type AppsInstallUninstallWithDependenciesValue = { + setAppWithDependenciesToInstall: (appWithDependencies: AppWithDependencies | null) => void; + setAppWithDependentsToUninstall: (appWithDependents: AppWithDependents | null) => void; +}; + +/** + * Defines setters for apps to install with their dependencies or apps to + * uninstall with their dependents. + * This context was introduced to avoid prop drilling. + */ +const AppsInstallUninstallWithDependenciesContext = React.createContext< + AppsInstallUninstallWithDependenciesValue | undefined +>(undefined); + +export const AppsInstallUninstallWithDependenciesContextProvider = + AppsInstallUninstallWithDependenciesContext.Provider; + +export function useSetAppsWithDependenciesToInstallUninstall() { + const contextValue = useContext(AppsInstallUninstallWithDependenciesContext); + if (contextValue === undefined) + throw new Error( + "useAppsInstallUninstallWithDependencies must be used within a context provider", + ); + return contextValue; +} diff --git a/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppInstallButton.tsx b/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppInstallButton.tsx index dd26fd937d0d..0b0643453e19 100644 --- a/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppInstallButton.tsx +++ b/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppInstallButton.tsx @@ -9,14 +9,14 @@ import { useAppInstallNeedsDeps } from "@ledgerhq/live-common/apps/react"; import styled from "styled-components/native"; import { IconsLegacy, Box } from "@ledgerhq/native-ui"; import { hasInstalledAnyAppSelector } from "../../../reducers/settings"; -import { installAppFirstTime } from "../../../actions/settings"; +import { setHasInstalledAnyApp } from "../../../actions/settings"; +import { useSetAppsWithDependenciesToInstallUninstall } from "../AppsInstallUninstallWithDependenciesContext"; type Props = { app: App; state: State; dispatch: (_: Action) => void; notEnoughMemoryToInstall: boolean; - setAppInstallWithDependencies: (_: { app: App; dependencies: App[] }) => void; storageWarning: (_: string) => void; }; @@ -34,7 +34,6 @@ export default function AppInstallButton({ state, dispatch: dispatchProps, notEnoughMemoryToInstall, - setAppInstallWithDependencies, storageWarning, }: Props) { const dispatch = useDispatch(); @@ -46,6 +45,8 @@ export default function AppInstallButton({ const needsDependencies = useAppInstallNeedsDeps(state, app); + const { setAppWithDependenciesToInstall } = useSetAppsWithDependenciesToInstallUninstall(); + const disabled = useMemo( () => !canBeInstalled || updateAllQueue.length > 0, [canBeInstalled, updateAllQueue.length], @@ -57,13 +58,13 @@ export default function AppInstallButton({ storageWarning(name); return; } - if (needsDependencies && setAppInstallWithDependencies) { - setAppInstallWithDependencies(needsDependencies); + if (needsDependencies) { + setAppWithDependenciesToInstall(needsDependencies); } else { dispatchProps({ type: "install", name }); } if (!hasInstalledAnyApp) { - dispatch(installAppFirstTime(true)); + dispatch(setHasInstalledAnyApp(true)); } }, [ disabled, @@ -71,7 +72,7 @@ export default function AppInstallButton({ dispatchProps, name, needsDependencies, - setAppInstallWithDependencies, + setAppWithDependenciesToInstall, hasInstalledAnyApp, notEnoughMemoryToInstall, storageWarning, diff --git a/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppRow.tsx b/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppRow.tsx index 8d1e605ec573..f6c840b9e935 100644 --- a/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppRow.tsx +++ b/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppRow.tsx @@ -17,8 +17,6 @@ type Props = { app: App; state: State; dispatch: (_: Action) => void; - setAppInstallWithDependencies: (_: { app: App; dependencies: App[] }) => void; - setAppUninstallWithDependencies: (_: { dependents: App[]; app: App }) => void; setStorageWarning: (value: string | null) => void; optimisticState: State; }; @@ -52,15 +50,7 @@ const VersionContainer = styled(Flex).attrs({ marginTop: 2, })``; -const AppRow = ({ - app, - state, - dispatch, - setAppInstallWithDependencies, - setAppUninstallWithDependencies, - setStorageWarning, - optimisticState, -}: Props) => { +const AppRow = ({ app, state, dispatch, setStorageWarning, optimisticState }: Props) => { const { name, bytes, version: appVersion, displayName } = app; const { installed, deviceInfo } = state; const canBeInstalled = useMemo(() => manager.canHandleInstall(app), [app]); @@ -111,8 +101,6 @@ const AppRow = ({ dispatch={dispatch} notEnoughMemoryToInstall={notEnoughMemoryToInstall} isInstalled={!!isInstalled} - setAppInstallWithDependencies={setAppInstallWithDependencies} - setAppUninstallWithDependencies={setAppUninstallWithDependencies} storageWarning={onSizePress} /> diff --git a/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppStateButton.tsx b/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppStateButton.tsx index 7de548f116e9..fe3ed4c600bb 100644 --- a/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppStateButton.tsx +++ b/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppStateButton.tsx @@ -16,8 +16,6 @@ type Props = { dispatch: (_: Action) => void; notEnoughMemoryToInstall: boolean; isInstalled: boolean; - setAppInstallWithDependencies: (_: { app: App; dependencies: App[] }) => void; - setAppUninstallWithDependencies: (_: { dependents: App[]; app: App }) => void; storageWarning: (appName: string) => void; }; @@ -34,8 +32,6 @@ const AppStateButton = ({ dispatch, notEnoughMemoryToInstall, isInstalled, - setAppInstallWithDependencies, - setAppUninstallWithDependencies, storageWarning, }: Props) => { const { installed, installQueue, uninstallQueue, updateAllQueue } = state; @@ -63,14 +59,7 @@ const AppStateButton = ({ case canUpdate: return ; case isInstalled: - return ( - - ); + return ; default: return ( ); diff --git a/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppUninstallButton.tsx b/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppUninstallButton.tsx index 1f6d94131b08..0ae277545bfa 100644 --- a/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppUninstallButton.tsx +++ b/apps/ledger-live-mobile/src/screens/Manager/AppsList/AppUninstallButton.tsx @@ -8,12 +8,12 @@ import type { Action, State } from "@ledgerhq/live-common/apps/index"; import styled from "styled-components/native"; import { IconsLegacy, Box } from "@ledgerhq/native-ui"; +import { useSetAppsWithDependenciesToInstallUninstall } from "../AppsInstallUninstallWithDependenciesContext"; type Props = { app: App; state: State; dispatch: (_: Action) => void; - setAppUninstallWithDependencies: (_: { dependents: App[]; app: App }) => void; size?: number; }; @@ -23,17 +23,14 @@ const ButtonContainer = styled(Box).attrs({ justifyContent: "center", })``; -const AppUninstallButton = ({ - app, - state, - dispatch, - setAppUninstallWithDependencies, - size = 48, -}: Props) => { +const AppUninstallButton = ({ app, state, dispatch, size = 48 }: Props) => { const { name } = app; const needsDependencies = useAppUninstallNeedsDeps(state, app); + const { setAppWithDependentsToUninstall: setAppUninstallWithDependencies } = + useSetAppsWithDependenciesToInstallUninstall(); + const uninstallApp = useCallback(() => { if (needsDependencies && setAppUninstallWithDependencies) setAppUninstallWithDependencies(needsDependencies); diff --git a/apps/ledger-live-mobile/src/screens/Manager/AppsScreen.tsx b/apps/ledger-live-mobile/src/screens/Manager/AppsScreen.tsx index 4b3ea3c4fce4..969d5d81aee4 100644 --- a/apps/ledger-live-mobile/src/screens/Manager/AppsScreen.tsx +++ b/apps/ledger-live-mobile/src/screens/Manager/AppsScreen.tsx @@ -55,8 +55,6 @@ type NavigationProps = BaseComposite< type Props = { state: State; dispatch: (_: Action) => void; - setAppInstallWithDependencies: (_: { app: App; dependencies: App[] }) => void; - setAppUninstallWithDependencies: (_: { dependents: App[]; app: App }) => void; setStorageWarning: (value: string | null) => void; deviceId: string; initialDeviceName?: string | null; @@ -76,8 +74,6 @@ type Props = { const AppsScreen = ({ state, dispatch, - setAppInstallWithDependencies, - setAppUninstallWithDependencies, setStorageWarning, updateModalOpened, deviceId, @@ -232,20 +228,11 @@ const AppsScreen = ({ app={item} state={state} dispatch={dispatch} - setAppInstallWithDependencies={setAppInstallWithDependencies} - setAppUninstallWithDependencies={setAppUninstallWithDependencies} setStorageWarning={setStorageWarning} optimisticState={optimisticState} /> ), - [ - state, - dispatch, - setAppInstallWithDependencies, - setAppUninstallWithDependencies, - setStorageWarning, - optimisticState, - ], + [state, dispatch, setStorageWarning, optimisticState], ); const lastSeenDevice = useSelector(lastSeenDeviceSelector); @@ -267,7 +254,6 @@ const AppsScreen = ({ initialDeviceName={initialDeviceName} pendingInstalls={pendingInstalls} deviceInfo={deviceInfo} - setAppUninstallWithDependencies={setAppUninstallWithDependencies} dispatch={dispatch} device={device} appList={deviceApps} @@ -325,7 +311,6 @@ const AppsScreen = ({ initialDeviceName, pendingInstalls, deviceInfo, - setAppUninstallWithDependencies, dispatch, device, deviceApps, diff --git a/apps/ledger-live-mobile/src/screens/Manager/Device/index.tsx b/apps/ledger-live-mobile/src/screens/Manager/Device/index.tsx index 19c0310a489a..0bd12a853ac9 100644 --- a/apps/ledger-live-mobile/src/screens/Manager/Device/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Manager/Device/index.tsx @@ -50,7 +50,6 @@ type Props = PropsWithChildren<{ pendingInstalls: boolean; deviceInfo: DeviceInfo; device: Device; - setAppUninstallWithDependencies: (params: { dependents: App[]; app: App }) => void; dispatch: (action: Action) => void; appList: App[]; onLanguageChange: () => void; @@ -69,7 +68,6 @@ const DeviceCard = ({ initialDeviceName, pendingInstalls, deviceInfo, - setAppUninstallWithDependencies, dispatch, appList, onLanguageChange, @@ -202,7 +200,6 @@ const DeviceCard = ({ state={state} dispatch={dispatch} appList={appList} - setAppUninstallWithDependencies={setAppUninstallWithDependencies} illustration={illustration} deviceInfo={deviceInfo} /> diff --git a/apps/ledger-live-mobile/src/screens/Manager/Manager.tsx b/apps/ledger-live-mobile/src/screens/Manager/Manager.tsx index ba8959dbb0fb..2990a7283f89 100644 --- a/apps/ledger-live-mobile/src/screens/Manager/Manager.tsx +++ b/apps/ledger-live-mobile/src/screens/Manager/Manager.tsx @@ -1,7 +1,6 @@ import React, { useState, useCallback, useEffect, memo, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { firstValueFrom, from } from "rxjs"; -import type { App } from "@ledgerhq/types-live"; import { predictOptimisticState } from "@ledgerhq/live-common/apps/index"; import { SyncSkipUnderPriority } from "@ledgerhq/live-common/bridge/react/index"; import { CommonActions } from "@react-navigation/native"; @@ -15,16 +14,21 @@ import GenericErrorBottomModal from "../../components/GenericErrorBottomModal"; import { TrackScreen } from "../../analytics"; import QuitManagerModal from "./Modals/QuitManagerModal"; import StorageWarningModal from "./Modals/StorageWarningModal"; -import AppDependenciesModal from "./Modals/AppDependenciesModal"; -import UninstallDependenciesModal from "./Modals/UninstallDependenciesModal"; +import InstallAppDependenciesModal from "./Modals/InstallAppDependenciesModal"; +import UninstallAppDependenciesModal from "./Modals/UninstallAppDependenciesModal"; import { useLockNavigation } from "../../components/RootNavigator/CustomBlockRouterNavigator"; -import { setLastSeenDeviceInfo } from "../../actions/settings"; +import { setHasInstalledAnyApp, setLastSeenDeviceInfo } from "../../actions/settings"; import { ScreenName } from "../../const"; import FirmwareUpdateScreen from "../../components/FirmwareUpdate"; import { ManagerNavigatorStackParamList } from "../../components/RootNavigator/types/ManagerNavigator"; import { BaseComposite, StackNavigatorProps } from "../../components/RootNavigator/types/helpers"; import { lastConnectedDeviceSelector } from "../../reducers/settings"; import { UpdateStep } from "../FirmwareUpdate"; +import { + AppWithDependencies, + AppWithDependents, + AppsInstallUninstallWithDependenciesContextProvider, +} from "./AppsInstallUninstallWithDependenciesContext"; type NavigationProps = BaseComposite< StackNavigatorProps @@ -91,15 +95,11 @@ const Manager = ({ navigation, route }: NavigationProps) => { /** storage warning modal state */ const [storageWarning, setStorageWarning] = useState(null); /** install app with dependencies modal state */ - const [appInstallWithDependencies, setAppInstallWithDependencies] = useState<{ - app: App; - dependencies: App[]; - } | null>(null); - /** uninstall app with dependencies modal state */ - const [appUninstallWithDependencies, setAppUninstallWithDependencies] = useState<{ - dependents: App[]; - app: App; - } | null>(null); + const [appWithDependenciesToInstall, setAppWithDependenciesToInstall] = + useState(null); + /** uninstall app with dependents modal state */ + const [appWithDependentsToUninstall, setAppWithDependentsToUninstall] = + useState(null); /** open error modal each time a new error appears in state.currentError */ useEffect(() => { @@ -139,14 +139,6 @@ const Manager = ({ navigation, route }: NavigationProps) => { const closeErrorModal = useCallback(() => setError(null), [setError]); - const resetAppInstallWithDependencies = useCallback(() => { - setAppInstallWithDependencies(null); - }, [setAppInstallWithDependencies]); - - const resetAppUninstallWithDependencies = useCallback(() => { - setAppUninstallWithDependencies(null); - }, [setAppUninstallWithDependencies]); - const closeQuitManagerModal = useCallback( () => setQuitManagerAction(null), [setQuitManagerAction], @@ -198,6 +190,37 @@ const Manager = ({ navigation, route }: NavigationProps) => { [device, navigation], ); + const appsInstallUninstallWithDependenciesContextValue = useMemo( + () => ({ + setAppWithDependenciesToInstall, + setAppWithDependentsToUninstall, + }), + [], + ); + + const onCloseInstallAppDependenciesModal = useCallback(() => { + setAppWithDependenciesToInstall(null); + }, []); + + const installAppWithDependencies = useCallback(() => { + if (appWithDependenciesToInstall) { + reduxDispatch(setHasInstalledAnyApp(true)); + dispatch({ type: "install", name: appWithDependenciesToInstall?.app.name }); + } + onCloseInstallAppDependenciesModal(); + }, [appWithDependenciesToInstall, onCloseInstallAppDependenciesModal, reduxDispatch, dispatch]); + + const onCloseUninstallAppDependenciesModal = useCallback(() => { + setAppWithDependentsToUninstall(null); + }, []); + + const uninstallAppsWithDependents = useCallback(() => { + if (appWithDependentsToUninstall) { + dispatch({ type: "uninstall", name: appWithDependentsToUninstall?.app.name }); + } + onCloseUninstallAppDependenciesModal(); + }, [appWithDependentsToUninstall, dispatch, onCloseUninstallAppDependenciesModal]); + return ( <> { appLength={result ? result.installed.length : 0} /> - + + + { uninstallQueue={uninstallQueue} /> - - void; + appWithDependenciesToInstall: AppWithDependencies | null; onClose: () => void; + installAppWithDependencies: () => void; }; const IconContainer = styled(Flex).attrs({ @@ -53,31 +47,14 @@ const ButtonsContainer = styled(Flex).attrs({ width: "100%", })``; -const CancelButton = styled(TouchableOpacity)` - align-items: center; - justify-content: center; - margin-top: 25; -`; - -function AppDependenciesModal({ - appInstallWithDependencies, - dispatch: dispatchProps, +function InstallAppDependenciesModal({ + appWithDependenciesToInstall, onClose, + installAppWithDependencies, }: Props) { - const dispatch = useDispatch(); - const hasInstalledAnyApp = useSelector(hasInstalledAnyAppSelector); - - const { app, dependencies = [] } = appInstallWithDependencies || {}; + const { app, dependencies = [] } = appWithDependenciesToInstall || {}; const { name } = app || {}; - const installAppDependencies = useCallback(() => { - if (!hasInstalledAnyApp) { - dispatch(installAppFirstTime(true)); - } - dispatchProps({ type: "install", name }); - onClose(); - }, [dispatch, dispatchProps, onClose, name, hasInstalledAnyApp]); - return ( @@ -112,14 +89,12 @@ function AppDependenciesModal({ - - - - - - + + + )} @@ -128,4 +103,4 @@ function AppDependenciesModal({ ); } -export default memo(AppDependenciesModal); +export default memo(InstallAppDependenciesModal); diff --git a/apps/ledger-live-mobile/src/screens/Manager/Modals/InstalledAppsModal.tsx b/apps/ledger-live-mobile/src/screens/Manager/Modals/InstalledAppsModal.tsx index 81d67c27ee14..346988f26284 100644 --- a/apps/ledger-live-mobile/src/screens/Manager/Modals/InstalledAppsModal.tsx +++ b/apps/ledger-live-mobile/src/screens/Manager/Modals/InstalledAppsModal.tsx @@ -1,6 +1,6 @@ import React, { memo, useCallback, useMemo, useEffect } from "react"; -import { Text, Flex, Button, BaseModal } from "@ledgerhq/native-ui"; +import { Text, Flex, Button } from "@ledgerhq/native-ui"; import { FlatList } from "react-native"; import { App, DeviceInfo } from "@ledgerhq/types-live"; import { State, Action } from "@ledgerhq/live-common/apps/index"; @@ -9,6 +9,7 @@ import AppIcon from "../AppsList/AppIcon"; import ByteSize from "../../../components/ByteSize"; import AppUninstallButton from "../AppsList/AppUninstallButton"; import AppProgressButton from "../AppsList/AppProgressButton"; +import QueuedDrawer from "../../../components/QueuedDrawer"; type HeaderProps = { illustration: JSX.Element; @@ -27,15 +28,9 @@ type UninstallButtonProps = { app: App; state: State; dispatch: (_: Action) => void; - setAppUninstallWithDependencies: (_: { dependents: App[]; app: App }) => void; }; -const UninstallButton = ({ - app, - state, - dispatch, - setAppUninstallWithDependencies, -}: UninstallButtonProps) => { +const UninstallButton = ({ app, state, dispatch }: UninstallButtonProps) => { const { uninstallQueue } = state; const uninstalling = useMemo(() => uninstallQueue.includes(app.name), [uninstallQueue, app.name]); const renderAppState = () => { @@ -43,15 +38,7 @@ const UninstallButton = ({ case uninstalling: return ; default: - return ( - - ); + return ; } }; @@ -62,11 +49,10 @@ type RowProps = { app: App; state: State; dispatch: (_: Action) => void; - setAppUninstallWithDependencies: (_: { dependents: App[]; app: App }) => void; deviceInfo: DeviceInfo; }; -const Row = ({ app, state, dispatch, setAppUninstallWithDependencies, deviceInfo }: RowProps) => ( +const Row = ({ app, state, dispatch, deviceInfo }: RowProps) => ( @@ -82,31 +68,17 @@ const Row = ({ app, state, dispatch, setAppUninstallWithDependencies, deviceInfo firmwareVersion={deviceInfo.version} /> - + ); -const modalStyleOverrides = { - modal: { - flex: 1, - justifyContent: "flex-end" as const, - margin: 0, - }, -}; - type Props = { isOpen: boolean; onClose: () => void; state: State; dispatch: (_: Action) => void; appList?: App[]; - setAppUninstallWithDependencies: (_: { dependents: App[]; app: App }) => void; illustration: JSX.Element; deviceInfo: DeviceInfo; }; @@ -117,7 +89,6 @@ const InstalledAppsModal = ({ state, dispatch, appList, - setAppUninstallWithDependencies, illustration, deviceInfo, }: Props) => { @@ -125,15 +96,9 @@ const InstalledAppsModal = ({ const renderItem = useCallback( ({ item }: { item: App }) => ( - + ), - [deviceInfo, dispatch, setAppUninstallWithDependencies, state], + [deviceInfo, dispatch, state], ); useEffect(() => { @@ -141,26 +106,19 @@ const InstalledAppsModal = ({ }, [appList, onClose]); return ( - - - "" + item.id} - ListHeaderComponent={
} - showsVerticalScrollIndicator={false} - /> - - - + ); }; diff --git a/apps/ledger-live-mobile/src/screens/Manager/Modals/UninstallDependenciesModal.tsx b/apps/ledger-live-mobile/src/screens/Manager/Modals/UninstallAppDependenciesModal.tsx similarity index 84% rename from apps/ledger-live-mobile/src/screens/Manager/Modals/UninstallDependenciesModal.tsx rename to apps/ledger-live-mobile/src/screens/Manager/Modals/UninstallAppDependenciesModal.tsx index 6d51f005afbb..656ca9b3a43e 100644 --- a/apps/ledger-live-mobile/src/screens/Manager/Modals/UninstallDependenciesModal.tsx +++ b/apps/ledger-live-mobile/src/screens/Manager/Modals/UninstallAppDependenciesModal.tsx @@ -1,8 +1,6 @@ import React, { memo, useCallback } from "react"; import { View } from "react-native"; import { Trans } from "react-i18next"; - -import { Action } from "@ledgerhq/live-common/apps/index"; import { App } from "@ledgerhq/types-live"; import styled, { DefaultTheme, useTheme } from "styled-components/native"; @@ -16,13 +14,14 @@ import ListTreeLine from "../../../icons/ListTreeLine"; import getWindowDimensions from "../../../logic/getWindowDimensions"; import { Theme } from "../../../colors"; +import { AppWithDependents } from "../AppsInstallUninstallWithDependenciesContext"; const { height } = getWindowDimensions(); type Props = { - appUninstallWithDependencies: { app: App; dependents: App[] }; - dispatch: (_: Action) => void; + appWithDependentsToUninstall: AppWithDependents | null; onClose: () => void; + uninstallAppsWithDependents: () => void; }; const LIST_HEIGHT = height - 420; @@ -56,16 +55,15 @@ const ButtonsContainer = styled(Flex).attrs({ width: "100%", })``; -const UninstallDependenciesModal = ({ appUninstallWithDependencies, dispatch, onClose }: Props) => { +const UninstallAppDependenciesModal = ({ + appWithDependentsToUninstall, + uninstallAppsWithDependents, + onClose, +}: Props) => { const { colors } = useTheme() as DefaultTheme & Theme; - const { app, dependents = [] } = appUninstallWithDependencies || {}; + const { app, dependents = [] } = appWithDependentsToUninstall || {}; const { name } = app || {}; - const unInstallApp = useCallback(() => { - dispatch({ type: "uninstall", name }); - onClose(); - }, [dispatch, onClose, name]); - const renderDepLine = useCallback( ({ item }: { item: App }) => ( + {app && dependents.length && ( @@ -115,7 +114,7 @@ const UninstallDependenciesModal = ({ appUninstallWithDependencies, dispatch, on }} /> - @@ -126,4 +125,4 @@ const UninstallDependenciesModal = ({ appUninstallWithDependencies, dispatch, on ); }; -export default memo(UninstallDependenciesModal); +export default memo(UninstallAppDependenciesModal); diff --git a/apps/ledger-live-mobile/src/screens/Onboarding/steps/setupDevice/scenes/ConnectNano.tsx b/apps/ledger-live-mobile/src/screens/Onboarding/steps/setupDevice/scenes/ConnectNano.tsx index 094451786d3f..30c5ce8e630c 100644 --- a/apps/ledger-live-mobile/src/screens/Onboarding/steps/setupDevice/scenes/ConnectNano.tsx +++ b/apps/ledger-live-mobile/src/screens/Onboarding/steps/setupDevice/scenes/ConnectNano.tsx @@ -11,7 +11,7 @@ import { TrackScreen } from "../../../../../analytics"; import Button from "../../../../../components/PreventDoubleClickButton"; import { - installAppFirstTime, + setHasInstalledAnyApp, setHasOrderedNano, setLastConnectedDevice, setReadOnlyMode, @@ -65,7 +65,7 @@ const ConnectNanoScene = ({ info.result.installed.length > 0 ); - dispatch(installAppFirstTime(hasAnyAppinstalled)); + dispatch(setHasInstalledAnyApp(hasAnyAppinstalled)); setDevice(undefined); dispatch(setReadOnlyMode(false)); dispatch(setHasOrderedNano(false)); diff --git a/apps/ledger-live-mobile/src/screens/PairDevices/index.tsx b/apps/ledger-live-mobile/src/screens/PairDevices/index.tsx index 0d4cc0703730..c7b7467b12d7 100644 --- a/apps/ledger-live-mobile/src/screens/PairDevices/index.tsx +++ b/apps/ledger-live-mobile/src/screens/PairDevices/index.tsx @@ -15,7 +15,7 @@ import TransportBLE from "../../react-native-hw-transport-ble"; import { GENUINE_CHECK_TIMEOUT } from "@utils/constants"; import { addKnownDevice } from "../../actions/ble"; import { - installAppFirstTime, + setHasInstalledAnyApp, setLastSeenDeviceInfo, setReadOnlyMode, } from "../../actions/settings"; @@ -154,7 +154,7 @@ function PairDevicesInner({ navigation, route }: NavigationProps) { const hasAnyAppInstalled = e.result && e.result.installed.length > 0; if (!hasAnyAppInstalled) { - dispatchRedux(installAppFirstTime(false)); + dispatchRedux(setHasInstalledAnyApp(false)); } } diff --git a/libs/ledger-live-common/src/apps/listApps/v2.ts b/libs/ledger-live-common/src/apps/listApps/v2.ts index c6762d4c73f0..5e00cfafb0b1 100644 --- a/libs/ledger-live-common/src/apps/listApps/v2.ts +++ b/libs/ledger-live-common/src/apps/listApps/v2.ts @@ -45,8 +45,16 @@ const listApps = (transport: Transport, deviceInfo: DeviceInfo): Observable; + /** The following are several asynchronous sequences running in parallel */ + + /** + * Sequence 1: obtain the full data regarding apps installed on the device + * -> list raw data of apps installed on device + * -> then filter apps (eliminate language packs and such) + * -> then fetch matching app metadata using apps' hashes + */ + let listAppsResponsePromise: Promise; if (deviceInfo.managerAllowed) { // If the user has already allowed a secure channel during this session we can directly // ask the device for the installed applications instead of going through a scriptrunner, @@ -77,6 +85,25 @@ const listApps = (transport: Transport, deviceInfo: DeviceInfo): Observable { + // Empty HashData can come from apps that are not real apps (such as language packs) + // or custom applications that have been sideloaded. + return result + .filter(({ hash_code_data }) => hash_code_data !== emptyHashData) + .map(({ hash, name }) => ({ hash, name })); + }); + + const listAppsAndMatchesPromise = filteredListAppsPromise.then(result => { + const hashes = result.map(({ hash }) => hash); + const matches = result.length ? ManagerAPI.getAppsByHash(hashes) : []; + return Promise.all([result, matches]); + }); + + /** + * Sequence 2: get information about current and latest firmware available + * for the device + */ + const deviceVersionPromise = ManagerAPI.getDeviceVersion(deviceInfo.targetId, provider); const currentFirmwarePromise = deviceVersionPromise.then(deviceVersion => @@ -94,30 +121,25 @@ const listApps = (transport: Transport, deviceInfo: DeviceInfo): Observable { - // Empty HashData can come from apps that are not real apps (such as langauge packs) - // or custom applications that have been sideloaded. - return result - .filter(({ hash_code_data }) => hash_code_data !== emptyHashData) - .map(({ hash, name }) => ({ hash, name })); - }); - - const listAppsAndMatchesPromise = filteredListAppsPromise.then(result => { - const hashes = result.map(({ hash }) => hash); - const matches = result.length ? ManagerAPI.getAppsByHash(hashes) : []; - return Promise.all([result, matches]); - }); - + /* Running all sequences 1 2 3 4 defined above in parallel */ const [[listApps, matches], catalogForDevice, firmware, sortedCryptoCurrencies] = await Promise.all([ listAppsAndMatchesPromise, @@ -126,14 +148,28 @@ const listApps = (transport: Transport, deviceInfo: DeviceInfo): Observable { + const crypto = app.currencyId && findCryptoCurrencyById(app.currencyId); + if (crypto) { + app.indexOfMarketCap = sortedCryptoCurrencies.indexOf(crypto); + } + }); + + /** + * Aggregate the data obtained above to build the list of installed apps + * with their full metadata. + */ + const installedList: App[] = []; - // Nb We can't reliably get the ordered result from the backend because of apps with - // inconsistent hashes. An iteration of the backend would be to return a key-value result - // instead of an unordered array but I think that's a needless optimization. - listApps.forEach(({ name: localName, hash: localHash }) => { - const matchFromHash = matches.find(({ hash }) => hash === localHash); - if (matchFromHash) { + listApps.forEach(({ name: localName, hash: localHash }, index) => { + const matchFromHash = matches[index]; + if (matchFromHash && matchFromHash.hash === localHash) { installedList.push(matchFromHash); return; } @@ -146,13 +182,6 @@ const listApps = (transport: Transport, deviceInfo: DeviceInfo): Observable { - const crypto = app.currencyId && findCryptoCurrencyById(app.currencyId); - if (crypto) { - app.indexOfMarketCap = sortedCryptoCurrencies.indexOf(crypto); - } - }); - log("list-apps", `${installedList.length} apps installed.`); log("list-apps", `${catalogForDevice.length} apps in catalog.`); @@ -189,6 +218,12 @@ const listApps = (transport: Transport, deviceInfo: DeviceInfo): Observable isDevMode || !isDevTools || name in installedAppNames) .map(({ name }) => name); + /** + * Obtain remaining metadata: + * - Ledger Stax custom picture: number of blocks taken in storage + * - Device name + * */ + // Stax specific, account for the size of the CLS for the storage bar. let customImageBlocks = 0; if (deviceModelId === DeviceModelId.stax && !deviceInfo.isRecoveryMode) { diff --git a/libs/ledger-live-common/src/apps/polyfill.ts b/libs/ledger-live-common/src/apps/polyfill.ts index a230e65256ee..2e52f64f83b9 100644 --- a/libs/ledger-live-common/src/apps/polyfill.ts +++ b/libs/ledger-live-common/src/apps/polyfill.ts @@ -1,11 +1,7 @@ // polyfill the unfinished support of apps logic import uniq from "lodash/uniq"; import semver from "semver"; -import { - listCryptoCurrencies, - findCryptoCurrencyById, - findCryptoCurrency, -} from "@ledgerhq/cryptoassets"; +import { listCryptoCurrencies, findCryptoCurrencyById } from "@ledgerhq/cryptoassets"; import { App, AppType, Application, ApplicationV2 } from "@ledgerhq/types-live"; import type { CryptoCurrency, CryptoCurrencyId } from "@ledgerhq/types-cryptoassets"; const directDep = {}; @@ -97,13 +93,18 @@ export const getDependencies = (appName: string, appVersion?: string): string[] export const getDependents = (appName: string): string[] => reverseDep[appName] || []; +function matchAppNameAndCryptoCurrency(appName: string, crypto: CryptoCurrency) { + return ( + appName.toLowerCase() === crypto.managerAppName.toLowerCase() && + (crypto.managerAppName !== "Ethereum" || + // if it's ethereum, we have a specific case that we must only allow the Ethereum app + appName === "Ethereum") + ); +} + export const polyfillApplication = (app: Application): Application => { - const crypto = listCryptoCurrencies(true, true).find( - crypto => - app.name.toLowerCase() === crypto.managerAppName.toLowerCase() && - (crypto.managerAppName !== "Ethereum" || - // if it's ethereum, we have a specific case that we must only allow the Ethereum app - app.name === "Ethereum"), + const crypto = listCryptoCurrencies(true, true).find(crypto => + matchAppNameAndCryptoCurrency(app.name, crypto), ); if (crypto && !app.currencyId) { @@ -114,13 +115,13 @@ export const polyfillApplication = (app: Application): Application => { }; export const getCurrencyIdFromAppName = ( - name: string, + appName: string, ): CryptoCurrencyId | "LBRY" | "groestcoin" | "osmo" | undefined => { const crypto = // try to find the "official" currency when possible (2 currencies can have the same manager app and ticker) - findCryptoCurrency(c => c.name === name) || - // Else take the first one with that manager app - findCryptoCurrency(c => c.managerAppName === name); + listCryptoCurrencies(true, true).find( + c => c.name === appName || matchAppNameAndCryptoCurrency(appName, c), + ); return crypto?.id; }; @@ -140,8 +141,9 @@ export const mapApplicationV2ToApp = ({ applicationType: type, compatibleWallets, parentName, + currencyId, ...rest -}: ApplicationV2): App => ({ +}: ApplicationV2) => ({ id, name, displayName, @@ -151,7 +153,7 @@ export const mapApplicationV2ToApp = ({ indexOfMarketCap: -1, // We don't know at this point. type: name === "Exchange" ? AppType.swap : type, ...rest, - currencyId: getCurrencyIdFromAppName(name), + currencyId: currencyId || getCurrencyIdFromAppName(name), compatibleWallets: parseCompatibleWallets(compatibleWallets, name), }); diff --git a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts index 14bf1657ac4f..dfd5a6e51c02 100644 --- a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts +++ b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts @@ -75,7 +75,6 @@ export const DEFAULT_FEATURES: Features = { syncOnboarding: DEFAULT_FEATURE, walletConnectEntryPoint: DEFAULT_FEATURE, counterValue: DEFAULT_FEATURE, - listAppsV2: DEFAULT_FEATURE, llmNewDeviceSelection: DEFAULT_FEATURE, llmNewFirmwareUpdateUx: DEFAULT_FEATURE, mockFeature: DEFAULT_FEATURE, @@ -91,7 +90,7 @@ export const DEFAULT_FEATURES: Features = { staxWelcomeScreen: DEFAULT_FEATURE, protectServicesDiscoverDesktop: DEFAULT_FEATURE, llmWalletQuickActions: DEFAULT_FEATURE, - + listAppsV2minor1: DEFAULT_FEATURE, ethStakingProviders: initFeature(), referralProgramDiscoverCard: initFeature(), newsfeedPage: initFeature(), diff --git a/libs/ledger-live-common/src/manager/api.ts b/libs/ledger-live-common/src/manager/api.ts index 144f9e47cec1..c92746d0570e 100644 --- a/libs/ledger-live-common/src/manager/api.ts +++ b/libs/ledger-live-common/src/manager/api.ts @@ -130,7 +130,10 @@ const applicationsByDevice: (params: { }); return r.data.application_versions; }, - p => `${p.provider}_${p.current_se_firmware_final_version}_${p.device_version}`, + p => + `${getEnv("MANAGER_API_BASE")}_${p.provider}_${p.current_se_firmware_final_version}_${ + p.device_version + }`, ); /** @@ -168,7 +171,7 @@ const catalogForDevice: (params: { return data.map(mapApplicationV2ToApp); }, - _ => "", + a => `${getEnv("MANAGER_API_BASE")}_${a.provider}_${a.targetId}_${a.firmwareVersion}`, ); const listApps: () => Promise> = makeLRUCache( @@ -189,7 +192,7 @@ const listApps: () => Promise> = makeLRUCache( return data; }, - () => "", + () => getEnv("MANAGER_API_BASE"), ); const listCategories = async (): Promise> => { @@ -218,7 +221,7 @@ const getMcus: () => Promise = makeLRUCache( }); return data; }, - () => "", + () => getEnv("MANAGER_API_BASE"), ); const compatibleMCUForDeviceInfo = ( @@ -320,7 +323,10 @@ const getLatestFirmware: (arg0: { return data.se_firmware_osu_version; }, - a => `${a.current_se_firmware_final_version}_${a.device_version}_${a.provider}`, + a => + `${getEnv("MANAGER_API_BASE")}_${a.current_se_firmware_final_version}_${a.device_version}_${ + a.provider + }`, ); const getCurrentOSU: (input: { @@ -345,7 +351,7 @@ const getCurrentOSU: (input: { }); return data; }, - a => `${a.version}_${a.deviceId}_${a.provider}`, + a => `${getEnv("MANAGER_API_BASE")}_${a.version}_${a.deviceId}_${a.provider}`, ); const getCurrentFirmware: (input: { version: string; @@ -373,7 +379,7 @@ const getCurrentFirmware: (input: { }); return data; }, - a => `${a.version}_${a.deviceId}_${a.provider}`, + a => `${getEnv("MANAGER_API_BASE")}_${a.version}_${a.deviceId}_${a.provider}`, ); const getFinalFirmwareById: (id: number) => Promise = makeLRUCache( async id => { @@ -392,20 +398,25 @@ const getFinalFirmwareById: (id: number) => Promise = makeLRUCach }); return data; }, - id => String(id), + id => `${getEnv("MANAGER_API_BASE")}}_${String(id)}`, ); /** + * Resolve applications details by hashes. + * Order of outputs matches order of inputs. + * If an application version is not found, a null is returned instead. + * If several versions match the same hash, only the latest one is returned. + * * Given an array of hashes that we can obtain by either listInstalledApps in this same * API (a websocket connection to a scriptrunner) or via direct apdus using hw/listApps.ts * retrieve all the information needed from the backend for those applications. */ -const getAppsByHash: (hashes: string[]) => Promise> = makeLRUCache( +const getAppsByHash: (hashes: string[]) => Promise> = makeLRUCache( async hashes => { const { data, }: { - data: Array; + data: Array; } = await network({ method: "POST", url: URL.format({ @@ -421,9 +432,9 @@ const getAppsByHash: (hashes: string[]) => Promise> = makeLRUCache( throw new NetworkDown(""); } - return data.map(mapApplicationV2ToApp); + return data.map(appV2 => (appV2 ? mapApplicationV2ToApp(appV2) : null)); }, - hashes => String(hashes), + hashes => `${getEnv("MANAGER_API_BASE")}_${hashes.join("-")}`, ); const getDeviceVersion: (targetId: string | number, provider: number) => Promise = @@ -458,7 +469,7 @@ const getDeviceVersion: (targetId: string | number, provider: number) => Promise }); return data; }, - (targetId, provider) => `${targetId}_${provider}`, + (targetId, provider) => `${getEnv("MANAGER_API_BASE")}_${targetId}_${provider}`, ); const install = ( diff --git a/libs/ledgerjs/packages/types-live/src/feature.ts b/libs/ledgerjs/packages/types-live/src/feature.ts index f63b69ecd172..ecc5072314c7 100644 --- a/libs/ledgerjs/packages/types-live/src/feature.ts +++ b/libs/ledgerjs/packages/types-live/src/feature.ts @@ -173,7 +173,7 @@ export type Features = CurrencyFeatures & { discover: Feature_Discover; protectServicesDiscoverDesktop: Feature_ProtectServicesDiscoverDesktop; transactionsAlerts: Feature_TransactionsAlerts; - listAppsV2: Feature_ListAppsV2; + listAppsV2minor1: Feature_ListAppsV2minor1; llmWalletQuickActions: Feature_LlmWalletQuickActions; cexDepositEntryPointsDesktop: Feature_CexDepositEntryPointsDesktop; cexDepositEntryPointsMobile: Feature_CexDepositEntryPointsMobile; @@ -469,7 +469,7 @@ export type Feature_PortfolioExchangeBanner = DefaultFeature; export type Feature_Objkt = DefaultFeature; export type Feature_EditEthTx = DefaultFeature; export type Feature_ProtectServicesDiscoverDesktop = DefaultFeature; -export type Feature_ListAppsV2 = DefaultFeature; +export type Feature_ListAppsV2minor1 = DefaultFeature; export type Feature_BrazeLearn = DefaultFeature; export type Feature_LlmNewDeviceSelection = DefaultFeature; export type Feature_LlmWalletQuickActions = DefaultFeature;