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

feat 💄(llm) Reborn Upsell Flex Drawer #8843

Merged
merged 1 commit into from
Jan 16, 2025
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/orange-chicken-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"live-mobile": minor
---

Create flex upsell drawer under ff to switch from the old LNX upsell drawer
Binary file not shown.
1 change: 1 addition & 0 deletions apps/ledger-live-mobile/assets/videos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export default {
infinityPassPart02Light: require("./infinityPassLight/infinityPassPart02.mp4"),
customLockScreenBannerLight: require("./customLockScreenBanner/customLockScreenBannerLight.mp4"),
customLockScreenBannerDark: require("./customLockScreenBanner/customLockScreenBannerDark.mp4"),
flex: require("./flex/BasicFlex.mp4"),
};
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ export default function BaseNavigator() {
options={{
headerStyle: styles.headerNoShadow,
}}
{...noNanoBuyNanoWallScreenOptions}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we want to remove this wall?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remove it from here but I catch it directly inside the screen where we call the discovery app so IMO it's ok the behaviour doesn't change and let me be able to open the ledger shop in reborn mode

/>
<Stack.Screen
name={ScreenName.Recover}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import React, { useMemo } from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { useTheme } from "styled-components/native";
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";

import { ScreenName } from "~/const";
import { getStackNavigatorConfig } from "~/navigation/navigatorConfig";
import GetDevice from "~/screens/GetDeviceScreen";
import GetFlex from "LLM/features/Reborn/screens/UpsellFlex";
import PurchaseDevice from "~/screens/PurchaseDevice";
import { BuyDeviceNavigatorParamList } from "./types/BuyDeviceNavigator";

Expand All @@ -14,11 +14,15 @@ const Stack = createStackNavigator<BuyDeviceNavigatorParamList>();
const BuyDeviceNavigator = () => {
const { colors } = useTheme();
const buyDeviceFromLive = useFeature("buyDeviceFromLive");
const upsellFlexFF = useFeature("llmRebornFlex");
const stackNavigationConfig = useMemo(() => getStackNavigatorConfig(colors, true), [colors]);

return (
<Stack.Navigator screenOptions={{ ...stackNavigationConfig, headerShown: false }}>
<Stack.Screen name={ScreenName.GetDevice} component={GetDevice} />
<Stack.Screen
name={ScreenName.GetDevice}
component={upsellFlexFF?.enabled ? GetFlex : GetDevice}
/>
{buyDeviceFromLive?.enabled && (
<Stack.Screen name={ScreenName.PurchaseDevice} component={PurchaseDevice} />
)}
Expand Down
2 changes: 1 addition & 1 deletion apps/ledger-live-mobile/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,7 @@
"desc": "Our products are the only hardware wallets certified for their security by national cyber security agencies."
},
"title": "You need a Ledger",
"desc": "For your security, Ledger Live only works with a device. You need a device in order to continue.",
"desc": "For your security,\n Ledger Live only works with a Ledger.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\n working here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep !

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"desc": "For your security,\n Ledger Live only works with a Ledger.",
"desc": "For your security,\n Ledger Live only works with a Ledger device.",

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the wording from Figma so I guess it's good

"cta": "Buy your Ledger now",
"footer": "I already have a Ledger, set it up",
"bannerTitle": "Top-notch security for your crypto and NFTs",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";
import { createStackNavigator } from "@react-navigation/stack";
import UpsellFlex from "../screens/UpsellFlex";
import { OnboardingContextProvider } from "~/screens/Onboarding/onboardingContext";

const Stack = createStackNavigator();

export const MockComponent = () => (
<OnboardingContextProvider>
<Stack.Navigator>
<Stack.Screen name="UpsellFlex" component={UpsellFlex} />
</Stack.Navigator>
</OnboardingContextProvider>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from "react";
import { render } from "@tests/test-renderer";
import { track } from "~/analytics";
import { MockComponent } from "./shared";

describe("UpsellFlex", () => {
it("Should render UpsellFlex", async () => {
const { getByText } = render(<MockComponent />);

expect(getByText(/you need a ledger/i)).toBeVisible();
expect(getByText(/buy your ledger now/i)).toBeVisible();
expect(getByText(/i already have a ledger, set it up/i)).toBeVisible();
});
});

it("Should call tracking correctly", async () => {
const { user, getByText } = render(<MockComponent />);
await user.press(getByText(/i already have a ledger, set it up/i));
expect(track).toHaveBeenCalledWith("message_clicked", {
message: "I already have a device, set it up now",
page: "Upsell Flex",
});

await user.press(getByText(/buy your ledger now/i));
expect(track).toHaveBeenCalledWith("message_clicked", {
message: "I already have a device, set it up now",
page: "Upsell Flex",
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import React from "react";
import useUpsellFlexModel from "./useUpsellFlexModel";
import {
Box,
Button,
Flex,
IconBoxList,
Icons,
ScrollListContainer,
Text,
} from "@ledgerhq/native-ui";
import { TouchableOpacity } from "react-native";
import styled from "styled-components/native";
import videoSources from "../../../../../../assets/videos";
import Video from "react-native-video";
import GradientContainer from "~/components/GradientContainer";
import { TrackScreen } from "~/analytics";

const videoSource = videoSources.flex;

const hitSlop = {
bottom: 10,
left: 24,
right: 24,
top: 10,
};

const StyledSafeAreaView = styled(Box)`
flex: 1;
background-color: ${p => p.theme.colors.background.default};
padding-top: ${p => p.theme.space[10]}px;
`;

const CloseButton = styled(TouchableOpacity)`
background-color: ${p => p.theme.colors.neutral.c30};
padding: 8px;
border-radius: 32px;
`;

const items = [
{
title: "buyDevice.0.title",
desc: "buyDevice.0.desc",
Icon: Icons.Coins,
},
{
title: "buyDevice.1.title",
desc: "buyDevice.1.desc",
Icon: Icons.GraphAsc,
},
{
title: "buyDevice.2.title",
desc: "buyDevice.2.desc",
Icon: Icons.Globe,
},
{
title: "buyDevice.3.title",
desc: "buyDevice.3.desc",
Icon: Icons.Flex,
},
];

const videoStyle = {
height: "100%",
};

type ViewProps = ReturnType<typeof useUpsellFlexModel>;

function View({
t,
handleBack,
setupDevice,
buyLedger,
colors,
readOnlyModeEnabled,
videoMounted,
}: ViewProps) {
return (
<StyledSafeAreaView>
{readOnlyModeEnabled ? <TrackScreen category="ReadOnly" name="Upsell Flex" /> : null}
<Flex
flexDirection="row"
alignItems="center"
justifyContent="flex-end"
width="100%"
position="absolute"
zIndex={10}
p={6}
top={50}
>
<CloseButton onPress={handleBack} hitSlop={hitSlop}>
<Icons.Close size="S" />
</CloseButton>
</Flex>
<ScrollListContainer>
<Flex
height={320}
borderTopLeftRadius={32}
borderTopRightRadius={32}
width="100%"
overflow="hidden"
opacity={videoMounted ? 0.8 : 0}
>
{videoMounted && (
<Video
disableFocus
source={videoSource}
style={{
backgroundColor: colors.background.main,
transform: [{ scale: 1.4 }],
...(videoStyle as object),
}}
muted
repeat={true}
/>
)}
<GradientContainer
color={colors.background.drawer}
startOpacity={0}
endOpacity={1}
direction="top-to-bottom"
containerStyle={{
position: "absolute",
borderRadius: 0,
left: 0,
bottom: 0,
width: "100%",
height: "30%",
}}
/>
</Flex>
<Flex p={6}>
<Text variant="h4" textAlign="center" lineHeight="32.4px">
{t("buyDevice.title")}
</Text>
<Flex mt={6} mb={8} justifyContent="center" alignItems="stretch">
<Text px={6} textAlign="center" variant="body" color="neutral.c70">
{t("buyDevice.desc")}
</Text>
</Flex>
<IconBoxList
iconShapes="circle"
itemContainerProps={{ pr: 6 }}
items={items.map(item => ({
Icon: <item.Icon size="S" />,
title: t(item.title),
description: (
<Text variant="paragraphLineHeight" color="neutral.c70">
{t(item.desc)}
</Text>
),
}))}
/>
</Flex>
</ScrollListContainer>
<Flex>
<Button
mx={6}
my={6}
type="main"
outline={false}
testID="getDevice-buy-button"
onPress={buyLedger}
size="large"
>
{t("buyDevice.cta")}
</Button>
<Flex px={6} pt={0} mb={8}>
<Button
type="default"
border={1}
borderColor="neutral.c50"
onPress={setupDevice}
size="large"
>
{t("buyDevice.footer")}
</Button>
</Flex>
</Flex>
</StyledSafeAreaView>
);
}

const UpsellFlex = () => <View {...useUpsellFlexModel()} />;

export default UpsellFlex;
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import { useNavigation } from "@react-navigation/native";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Linking } from "react-native";
import { useSelector, useDispatch } from "react-redux";
import { useTheme } from "styled-components/native";
import { setOnboardingHasDevice } from "~/actions/settings";
import { track } from "~/analytics";
import { BuyDeviceNavigatorParamList } from "~/components/RootNavigator/types/BuyDeviceNavigator";
import {
BaseNavigationComposite,
StackNavigatorNavigation,
} from "~/components/RootNavigator/types/helpers";
import { OnboardingNavigatorParamList } from "~/components/RootNavigator/types/OnboardingNavigator";
import useIsAppInBackground from "~/components/useIsAppInBackground";
import { ScreenName, NavigatorName } from "~/const";
import { readOnlyModeEnabledSelector } from "~/reducers/settings";
import { useNavigationInterceptor } from "~/screens/Onboarding/onboardingContext";
import { urls } from "~/utils/urls";

type NavigationProp = BaseNavigationComposite<
| StackNavigatorNavigation<BuyDeviceNavigatorParamList, ScreenName.GetDevice>
| StackNavigatorNavigation<OnboardingNavigatorParamList, ScreenName.GetDevice>
>;

const useUpsellFlexModel = () => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp>();
const { colors } = useTheme();
const { setShowWelcome, setFirstTimeOnboarding } = useNavigationInterceptor();
const buyDeviceFromLive = useFeature("buyDeviceFromLive");
const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector);
const dispatch = useDispatch();
const currentNavigation = navigation.getParent()?.getParent()?.getState().routes[0].name;
const isInOnboarding = currentNavigation === NavigatorName.BaseOnboarding;

const handleBack = useCallback(() => {
navigation.goBack();
if (readOnlyModeEnabled) {
track("button_clicked", {
button: "close",
page: "Upsell Flex",
});
}
}, [readOnlyModeEnabled, navigation]);

const setupDevice = useCallback(() => {
setShowWelcome(false);
setFirstTimeOnboarding(false);
if (isInOnboarding) dispatch(setOnboardingHasDevice(true));
navigation.navigate(NavigatorName.BaseOnboarding, {
screen: NavigatorName.Onboarding,
params: {
screen: ScreenName.OnboardingDeviceSelection,
},
});
if (readOnlyModeEnabled) {
track("message_clicked", {
message: "I already have a device, set it up now",
page: "Upsell Flex",
});
}
}, [
setShowWelcome,
setFirstTimeOnboarding,
isInOnboarding,
dispatch,
navigation,
readOnlyModeEnabled,
]);

const buyLedger = useCallback(() => {
if (buyDeviceFromLive?.enabled) {
// FIXME: ScreenName.PurchaseDevice does not exist when coming from the Onboarding navigator
// @ts-expect-error This seem very impossible to type because ts is right…
navigation.navigate(ScreenName.PurchaseDevice);
} else {
Linking.openURL(urls.buyFlex);
}
}, [buyDeviceFromLive?.enabled, navigation]);

const videoMounted = !useIsAppInBackground();

return {
t,
handleBack,
setupDevice,
buyLedger,
colors,
readOnlyModeEnabled,
videoMounted,
};
};

export default useUpsellFlexModel;
Loading
Loading