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 (