Skip to content

Feat/deeplink app install#15048

Open
fAnselmi-Ledger wants to merge 2 commits intodevelopfrom
feat/deeplink-app-install
Open

Feat/deeplink app install#15048
fAnselmi-Ledger wants to merge 2 commits intodevelopfrom
feat/deeplink-app-install

Conversation

@fAnselmi-Ledger
Copy link
Contributor

@fAnselmi-Ledger fAnselmi-Ledger commented Mar 4, 2026

✅ Checklist

  • npx changeset was attached.
  • Covered by automatic tests.
  • Impact of the changes:
    • ...

📝 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

  • New feature: DeeplinkInstallApp (src/mvvm/features/DeeplinkInstallApp/)
  • Deeplink handling: DeeplinksProvider intercepts wallet / portfolio hostnames with an installApp query param and dispatches to the new flow
  • Allowlisted app map: appInstallMap.ts defines which apps can be installed via deeplink, with per-app display names, analytics identifiers, and custom copy keys
  • Redux slice: deeplinkInstallApp reducer manages drawer open/close state, the target app, and selected device
  • Multi-step drawer: DeeplinkInstallAppDrawer renders four steps: Confirmation, Installing (with progress + allow-manager prompt), Success, and Error
  • Device selection screen: Full-screen DeviceSelectionScreen with SelectDevice2
  • Error handling: Uses the standard TranslatedError component so all known errors
  • i18n: All strings use useTranslation
  • Analytics: Tracks button_clicked on install confirmation and TrackScreen on success

❓ Context


🧐 Checklist for the PR Reviewers

  • The code aligns with the requirements described in the linked JIRA or GitHub issue.
  • The PR description clearly documents the changes made and explains any technical trade-offs or design decisions.
  • There are no undocumented trade-offs, technical debt, or maintainability issues.
  • The PR has been tested thoroughly, and any potential edge cases have been considered and handled.
  • Any new dependencies have been justified and documented.
  • Performance considerations have been taken into account. (changes have been profiled or benchmarked if necessary)

@fAnselmi-Ledger fAnselmi-Ledger requested review from a team as code owners March 4, 2026 16:17
Copilot AI review requested due to automatic review settings March 4, 2026 16:17
@live-github-bot live-github-bot bot added mobile Has changes in LLM translations Translation files have been touched labels Mar 4, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 4, 2026

⚠️ E2E tests are required

Changes detected require e2e testing before merge (even before asking for any review).

📱 Mobile

-> Run Mobile E2E

  • Select "Run workflow"
  • Branch: feat/deeplink-app-install
  • Device: nanoX

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DeeplinkInstallApp MVVM feature (drawer UI, device selection screen, install ViewModels, allowlist map).
  • Hooks deeplink parsing in DeeplinksProvider to dispatch the install flow when installApp is 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";
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
import { Flex, Text, Button, Icons } from "@ledgerhq/native-ui";
import { Flex, Text, Button, Icons } from "@ledgerhq/lumen-ui-rnative";

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +25
closeDeeplinkInstallAppDrawer: state => {
state.isDrawerOpen = false;
state.appToInstall = null;
},
setSelectedDevice: (state, action: PayloadAction<Device | null>) => {
state.selectedDevice = action.payload;
},
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +27
// Drawer content should not be visible when closed
expect(screen.queryByText("Install App")).toBeNull();
});
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,120 @@
import React from "react";
import { Platform } from "react-native";
import { Flex, Text, Icons, Button } from "@ledgerhq/native-ui";
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
import { Flex, Text, Icons, Button } from "@ledgerhq/native-ui";
import { Flex, Text, Icons, Button } from "@ledgerhq/lumen-ui-rnative";

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +6
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";
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +34
return APP_INSTALL_MAP[appKey] ?? null;
};

export const isValidInstallApp = (appKey: string): boolean => {
return appKey in APP_INSTALL_MAP;
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
* 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
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// TODO: uncomment these when ready to use

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
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";
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +8
import { Flex, Text, Icons } from "@ledgerhq/native-ui";
import { Pressable } from "react-native";
import SafeAreaView from "~/components/SafeAreaView";
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +90
<Pressable onPress={handleClose} hitSlop={8}>
<Flex
borderRadius={32}
p={2}
alignItems="center"
justifyContent="center"
backgroundColor="opacityDefault.c05"
>
<Icons.Close size="XS" />
</Flex>
</Pressable>
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@fAnselmi-Ledger fAnselmi-Ledger force-pushed the feat/deeplink-app-install branch from ca8fd41 to 36832dc Compare March 4, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mobile Has changes in LLM translations Translation files have been touched

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants