Skip to content

Commit

Permalink
feat(mobile): update FW alert on Home + remote feature flag (#16318)
Browse files Browse the repository at this point in the history
* feat(mobile): update FW alert on homescreen

* feat(mobile): remote feature flag for FW update
  • Loading branch information
Nodonisko authored Jan 17, 2025
1 parent cedad41 commit 059e89b
Show file tree
Hide file tree
Showing 15 changed files with 229 additions and 47 deletions.
2 changes: 2 additions & 0 deletions suite-common/message-system/src/messageSystemTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export const Feature = {
ethClaim: 'eth.staking.claim',
firmwareRevisionCheck: 'security.firmware.check',
firmwareHashCheck: 'security.firmware.hashCheck',
// FW update feature flag implemented only for mobile app
firmwareUpdate: 'device.firmware.update',
} as const;

export type FeatureDomain = (typeof Feature)[keyof typeof Feature];
Expand Down
56 changes: 31 additions & 25 deletions suite-native/atoms/src/Stack.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ReactNode } from 'react';
import React, { ReactNode } from 'react';
import Animated from 'react-native-reanimated';
import { View } from 'react-native';

import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
import { NativeSpacing } from '@trezor/theme';
Expand Down Expand Up @@ -27,30 +29,34 @@ const spacerStyle = prepareNativeStyle<SpacerStyleProps>((utils, { spacing, orie
};
});

export const Stack = ({
children,
style,
spacing,
orientation = 'vertical',
...rest
}: StackProps) => {
const { applyStyle } = useNativeStyles();

return (
<Box
style={[
applyStyle(spacerStyle, {
spacing,
orientation,
}),
style,
]}
{...rest}
>
{children}
</Box>
);
};
export const Stack = React.forwardRef<View, StackProps>(
({ children, style, spacing, orientation = 'vertical', ...rest }: StackProps, ref) => {
const { applyStyle } = useNativeStyles();

return (
<Box
ref={ref}
style={[
applyStyle(spacerStyle, {
spacing,
orientation,
}),
style,
]}
{...rest}
>
{children}
</Box>
);
},
);

export const VStack = Stack;
export const HStack = (props: StackProps) => <Stack {...props} orientation="horizontal" />;

const AnimatedStack = Animated.createAnimatedComponent(Stack);
AnimatedStack.displayName = 'AnimatedStack';
export const AnimatedVStack = AnimatedStack;
export const AnimatedHStack = (props: StackProps) => (
<AnimatedStack {...props} orientation="horizontal" />
);
3 changes: 0 additions & 3 deletions suite-native/feature-flags/src/featureFlagsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export const FeatureFlag = {
IsCardanoSendEnabled: 'isCardanoSendEnabled',
IsRegtestEnabled: 'isRegtestEnabled',
IsConnectPopupEnabled: 'IsConnectPopupEnabled',
IsFirmwareUpdateEnabled: 'IsFirmwareUpdateEnabled',
AreEthL2sEnabled: 'AreEthL2sEnabled',
} as const;
export type FeatureFlag = (typeof FeatureFlag)[keyof typeof FeatureFlag];
Expand All @@ -24,7 +23,6 @@ export const featureFlagsInitialState: FeatureFlagsState = {
[FeatureFlag.IsCardanoSendEnabled]: isAndroid() && isDevelopOrDebugEnv(),
[FeatureFlag.IsRegtestEnabled]: isDebugEnv() || isDetoxTestBuild(),
[FeatureFlag.IsConnectPopupEnabled]: isDevelopOrDebugEnv(),
[FeatureFlag.IsFirmwareUpdateEnabled]: isDevelopOrDebugEnv(),
[FeatureFlag.AreEthL2sEnabled]: isDebugEnv(),
};

Expand All @@ -33,7 +31,6 @@ export const featureFlagsPersistedKeys: Array<keyof FeatureFlagsState> = [
FeatureFlag.IsCardanoSendEnabled,
FeatureFlag.IsRegtestEnabled,
FeatureFlag.IsConnectPopupEnabled,
FeatureFlag.IsFirmwareUpdateEnabled,
FeatureFlag.AreEthL2sEnabled,
];

Expand Down
1 change: 1 addition & 0 deletions suite-native/firmware/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@shopify/react-native-skia": "^1.5.10",
"@suite-common/firmware": "workspace:*",
"@suite-common/icons": "workspace:*",
"@suite-common/message-system": "workspace:*",
"@suite-native/link": "workspace:*",
"@trezor/connect": "workspace:*",
"@trezor/react-native-usb": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useSelector } from 'react-redux';

