Skip to content

Commit

Permalink
Rework remote promo sheet logic / designs (#6085)
Browse files Browse the repository at this point in the history
* immediately set isShown when we're going to show a sheet

* improvements to remote promo sheets

* partialize persistence and fix some light mode / dark mode oddities

* add targeted version check

* rename func

* fix type

* fix exported function name

* fix promo sheet check to handle all multiple addresses

* improve checks

* re-memo sync

* cleanup

* undo address specific store change

---------

Co-authored-by: Ibrahim Taveras <[email protected]>
  • Loading branch information
walmat and ibrahimtaveras00 authored Sep 16, 2024
1 parent 2aa7bae commit a0d629f
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 178 deletions.
196 changes: 90 additions & 106 deletions src/components/PromoSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import React, { useCallback, useEffect, useReducer } from 'react';
import { ImageSourcePropType, Dimensions, StatusBar, ImageBackground } from 'react-native';
import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
import { ImageSourcePropType, StatusBar, ImageBackground } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import MaskedView from '@react-native-masked-view/masked-view';
import { SheetActionButton, SheetHandle, SlackSheet } from '@/components/sheet';
import { CampaignKey } from '@/components/remote-promo-sheet/localCampaignChecks';
import { analyticsV2 } from '@/analytics';
import { AccentColorProvider, Box, Inset, Row, Rows, Stack, Text, Bleed, Column, Columns } from '@/design-system';
import { AccentColorProvider, Box, Stack, Text, Bleed, Column, Columns, useForegroundColor, useAccentColor } from '@/design-system';
import { useDimensions } from '@/hooks';
import { sharedCoolModalTopOffset } from '@/navigation/config';
import { useTheme } from '@/theme';
import { IS_IOS, IS_ANDROID } from '@/env';

const MIN_HEIGHT = 740;
import { IS_ANDROID } from '@/env';
import { safeAreaInsetValues } from '@/utils';

type SheetActionButtonProps = {
label: string;
Expand Down Expand Up @@ -54,8 +52,9 @@ export function PromoSheet({
secondaryButtonProps,
items,
}: PromoSheetProps) {
const { width: deviceWidth, height: deviceHeight } = useDimensions();
const { colors } = useTheme();
const { width: deviceWidth, height: deviceHeight } = useDimensions();
const labelTertiary = useForegroundColor('labelTertiary');
const renderedAt = Date.now();
const [activated, activate] = useReducer(() => true, false);

Expand All @@ -82,10 +81,7 @@ export function PromoSheet({
primaryButtonProps.onPress();
}, [activate, campaignKey, primaryButtonProps, renderedAt]);

// We are not using `isSmallPhone` from `useDimensions` here as we
// want to explicitly set a min height.
const isSmallPhone = deviceHeight < MIN_HEIGHT;
const contentHeight = deviceHeight - (!isSmallPhone ? sharedCoolModalTopOffset : 0);
const contentHeight = deviceHeight - safeAreaInsetValues.top;

return (
<SlackSheet
Expand All @@ -99,103 +95,91 @@ export function PromoSheet({
<StatusBar barStyle="light-content" />
<AccentColorProvider color={backgroundColor}>
<Box background="accent" style={{ height: contentHeight }} testID={campaignKey}>
{/* @ts-ignore */}
<Box as={ImageBackground} height="full" source={backgroundImage}>
<Rows>
<Row>
<Stack space={{ custom: isSmallPhone ? 46 : 54 }}>
<Box>
<Box height={{ custom: isSmallPhone ? 195 : 265 }} width="full">
{/* @ts-ignore */}
<Box
as={ImageBackground}
height={{
custom: deviceWidth / headerImageAspectRatio,
}}
marginTop={{ custom: isSmallPhone ? -70 : 0 }}
source={headerImage}
width="full"
>
{/* @ts-ignore */}
<SheetHandle alignSelf="center" color={sheetHandleColor} style={{ marginTop: isSmallPhone ? 75 : 5 }} />
</Box>
</Box>
<Stack alignHorizontal="center" space={{ custom: 13 }}>
<Text color="labelSecondary" size="15pt" weight="heavy">
{subHeader}
</Text>
<Text color="label" size="30pt" weight="heavy">
{header}
</Text>
</Stack>
</Box>
<Inset horizontal={{ custom: 43.5 }}>
<Stack space={isSmallPhone ? '24px' : '36px'}>
{items.map(item => (
<Columns key={item.title} space={{ custom: 13 }}>
<Column width="content">
<MaskedView
maskElement={
<Box paddingTop={IS_ANDROID ? '6px' : undefined}>
<Text align="center" color="accent" size="30pt" weight="bold">
{item.icon}
</Text>
</Box>
}
style={{ width: 42 }}
>
<Box
as={LinearGradient}
colors={item.gradient}
end={{ x: 0.5, y: 1 }}
height={{ custom: 50 }}
marginTop="-10px"
start={{ x: 0, y: 0 }}
width="full"
/>
</MaskedView>
</Column>
<Bleed top="3px">
<Stack space="12px">
<Text color="label" size="17pt" weight="bold">
{item.title}
<Box>
<Box
as={ImageBackground}
height={{
custom: deviceWidth / headerImageAspectRatio,
}}
source={headerImage}
width="full"
>
<SheetHandle alignSelf="center" color={sheetHandleColor} style={{ marginTop: 5 }} />
</Box>
</Box>
<Box paddingVertical="28px" height={{ custom: deviceHeight - deviceWidth / headerImageAspectRatio - 58 }} flexGrow={1}>
<Box alignItems="center" paddingHorizontal="20px" paddingBottom="20px" gap={14}>
<Text color="labelSecondary" size="15pt" align="center" weight="heavy">
{subHeader}
</Text>
<Text color="label" align="center" size="30pt" weight="heavy">
{header}
</Text>
</Box>
<Box flexGrow={1} paddingHorizontal="20px" paddingVertical="24px">
<Stack space="24px">
{items.map(item => (
<Columns key={item.title} space={{ custom: 13 }}>
<Column width="content">
<MaskedView
maskElement={
<Box paddingTop={IS_ANDROID ? '6px' : undefined}>
<Text align="center" color="accent" size="30pt" weight="bold">
{item.icon}
</Text>
<Text color="labelSecondary" size="15pt" weight="medium">
{item.description}
</Text>
</Stack>
</Bleed>
</Columns>
))}
</Stack>
</Inset>
</Box>
}
style={{ width: 42 }}
>
<Box
as={LinearGradient}
colors={item.gradient}
end={{ x: 0.5, y: 1 }}
height={{ custom: 50 }}
marginTop="-10px"
start={{ x: 0, y: 0 }}
width="full"
/>
</MaskedView>
</Column>
<Bleed top="3px">
<Stack space="12px">
<Text color="label" size="17pt" weight="bold">
{item.title}
</Text>
<Text color="labelSecondary" size="15pt" weight="medium">
{item.description}
</Text>
</Stack>
</Bleed>
</Columns>
))}
</Stack>
</Box>
<Box paddingHorizontal="20px">
<Stack space="12px">
<SheetActionButton
color={primaryButtonProps.color || accentColor}
label={primaryButtonProps.label}
lightShadows
onPress={primaryButtonOnPress}
textColor={primaryButtonProps.textColor}
textSize="large"
weight="heavy"
/>
<SheetActionButton
color={secondaryButtonProps.color || colors.transparent}
isTransparent
label={secondaryButtonProps.label}
onPress={secondaryButtonProps.onPress || (() => {})}
textColor={secondaryButtonProps.textColor || labelTertiary}
textSize="large"
weight="heavy"
/>
</Stack>
</Row>
<Row height="content">
<Inset bottom={isSmallPhone && IS_IOS ? '24px' : '42px (Deprecated)'} horizontal="19px (Deprecated)">
<Stack space="12px">
<SheetActionButton
color={primaryButtonProps.color || accentColor}
label={primaryButtonProps.label}
lightShadows
onPress={primaryButtonOnPress}
textColor={primaryButtonProps.textColor || backgroundColor}
textSize="large"
weight="heavy"
/>
<SheetActionButton
color={secondaryButtonProps.color || colors.transparent}
isTransparent
label={secondaryButtonProps.label}
onPress={secondaryButtonProps.onPress || (() => {})}
textColor={secondaryButtonProps.textColor || accentColor}
textSize="large"
weight="heavy"
/>
</Stack>
</Inset>
</Row>
</Rows>
</Box>
</Box>
</Box>
</Box>
</AccentColorProvider>
Expand Down
47 changes: 34 additions & 13 deletions src/components/remote-promo-sheet/RemotePromoSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import c from 'chroma-js';
import React, { useCallback, useEffect } from 'react';
import { useRoute, RouteProp } from '@react-navigation/native';
import { get } from 'lodash';
Expand All @@ -14,6 +15,8 @@ import { Language } from '@/languages';
import { useAccountSettings } from '@/hooks';
import { remotePromoSheetsStore } from '@/state/remotePromoSheets/remotePromoSheets';
import { RootStackParamList } from '@/navigation/types';
import { Colors } from '@/styles';
import { getHighContrastColor } from '@/__swaps__/utils/swaps';

const DEFAULT_HEADER_HEIGHT = 285;
const DEFAULT_HEADER_WIDTH = 390;
Expand All @@ -30,6 +33,18 @@ const enum ButtonType {
External = 'External',
}

const getHexOrThemeColor = (colors: Colors, hexOrThemeString: string | null | undefined, fallbackColor: string) => {
if (!hexOrThemeString) {
return get(colors, fallbackColor);
}

if (c.valid(hexOrThemeString)) {
return hexOrThemeString;
}

return get(colors, hexOrThemeString) ?? get(colors, fallbackColor);
};

const getKeyForLanguage = (key: string, promoSheet: any, language: Language) => {
if (!promoSheet) {
return '';
Expand All @@ -48,26 +63,21 @@ const getKeyForLanguage = (key: string, promoSheet: any, language: Language) =>
};

export function RemotePromoSheet() {
const { colors } = useTheme();
const { colors, isDarkMode } = useTheme();
const { goBack, navigate } = useNavigation();
const { params } = useRoute<RouteProp<RootStackParamList, 'RemotePromoSheet'>>();
const { campaignId, campaignKey } = params;
const { language } = useAccountSettings();

useEffect(() => {
remotePromoSheetsStore.setState({
isShown: true,
lastShownTimestamp: Date.now(),
});

return () => {
remotePromoSheetsStore.setState({
isShown: false,
});
};
}, []);

const { data, error } = usePromoSheetQuery(
const { data } = usePromoSheetQuery(
{
id: campaignId,
},
Expand Down Expand Up @@ -100,7 +110,8 @@ export function RemotePromoSheet() {
);
}, [goBack, navigate, data?.promoSheet]);

if (!data?.promoSheet || error) {
if (!data?.promoSheet) {
goBack();
return null;
}

Expand All @@ -116,9 +127,17 @@ export function RemotePromoSheet() {
secondaryButtonProps,
} = data.promoSheet;

const accentColor = (colors as { [key: string]: any })[accentColorString as string] ?? accentColorString;
const backgroundColor = (colors as { [key: string]: any })[backgroundColorString as string] ?? backgroundColorString;
const sheetHandleColor = (colors as { [key: string]: any })[sheetHandleColorString as string] ?? sheetHandleColorString;
const accentColor = getHexOrThemeColor(colors, accentColorString, 'appleBlue');
const backgroundColor = getHexOrThemeColor(colors, backgroundColorString, 'white');
const sheetHandleColor = getHexOrThemeColor(colors, sheetHandleColorString, 'whiteLabel');
const primaryButtonBgColor = getHexOrThemeColor(colors, primaryButtonProps.color, 'appleBlue');
const primaryButtonTextColor = getHexOrThemeColor(colors, primaryButtonProps.textColor, 'whiteLabel');
const secondaryButtonBgColor = getHexOrThemeColor(colors, secondaryButtonProps.color, 'transparent');
const secondaryButtonTextColor = getHexOrThemeColor(
colors,
secondaryButtonProps.textColor || getHighContrastColor(backgroundColor)[isDarkMode ? 'dark' : 'light'],
'whiteLabel'
);

const backgroundSignedImageUrl = backgroundImage?.url ? maybeSignUri(backgroundImage.url) : undefined;
const headerSignedImageUrl = headerImage?.url ? maybeSignUri(headerImage.url) : undefined;
Expand All @@ -136,13 +155,15 @@ export function RemotePromoSheet() {
subHeader={getKeyForLanguage('subHeader', data.promoSheet, language as Language)}
primaryButtonProps={{
...primaryButtonProps,
...(primaryButtonProps.color ? { color: get(colors, primaryButtonProps.color) } : {}),
...(primaryButtonProps.textColor ? { textColor: get(colors, primaryButtonProps.textColor) } : {}),
color: primaryButtonBgColor,
textColor: primaryButtonTextColor,
label: getKeyForLanguage('primaryButtonProps.label', data.promoSheet, language as Language),
onPress: getButtonForType(data.promoSheet.primaryButtonProps.type),
}}
secondaryButtonProps={{
...secondaryButtonProps,
color: secondaryButtonBgColor,
textColor: secondaryButtonTextColor,
label: getKeyForLanguage('secondaryButtonProps.label', data.promoSheet, language as Language),
onPress: goBack,
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { isTargetedVersionOrNewer } from '../isTargetedVersionOrNewer';
import * as DeviceInfo from 'react-native-device-info';

jest.mock('react-native-device-info', () => ({
getVersion: jest.fn(),
}));

describe('isTargetedVersionOrNewer', () => {
afterEach(() => {
jest.resetAllMocks();
});

it('should return true when current app version is newer than version to check', async () => {
(DeviceInfo.getVersion as jest.Mock).mockReturnValue('2.0.0');
const result = await isTargetedVersionOrNewer('1.9.0');
expect(result).toBe(true);
});

it('should return false when current app version is older than version to check', async () => {
(DeviceInfo.getVersion as jest.Mock).mockReturnValue('1.8.0');
const result = await isTargetedVersionOrNewer('1.9.0');
expect(result).toBe(false);
});

it('should return true when versions are equal', async () => {
(DeviceInfo.getVersion as jest.Mock).mockReturnValue('1.9.0');
const result = await isTargetedVersionOrNewer('1.9.0');
expect(result).toBe(true);
});

it('should handle patch versions correctly', async () => {
(DeviceInfo.getVersion as jest.Mock).mockReturnValue('1.9.1');
const result = await isTargetedVersionOrNewer('1.9.0');
expect(result).toBe(true);
});

it('should handle versions with different number of parts', async () => {
(DeviceInfo.getVersion as jest.Mock).mockReturnValue('2.0');
const result = await isTargetedVersionOrNewer('1.9.9');
expect(result).toBe(true);
});

it('should return true when versions are equal but have different number of parts', async () => {
(DeviceInfo.getVersion as jest.Mock).mockReturnValue('2.0.0');
const result = await isTargetedVersionOrNewer('2.0');
expect(result).toBe(true);
});

it('should return false when current app version is older with different number of parts', async () => {
(DeviceInfo.getVersion as jest.Mock).mockReturnValue('1.9');
const result = await isTargetedVersionOrNewer('2.0.0');
expect(result).toBe(false);
});
});
Loading

0 comments on commit a0d629f

Please sign in to comment.