diff --git a/.changeset/tricky-mugs-battle.md b/.changeset/tricky-mugs-battle.md
new file mode 100644
index 000000000000..d35d2594d5f5
--- /dev/null
+++ b/.changeset/tricky-mugs-battle.md
@@ -0,0 +1,5 @@
+---
+"live-mobile": minor
+---
+
+Add install set of apps step in sync onboarding
diff --git a/apps/ledger-live-mobile/src/components/DeviceAction/InstallSetOfApps/index.tsx b/apps/ledger-live-mobile/src/components/DeviceAction/InstallSetOfApps/index.tsx
index edef522aab06..d96baa72c43a 100644
--- a/apps/ledger-live-mobile/src/components/DeviceAction/InstallSetOfApps/index.tsx
+++ b/apps/ledger-live-mobile/src/components/DeviceAction/InstallSetOfApps/index.tsx
@@ -2,6 +2,7 @@ import React, { useCallback, useState, useMemo } from "react";
import { Trans } from "react-i18next";
import { createAction } from "@ledgerhq/live-common/hw/actions/app";
import type { Device } from "@ledgerhq/live-common/hw/actions/types";
+import withRemountableWrapper from "@ledgerhq/live-common/hoc/withRemountableWrapper";
import connectApp from "@ledgerhq/live-common/hw/connectApp";
import { Flex, Text } from "@ledgerhq/native-ui";
import { getDeviceModel } from "@ledgerhq/devices";
@@ -16,7 +17,7 @@ type Props = {
dependencies?: string[];
device: Device;
onResult: (done: boolean) => void;
- onError: (error: Error) => void;
+ onError?: (error: Error) => void;
};
const action = createAction(connectApp);
@@ -33,7 +34,8 @@ const InstallSetOfApps = ({
device: selectedDevice,
onResult,
onError,
-}: Props) => {
+ remountMe,
+}: Props & { remountMe: () => void }) => {
const [userConfirmed, setUserConfirmed] = useState(false);
const productName = getDeviceModel(selectedDevice.modelId).productName;
@@ -63,10 +65,15 @@ const InstallSetOfApps = ({
} = status;
const onWrappedError = useCallback(() => {
- if (onError && error) {
- onError(error);
+ if (error) {
+ if (onError) {
+ onError(error);
+ }
+ // We force the component to remount for action.useHook to re-run from
+ // scratch and reset the status value
+ remountMe();
}
- }, [error, onError]);
+ }, [remountMe, error, onError]);
if (opened) {
onResult(true);
@@ -134,4 +141,4 @@ const InstallSetOfApps = ({
);
};
-export default InstallSetOfApps;
+export default withRemountableWrapper(InstallSetOfApps);
diff --git a/apps/ledger-live-mobile/src/locales/en/common.json b/apps/ledger-live-mobile/src/locales/en/common.json
index 995866857c6e..e9ec5723cd4f 100644
--- a/apps/ledger-live-mobile/src/locales/en/common.json
+++ b/apps/ledger-live-mobile/src/locales/en/common.json
@@ -1807,6 +1807,9 @@
}
}
},
+ "appsStep": {
+ "title": "{{productName}} applications"
+ },
"readyStep": {
"title": "{{productName}} is ready"
},
diff --git a/apps/ledger-live-mobile/src/screens/SyncOnboarding/index.tsx b/apps/ledger-live-mobile/src/screens/SyncOnboarding/index.tsx
index ff7525642012..02bceefe25cf 100644
--- a/apps/ledger-live-mobile/src/screens/SyncOnboarding/index.tsx
+++ b/apps/ledger-live-mobile/src/screens/SyncOnboarding/index.tsx
@@ -22,6 +22,7 @@ import { useTranslation } from "react-i18next";
import { getDeviceModel } from "@ledgerhq/devices";
import { useDispatch } from "react-redux";
import { CompositeScreenProps } from "@react-navigation/native";
+import useFeature from "@ledgerhq/live-common/featureFlags/useFeature";
import { addKnownDevice } from "../../actions/ble";
import { NavigatorName, ScreenName } from "../../const";
@@ -43,6 +44,7 @@ import {
} from "../../components/RootNavigator/types/BaseNavigator";
import { RootStackParamList } from "../../components/RootNavigator/types/RootNavigator";
import { SyncOnboardingStackParamList } from "../../components/RootNavigator/types/SyncOnboardingNavigator";
+import InstallSetOfApps from "../../components/DeviceAction/InstallSetOfApps";
type StepStatus = "completed" | "active" | "inactive";
@@ -73,12 +75,15 @@ const normalResyncOverlayDisplayDelayMs = 10000;
const longResyncOverlayDisplayDelayMs = 60000;
const readyRedirectDelayMs = 2500;
+const fallbackDefaultAppsToInstall = ["Bitcoin", "Ethereum", "Polygon"];
+
// Because of https://github.com/typescript-eslint/typescript-eslint/issues/1197
enum CompanionStepKey {
Paired = 0,
Pin,
Seed,
SoftwareCheck,
+ Apps,
Ready,
Exit,
}
@@ -96,21 +101,48 @@ export const SyncOnboarding = ({
}: SyncOnboardingCompanionProps) => {
const { t } = useTranslation();
const dispatchRedux = useDispatch();
+ const deviceInitialApps = useFeature("deviceInitialApps");
const { device } = route.params;
const productName =
getDeviceModel(device.modelId).productName || device.modelId;
const deviceName = device.deviceName || productName;
+ const initialAppsToInstall =
+ deviceInitialApps?.params?.apps || fallbackDefaultAppsToInstall;
+
const handleSoftwareCheckComplete = useCallback(() => {
setCompanionStepKey(nextStepKey(CompanionStepKey.SoftwareCheck));
}, []);
+ const handleInstallAppsComplete = useCallback(() => {
+ setCompanionStepKey(nextStepKey(CompanionStepKey.Apps));
+ }, []);
+
const formatEstimatedTime = (estimatedTime: number) =>
t("syncOnboarding.estimatedTimeFormat", {
estimatedTime: estimatedTime / 60,
});
+ const installSetOfAppsSteps: Step[] = useMemo(
+ () => [
+ {
+ key: CompanionStepKey.Apps,
+ title: t("syncOnboarding.appsStep.title", { productName }),
+ status: "inactive",
+ estimatedTime: 60,
+ renderBody: () => (
+
+ ),
+ },
+ ],
+ [productName, t, device, handleInstallAppsComplete, initialAppsToInstall],
+ );
+
const defaultCompanionSteps: Step[] = useMemo(
() => [
{
@@ -162,14 +194,31 @@ export const SyncOnboarding = ({
/>
),
},
+ ],
+ [t, productName, device, handleSoftwareCheckComplete],
+ );
+
+ const getCompanionSteps = useCallback(() => {
+ let steps = defaultCompanionSteps;
+
+ if (deviceInitialApps?.enabled) {
+ steps = steps.concat(installSetOfAppsSteps);
+ }
+
+ return steps.concat([
{
key: CompanionStepKey.Ready,
title: t("syncOnboarding.readyStep.title", { productName }),
status: "inactive",
},
- ],
- [t, productName, device, handleSoftwareCheckComplete],
- );
+ ]);
+ }, [
+ t,
+ productName,
+ defaultCompanionSteps,
+ installSetOfAppsSteps,
+ deviceInitialApps?.enabled,
+ ]);
const [stopPolling, setStopPolling] = useState(false);
const [pollingPeriodMs, setPollingPeriodMs] = useState(
@@ -193,7 +242,7 @@ export const SyncOnboarding = ({
const [isHelpDrawerOpen, setHelpDrawerOpen] = useState(false);
const [companionSteps, setCompanionSteps] = useState(
- defaultCompanionSteps,
+ getCompanionSteps(),
);
const [companionStepKey, setCompanionStepKey] = useState(
CompanionStepKey.Paired,
@@ -398,7 +447,7 @@ export const SyncOnboarding = ({
}
setCompanionSteps(
- defaultCompanionSteps.map(step => {
+ getCompanionSteps().map(step => {
const stepStatus =
step.key > companionStepKey
? "inactive"
@@ -419,7 +468,7 @@ export const SyncOnboarding = ({
readyRedirectTimerRef.current = null;
}
};
- }, [companionStepKey, defaultCompanionSteps, handleDeviceReady]);
+ }, [companionStepKey, getCompanionSteps, handleDeviceReady]);
return (