import {
selectIsFeatureEnabled,
Feature,
MessageSystemRootState,
} from '@suite-common/message-system';

export const useIsFirmwareUpdateFeatureEnabled = () => {
const isFirmwareUpdateEnabled = useSelector((state: MessageSystemRootState) =>
selectIsFeatureEnabled(state, Feature.firmwareUpdate, true),
);

return isFirmwareUpdateEnabled;
};
1 change: 1 addition & 0 deletions suite-native/firmware/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './nativeFirmwareSlice';
export * from './components/UpdateProgressIndicatorDemo';
export * from './screens/FirmwareUpdateInProgressScreen';
export * from './hooks/useIsFirmwareUpdateFeatureEnabled';
3 changes: 3 additions & 0 deletions suite-native/firmware/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"references": [
{ "path": "../../suite-common/firmware" },
{ "path": "../../suite-common/icons" },
{
"path": "../../suite-common/message-system"
},
{ "path": "../link" },
{ "path": "../../packages/connect" },
{
Expand Down
8 changes: 8 additions & 0 deletions suite-native/intl/src/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ export const en = {
enable: 'Enable',
},
},
firmwareUpdateAlert: {
title: 'New Trezor firmware version available.',
version: 'Version {version}',
button: {
close: 'Close',
update: 'Update',
},
},
},
accounts: {
accountLabelFieldHint: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const featureFlagsTitleMap = {
[FeatureFlagEnum.IsCardanoSendEnabled]: 'Cardano send',
[FeatureFlagEnum.IsRegtestEnabled]: 'Regtest',
[FeatureFlagEnum.IsConnectPopupEnabled]: 'Connect Popup',
[FeatureFlagEnum.IsFirmwareUpdateEnabled]: 'Firmware update',
[FeatureFlagEnum.AreEthL2sEnabled]: 'Eth L2s',
} as const satisfies Record<FeatureFlagEnum, string>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
DeviceSettingsStackParamList,
} from '@suite-native/navigation';
import { isDevelopOrDebugEnv } from '@suite-native/config';
import { FeatureFlag, useFeatureFlag } from '@suite-native/feature-flags';
import { useIsFirmwareUpdateFeatureEnabled } from '@suite-native/firmware';

import { DeviceSettingsCardLayout } from './DeviceSettingsCardLayout';

Expand Down Expand Up @@ -66,7 +66,7 @@ export const DeviceFirmwareCard = () => {
selectIsDiscoveryActiveByDeviceState(state, device?.state),
);
const navigation = useNavigation<NavigationProp>();
const isFirmwareUpdateEnabled = useFeatureFlag(FeatureFlag.IsFirmwareUpdateEnabled);
const isFirmwareUpdateEnabled = useIsFirmwareUpdateFeatureEnabled();

if (!device || !deviceModel) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
selectDeviceState,
} from '@suite-common/wallet-core';
import { useAlert } from '@suite-native/alerts';
import { useIsFirmwareUpdateFeatureEnabled } from '@suite-native/firmware';

import { FirmwareUpdateVersionCard } from './FirmwareVersionCard';

