From 41f33e8900d922ae5ba819d892ddcd639ee56747 Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Thu, 9 Jan 2025 16:41:53 +0100 Subject: [PATCH] fix(ui): add custom Switch component (#2427) --- src/components/List.tsx | 14 +-- src/components/Switch.tsx | 114 ++++++++++++++++++ src/components/SwitchRow.tsx | 2 +- src/screens/Settings/PIN/AskForBiometrics.tsx | 2 +- src/screens/Settings/PIN/Result.tsx | 2 +- src/screens/Wallets/Send/CoinSelection.tsx | 3 +- src/styles/components.ts | 16 --- 7 files changed, 126 insertions(+), 27 deletions(-) create mode 100644 src/components/Switch.tsx diff --git a/src/components/List.tsx b/src/components/List.tsx index 5ad911eb6..ed326d5a6 100644 --- a/src/components/List.tsx +++ b/src/components/List.tsx @@ -11,7 +11,6 @@ import { import { SvgProps } from 'react-native-svg'; import isEqual from 'lodash/isEqual'; -import { Switch } from '../styles/components'; import { BodyM, BodyMSB, @@ -21,6 +20,7 @@ import { Caption, } from '../styles/text'; import { ChevronRight, Checkmark } from '../styles/icons'; +import Switch from '../components/Switch'; import DraggableList from '../screens/Settings/PaymentPreference/DraggableList'; const _SectionHeader = memo( @@ -87,14 +87,14 @@ export type ItemData = SwitchItem | ButtonItem | TextButtonItem | DraggableItem; export type SwitchItem = { type: EItemType.switch; + enabled: boolean; title: string; Icon?: React.FC; iconColor?: string; - enabled?: boolean; disabled?: boolean; hide?: boolean; - onPress?: () => void; testID?: string; + onPress?: () => void; }; export type ButtonItem = { @@ -107,10 +107,10 @@ export type ButtonItem = { iconColor?: string; disabled?: boolean; enabled?: boolean; + loading?: boolean; hide?: boolean; - onPress?: () => void; testID?: string; - loading?: boolean; + onPress?: () => void; }; export type TextButtonItem = { @@ -122,8 +122,8 @@ export type TextButtonItem = { iconColor?: string; enabled?: boolean; hide?: boolean; - onPress?: () => void; testID?: string; + onPress?: () => void; }; export type DraggableItem = { @@ -131,8 +131,8 @@ export type DraggableItem = { value: TItemDraggable[]; title: string; hide?: boolean; - onDragEnd?: (data: TItemDraggable[]) => void; testID?: string; + onDragEnd?: (data: TItemDraggable[]) => void; }; const _Item = memo((item: ItemData): ReactElement => { diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx new file mode 100644 index 000000000..074d96659 --- /dev/null +++ b/src/components/Switch.tsx @@ -0,0 +1,114 @@ +import React, { ReactElement } from 'react'; +import { Pressable, StyleSheet } from 'react-native'; +import Animated, { + Easing, + interpolate, + interpolateColor, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withTiming, +} from 'react-native-reanimated'; + +import colors from '../styles/colors'; +import { IThemeColors } from '../styles/themes'; + +const duration = 300; +const defaultHeight = 32; +const defaultWidth = 52; + +const Switch = ({ + value, + disabled, + color, + onValueChange, +}: { + value: boolean; + disabled?: boolean; + color?: keyof IThemeColors; + onValueChange?: () => void; +}): ReactElement => { + const height = useSharedValue(defaultHeight); + const width = useSharedValue(defaultWidth); + const sharedValue = useDerivedValue(() => { + return value ? 1 : 0; + }); + + const thumbColor = disabled ? '#A0A0A0' : colors.white; + const trackColor = color ? colors[color] : colors.brand; + const trackColors = { on: trackColor, off: '#3A3A3C' }; + + const trackAnimatedStyle = useAnimatedStyle(() => { + const animatedColor = interpolateColor( + sharedValue.value, + [0, 1], + [trackColors.off, trackColors.on], + ); + const colorValue = withTiming(animatedColor, { + duration, + easing: Easing.inOut(Easing.ease), + }); + + return { + backgroundColor: colorValue, + borderRadius: height.value / 2, + }; + }); + + const thumbAnimatedStyle = useAnimatedStyle(() => { + const moveValue = interpolate( + sharedValue.value, + [0, 1], + [0, width.value - height.value], + ); + const translateValue = withTiming(moveValue, { + duration, + easing: Easing.bezier(0.61, 0.46, 0.3, 1.07), + }); + + return { + transform: [{ translateX: translateValue }], + borderRadius: height.value / 2, + }; + }); + + const onPress = (): void => { + if (!disabled) { + onValueChange?.(); + } + }; + + return ( + + { + height.value = e.nativeEvent.layout.height; + width.value = e.nativeEvent.layout.width; + }}> + + + + ); +}; + +const styles = StyleSheet.create({ + track: { + alignItems: 'flex-start', + height: defaultHeight, + width: defaultWidth, + padding: 4, + }, + thumb: { + height: '100%', + aspectRatio: 1, + }, +}); + +export default Switch; diff --git a/src/components/SwitchRow.tsx b/src/components/SwitchRow.tsx index 2cf1ac54c..8b62569b6 100644 --- a/src/components/SwitchRow.tsx +++ b/src/components/SwitchRow.tsx @@ -6,8 +6,8 @@ import { View, ViewStyle, } from 'react-native'; -import { Switch } from '../styles/components'; import { IThemeColors } from '../styles/themes'; +import Switch from '../components/Switch'; const SwitchRow = ({ children, diff --git a/src/screens/Settings/PIN/AskForBiometrics.tsx b/src/screens/Settings/PIN/AskForBiometrics.tsx index 343799153..239f49f52 100644 --- a/src/screens/Settings/PIN/AskForBiometrics.tsx +++ b/src/screens/Settings/PIN/AskForBiometrics.tsx @@ -15,13 +15,13 @@ import { } from 'react-native'; import { useTranslation } from 'react-i18next'; -import { Switch } from '../../../styles/components'; import { BodyMSB, BodyM } from '../../../styles/text'; import { FaceIdIcon, TouchIdIcon } from '../../../styles/icons'; import BottomSheetNavigationHeader from '../../../components/BottomSheetNavigationHeader'; import SafeAreaInset from '../../../components/SafeAreaInset'; import GradientView from '../../../components/GradientView'; import Button from '../../../components/buttons/Button'; +import Switch from '../../../components/Switch'; import { IsSensorAvailableResult } from '../../../components/Biometrics'; import { useAppDispatch } from '../../../hooks/redux'; import { useBottomSheetScreenBackPress } from '../../../hooks/bottomSheet'; diff --git a/src/screens/Settings/PIN/Result.tsx b/src/screens/Settings/PIN/Result.tsx index 3a3c8c057..8a9518121 100644 --- a/src/screens/Settings/PIN/Result.tsx +++ b/src/screens/Settings/PIN/Result.tsx @@ -2,12 +2,12 @@ import React, { memo, ReactElement, useMemo } from 'react'; import { StyleSheet, View, Pressable, Image } from 'react-native'; import { useTranslation } from 'react-i18next'; -import { Switch } from '../../../styles/components'; import { BodyM, BodyMSB } from '../../../styles/text'; import BottomSheetNavigationHeader from '../../../components/BottomSheetNavigationHeader'; import SafeAreaInset from '../../../components/SafeAreaInset'; import GradientView from '../../../components/GradientView'; import Button from '../../../components/buttons/Button'; +import Switch from '../../../components/Switch'; import { useAppDispatch, useAppSelector } from '../../../hooks/redux'; import { useBottomSheetScreenBackPress } from '../../../hooks/bottomSheet'; import { closeSheet } from '../../../store/slices/ui'; diff --git a/src/screens/Wallets/Send/CoinSelection.tsx b/src/screens/Wallets/Send/CoinSelection.tsx index 6f363a493..365af426b 100644 --- a/src/screens/Wallets/Send/CoinSelection.tsx +++ b/src/screens/Wallets/Send/CoinSelection.tsx @@ -3,12 +3,13 @@ import { StyleSheet, View } from 'react-native'; import { BottomSheetScrollView } from '@gorhom/bottom-sheet'; import { useTranslation } from 'react-i18next'; -import { ScrollView, Switch } from '../../../styles/components'; +import { ScrollView } from '../../../styles/components'; import { Subtitle, BodyMSB, BodySSB, Caption13Up } from '../../../styles/text'; import GradientView from '../../../components/GradientView'; import BottomSheetNavigationHeader from '../../../components/BottomSheetNavigationHeader'; import SafeAreaInset from '../../../components/SafeAreaInset'; import Button from '../../../components/buttons/Button'; +import Switch from '../../../components/Switch'; import Tag from '../../../components/Tag'; import useColors from '../../../hooks/colors'; diff --git a/src/styles/components.ts b/src/styles/components.ts index 3d45f705c..672f24b5d 100644 --- a/src/styles/components.ts +++ b/src/styles/components.ts @@ -2,7 +2,6 @@ import { ColorValue, Platform, PressableProps, - Switch as RNSwitch, ScrollViewProps, TouchableOpacity as RNTouchableOpacity, TouchableHighlight as RNTouchableHighlight, @@ -12,7 +11,6 @@ import { ViewProps, TextInput as RNTextInput, TextInputProps as RNTextInputProps, - SwitchProps, } from 'react-native'; import Color from 'color'; import Animated, { AnimatedProps } from 'react-native-reanimated'; @@ -126,20 +124,6 @@ export const Pressable = styled(RNPressable)( }), ); -export const Switch = styled(RNSwitch).attrs( - (props) => ({ - trackColor: { - false: '#3A3A3C', - true: props.color - ? props.theme.colors[props.color] - : props.theme.colors.brand, - }, - thumbColor: props.disabled ? '#A0A0A0' : 'white', - ios_backgroundColor: '#3A3A3C', - ...props, - }), -)(() => ({})); - export const TextInput = styled(RNTextInput).attrs((props) => ({ keyboardAppearance: props.theme.id, selectionColor: colors.brand,