Skip to content

Commit

Permalink
✨ (llm) add entry points for reborn LP variant A (#8464)
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasWerey authored Dec 2, 2024
1 parent 208d94a commit 8cc1438
Show file tree
Hide file tree
Showing 16 changed files with 323 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/clean-olives-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"live-mobile": minor
---

Update entry points to reborn LP on read only mode
22 changes: 21 additions & 1 deletion apps/ledger-live-mobile/src/components/FabActions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { useAnalytics } from "~/analytics";
import { WrappedButtonProps } from "../wrappedUi/Button";
import { NavigatorName } from "~/const";
import { useRoute } from "@react-navigation/native";
import { useRebornFlow } from "LLM/features/Reborn/hooks/useRebornFlow";
import { useSelector } from "react-redux";
import { hasOrderedNanoSelector, readOnlyModeEnabledSelector } from "~/reducers/settings";

export type ModalOnDisabledClickComponentProps = {
account?: AccountLike;
Expand Down Expand Up @@ -96,6 +99,10 @@ export const FabButtonBarProvider = ({

const navigation = useNavigation<StackNavigationProp<ParamListBase, string, NavigatorName>>();

const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector);
const hasOrderedNano = useSelector(hasOrderedNanoSelector);
const { navigateToRebornFlow } = useRebornFlow();

const router = useRoute();

const onNavigate = useCallback(
Expand Down Expand Up @@ -134,6 +141,11 @@ export const FabButtonBarProvider = ({
(data: Omit<ActionButtonEvent, "label" | "Icon">) => {
const { navigationParams, confirmModalProps, linkUrl, event, eventProperties, id } = data;

if (readOnlyModeEnabled && !hasOrderedNano) {
navigateToRebornFlow();
return;
}

if (!confirmModalProps) {
if (event) {
track(event, { page: router.name, ...globalEventProperties, ...eventProperties });
Expand All @@ -157,7 +169,15 @@ export const FabButtonBarProvider = ({
setIsModalInfoOpened(true);
}
},
[globalEventProperties, onNavigate, track, router.name],
[
readOnlyModeEnabled,
hasOrderedNano,
navigateToRebornFlow,
track,
router.name,
globalEventProperties,
onNavigate,
],
);

const onContinue = useCallback(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
} from "../../RootNavigator/types/helpers";
import { BaseNavigatorStackParamList } from "../../RootNavigator/types/BaseNavigator";
import QueuedDrawer from "../../QueuedDrawer";
import { useRebornFlow } from "LLM/features/Reborn/hooks/useRebornFlow";
import { useSelector } from "react-redux";
import { readOnlyModeEnabledSelector, hasOrderedNanoSelector } from "~/reducers/settings";

function ZeroBalanceDisabledModalContent({
account,
Expand All @@ -26,10 +29,17 @@ function ZeroBalanceDisabledModalContent({
const { t } = useTranslation();
const navigation =
useNavigation<RootNavigationComposite<StackNavigatorNavigation<BaseNavigatorStackParamList>>>();
const { navigateToRebornFlow } = useRebornFlow();
const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector);
const hasOrderedNano = useSelector(hasOrderedNanoSelector);

const actionCurrency = account ? getAccountCurrency(account) : currency;

const goToBuy = useCallback(() => {
if (readOnlyModeEnabled && !hasOrderedNano) {
navigateToRebornFlow();
return;
}
navigation.navigate(NavigatorName.Exchange, {
screen: ScreenName.ExchangeBuy,
params: {
Expand All @@ -38,9 +48,21 @@ function ZeroBalanceDisabledModalContent({
},
});
onClose();
}, [account?.id, actionCurrency?.id, navigation, onClose]);
}, [
account?.id,
actionCurrency?.id,
hasOrderedNano,
navigateToRebornFlow,
navigation,
onClose,
readOnlyModeEnabled,
]);

const goToReceive = useCallback(() => {
if (readOnlyModeEnabled && !hasOrderedNano) {
navigateToRebornFlow();
return;
}
if (account) {
navigation.navigate(NavigatorName.ReceiveFunds, {
screen: ScreenName.ReceiveConfirmation,
Expand All @@ -60,7 +82,16 @@ function ZeroBalanceDisabledModalContent({
});
}
onClose();
}, [account, parentAccount?.id, actionCurrency, navigation, onClose]);
}, [
readOnlyModeEnabled,
hasOrderedNano,
account,
onClose,
navigateToRebornFlow,
navigation,
actionCurrency,
parentAccount?.id,
]);

return (
<QueuedDrawer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { MainNavigatorParamList } from "./types/MainNavigator";
import { isMainNavigatorVisibleSelector } from "~/reducers/appstate";
import EarnLiveAppNavigator from "./EarnLiveAppNavigator";
import { getStakeLabelLocaleBased } from "~/helpers/getStakeLabelLocaleBased";
import { useRebornFlow } from "LLM/features/Reborn/hooks/useRebornFlow";

const Tab = createBottomTabNavigator<MainNavigatorParamList>();

Expand All @@ -37,6 +38,8 @@ export default function MainNavigator() {
const managerNavLockCallback = useManagerNavLockCallback();
const web3hub = useFeature("web3hub");
const earnYiedlLabel = getStakeLabelLocaleBased();
const { navigateToRebornFlow } = useRebornFlow();

const insets = useSafeAreaInsets();
const tabBar = useMemo(
() =>
Expand Down Expand Up @@ -119,9 +122,14 @@ export default function MainNavigator() {
tabPress: e => {
e.preventDefault();
managerLockAwareCallback(() => {
navigation.navigate(NavigatorName.Earn, {
screen: ScreenName.Earn,
});
if (readOnlyModeEnabled && hasOrderedNano) {
navigation.navigate(ScreenName.PostBuyDeviceSetupNanoWallScreen);
} else if (readOnlyModeEnabled) {
navigateToRebornFlow();
} else
navigation.navigate(NavigatorName.Earn, {
screen: ScreenName.Earn,
});
});
},
})}
Expand Down Expand Up @@ -190,7 +198,7 @@ export default function MainNavigator() {
if (readOnlyModeEnabled && hasOrderedNano) {
navigation.navigate(ScreenName.PostBuyDeviceSetupNanoWallScreen);
} else if (readOnlyModeEnabled) {
navigation.navigate(NavigatorName.BuyDevice);
navigateToRebornFlow();
} else {
navigation.navigate(NavigatorName.MyLedger, {
screen: ScreenName.MyLedgerChooseDevice,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import ProtectConnectionInformationModal from "~/screens/Onboarding/steps/setupD
import { NavigationHeaderBackButton } from "../NavigationHeaderBackButton";
import AccessExistingWallet from "~/screens/Onboarding/steps/accessExistingWallet";
import AnalyticsOptInPromptNavigator from "./AnalyticsOptInPromptNavigator";
import LandingPagesNavigator from "./LandingPagesNavigator";

const Stack = createStackNavigator<OnboardingNavigatorParamList>();
const OnboardingPreQuizModalStack =
Expand Down Expand Up @@ -240,6 +241,7 @@ export default function OnboardingNavigator() {
options={{ headerShown: false }}
component={AnalyticsOptInPromptNavigator}
/>
<Stack.Screen name={NavigatorName.LandingPages} component={LandingPagesNavigator} />
</Stack.Navigator>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NavigatorScreenParams } from "@react-navigation/native";

import { NavigatorName, ScreenName } from "~/const";
import { AnalyticsOptInPromptNavigatorParamList } from "./AnalyticsOptInPromptNavigator";
import { LandingPagesNavigatorParamList } from "./LandingPagesNavigator";

export type OnboardingPreQuizModalNavigatorParamList = {
[ScreenName.OnboardingPreQuizModal]: { onNext?: () => void };
Expand Down Expand Up @@ -60,4 +61,5 @@ export type OnboardingNavigatorParamList = {
filterByDeviceModelId: DeviceModelId;
};
[NavigatorName.AnalyticsOptInPrompt]: NavigatorScreenParams<AnalyticsOptInPromptNavigatorParamList>;
[NavigatorName.LandingPages]: NavigatorScreenParams<LandingPagesNavigatorParamList>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ export const useDynamicContentLogic = () => {
dispatch(setDynamicContentAssetsCards(assetCards));
dispatch(setDynamicContentNotificationCards(notificationCards));
dispatch(setDynamicContentLearnCards(learnCards));
dispatch(setIsDynamicContentLoading(false));
dispatch(setDynamicContentLandingPageStickyCtaCards(landingPageStickyCtaCards));
dispatch(setIsDynamicContentLoading(false));
}, [Braze, dismissedContentCardsIds, dispatch]);

const clearOldDismissedContentCards = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { NavigatorName, ScreenName } from "~/const";
import { track } from "~/analytics";
import { WrappedButtonProps } from "~/components/wrappedUi/Button";
import { Props as ThemeProps } from "~/components/theme/ForceTheme";
import { useRebornFlow } from "../../hooks/useRebornFlow";

import buyFlexSource from "~/images/illustration/Shared/_FlexTop.png";
import buyDoubleFlexSource from "~/images/illustration/Shared/_FlexTwoSides.png";
Expand Down Expand Up @@ -48,6 +49,7 @@ const useBuyDeviceBannerModel = ({
useNavigation<RootNavigationComposite<StackNavigatorNavigation<BaseNavigatorStackParamList>>>();

const revertTheme: ThemeProps["selectedPalette"] = theme === "light" ? "dark" : "light";
const { navigateToRebornFlow } = useRebornFlow();

const imageSource: ImageSourcePropType = (() => {
switch (image) {
Expand All @@ -63,8 +65,8 @@ const useBuyDeviceBannerModel = ({
})();

const handleOnPress = useCallback(() => {
navigate(NavigatorName.BuyDevice);
}, [navigate]);
navigateToRebornFlow();
}, [navigateToRebornFlow]);

const handleSetupCtaOnPress = useCallback(() => {
navigate(NavigatorName.BaseOnboarding, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const RebornAnalytics = {
FALLBACK_REBORN: "Fallback_Reborn",
REBORN_LP: "reborn_LP",
} as const;

export default RebornAnalytics;
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useRef } from "react";
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import { ABTestingVariants } from "@ledgerhq/types-live";
import { useNavigation } from "@react-navigation/native";
import {
RootNavigationComposite,
StackNavigatorNavigation,
} from "~/components/RootNavigator/types/helpers";
import { BaseNavigatorStackParamList } from "~/components/RootNavigator/types/BaseNavigator";
import { NavigatorName, ScreenName } from "~/const";
import { CategoryContentCard, LandingPageUseCase } from "~/dynamicContent/types";
import { filterCategoriesByLocation, formatCategories } from "~/dynamicContent/utils";
import useDynamicContent from "~/dynamicContent/useDynamicContent";
import { ContentCard } from "@braze/react-native-sdk";
import { track } from "~/analytics";
import { useDynamicContentLogic } from "~/dynamicContent/useDynamicContentLogic";
import useFetchWithTimeout from "LLM/hooks/useFetchWithTimeout";
import RebornAnalytics from "../constants/analytics";

type NavigationProps = RootNavigationComposite<
StackNavigatorNavigation<BaseNavigatorStackParamList>
>;

const FETCH_TIMEOUT = 3000;

export function useRebornFlow(isFromOnboarding = false) {
const { navigate } = useNavigation<NavigationProps>();
const rebornFeatureFlag = useFeature("llmRebornLP");
const featureFlagEnabled = rebornFeatureFlag?.enabled;
const variant = getVariant(rebornFeatureFlag?.params?.variant);
const { categoriesCards, mobileCards } = useDynamicContent();
const { fetchData, refreshDynamicContent } = useDynamicContentLogic();
const canDisplayLP = useRef(false);

const fetchWithTimeout = useFetchWithTimeout(FETCH_TIMEOUT);

const fetchAllData = async () => {
refreshDynamicContent();
try {
await fetchWithTimeout(fetchData);
} catch (error) {
canDisplayLP.current = false;
}
};

const checkIfCanDisplayLP = async (LP: LandingPageUseCase) => {
const result = await hasContentCardToDisplay(LP, categoriesCards, mobileCards);
canDisplayLP.current = result;
};

const navigateToLandingPage = async (LP: LandingPageUseCase) => {
await checkIfCanDisplayLP(LP);
if (!canDisplayLP.current) {
await fetchAllData();
await checkIfCanDisplayLP(LP);
}

if (canDisplayLP.current && !isFromOnboarding) {
track(RebornAnalytics.REBORN_LP);
navigate(NavigatorName.LandingPages, {
screen: ScreenName.GenericLandingPage,
params: {
useCase: LP,
},
});
} else {
track(RebornAnalytics.FALLBACK_REBORN);
navigate(NavigatorName.BuyDevice);
}
};

const navigateToRebornFlow = () => {
if (!featureFlagEnabled) {
navigate(NavigatorName.BuyDevice);
return;
}

switch (variant) {
case ABTestingVariants.variantA:
navigateToLandingPage(LandingPageUseCase.LP_Reborn1);
break;
case ABTestingVariants.variantB:
navigateToLandingPage(LandingPageUseCase.LP_Reborn2);
break;
default:
navigate(NavigatorName.BuyDevice);
break;
}
};

return {
navigateToRebornFlow,
rebornFeatureFlagEnabled: featureFlagEnabled,
rebornVariant: variant,
};
}

const getVariant = (variant?: ABTestingVariants): ABTestingVariants =>
variant === ABTestingVariants.variantB ? ABTestingVariants.variantB : ABTestingVariants.variantA;

const hasContentCardToDisplay = async (
lpLocation: LandingPageUseCase,
categoriesCards: CategoryContentCard[],
mobileCards: ContentCard[],
) => {
const categoriesToDisplay = filterCategoriesByLocation(categoriesCards, lpLocation);
const categoriesFormatted = formatCategories(categoriesToDisplay, mobileCards);

return categoriesFormatted.length > 0;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { renderHook, act } from "@tests/test-renderer";
import useFetchWithTimeout from "../useFetchWithTimeout";

describe("useFetchWithTimeout", () => {
it("should resolve the fetch function result within the timeout", async () => {
const fetchFunction = jest.fn().mockResolvedValue("data");
const { result } = renderHook(() => useFetchWithTimeout(300));

await act(async () => {
const data = await result.current(fetchFunction);
expect(data).toBe("data");
});

expect(fetchFunction).toHaveBeenCalledTimes(1);
});

it("should reject if the fetch function takes longer than the timeout", async () => {
jest.useFakeTimers();
const fetchFunction = jest
.fn()
.mockImplementation(() => new Promise(resolve => setTimeout(() => resolve("data"), 600)));
const { result } = renderHook(() => useFetchWithTimeout(300));

act(() => {
const fetchPromise = result.current(fetchFunction);
jest.advanceTimersByTime(400);
return expect(fetchPromise).rejects.toThrow("Fetch timed out");
});

jest.runAllTimers();
expect(fetchFunction).toHaveBeenCalledTimes(1);
});

it("should reject if the fetch function throws an error", async () => {
const fetchFunction = jest.fn().mockRejectedValue(new Error("Fetch error"));
const { result } = renderHook(() => useFetchWithTimeout(300));

await act(async () => {
await expect(result.current(fetchFunction)).rejects.toThrow("Fetch error");
});

expect(fetchFunction).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit 8cc1438

Please sign in to comment.