Skip to content

Commit

Permalink
feat(mobile): add missing bottom sheet to FW update
Browse files Browse the repository at this point in the history
  • Loading branch information
Nodonisko committed Dec 8, 2024
1 parent c9c15b4 commit 825216e
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 77 deletions.
29 changes: 29 additions & 0 deletions suite-native/atoms/src/NumberedListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ReactNode } from 'react';

import { Color, TypographyStyle } from '@trezor/theme';

import { Box } from './Box';
import { Text } from './Text';
import { HStack } from './Stack';

type NumberedListItemProps = {
children: ReactNode;
variant?: TypographyStyle;
color?: Color;
number: number;
};

export const NumberedListItem = ({ children, variant, color, number }: NumberedListItemProps) => (
<HStack>
<Box style={{ minWidth: 16 }}>
<Text variant={variant} color={color}>
{number}.
</Text>
</Box>
<Box flexShrink={1}>
<Text variant={variant} color={color}>
{children}
</Text>
</Box>
</HStack>
);
9 changes: 3 additions & 6 deletions suite-native/atoms/src/Sheet/BottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState, ReactNode } from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated from 'react-native-reanimated';
import Animated, { LinearTransition } from 'react-native-reanimated';
import { ScrollView, PanGestureHandler } from 'react-native-gesture-handler';
import { GestureResponderEvent, Pressable } from 'react-native';

Expand Down Expand Up @@ -77,18 +77,14 @@ export const BottomSheet = ({
}
}, [isVisible, openSheetAnimated]);

const handleCloseSheet = () => {
closeSheetAnimated();
};

const handlePressOutside = (event: GestureResponderEvent) => {
if (event.target === event.currentTarget) closeSheetAnimated();
};

const insetBottom = Math.max(insets.bottom, DEFAULT_INSET_BOTTOM);

return (
<BottomSheetContainer isVisible={isVisible} onClose={handleCloseSheet}>
<BottomSheetContainer isVisible={isVisible} onClose={closeSheetAnimated}>
<Animated.View
style={[animatedSheetWithOverlayStyle, applyStyle(sheetWithOverlayStyle)]}
>
Expand All @@ -107,6 +103,7 @@ export const BottomSheet = ({
insetBottom,
}),
]}
layout={LinearTransition}
>
<BottomSheetHeader
title={title}
Expand Down
1 change: 1 addition & 0 deletions suite-native/atoms/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export * from './Skeleton/BoxSkeleton';
export * from './Skeleton/ListItemSkeleton';
export * from './IconListItem';
export * from './BulletListItem';
export * from './NumberedListItem';
export * from './SelectableItem';
export * from './constants';
export * from './useIllustrationColors';
Expand Down
84 changes: 84 additions & 0 deletions suite-native/firmware/src/components/MayBeStuckedBottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useState } from 'react';
import Animated, { FadeIn } from 'react-native-reanimated';

import { BottomSheet, Box, Button, NumberedListItem, Text, VStack } from '@suite-native/atoms';
import { Translation } from '@suite-native/intl';

type MayBeStuckedBottomSheetProps = {
isOpened: boolean;
onClose: () => void;
};