Expand All @@ -42,6 +43,7 @@ export const FirmwareUpdateScreen = () => {
const isDiscoveryRunning = useSelector((state: DiscoveryRootState & DeviceRootState) =>
selectIsDiscoveryActiveByDeviceState(state, deviceState),
);
const isFirmwareUpdateEnabled = useIsFirmwareUpdateFeatureEnabled();

const handleShowSeedBottomSheet = useCallback(() => {
showAlert({
Expand All @@ -66,7 +68,7 @@ export const FirmwareUpdateScreen = () => {
<Button
onPress={handleShowSeedBottomSheet}
style={applyStyle(firmwareUpdateButtonStyle)}
isDisabled={isDiscoveryRunning}
isDisabled={isDiscoveryRunning || !isFirmwareUpdateEnabled}
isLoading={isDiscoveryRunning}
>
<Translation id="moduleDeviceSettings.firmware.firmwareUpdateScreen.updateButton" />
Expand Down
1 change: 1 addition & 0 deletions suite-native/module-home/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"jotai": "1.9.1",
"react": "18.2.0",
"react-native": "0.76.1",
"react-native-reanimated": "3.16.7",
"react-native-svg": "15.9.0",
"react-redux": "8.0.7"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { useSelector } from 'react-redux';
import { useMemo } from 'react';

import { useNavigation } from '@react-navigation/native';
import { atom, useAtomValue, useSetAtom } from 'jotai';

import { Box, Button, HStack, VStack, Text } from '@suite-native/atoms';
import { Icon } from '@suite-native/icons';
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
import {
DeviceRootState,
DiscoveryRootState,
selectDeviceId,
selectDeviceReleaseInfo,
selectDeviceState,
selectDeviceUpdateFirmwareVersion,
selectIsDeviceConnectedAndAuthorized,
selectIsDiscoveryActiveByDeviceState,
selectIsPortfolioTrackerDevice,
} from '@suite-common/wallet-core';
import {
RootStackParamList,
RootStackRoutes,
StackNavigationProps,
DeviceStackRoutes,
} from '@suite-native/navigation';
import { Translation } from '@suite-native/intl';
import { useIsFirmwareUpdateFeatureEnabled } from '@suite-native/firmware';

const containerStyle = prepareNativeStyle(utils => ({
flexDirection: 'row',
alignItems: 'flex-start',
borderRadius: utils.borders.radii.r12,
borderWidth: 1,
borderColor: utils.colors.backgroundAlertBlueSubtleOnElevationNegative,
backgroundColor: utils.colors.backgroundAlertBlueSubtleOnElevation1,
padding: utils.spacings.sp16,
gap: utils.spacings.sp12,
marginHorizontal: utils.spacings.sp16,
}));

const flex1Style = {
flex: 1,
};

type CloseStateItem = {
deviceId: string;
version: string;
};
const closeStateAtom = atom<CloseStateItem[]>([]);

export const FirmwareUpdateAlert = () => {
const { applyStyle } = useNativeStyles();
const updateFirmwareVersion = useSelector(selectDeviceUpdateFirmwareVersion);
const deviceReleaseInfo = useSelector(selectDeviceReleaseInfo);
const isPortfolioTrackerDevice = useSelector(selectIsPortfolioTrackerDevice);
const deviceId = useSelector(selectDeviceId);
const isConnected = useSelector(selectIsDeviceConnectedAndAuthorized);
const deviceState = useSelector(selectDeviceState);
const isDiscoveryRunning = useSelector((state: DiscoveryRootState & DeviceRootState) =>
selectIsDiscoveryActiveByDeviceState(state, deviceState),
);
const navigation =
useNavigation<StackNavigationProps<RootStackParamList, RootStackRoutes.AppTabs>>();
const setCloseState = useSetAtom(closeStateAtom);

const isClosedAtom = useMemo(
() =>
atom(get =>
get(closeStateAtom).some(
item => item.deviceId === deviceId && item.version === updateFirmwareVersion,
),
),
[deviceId, updateFirmwareVersion],
);

const isClosed = useAtomValue(isClosedAtom);
const isFirmwareUpdateEnabled = useIsFirmwareUpdateFeatureEnabled();

const isUpgradable = deviceReleaseInfo?.isNewer;

const handleUpdateFirmware = () => {
navigation.navigate(RootStackRoutes.DeviceSettingsStack, {
screen: DeviceStackRoutes.FirmwareUpdate,
});
};

const handleClose = () => {
if (!deviceId || !updateFirmwareVersion) return;

setCloseState(prev => [...prev, { deviceId, version: updateFirmwareVersion }]);
};

if (!isFirmwareUpdateEnabled) {
return null;
}

if (
!isUpgradable ||
isPortfolioTrackerDevice ||
isDiscoveryRunning ||
!isConnected ||
isClosed
) {
return null;
}

return (
<Animated.View style={applyStyle(containerStyle)} entering={FadeIn} exiting={FadeOut}>
<Icon name="info" size="large" />
<VStack spacing="sp12" style={flex1Style}>
<Box>
<Text variant="highlight">
<Translation id="moduleHome.firmwareUpdateAlert.title" />
</Text>
<Text>
<Translation
id="moduleHome.firmwareUpdateAlert.version"
values={{ version: updateFirmwareVersion }}
/>
</Text>
</Box>
<HStack spacing="sp8" style={flex1Style}>
<Button colorScheme="blueElevation0" onPress={handleClose} style={flex1Style}>
<Translation id="moduleHome.firmwareUpdateAlert.button.close" />
</Button>
<Button
colorScheme="blueBold"
onPress={handleUpdateFirmware}
style={flex1Style}
>
<Translation id="moduleHome.firmwareUpdateAlert.button.update" />
</Button>
</HStack>
</VStack>
</Animated.View>
);
};
Loading

0 comments on commit 059e89b

Please sign in to comment.