-
Notifications
You must be signed in to change notification settings - Fork 360
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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.", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep ! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||||||
|
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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