Skip to content

Commit

Permalink
feat(llm/post onboarding): auto redirection to Recover upsell & post …
Browse files Browse the repository at this point in the history
…onboarding
  • Loading branch information
ofreyssinet-ledger committed Oct 29, 2024
1 parent 4adfd20 commit 07016ab
Show file tree
Hide file tree
Showing 20 changed files with 523 additions and 108 deletions.
5 changes: 4 additions & 1 deletion .changeset/green-rice-cheer.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
"live-mobile": patch
---

Hide Backup section during onboarding on Flex and Stax
Stax/Flex onboarding:
- Hide "Backup with Recover" section
- Auto redirect to Recover upsell between the onboarding and the post onboarding

6 changes: 6 additions & 0 deletions apps/ledger-live-mobile/src/actions/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
SettingsAddStarredMarketcoinsPayload,
SettingsRemoveStarredMarketcoinsPayload,
SettingsSetFromLedgerSyncOnboardingPayload,
SettingsSetHasBeenRedirectedToPostOnboardingPayload,
} from "./types";
import { ImageType } from "~/components/CustomImage/types";

Expand Down Expand Up @@ -264,6 +265,11 @@ export const setHasBeenUpsoldProtect = createAction<SettingsSetHasBeenUpsoldProt
SettingsActionTypes.SET_HAS_BEEN_UPSOLD_PROTECT,
);

export const setHasBeenRedirectedToPostOnboarding =
createAction<SettingsSetHasBeenRedirectedToPostOnboardingPayload>(
SettingsActionTypes.SET_HAS_BEEN_REDIRECTED_TO_POST_ONBOARDING,
);

export const setGeneralTermsVersionAccepted = createAction<SettingsSetGeneralTermsVersionAccepted>(
SettingsActionTypes.SET_GENERAL_TERMS_VERSION_ACCEPTED,
);
Expand Down
3 changes: 3 additions & 0 deletions apps/ledger-live-mobile/src/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useEffect } 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,
]);
}
Original file line number Diff line number Diff line change
@@ -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],
);
}
Original file line number Diff line number Diff line change
@@ -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,
]);
}
Original file line number Diff line number Diff line change
@@ -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 },
},
]);
});
});
Loading

0 comments on commit 07016ab

Please sign in to comment.