From e422f97c4c49d473b9371a3dca7ea46e6b42e876 Mon Sep 17 00:00:00 2001 From: Olivier Freyssinet Date: Mon, 28 Oct 2024 19:29:43 +0100 Subject: [PATCH] wip --- .../src/actions/settings.ts | 6 + apps/ledger-live-mobile/src/actions/types.ts | 3 + .../useAutoRedirectToPostOnboarding/index.ts | 39 ++++ .../useOpenPostOnboardingCallback.ts | 20 ++ .../useOpenProtectUpsellCallback.ts | 73 ++++++ .../useShouldRedirect.test.ts | 215 ++++++++++++++++++ .../useShouldRedirect.ts | 40 ++++ .../src/navigation/DeeplinksProvider.tsx | 1 + .../src/reducers/settings.ts | 9 + apps/ledger-live-mobile/src/reducers/types.ts | 1 + .../screens/MyLedgerChooseDevice/index.tsx | 3 + .../src/screens/Portfolio/index.tsx | 109 +-------- .../PostOnboarding/PostOnboardingHub.tsx | 13 +- .../Settings/Debug/Configuration/index.tsx | 11 + .../SyncOnboarding/CompletionScreen.tsx | 34 +-- .../SyncOnboardingCompanion.tsx | 2 +- .../src/hooks/recoverFeatureFlag.ts | 15 ++ 17 files changed, 456 insertions(+), 138 deletions(-) create mode 100644 apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/index.ts create mode 100644 apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenPostOnboardingCallback.ts create mode 100644 apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenProtectUpsellCallback.ts create mode 100644 apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.test.ts create mode 100644 apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.ts diff --git a/apps/ledger-live-mobile/src/actions/settings.ts b/apps/ledger-live-mobile/src/actions/settings.ts index 3e3065bc28ed..2e01412b6e31 100755 --- a/apps/ledger-live-mobile/src/actions/settings.ts +++ b/apps/ledger-live-mobile/src/actions/settings.ts @@ -70,6 +70,7 @@ import { SettingsAddStarredMarketcoinsPayload, SettingsRemoveStarredMarketcoinsPayload, SettingsSetFromLedgerSyncOnboardingPayload, + SettingsSetHasBeenRedirectedToPostOnboardingPayload, } from "./types"; import { ImageType } from "~/components/CustomImage/types"; @@ -264,6 +265,11 @@ export const setHasBeenUpsoldProtect = createAction( + SettingsActionTypes.SET_HAS_BEEN_REDIRECTED_TO_POST_ONBOARDING, + ); + export const setGeneralTermsVersionAccepted = createAction( SettingsActionTypes.SET_GENERAL_TERMS_VERSION_ACCEPTED, ); diff --git a/apps/ledger-live-mobile/src/actions/types.ts b/apps/ledger-live-mobile/src/actions/types.ts index a4e20fdd3dd9..7493911df020 100644 --- a/apps/ledger-live-mobile/src/actions/types.ts +++ b/apps/ledger-live-mobile/src/actions/types.ts @@ -273,6 +273,7 @@ export enum SettingsActionTypes { SET_FEATURE_FLAGS_BANNER_VISIBLE = "SET_FEATURE_FLAGS_BANNER_VISIBLE", SET_DEBUG_APP_LEVEL_DRAWER_OPENED = "SET_DEBUG_APP_LEVEL_DRAWER_OPENED", SET_HAS_BEEN_UPSOLD_PROTECT = "SET_HAS_BEEN_UPSOLD_PROTECT", + SET_HAS_BEEN_REDIRECTED_TO_POST_ONBOARDING = "SET_HAS_BEEN_REDIRECTED_TO_POST_ONBOARDING", SET_GENERAL_TERMS_VERSION_ACCEPTED = "SET_GENERAL_TERMS_VERSION_ACCEPTED", SET_ONBOARDING_TYPE = "SET_ONBOARDING_TYPE", SET_CLOSED_NETWORK_BANNER = "SET_CLOSED_NETWORK_BANNER", @@ -379,6 +380,8 @@ export type SettingsSetDebugAppLevelDrawerOpenedPayload = SettingsState["debugAppLevelDrawerOpened"]; export type SettingsSetHasBeenUpsoldProtectPayload = SettingsState["hasBeenUpsoldProtect"]; +export type SettingsSetHasBeenRedirectedToPostOnboardingPayload = + SettingsState["hasBeenRedirectedToPostOnboarding"]; export type SettingsCompleteOnboardingPayload = void | SettingsState["hasCompletedOnboarding"]; export type SettingsSetGeneralTermsVersionAccepted = SettingsState["generalTermsVersionAccepted"]; diff --git a/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/index.ts b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/index.ts new file mode 100644 index 000000000000..a906689d6986 --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/index.ts @@ -0,0 +1,39 @@ +import { useEffect, useRef } from "react"; +import { useSelector } from "react-redux"; +import { lastConnectedDeviceSelector } from "~/reducers/settings"; +import { useOpenPostOnboardingCallback } from "./useOpenPostOnboardingCallback"; +import { useShouldRedirect } from "./useShouldRedirect"; +import { useOpenProtectUpsellCallback } from "./useOpenProtectUpsellCallback"; +import { useIsFocused } from "@react-navigation/core"; + +/** + * Redirects the user to the post onboarding or the protect (Ledger Recover) upsell if needed + * */ +export function useAutoRedirectToPostOnboarding() { + const focused = useIsFocused(); + const lastConnectedDevice = useSelector(lastConnectedDeviceSelector); + + const { shouldRedirectToProtectUpsell, shouldRedirectToPostOnboarding } = useShouldRedirect(); + + const openProtectUpsell = useOpenProtectUpsellCallback(); + const openPostOnboarding = useOpenPostOnboardingCallback(); + + const isFocused = useIsFocused(); + + useEffect(() => { + if (!isFocused) return; + if (shouldRedirectToProtectUpsell) { + openProtectUpsell(); + } else if (shouldRedirectToPostOnboarding && lastConnectedDevice) { + openPostOnboarding(lastConnectedDevice.modelId); + } + }, [ + lastConnectedDevice, + openPostOnboarding, + openProtectUpsell, + shouldRedirectToPostOnboarding, + shouldRedirectToProtectUpsell, + focused, + isFocused, + ]); +} diff --git a/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenPostOnboardingCallback.ts b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenPostOnboardingCallback.ts new file mode 100644 index 000000000000..38bcb51ebc04 --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenPostOnboardingCallback.ts @@ -0,0 +1,20 @@ +import { useStartPostOnboardingCallback } from "@ledgerhq/live-common/postOnboarding/hooks/useStartPostOnboardingCallback"; +import { DeviceModelId } from "@ledgerhq/types-devices"; +import { useCallback } from "react"; + +/** + * Returns a callback to open the post onboarding screen + * */ +export function useOpenPostOnboardingCallback() { + const startPostOnboarding = useStartPostOnboardingCallback(); + return useCallback( + (deviceModelId: DeviceModelId) => { + startPostOnboarding({ + deviceModelId: deviceModelId, + resetNavigationStack: false, + fallbackIfNoAction: () => {}, + }); + }, + [startPostOnboarding], + ); +} diff --git a/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenProtectUpsellCallback.ts b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenProtectUpsellCallback.ts new file mode 100644 index 000000000000..5e40f0c89f2f --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenProtectUpsellCallback.ts @@ -0,0 +1,73 @@ +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { + Source, + useAlreadyOnboardedURI, + useHomeURI, + usePostOnboardingURI, + useTouchScreenOnboardingUpsellURI, +} from "@ledgerhq/live-common/hooks/recoverFeatureFlag"; +import { DeviceModelId } from "@ledgerhq/types-devices"; +import { useIsFocused } from "@react-navigation/core"; +import { useCallback, useEffect, useState } from "react"; +import { Linking } from "react-native"; +import { useDispatch, useSelector } from "react-redux"; +import { setHasBeenUpsoldProtect } from "~/actions/settings"; +import { internetReachable } from "~/logic/internetReachable"; +import { lastConnectedDeviceSelector, onboardingTypeSelector } from "~/reducers/settings"; +import { OnboardingType } from "~/reducers/types"; + +/** + * Returns a callback to open the Protect (Ledger Recover) upsell + * */ +export function useOpenProtectUpsellCallback() { + const lastConnectedDevice = useSelector(lastConnectedDeviceSelector); + const onboardingType = useSelector(onboardingTypeSelector); + const protectFeature = useFeature("protectServicesMobile"); + const recoverAlreadyOnboardedURI = useAlreadyOnboardedURI(protectFeature); + const recoverPostOnboardingURI = usePostOnboardingURI(protectFeature); + const touchScreenURI = useTouchScreenOnboardingUpsellURI( + protectFeature, + Source.LLM_ONBOARDING_24, + ); + const recoverHomeURI = useHomeURI(protectFeature); + const dispatch = useDispatch(); + const [redirectionStarted, setRedirectionStarted] = useState(false); + const isFocused = useIsFocused(); + + useEffect(() => { + if (redirectionStarted && !isFocused) { + dispatch(setHasBeenUpsoldProtect(true)); + } + }, [redirectionStarted, isFocused, dispatch]); + + return useCallback(async () => { + const internetConnected = await internetReachable(); + if (internetConnected && protectFeature?.enabled) { + if ( + lastConnectedDevice && + touchScreenURI && + [DeviceModelId.stax, DeviceModelId.europa].includes(lastConnectedDevice.modelId) + ) { + Linking.openURL(touchScreenURI); + setRedirectionStarted(true); + } else if (recoverPostOnboardingURI && onboardingType === OnboardingType.restore) { + Linking.openURL(recoverPostOnboardingURI); + setRedirectionStarted(true); + } else if (recoverHomeURI && onboardingType === OnboardingType.setupNew) { + Linking.openURL(recoverHomeURI); + setRedirectionStarted(true); + } else if (recoverAlreadyOnboardedURI) { + Linking.openURL(recoverAlreadyOnboardedURI); + setRedirectionStarted(true); + } + } + }, [ + lastConnectedDevice, + onboardingType, + protectFeature?.enabled, + recoverAlreadyOnboardedURI, + recoverHomeURI, + recoverPostOnboardingURI, + touchScreenURI, + ]); +} diff --git a/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.test.ts b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.test.ts new file mode 100644 index 000000000000..c81e2a0cf7d0 --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.test.ts @@ -0,0 +1,215 @@ +import { Device } from "@ledgerhq/live-common/hw/actions/types"; +import { useShouldRedirect } from "./useShouldRedirect"; +import { DeviceModelId } from "@ledgerhq/types-devices"; + +jest.mock("react-redux", () => ({ + useSelector: (fn: () => void) => fn(), +})); + +jest.mock("@ledgerhq/live-common/featureFlags/index", () => ({ + useFeature: jest.fn(), +})); + +jest.mock("~/reducers/settings", () => ({ + hasBeenUpsoldProtectSelector: jest.fn(), + hasBeenRedirectedToPostOnboardingSelector: jest.fn(), + lastConnectedDeviceSelector: jest.fn(), +})); + +const { useFeature } = jest.requireMock("@ledgerhq/live-common/featureFlags/index"); +const { + hasBeenUpsoldProtectSelector, + hasBeenRedirectedToPostOnboardingSelector, + lastConnectedDeviceSelector, +} = jest.requireMock("~/reducers/settings"); + +function mockUseFeature(value: { enabled: boolean }) { + useFeature.mockReturnValue(value); +} +function mockHasBeenUpsoldProtect(value: boolean) { + hasBeenUpsoldProtectSelector.mockReturnValue(value); +} + +function mockHasRedirectedToPostOnboarding(value: boolean) { + hasBeenRedirectedToPostOnboardingSelector.mockReturnValue(value); +} + +function mockLastConnectedDevice(value: Device) { + lastConnectedDeviceSelector.mockReturnValue(value); +} + +type Scenario = { + device: { modelId: DeviceModelId }; + featureFlagEnabled: boolean; + expected: { shouldRedirectToProtectUpsell: boolean; shouldRedirectToPostOnboarding: boolean }; +}; + +function testScenarios(scenarios: Scenario[]) { + scenarios.forEach(scenario => { + it(`should return ${JSON.stringify(scenario.expected)} for ${JSON.stringify(scenario.device)} and feature flag enabled: ${scenario.featureFlagEnabled}`, () => { + mockLastConnectedDevice(scenario.device as Device); + mockUseFeature({ enabled: scenario.featureFlagEnabled }); + + const result = useShouldRedirect(); + + expect( + [result.shouldRedirectToPostOnboarding, result.shouldRedirectToProtectUpsell].filter( + Boolean, + ).length, + ).toBeLessThanOrEqual(1); + + expect(result).toEqual(scenario.expected); + }); + }); +} + +describe("useShouldRedirect", () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("user HAS NOT BEEN UPSOLD protect & HAS NOT BEEN REDIRECTED to post onboarding", () => { + beforeEach(() => { + mockHasBeenUpsoldProtect(false); + mockHasRedirectedToPostOnboarding(false); + }); + + testScenarios([ + { + device: { modelId: DeviceModelId.nanoSP }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: true }, + }, + { + device: { modelId: DeviceModelId.nanoSP }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: true }, + }, + { + device: { modelId: DeviceModelId.nanoX }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.stax }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: true }, + }, + { + device: { modelId: DeviceModelId.stax }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.europa }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: true }, + }, + { + device: { modelId: DeviceModelId.europa }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + ]); + }); + + describe("user HAS BEEN UPSOLD protect & HAS NOT BEEN REDIRECTED to post onboarding", () => { + beforeEach(() => { + mockHasBeenUpsoldProtect(true); + mockHasRedirectedToPostOnboarding(false); + }); + + [ + DeviceModelId.nanoS, + DeviceModelId.nanoSP, + DeviceModelId.nanoX, + DeviceModelId.stax, + DeviceModelId.europa, + ].forEach(modelId => { + [true, false].forEach(featureFlagEnabled => + testScenarios([ + { + device: { modelId }, + featureFlagEnabled, + expected: { + shouldRedirectToProtectUpsell: false, + shouldRedirectToPostOnboarding: true, + }, + }, + ]), + ); + }); + }); + + describe("user HAS BEEN UPSOLD PROTECT & HAS BEEN REDIRECTED to post onboarding", () => { + beforeEach(() => { + mockHasBeenUpsoldProtect(true); + mockHasRedirectedToPostOnboarding(true); + }); + [ + DeviceModelId.nanoS, + DeviceModelId.nanoSP, + DeviceModelId.nanoX, + DeviceModelId.stax, + DeviceModelId.europa, + ].forEach(modelId => { + [true, false].forEach(featureFlagEnabled => + testScenarios([ + { + device: { modelId }, + featureFlagEnabled, + expected: { + shouldRedirectToProtectUpsell: false, + shouldRedirectToPostOnboarding: false, + }, + }, + ]), + ); + }); + }); + + describe("user HAS NOT BEEN UPSOLD protect & HAS BEEN REDIRECTED to post onboarding", () => { + beforeEach(() => { + mockHasBeenUpsoldProtect(false); + mockHasRedirectedToPostOnboarding(true); + }); + + testScenarios([ + { + device: { modelId: DeviceModelId.nanoSP }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.nanoSP }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.nanoX }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.stax }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.stax }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.europa }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.europa }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + ]); + }); +}); diff --git a/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.ts b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.ts new file mode 100644 index 000000000000..9132cff6323b --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.ts @@ -0,0 +1,40 @@ +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { DeviceModelId } from "@ledgerhq/types-devices"; +import { useSelector } from "react-redux"; +import { + hasBeenRedirectedToPostOnboardingSelector, + hasBeenUpsoldProtectSelector, + lastConnectedDeviceSelector, +} from "~/reducers/settings"; + +/** + * Returns whether the user should be redirected to the Protect upsell or the post onboarding + * */ +export function useShouldRedirect(): { + shouldRedirectToProtectUpsell: boolean; + shouldRedirectToPostOnboarding: boolean; +} { + const hasBeenUpsoldProtect = useSelector(hasBeenUpsoldProtectSelector); + const hasRedirectedToPostOnboarding = useSelector(hasBeenRedirectedToPostOnboardingSelector); + const recoverUpsellRedirection = useFeature("recoverUpsellRedirection"); + const lastConnectedDevice = useSelector(lastConnectedDeviceSelector); + const eligibleDevicesForUpsell = recoverUpsellRedirection?.enabled + ? [DeviceModelId.stax, DeviceModelId.europa] + : [DeviceModelId.nanoX]; + + const eligibleForUpsell = lastConnectedDevice?.modelId + ? eligibleDevicesForUpsell.includes(lastConnectedDevice.modelId) + : false; + + const shouldRedirectToProtectUpsell = !hasBeenUpsoldProtect && eligibleForUpsell; + + const shouldRedirectToPostOnboarding = + Boolean(lastConnectedDevice) && + !hasRedirectedToPostOnboarding && + (eligibleForUpsell ? hasBeenUpsoldProtect : true); + + return { + shouldRedirectToProtectUpsell, + shouldRedirectToPostOnboarding, + }; +} diff --git a/apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx b/apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx index 177faea6b79c..98224bca10c2 100644 --- a/apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx +++ b/apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx @@ -450,6 +450,7 @@ export const DeeplinksProvider = ({ : getOnboardingLinkingOptions(!!userAcceptedTerms)), subscribe(listener) { const sub = Linking.addEventListener("url", ({ url }) => { + console.log("Deeplink URL", url); // Prevent default deep link if we're already in a wallet connect route. const navigationState = navigationRef.current?.getState(); if ( diff --git a/apps/ledger-live-mobile/src/reducers/settings.ts b/apps/ledger-live-mobile/src/reducers/settings.ts index b48a119e8a55..8dae55d36e0b 100644 --- a/apps/ledger-live-mobile/src/reducers/settings.ts +++ b/apps/ledger-live-mobile/src/reducers/settings.ts @@ -165,6 +165,7 @@ export const INITIAL_STATE: SettingsState = { debugAppLevelDrawerOpened: false, dateFormat: "default", hasBeenUpsoldProtect: false, + hasBeenRedirectedToPostOnboarding: false, onboardingType: null, depositFlow: { hasClosedNetworkBanner: false, @@ -601,6 +602,12 @@ const handlers: ReducerMap = { ...state, hasBeenUpsoldProtect: (action as Action).payload, }), + [SettingsActionTypes.SET_HAS_BEEN_REDIRECTED_TO_POST_ONBOARDING]: (state, action) => ({ + ...state, + hasBeenRedirectedToPostOnboarding: ( + action as Action + ).payload, + }), [SettingsActionTypes.SET_GENERAL_TERMS_VERSION_ACCEPTED]: (state, action) => ({ ...state, generalTermsVersionAccepted: (action as Action).payload, @@ -874,6 +881,8 @@ export const featureFlagsBannerVisibleSelector = (state: State) => export const debugAppLevelDrawerOpenedSelector = (state: State) => state.settings.debugAppLevelDrawerOpened; export const hasBeenUpsoldProtectSelector = (state: State) => state.settings.hasBeenUpsoldProtect; +export const hasBeenRedirectedToPostOnboardingSelector = (state: State) => + state.settings.hasBeenRedirectedToPostOnboarding; export const generalTermsVersionAcceptedSelector = (state: State) => state.settings.generalTermsVersionAccepted; export const userNpsSelector = (state: State) => state.settings.userNps; diff --git a/apps/ledger-live-mobile/src/reducers/types.ts b/apps/ledger-live-mobile/src/reducers/types.ts index 7dc9c9aae716..24f3584fb53b 100644 --- a/apps/ledger-live-mobile/src/reducers/types.ts +++ b/apps/ledger-live-mobile/src/reducers/types.ts @@ -255,6 +255,7 @@ export type SettingsState = { debugAppLevelDrawerOpened: boolean; dateFormat: string; hasBeenUpsoldProtect: boolean; + hasBeenRedirectedToPostOnboarding: boolean; generalTermsVersionAccepted?: string; depositFlow: { hasClosedNetworkBanner: boolean; diff --git a/apps/ledger-live-mobile/src/screens/MyLedgerChooseDevice/index.tsx b/apps/ledger-live-mobile/src/screens/MyLedgerChooseDevice/index.tsx index bdb76854353b..56c50b732943 100644 --- a/apps/ledger-live-mobile/src/screens/MyLedgerChooseDevice/index.tsx +++ b/apps/ledger-live-mobile/src/screens/MyLedgerChooseDevice/index.tsx @@ -22,6 +22,7 @@ import { MyLedgerNavigatorStackParamList } from "~/components/RootNavigator/type import { useManagerDeviceAction } from "~/hooks/deviceActions"; import ContentCardsLocation from "~/dynamicContent/ContentCardsLocation"; import { ContentCardLocation } from "~/dynamicContent/types"; +import { useAutoRedirectToPostOnboarding } from "~/hooks/useAutoRedirectToPostOnboarding"; type NavigationProps = BaseComposite< StackNavigatorProps @@ -46,6 +47,8 @@ const ChooseDevice: React.FC = ({ isFocused }) => { const navigation = useNavigation(); const { params } = useRoute(); + useAutoRedirectToPostOnboarding(); + const onSelectDevice = (device?: Device) => { if (device) track("ManagerDeviceEntered", { diff --git a/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx b/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx index ca97ae41f79d..ffdacc790d7d 100644 --- a/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx @@ -1,26 +1,14 @@ -import React, { useCallback, useMemo, useState, useEffect } from "react"; -import { useSelector, useDispatch } from "react-redux"; -import { ListRenderItemInfo, Linking, Platform } from "react-native"; +import React, { useCallback, useMemo, useState } from "react"; +import { useSelector } from "react-redux"; +import { ListRenderItemInfo, Platform } from "react-native"; import { useTranslation } from "react-i18next"; import { useFocusEffect } from "@react-navigation/native"; import { Box, Flex } from "@ledgerhq/native-ui"; import { useTheme } from "styled-components/native"; import useEnv from "@ledgerhq/live-common/hooks/useEnv"; import { ReactNavigationPerformanceView } from "@shopify/react-native-performance-navigation"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; import WalletTabSafeAreaView from "~/components/WalletTab/WalletTabSafeAreaView"; -import { - useAlreadyOnboardedURI, - usePostOnboardingURI, - useHomeURI, -} from "@ledgerhq/live-common/hooks/recoverFeatureFlag"; import { useRefreshAccountsOrdering } from "~/actions/general"; -import { - hasBeenUpsoldProtectSelector, - lastConnectedDeviceSelector, - onboardingTypeSelector, -} from "~/reducers/settings"; -import { setHasBeenUpsoldProtect } from "~/actions/settings"; import Carousel from "~/components/Carousel"; import { ScreenName } from "~/const"; import FirmwareUpdateBanner from "LLM/features/FirmwareUpdate/components/UpdateBanner"; @@ -45,15 +33,12 @@ import { hasTokenAccountsNotBlackListedWithPositiveBalanceSelector, } from "~/reducers/accounts"; import PortfolioAssets from "./PortfolioAssets"; -import { internetReachable } from "~/logic/internetReachable"; import { UpdateStep } from "../FirmwareUpdate"; -import { OnboardingType } from "~/reducers/types"; import ContentCardsLocation from "~/dynamicContent/ContentCardsLocation"; import { ContentCardLocation } from "~/dynamicContent/types"; import usePortfolioAnalyticsOptInPrompt from "~/hooks/analyticsOptInPrompt/usePorfolioAnalyticsOptInPrompt"; import AddAccountDrawer from "LLM/features/Accounts/screens/AddAccount"; -import { DeviceModelId } from "@ledgerhq/types-devices"; -import { useStartPostOnboardingCallback } from "@ledgerhq/live-common/postOnboarding/hooks"; +import { useAutoRedirectToPostOnboarding } from "~/hooks/useAutoRedirectToPostOnboarding"; export { default as PortfolioTabIcon } from "./TabIcon"; @@ -65,92 +50,6 @@ const RefreshableCollapsibleHeaderFlatList = globalSyncRefreshControl(Collapsibl progressViewOffset: Platform.OS === "android" ? 64 : 0, }); -const hasRedirectedToPostOnboardingSelector = () => true; - -const setHasRedirectedToPostOnboarding = () => {}; - -function useOpenProtectUpsellCallback() { - const onboardingType = useSelector(onboardingTypeSelector); - const protectFeature = useFeature("protectServicesMobile"); - const recoverAlreadyOnboardedURI = useAlreadyOnboardedURI(protectFeature); - const recoverPostOnboardingURI = usePostOnboardingURI(protectFeature); - const recoverHomeURI = useHomeURI(protectFeature); - const dispatch = useDispatch(); - return useCallback(async () => { - const internetConnected = await internetReachable(); - if (internetConnected && protectFeature?.enabled) { - if (recoverPostOnboardingURI && onboardingType === OnboardingType.restore) { - Linking.openURL(recoverPostOnboardingURI); - dispatch(setHasBeenUpsoldProtect(true)); - } else if (recoverHomeURI && onboardingType === OnboardingType.setupNew) { - Linking.openURL(recoverHomeURI); - dispatch(setHasBeenUpsoldProtect(true)); - } else if (recoverAlreadyOnboardedURI) { - Linking.openURL(recoverAlreadyOnboardedURI); - dispatch(setHasBeenUpsoldProtect(true)); - } - } - }, [ - dispatch, - onboardingType, - protectFeature?.enabled, - recoverAlreadyOnboardedURI, - recoverHomeURI, - recoverPostOnboardingURI, - ]); -} - -function useOpenPostOnboardingCallback(deviceModelId?: DeviceModelId) { - const dispatch = useDispatch(); - const startPostOnboarding = useStartPostOnboardingCallback(); - return useCallback(() => { - startPostOnboarding({ - deviceModelId: deviceModelId, - resetNavigationStack: false, - fallbackIfNoAction: () => {}, - }); - dispatch(setHasRedirectedToPostOnboarding(true)); - }, [deviceModelId, dispatch, startPostOnboarding]); -} - -/** - * Redirects the user to the post onboarding or the protect upsell if needed - * */ -function useAutoRedirectToPostOnboarding() { - const hasBeenUpsoldProtect = useSelector(hasBeenUpsoldProtectSelector); - const hasRedirectedToPostOnboarding = useSelector(hasRedirectedToPostOnboardingSelector); - const recoverUpsellRedirection = useFeature("recoverUpsellRedirection"); - const lastConnectedDevice = useSelector(lastConnectedDeviceSelector); - - const eligibleDevicesForUpsell = recoverUpsellRedirection?.enabled - ? [DeviceModelId.stax, DeviceModelId.europa] - : [DeviceModelId.nanoX]; - - const eligibleForUpsell = - lastConnectedDevice?.modelId && eligibleDevicesForUpsell.includes(lastConnectedDevice.modelId); - - const shouldRedirectToProtectUpsell = !hasBeenUpsoldProtect && eligibleForUpsell; - - const shouldRedirectToPostOnboarding = - !hasRedirectedToPostOnboarding && (eligibleForUpsell ? hasBeenUpsoldProtect : true); - - const openProtectUpsell = useOpenProtectUpsellCallback(); - const openPostOnboarding = useOpenPostOnboardingCallback(lastConnectedDevice?.modelId); - - useEffect(() => { - if (shouldRedirectToProtectUpsell) { - openProtectUpsell(); - } else if (shouldRedirectToPostOnboarding) { - openPostOnboarding(); - } - }, [ - openPostOnboarding, - openProtectUpsell, - shouldRedirectToPostOnboarding, - shouldRedirectToProtectUpsell, - ]); -} - function PortfolioScreen({ navigation }: NavigationProps) { const hideEmptyTokenAccount = useEnv("HIDE_EMPTY_TOKEN_ACCOUNTS"); const { t } = useTranslation(); diff --git a/apps/ledger-live-mobile/src/screens/PostOnboarding/PostOnboardingHub.tsx b/apps/ledger-live-mobile/src/screens/PostOnboarding/PostOnboardingHub.tsx index 557a6a607b03..a8353d65306f 100644 --- a/apps/ledger-live-mobile/src/screens/PostOnboarding/PostOnboardingHub.tsx +++ b/apps/ledger-live-mobile/src/screens/PostOnboarding/PostOnboardingHub.tsx @@ -14,25 +14,22 @@ import { TrackScreen } from "~/analytics"; import Link from "~/components/wrappedUi/Link"; import { useCompletePostOnboarding } from "~/logic/postOnboarding/useCompletePostOnboarding"; import { ScrollContainer } from "@ledgerhq/native-ui"; +import { setHasBeenRedirectedToPostOnboarding } from "~/actions/settings"; const PostOnboardingHub = () => { const dispatch = useDispatch(); const { t } = useTranslation(); const { actionsState, deviceModelId } = usePostOnboardingHubState(); const closePostOnboarding = useCompletePostOnboarding(); - const clearLastActionCompleted = useCallback(() => { - dispatch(clearPostOnboardingLastActionCompleted()); - }, [dispatch]); - - useEffect( + useEffect(() => { /** * The last action context (specific title & popup) should only be visible * the 1st time the hub is navigated to after that action was completed. * So here we clear the last action completed. * */ - () => clearLastActionCompleted, - [clearLastActionCompleted], - ); + dispatch(clearPostOnboardingLastActionCompleted()); + dispatch(setHasBeenRedirectedToPostOnboarding(true)); + }, [dispatch]); const navigateToMainScreen = useCallback(() => { closePostOnboarding(); diff --git a/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/index.tsx b/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/index.tsx index 369b7308e0c0..76cd3a02cf24 100644 --- a/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/index.tsx @@ -17,9 +17,12 @@ import ResetOnboardingStateRow from "./ResetOnboardingStateRow"; import NftMetadataServiceRow from "./NftMetadataServiceRow"; import HasStaxEuropaRows from "./HasStaxEuropaRows"; import SkipOnboardingRow from "./SkipOnboardingRow"; +import { useDispatch } from "react-redux"; +import { setHasBeenRedirectedToPostOnboarding, setHasBeenUpsoldProtect } from "~/actions/settings"; export default function Configuration() { const navigation = useNavigation>(); + const dispatch = useDispatch(); return ( @@ -40,6 +43,14 @@ export default function Configuration() { + { + dispatch(setHasBeenUpsoldProtect(false)); + dispatch(setHasBeenRedirectedToPostOnboarding(false)); + }} + /> diff --git a/apps/ledger-live-mobile/src/screens/SyncOnboarding/CompletionScreen.tsx b/apps/ledger-live-mobile/src/screens/SyncOnboarding/CompletionScreen.tsx index a13e39facb50..efbbebfc2420 100644 --- a/apps/ledger-live-mobile/src/screens/SyncOnboarding/CompletionScreen.tsx +++ b/apps/ledger-live-mobile/src/screens/SyncOnboarding/CompletionScreen.tsx @@ -1,8 +1,7 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useEffect } from "react"; import { Flex } from "@ledgerhq/native-ui"; import { StackScreenProps } from "@react-navigation/stack"; import { TouchableWithoutFeedback } from "react-native-gesture-handler"; -import { useStartPostOnboardingCallback } from "@ledgerhq/live-common/postOnboarding/hooks/useStartPostOnboardingCallback"; import { NavigatorName, ScreenName } from "~/const"; import { SyncOnboardingStackParamList } from "~/components/RootNavigator/types/SyncOnboardingNavigator"; @@ -10,7 +9,8 @@ import { BaseComposite, RootNavigation } from "~/components/RootNavigator/types/ import { DeviceModelId } from "@ledgerhq/devices"; import EuropaCompletionView from "./EuropaCompletionView"; import StaxCompletionView from "./StaxCompletionView"; -import { useFeature } from "@ledgerhq/live-common/featureFlags"; +import { useDispatch } from "react-redux"; +import { setHasBeenRedirectedToPostOnboarding } from "~/actions/settings"; type Props = BaseComposite< StackScreenProps @@ -18,10 +18,11 @@ type Props = BaseComposite< const CompletionScreen = ({ navigation, route }: Props) => { const { device } = route.params; + const dispatch = useDispatch(); - const recoverUpsellRedirection = useFeature("recoverUpsellRedirection"); - - const startPostOnboarding = useStartPostOnboardingCallback(); + useEffect(() => { + dispatch(setHasBeenRedirectedToPostOnboarding(false)); + }, [dispatch]); const redirectToMainScreen = useCallback(() => { (navigation as unknown as RootNavigation).reset({ @@ -41,28 +42,13 @@ const CompletionScreen = ({ navigation, route }: Props) => { }); }, [navigation]); - const redirectToPostOnboarding = useCallback(() => { - startPostOnboarding({ - deviceModelId: device.modelId, - resetNavigationStack: true, - fallbackIfNoAction: () => - // Resets the navigation stack to avoid allowing to go back to the onboarding welcome screen - // FIXME: bindings to react-navigation seem to have issues with composites - redirectToMainScreen(), - }); - }, [device.modelId, redirectToMainScreen, startPostOnboarding]); - - const redirectToNextScreen = recoverUpsellRedirection?.enabled - ? redirectToMainScreen // The main screen will handle the redirection to the upsell screen - : redirectToPostOnboarding; - return ( - + {device.modelId === DeviceModelId.europa ? ( - + ) : ( - + )} diff --git a/apps/ledger-live-mobile/src/screens/SyncOnboarding/SyncOnboardingCompanion.tsx b/apps/ledger-live-mobile/src/screens/SyncOnboarding/SyncOnboardingCompanion.tsx index 79458d1e7f0c..fa164a3bc8ea 100644 --- a/apps/ledger-live-mobile/src/screens/SyncOnboarding/SyncOnboardingCompanion.tsx +++ b/apps/ledger-live-mobile/src/screens/SyncOnboarding/SyncOnboardingCompanion.tsx @@ -18,7 +18,7 @@ import { useTranslation } from "react-i18next"; import { getDeviceModel } from "@ledgerhq/devices"; import { Device } from "@ledgerhq/live-common/hw/actions/types"; import { useDispatch } from "react-redux"; -import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; import { SeedPhraseType, StorylyInstanceID } from "@ledgerhq/types-live"; import { DeviceModelId } from "@ledgerhq/types-devices"; diff --git a/libs/ledger-live-common/src/hooks/recoverFeatureFlag.ts b/libs/ledger-live-common/src/hooks/recoverFeatureFlag.ts index 0ae1ac130ad4..a5c0b09a4a21 100644 --- a/libs/ledger-live-common/src/hooks/recoverFeatureFlag.ts +++ b/libs/ledger-live-common/src/hooks/recoverFeatureFlag.ts @@ -172,6 +172,8 @@ export function useCustomURI( if (source && deeplinkCampaign) { uri.searchParams.append("ajs_recover_source", source); uri.searchParams.append("ajs_recover_campaign", deeplinkCampaign); + uri.searchParams.append("ajs_prop_source", source); + uri.searchParams.append("ajs_prop_campaign", deeplinkCampaign); } return uri; @@ -190,3 +192,16 @@ export function useCustomPath( return usePath(servicesConfig, uri); } + +export enum Source { + LLM_ONBOARDING_24 = "llm-onboarding-24", + LLD_ONBOARDING_24 = "lld-onboarding-24", +} + +export function useTouchScreenOnboardingUpsellURI( + servicesConfig: Feature_ProtectServicesMobile | null, + source: Source, +): string | undefined { + const campaign = "touchscreen-onboarding"; + return useCustomURI(servicesConfig, "upsell", source, campaign); +}