Skip to content

Commit 79489d7

Browse files
authored
feat 💄(llm) Reborn Upsell Flex Drawer (LedgerHQ#8843)
feat 💄(llm): update LNX drawer in Reborn
1 parent 7de2a5f commit 79489d7

File tree

16 files changed

+351
-7
lines changed

16 files changed

+351
-7
lines changed

.changeset/orange-chicken-cross.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"live-mobile": minor
3+
---
4+
5+
Create flex upsell drawer under ff to switch from the old LNX upsell drawer
Binary file not shown.

apps/ledger-live-mobile/assets/videos/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export default {
1010
infinityPassPart02Light: require("./infinityPassLight/infinityPassPart02.mp4"),
1111
customLockScreenBannerLight: require("./customLockScreenBanner/customLockScreenBannerLight.mp4"),
1212
customLockScreenBannerDark: require("./customLockScreenBanner/customLockScreenBannerDark.mp4"),
13+
flex: require("./flex/BasicFlex.mp4"),
1314
};

apps/ledger-live-mobile/src/components/RootNavigator/BaseNavigator.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,6 @@ export default function BaseNavigator() {
209209
options={{
210210
headerStyle: styles.headerNoShadow,
211211
}}
212-
{...noNanoBuyNanoWallScreenOptions}
213212
/>
214213
<Stack.Screen
215214
name={ScreenName.Recover}

apps/ledger-live-mobile/src/components/RootNavigator/BuyDeviceNavigator.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import React, { useMemo } from "react";
22
import { createStackNavigator } from "@react-navigation/stack";
33
import { useTheme } from "styled-components/native";
44
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
5-
65
import { ScreenName } from "~/const";
76
import { getStackNavigatorConfig } from "~/navigation/navigatorConfig";
87
import GetDevice from "~/screens/GetDeviceScreen";
8+
import GetFlex from "LLM/features/Reborn/screens/UpsellFlex";
99
import PurchaseDevice from "~/screens/PurchaseDevice";
1010
import { BuyDeviceNavigatorParamList } from "./types/BuyDeviceNavigator";
1111

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

1920
return (
2021
<Stack.Navigator screenOptions={{ ...stackNavigationConfig, headerShown: false }}>
21-
<Stack.Screen name={ScreenName.GetDevice} component={GetDevice} />
22+
<Stack.Screen
23+
name={ScreenName.GetDevice}
24+
component={upsellFlexFF?.enabled ? GetFlex : GetDevice}
25+
/>
2226
{buyDeviceFromLive?.enabled && (
2327
<Stack.Screen name={ScreenName.PurchaseDevice} component={PurchaseDevice} />
2428
)}

apps/ledger-live-mobile/src/locales/en/common.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1294,7 +1294,7 @@
12941294
"desc": "Our products are the only hardware wallets certified for their security by national cyber security agencies."
12951295
},
12961296
"title": "You need a Ledger",
1297-
"desc": "For your security, Ledger Live only works with a device. You need a device in order to continue.",
1297+
"desc": "For your security,\n Ledger Live only works with a Ledger.",
12981298
"cta": "Buy your Ledger now",
12991299
"footer": "I already have a Ledger, set it up",
13001300
"bannerTitle": "Top-notch security for your crypto and NFTs",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from "react";
2+
import { createStackNavigator } from "@react-navigation/stack";
3+
import UpsellFlex from "../screens/UpsellFlex";
4+
import { OnboardingContextProvider } from "~/screens/Onboarding/onboardingContext";
5+
6+
const Stack = createStackNavigator();
7+
8+
export const MockComponent = () => (
9+
<OnboardingContextProvider>
10+
<Stack.Navigator>
11+
<Stack.Screen name="UpsellFlex" component={UpsellFlex} />
12+
</Stack.Navigator>
13+
</OnboardingContextProvider>
14+
);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from "react";
2+
import { render } from "@tests/test-renderer";
3+
import { track } from "~/analytics";
4+
import { MockComponent } from "./shared";
5+
6+
describe("UpsellFlex", () => {
7+
it("Should render UpsellFlex", async () => {
8+
const { getByText } = render(<MockComponent />);
9+
10+
expect(getByText(/you need a ledger/i)).toBeVisible();
11+
expect(getByText(/buy your ledger now/i)).toBeVisible();
12+
expect(getByText(/i already have a ledger, set it up/i)).toBeVisible();
13+
});
14+
});
15+
16+
it("Should call tracking correctly", async () => {
17+
const { user, getByText } = render(<MockComponent />);
18+
await user.press(getByText(/i already have a ledger, set it up/i));
19+
expect(track).toHaveBeenCalledWith("message_clicked", {
20+
message: "I already have a device, set it up now",
21+
page: "Upsell Flex",
22+
});
23+
24+
await user.press(getByText(/buy your ledger now/i));
25+
expect(track).toHaveBeenCalledWith("message_clicked", {
26+
message: "I already have a device, set it up now",
27+
page: "Upsell Flex",
28+
});
29+
});
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import React from "react";
2+
import useUpsellFlexModel from "./useUpsellFlexModel";
3+
import {
4+
Box,
5+
Button,
6+
Flex,
7+
IconBoxList,
8+
Icons,
9+
ScrollListContainer,
10+
Text,
11+
} from "@ledgerhq/native-ui";
12+
import { TouchableOpacity } from "react-native";
13+
import styled from "styled-components/native";
14+
import videoSources from "../../../../../../assets/videos";
15+
import Video from "react-native-video";
16+
import GradientContainer from "~/components/GradientContainer";
17+
import { TrackScreen } from "~/analytics";
18+
19+
const videoSource = videoSources.flex;
20+
21+
const hitSlop = {
22+
bottom: 10,
23+
left: 24,
24+
right: 24,
25+
top: 10,
26+
};
27+
28+
const StyledSafeAreaView = styled(Box)`
29+
flex: 1;
30+
background-color: ${p => p.theme.colors.background.default};
31+
padding-top: ${p => p.theme.space[10]}px;
32+
`;
33+
34+
const CloseButton = styled(TouchableOpacity)`
35+
background-color: ${p => p.theme.colors.neutral.c30};
36+
padding: 8px;
37+
border-radius: 32px;
38+
`;
39+
40+
const items = [
41+
{
42+
title: "buyDevice.0.title",
43+
desc: "buyDevice.0.desc",
44+
Icon: Icons.Coins,
45+
},
46+
{
47+
title: "buyDevice.1.title",
48+
desc: "buyDevice.1.desc",
49+
Icon: Icons.GraphAsc,
50+
},
51+
{
52+
title: "buyDevice.2.title",
53+
desc: "buyDevice.2.desc",
54+
Icon: Icons.Globe,
55+
},
56+
{
57+
title: "buyDevice.3.title",
58+
desc: "buyDevice.3.desc",
59+
Icon: Icons.Flex,
60+
},
61+
];
62+
63+
const videoStyle = {
64+
height: "100%",
65+
};
66+
67+
type ViewProps = ReturnType<typeof useUpsellFlexModel>;
68+
69+
function View({
70+
t,
71+
handleBack,
72+
setupDevice,
73+
buyLedger,
74+
colors,
75+
readOnlyModeEnabled,
76+
videoMounted,
77+
}: ViewProps) {
78+
return (
79+
<StyledSafeAreaView>
80+
{readOnlyModeEnabled ? <TrackScreen category="ReadOnly" name="Upsell Flex" /> : null}
81+
<Flex
82+
flexDirection="row"
83+
alignItems="center"
84+
justifyContent="flex-end"
85+
width="100%"
86+
position="absolute"
87+
zIndex={10}
88+
p={6}
89+
top={50}
90+
>
91+
<CloseButton onPress={handleBack} hitSlop={hitSlop}>
92+
<Icons.Close size="S" />
93+
</CloseButton>
94+
</Flex>
95+
<ScrollListContainer>
96+
<Flex
97+
height={320}
98+
borderTopLeftRadius={32}
99+
borderTopRightRadius={32}
100+
width="100%"
101+
overflow="hidden"
102+
opacity={videoMounted ? 0.8 : 0}
103+
>
104+
{videoMounted && (
105+
<Video
106+
disableFocus
107+
source={videoSource}
108+
style={{
109+
backgroundColor: colors.background.main,
110+
transform: [{ scale: 1.4 }],
111+
...(videoStyle as object),
112+
}}
113+
muted
114+
repeat={true}
115+
/>
116+
)}
117+
<GradientContainer
118+
color={colors.background.drawer}
119+
startOpacity={0}
120+
endOpacity={1}
121+
direction="top-to-bottom"
122+
containerStyle={{
123+
position: "absolute",
124+
borderRadius: 0,
125+
left: 0,
126+
bottom: 0,
127+
width: "100%",
128+
height: "30%",
129+
}}
130+
/>
131+
</Flex>
132+
<Flex p={6}>
133+
<Text variant="h4" textAlign="center" lineHeight="32.4px">
134+
{t("buyDevice.title")}
135+
</Text>
136+
<Flex mt={6} mb={8} justifyContent="center" alignItems="stretch">
137+
<Text px={6} textAlign="center" variant="body" color="neutral.c70">
138+
{t("buyDevice.desc")}
139+
</Text>
140+
</Flex>
141+
<IconBoxList
142+
iconShapes="circle"
143+
itemContainerProps={{ pr: 6 }}
144+
items={items.map(item => ({
145+
Icon: <item.Icon size="S" />,
146+
title: t(item.title),
147+
description: (
148+
<Text variant="paragraphLineHeight" color="neutral.c70">
149+
{t(item.desc)}
150+
</Text>
151+
),
152+
}))}
153+
/>
154+
</Flex>
155+
</ScrollListContainer>
156+
<Flex>
157+
<Button
158+
mx={6}
159+
my={6}
160+
type="main"
161+
outline={false}
162+
testID="getDevice-buy-button"
163+
onPress={buyLedger}
164+
size="large"
165+
>
166+
{t("buyDevice.cta")}
167+
</Button>
168+
<Flex px={6} pt={0} mb={8}>
169+
<Button
170+
type="default"
171+
border={1}
172+
borderColor="neutral.c50"
173+
onPress={setupDevice}
174+
size="large"
175+
>
176+
{t("buyDevice.footer")}
177+
</Button>
178+
</Flex>
179+
</Flex>
180+
</StyledSafeAreaView>
181+
);
182+
}
183+
184+
const UpsellFlex = () => <View {...useUpsellFlexModel()} />;
185+
186+
export default UpsellFlex;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
2+
import { useNavigation } from "@react-navigation/native";
3+
import { useCallback } from "react";
4+
import { useTranslation } from "react-i18next";
5+
import { Linking } from "react-native";
6+
import { useSelector, useDispatch } from "react-redux";
7+
import { useTheme } from "styled-components/native";
8+
import { setOnboardingHasDevice } from "~/actions/settings";
9+
import { track } from "~/analytics";
10+
import { BuyDeviceNavigatorParamList } from "~/components/RootNavigator/types/BuyDeviceNavigator";
11+
import {
12+
BaseNavigationComposite,
13+
StackNavigatorNavigation,
14+
} from "~/components/RootNavigator/types/helpers";
15+
import { OnboardingNavigatorParamList } from "~/components/RootNavigator/types/OnboardingNavigator";
16+
import useIsAppInBackground from "~/components/useIsAppInBackground";
17+
import { ScreenName, NavigatorName } from "~/const";
18+
import { readOnlyModeEnabledSelector } from "~/reducers/settings";
19+
import { useNavigationInterceptor } from "~/screens/Onboarding/onboardingContext";
20+
import { urls } from "~/utils/urls";
21+
22+
type NavigationProp = BaseNavigationComposite<
23+
| StackNavigatorNavigation<BuyDeviceNavigatorParamList, ScreenName.GetDevice>
24+
| StackNavigatorNavigation<OnboardingNavigatorParamList, ScreenName.GetDevice>
25+
>;
26+
27+
const useUpsellFlexModel = () => {
28+
const { t } = useTranslation();
29+
const navigation = useNavigation<NavigationProp>();
30+
const { colors } = useTheme();
31+
const { setShowWelcome, setFirstTimeOnboarding } = useNavigationInterceptor();
32+
const buyDeviceFromLive = useFeature("buyDeviceFromLive");
33+
const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector);
34+
const dispatch = useDispatch();
35+
const currentNavigation = navigation.getParent()?.getParent()?.getState().routes[0].name;
36+
const isInOnboarding = currentNavigation === NavigatorName.BaseOnboarding;
37+
38+
const handleBack = useCallback(() => {
39+
navigation.goBack();
40+
if (readOnlyModeEnabled) {
41+
track("button_clicked", {
42+
button: "close",
43+
page: "Upsell Flex",
44+
});
45+
}
46+
}, [readOnlyModeEnabled, navigation]);
47+
48+
const setupDevice = useCallback(() => {
49+
setShowWelcome(false);
50+
setFirstTimeOnboarding(false);
51+
if (isInOnboarding) dispatch(setOnboardingHasDevice(true));
52+
navigation.navigate(NavigatorName.BaseOnboarding, {
53+
screen: NavigatorName.Onboarding,
54+
params: {
55+
screen: ScreenName.OnboardingDeviceSelection,
56+
},
57+
});
58+
if (readOnlyModeEnabled) {
59+
track("message_clicked", {
60+
message: "I already have a device, set it up now",
61+
page: "Upsell Flex",
62+
});
63+
}
64+
}, [
65+
setShowWelcome,
66+
setFirstTimeOnboarding,
67+
isInOnboarding,
68+
dispatch,
69+
navigation,
70+
readOnlyModeEnabled,
71+
]);
72+
73+
const buyLedger = useCallback(() => {
74+
if (buyDeviceFromLive?.enabled) {
75+
// FIXME: ScreenName.PurchaseDevice does not exist when coming from the Onboarding navigator
76+
// @ts-expect-error This seem very impossible to type because ts is right…
77+
navigation.navigate(ScreenName.PurchaseDevice);
78+
} else {
79+
Linking.openURL(urls.buyFlex);
80+
}
81+
}, [buyDeviceFromLive?.enabled, navigation]);
82+
83+
const videoMounted = !useIsAppInBackground();
84+
85+
return {
86+
t,
87+
handleBack,
88+
setupDevice,
89+
buyLedger,
90+
colors,
91+
readOnlyModeEnabled,
92+
videoMounted,
93+
};
94+
};
95+
96+
export default useUpsellFlexModel;

0 commit comments

Comments
 (0)