export const MayBeStuckedBottomSheet = ({ isOpened, onClose }: MayBeStuckedBottomSheetProps) => {
const [visiblePart, setVisiblePart] = useState<number>(1);

const handleClose = () => {
onClose();
setVisiblePart(1);
};

return (
<BottomSheet
isVisible={isOpened}
onClose={onClose}
isCloseDisplayed={false}
paddingHorizontal="sp24"
>
{visiblePart === 1 && (
<Animated.View>
<VStack spacing="sp24">
<VStack alignItems="center" spacing="sp8">
<Text textAlign="center" variant="titleSmall">
<Translation id="moduleDeviceSettings.firmware.stuckedBottomSheet.part1.title" />
</Text>
<Text textAlign="center" color="textSubdued">
<Translation id="moduleDeviceSettings.firmware.stuckedBottomSheet.part1.description" />
</Text>
</VStack>

<VStack spacing="sp16">
<Button onPress={() => setVisiblePart(2)} colorScheme="yellowBold">
<Translation id="moduleDeviceSettings.firmware.stuckedBottomSheet.part1.continueButton" />
</Button>
<Button onPress={handleClose} colorScheme="yellowElevation0">
<Translation id="moduleDeviceSettings.firmware.stuckedBottomSheet.part1.closeButton" />
</Button>
</VStack>
</VStack>
</Animated.View>
)}
{visiblePart === 2 && (
<Animated.View entering={FadeIn}>
<VStack spacing="sp24">
<VStack spacing="sp8">
<Text variant="titleSmall">
<Translation id="moduleDeviceSettings.firmware.stuckedBottomSheet.part2.title" />
</Text>
<Text color="textSubdued">
<Translation id="moduleDeviceSettings.firmware.stuckedBottomSheet.part2.subtitle" />
</Text>
</VStack>

<VStack spacing="sp2">
<NumberedListItem number={1}>
<Translation id="moduleDeviceSettings.firmware.stuckedBottomSheet.part2.tip1" />
</NumberedListItem>
<NumberedListItem number={2}>
<Translation id="moduleDeviceSettings.firmware.stuckedBottomSheet.part2.tip2" />
</NumberedListItem>
<NumberedListItem number={3}>
<Translation id="moduleDeviceSettings.firmware.stuckedBottomSheet.part2.tip3" />
</NumberedListItem>
</VStack>

<Box flex={1}>
<Button onPress={handleClose} colorScheme="primary">
<Translation id="moduleDeviceSettings.firmware.stuckedBottomSheet.part2.gotItButton" />
</Button>
</Box>
</VStack>
</Animated.View>
)}
</BottomSheet>
);
};
86 changes: 25 additions & 61 deletions suite-native/firmware/src/components/UpdateProgressIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable no-self-assign */
import { useEffect, useMemo } from 'react';
import { Platform } from 'react-native';
import { useEffect } from 'react';
import {
SharedValue,
useDerivedValue,
Expand All @@ -18,21 +17,19 @@ import {
ImageSVG,
MatrixColorFilterProps,
Paint,
Paragraph,
Path,
Shadow,
Skia,
TextAlign,
useSVG,
} from '@shopify/react-native-skia';

import { useNativeStyles } from '@trezor/styles';

const CANVAS_SIZE = 160;
const CIRCLE_DIAMETER = 128;
const CIRCLE_DIAMETER = 144;
// 1.25 is used to make sure that the circle is not cut off when animating using withSpring
const CANVAS_SIZE = CIRCLE_DIAMETER * 1.25;
const CIRCLE_CENTER = CANVAS_SIZE / 2;

const PROGRESS_STROKE_WIDTH = 5;
const PROGRESS_STROKE_WIDTH = 3;

const CHECKMARK_OFFSET_X = 10; // Adjust left/right position
const CHECKMARK_OFFSET_Y = 0; // Adjust up/down position
Expand All @@ -49,12 +46,6 @@ const progressCirclePath = Skia.Path.MakeFromSVGString(
`M ${CIRCLE_CENTER},${CIRCLE_CENTER - (CIRCLE_DIAMETER - PROGRESS_STROKE_WIDTH) / 2} A ${(CIRCLE_DIAMETER - PROGRESS_STROKE_WIDTH) / 2},${(CIRCLE_DIAMETER - PROGRESS_STROKE_WIDTH) / 2} 0 1,1 ${CIRCLE_CENTER},${CIRCLE_CENTER + (CIRCLE_DIAMETER - PROGRESS_STROKE_WIDTH) / 2} A ${(CIRCLE_DIAMETER - PROGRESS_STROKE_WIDTH) / 2},${(CIRCLE_DIAMETER - PROGRESS_STROKE_WIDTH) / 2} 0 1,1 ${CIRCLE_CENTER},${CIRCLE_CENTER - (CIRCLE_DIAMETER - PROGRESS_STROKE_WIDTH) / 2}`,
)!;

const fontStyle = {
fontFamily: Platform.select({ ios: 'Helvetica', default: 'serif' }),
fontSize: 34,
letterSpacing: -1.4,
};

// For some reason you can't easily animate opacity of SVG image, so we need to do it manually.
const AnimatedOpacity = ({
opacity,
Expand Down Expand Up @@ -152,7 +143,7 @@ export const UpdateProgressIndicator = ({
checkmarkAnimationProgress.value = 0;
progressEnd.value = withSpring(progress / 100);

trezorLogoOpacity.value = 0;
trezorLogoOpacity.value = withTiming(1, { duration: 600 });
errorSvgOpacity.value = 0;
paragraphOpacity.value = withTiming(1, { duration: 600 });
}
Expand Down Expand Up @@ -195,52 +186,17 @@ export const UpdateProgressIndicator = ({
paragraphOpacity,
]);

const paragraph = useMemo(() => {
if (!isInProgress) return null;

return Skia.ParagraphBuilder.Make({
textAlign: TextAlign.Center,
})
.pushStyle({ ...fontStyle, color: Skia.Color(utils.colors.textPrimaryDefault) })
.addText(`${progress}%`)
.build();
}, [progress, utils.colors.textPrimaryDefault, isInProgress]);

const isDone = isSuccess || isError;

return (
<Canvas style={{ width: CANVAS_SIZE, height: CANVAS_SIZE }}>
<Circle
cx={CIRCLE_CENTER}
cy={CIRCLE_CENTER}
r={CIRCLE_DIAMETER / 2}
color={utils.colors.backgroundSurfaceElevation1}
></Circle>

{isInProgress && (
<AnimatedOpacity opacity={paragraphOpacity}>
<Paragraph
paragraph={paragraph}
x={0}
y={CIRCLE_CENTER - fontStyle.fontSize / 2}
width={CANVAS_SIZE}
/>
</AnimatedOpacity>
)}

<AnimatedOpacity opacity={trezorLogoOpacity}>
<ImageSVG
svg={trezorLogoSvg}
x={CIRCLE_CENTER - CIRCLE_DIAMETER / 4}
y={CIRCLE_CENTER - CIRCLE_DIAMETER / 4}
color={utils.colors.textPrimaryDefault}
width={CIRCLE_DIAMETER / 2}
height={CIRCLE_DIAMETER / 2}
opacity={trezorLogoOpacity}
/>
</AnimatedOpacity>

<Group>
<Path
path={progressCirclePath}
color={utils.colors.backgroundPrimarySubtleOnElevationNegative}
strokeCap="round"
strokeJoin="round"
strokeWidth={PROGRESS_STROKE_WIDTH}
style="stroke"
/>
<Path
path={progressCirclePath}
start={0}
Expand All @@ -250,16 +206,24 @@ export const UpdateProgressIndicator = ({
strokeJoin="round"
strokeWidth={PROGRESS_STROKE_WIDTH}
style="stroke"
>
{!isDone && <Shadow dx={0} dy={2} blur={4} color="rgba(0,0,0,0.1)" />}
</Path>
/>
</Group>
<Circle
cx={CIRCLE_CENTER}
cy={CIRCLE_CENTER}
r={animatedBackgroundRadius}
color={backgroundColorFinished}
/>
<AnimatedOpacity opacity={trezorLogoOpacity}>
<ImageSVG
svg={trezorLogoSvg}
x={CIRCLE_CENTER - CIRCLE_DIAMETER / 4}
y={CIRCLE_CENTER - CIRCLE_DIAMETER / 4}
color={utils.colors.textPrimaryDefault}
width={CIRCLE_DIAMETER / 2}
height={CIRCLE_DIAMETER / 2}
/>
</AnimatedOpacity>
{isSuccess && (
<Path
path={checkmarkPath}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import Animated, {
FadeInDown,
FadeInUp,
Expand Down Expand Up @@ -30,6 +30,8 @@ import {
UpdateProgressIndicatorStatus,
} from '../components/UpdateProgressIndicator';
import { useFirmware } from '../hooks/useFirmware';
import { MayBeStuckedBottomSheet } from '../components/MayBeStuckedBottomSheet';

type NavigationProp = StackNavigationProps<
DeviceSettingsStackParamList,
DeviceStackRoutes.FirmwareUpdateInProgress
Expand All @@ -46,6 +48,8 @@ export const FirmwareUpdateInProgressScreen = () => {
const dispatch = useDispatch();
const { applyStyle } = useNativeStyles();
const navigation = useNavigation<NavigationProp>();
const [isMayBeStuckedBottomSheetOpened, setIsMayBeStuckedBottomSheetOpened] =
useState<boolean>(false);
const { bottom: bottomSafeAreaInset } = useSafeAreaInsets();
const {
operation,
Expand Down Expand Up @@ -116,8 +120,12 @@ export const FirmwareUpdateInProgressScreen = () => {
startFirmwareUpdate();
}, [startFirmwareUpdate, resetReducer]);

const handleMayBeStucked = useCallback(() => {
// todo
const openMayBeStuckedBottomSheet = useCallback(() => {
setIsMayBeStuckedBottomSheetOpened(true);
}, []);

const closeMayBeStuckedBottomSheet = useCallback(() => {
setIsMayBeStuckedBottomSheetOpened(false);
}, []);

const handleContactSupport = useCallback(() => {
Expand Down Expand Up @@ -192,7 +200,7 @@ export const FirmwareUpdateInProgressScreen = () => {
bottom: bottomButtonOffset,
})}
>
<Button onPress={handleMayBeStucked} colorScheme="tertiaryElevation0">
<Button onPress={openMayBeStuckedBottomSheet} colorScheme="tertiaryElevation0">
<Translation id="moduleDeviceSettings.firmware.firmwareUpdateProgress.stuckButton" />
</Button>
</Animated.View>
Expand All @@ -204,6 +212,10 @@ export const FirmwareUpdateInProgressScreen = () => {
}
/>
)}
<MayBeStuckedBottomSheet
isOpened={isMayBeStuckedBottomSheetOpened}
onClose={closeMayBeStuckedBottomSheet}
/>
</Screen>
);
};
Loading

0 comments on commit 825216e

Please sign in to comment.