Conversation
|
There was a problem hiding this comment.
Pull request overview
Adds a new Ledger Live Mobile deeplink-driven “install app” flow that opens an installation drawer and routes users through device selection and install progress when ledgerlive://wallet|portfolio?installApp=<appKey> is received.
Changes:
- Introduces the
DeeplinkInstallAppMVVM feature (drawer UI, device selection screen, install ViewModels, allowlist map). - Hooks deeplink parsing in
DeeplinksProviderto dispatch the install flow wheninstallAppis present and allowlisted. - Adds Redux state/slice + navigation wiring, translations, analytics enums, and tests + changeset.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/ledger-live-mobile/src/reducers/types.ts | Adds deeplinkInstallApp state typing to root Redux state. |
| apps/ledger-live-mobile/src/reducers/index.ts | Registers the new deeplinkInstallApp reducer. |
| apps/ledger-live-mobile/src/reducers/deeplinkInstallApp.ts | New Redux slice/actions/selectors for drawer/app/device selection state. |
| apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx | Dispatches install drawer open on `wallet |
| apps/ledger-live-mobile/src/mvvm/features/DeeplinkInstallApp/screens/DeviceSelectionScreen/index.tsx | Adds a device selection screen to choose a device before installing. |
| apps/ledger-live-mobile/src/mvvm/features/DeeplinkInstallApp/index.ts | Public exports for the feature (drawer + allowlist helpers). |
| apps/ledger-live-mobile/src/mvvm/features/DeeplinkInstallApp/constants/appInstallMap.ts | Defines the allowlist/config for apps installable via deeplink. |
| apps/ledger-live-mobile/src/mvvm/features/DeeplinkInstallApp/components/InstallDrawer/useInstallingContentViewModel.ts | ViewModel driving install status/progress and allow-manager prompting. |
| apps/ledger-live-mobile/src/mvvm/features/DeeplinkInstallApp/components/InstallDrawer/useInstallingContentViewModel.test.ts | Unit tests for install ViewModel behavior and timer logic. |
| apps/ledger-live-mobile/src/mvvm/features/DeeplinkInstallApp/components/InstallDrawer/useDeeplinkInstallDrawer.ts | ViewModel orchestrating drawer step transitions + navigation to device selection. |
| apps/ledger-live-mobile/src/mvvm/features/DeeplinkInstallApp/components/InstallDrawer/useDeeplinkInstallDrawer.test.ts | Unit tests for drawer ViewModel state transitions and navigation/analytics calls. |
| apps/ledger-live-mobile/src/mvvm/features/DeeplinkInstallApp/components/InstallDrawer/index.tsx | Drawer UI rendering confirmation/installing/success/error steps. |
| apps/ledger-live-mobile/src/mvvm/features/DeeplinkInstallApp/components/InstallDrawer/InstallingContent.tsx | Installing step UI (progress, resolving state, allow-manager state). |
| apps/ledger-live-mobile/src/mvvm/features/DeeplinkInstallApp/components/InstallDrawer/ConfirmationStep.tsx | Confirmation step UI and CTAs. |
| apps/ledger-live-mobile/src/mvvm/features/DeeplinkInstallApp/integrations/DeeplinkInstallApp.test.tsx | Minimal integration test coverage for the drawer rendering logic. |
| apps/ledger-live-mobile/src/locales/en/common.json | Adds English i18n strings for the deeplink install flow. |
| apps/ledger-live-mobile/src/const/navigation.ts | Adds DeeplinkInstallAppDeviceSelection screen name. |
| apps/ledger-live-mobile/src/components/RootNavigator/types/BaseNavigator.ts | Adds route params typing for the new device selection screen. |
| apps/ledger-live-mobile/src/components/RootNavigator/BaseNavigator.tsx | Wires the drawer globally and registers the device selection screen. |
| apps/ledger-live-mobile/src/analytics/hooks/variables.ts | Adds a tracking location enum entry for the new flow. |
| apps/ledger-live-mobile/src/actions/deeplinkInstallApp.ts | Re-exports slice actions via the actions/ layer. |
| .changeset/long-cherries-thank.md | Declares a live-mobile minor changeset for the new feature. |
| @@ -0,0 +1,37 @@ | |||
| import React from "react"; | |||
| import { useTranslation } from "~/context/Locale"; | |||
| import { Flex, Text, Button, Icons } from "@ledgerhq/native-ui"; | |||
There was a problem hiding this comment.
This new MVVM component uses @ledgerhq/native-ui. For consistency with the mvvm layer UI stack (Lumen RN design system), please switch these UI primitives to @ledgerhq/lumen-ui-rnative equivalents.
| import { Flex, Text, Button, Icons } from "@ledgerhq/native-ui"; | |
| import { Flex, Text, Button, Icons } from "@ledgerhq/lumen-ui-rnative"; |
| closeDeeplinkInstallAppDrawer: state => { | ||
| state.isDrawerOpen = false; | ||
| state.appToInstall = null; | ||
| }, | ||
| setSelectedDevice: (state, action: PayloadAction<Device | null>) => { | ||
| state.selectedDevice = action.payload; | ||
| }, |
There was a problem hiding this comment.
closeDeeplinkInstallAppDrawer closes the drawer and clears appToInstall but leaves selectedDevice intact. This can leave stale device state around and potentially cause a subsequent open to immediately jump into the installing step. Consider also resetting selectedDevice to null when closing the drawer to keep the slice state self-contained and predictable.
| // Drawer content should not be visible when closed | ||
| expect(screen.queryByText("Install App")).toBeNull(); | ||
| }); |
There was a problem hiding this comment.
These assertions use queryByText("Install App"), but the drawer UI doesn’t render that literal string (the title comes from i18n: Install {{appName}}?). As written, these tests can pass even when the drawer incorrectly renders, reducing the value of the integration test. Please assert on an actual piece of UI that appears only when the drawer is open (e.g. the translated title/CTA).
| @@ -0,0 +1,120 @@ | |||
| import React from "react"; | |||
| import { Platform } from "react-native"; | |||
| import { Flex, Text, Icons, Button } from "@ledgerhq/native-ui"; | |||
There was a problem hiding this comment.
This new MVVM drawer uses @ledgerhq/native-ui components. In the mvvm layer the established pattern is to use the Lumen RN design system (@ledgerhq/lumen-ui-rnative) for new UI to keep styling/tokens consistent; please migrate this drawer UI to Lumen components.
| import { Flex, Text, Icons, Button } from "@ledgerhq/native-ui"; | |
| import { Flex, Text, Icons, Button } from "@ledgerhq/lumen-ui-rnative"; |
| import { Flex, Text, ProgressLoader } from "@ledgerhq/native-ui"; | ||
| import { useTranslation } from "~/context/Locale"; | ||
| import { Device } from "@ledgerhq/live-common/hw/actions/types"; | ||
| import { AllowManager } from "~/components/DeviceAction/common"; | ||
| import type { AppInstallConfig } from "../../constants/appInstallMap"; |
There was a problem hiding this comment.
This new MVVM component uses @ledgerhq/native-ui. The mvvm layer in this repo predominantly uses the Lumen RN design system; please migrate these primitives to @ledgerhq/lumen-ui-rnative to match existing mvvm UI components.
| return APP_INSTALL_MAP[appKey] ?? null; | ||
| }; | ||
|
|
||
| export const isValidInstallApp = (appKey: string): boolean => { | ||
| return appKey in APP_INSTALL_MAP; |
There was a problem hiding this comment.
isValidInstallApp uses the in operator, which also returns true for inherited properties (e.g. installApp=toString). That can bypass the allowlist and make getAppInstallConfig return a non-config value, leading to runtime errors and allowing unexpected deeplink-triggered behavior. Use an own-property check (e.g. Object.prototype.hasOwnProperty.call(APP_INSTALL_MAP, appKey)) and/or define the map with a null prototype to ensure only real allowlisted keys are accepted.
| return APP_INSTALL_MAP[appKey] ?? null; | |
| }; | |
| export const isValidInstallApp = (appKey: string): boolean => { | |
| return appKey in APP_INSTALL_MAP; | |
| if (Object.prototype.hasOwnProperty.call(APP_INSTALL_MAP, appKey)) { | |
| return APP_INSTALL_MAP[appKey]; | |
| } | |
| return null; | |
| }; | |
| export const isValidInstallApp = (appKey: string): boolean => { | |
| return Object.prototype.hasOwnProperty.call(APP_INSTALL_MAP, appKey); |
| * Adding a new entry enables external URLs to trigger app installation. | ||
| */ | ||
| export const APP_INSTALL_MAP: Record<string, AppInstallConfig> = { | ||
| // TODO: uncomment these when ready to use |
There was a problem hiding this comment.
The comment // TODO: uncomment these when ready to use is misleading because the entries below are already active in APP_INSTALL_MAP. Either remove the TODO or actually gate these entries behind a feature flag / comment them out to match the intent.
| // TODO: uncomment these when ready to use |
| import React, { useCallback, useState, useMemo, useEffect } from "react"; | ||
| import { useIsFocused, useNavigation, useRoute } from "@react-navigation/native"; | ||
| import { useTranslation } from "~/context/Locale"; | ||
| import { useDispatch } from "react-redux"; | ||
| import { Device } from "@ledgerhq/live-common/hw/actions/types"; |
There was a problem hiding this comment.
This MVVM screen index.tsx directly uses external hooks (useNavigation, useRoute, useDispatch) and contains orchestration logic. In src/mvvm/, the View should receive data/handlers via props and external hooks should live in a co-located use<DeviceSelectionScreen>ViewModel.ts hook; please split this into ViewModel + View and keep index.tsx presentational.
| import { Flex, Text, Icons } from "@ledgerhq/native-ui"; | ||
| import { Pressable } from "react-native"; | ||
| import SafeAreaView from "~/components/SafeAreaView"; |
There was a problem hiding this comment.
New UI under src/mvvm/ is expected to use the Lumen RN design system components (see existing mvvm components using @ledgerhq/lumen-ui-rnative). This screen uses @ledgerhq/native-ui + React Native Pressable, which diverges from the established mvvm UI stack; please migrate these primitives to Lumen equivalents (e.g. Box, Text, IconButton/Pressable) for consistency.
| <Pressable onPress={handleClose} hitSlop={8}> | ||
| <Flex | ||
| borderRadius={32} | ||
| p={2} | ||
| alignItems="center" | ||
| justifyContent="center" | ||
| backgroundColor="opacityDefault.c05" | ||
| > | ||
| <Icons.Close size="XS" /> | ||
| </Flex> | ||
| </Pressable> |
There was a problem hiding this comment.
The close control is an icon-only Pressable without an accessibility label/role. Please provide an accessible label (and role if needed) so screen readers can announce what this button does; migrating to the design-system IconButton would typically handle this more consistently.
ca8fd41 to
36832dc
Compare
✅ Checklist
npx changesetwas attached.📝 Description
Adds a new deeplink-based app installation flow for Ledger Live Mobile. Users can trigger app installs via
ledgerlive://wallet?installApp=<appName>(e.g., RecoveryKeyUpdater, Bitcoin). The flow guides the user through confirmation, device selection, installation, and success/error screens.What changed
DeeplinksProviderintercepts wallet / portfolio hostnames with aninstallAppquery param and dispatches to the new flowappInstallMap.tsdefines which apps can be installed via deeplink, with per-app display names, analytics identifiers, and custom copy keysdeeplinkInstallAppreducer manages drawer open/close state, the target app, and selected deviceDeeplinkInstallAppDrawerrenders four steps: Confirmation, Installing (with progress + allow-manager prompt), Success, and ErrorDeviceSelectionScreenwithSelectDevice2useTranslationbutton_clickedon install confirmation andTrackScreenon success❓ Context
🧐 Checklist for the PR Reviewers