Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ (llm) add entry points for reborn LP variant A #8464

Merged
merged 1 commit into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -59,4 +60,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);
cgrellard-ledger marked this conversation as resolved.
Show resolved Hide resolved
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
Loading