import React, { FC, useContext, useRef } from 'react'; import { Animated, StyleSheet, TouchableOpacity, TouchableOpacityProps, View, ViewStyle, GestureResponderEvent, } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import { useComponentId } from '../Application'; import { ApplicationContext, ComponentContext, MiniAppContext, SkeletonContext, } from '../Context'; import { Text } from '../Text'; import { Typography } from '../Text/types'; import { Colors, Spacing } from '../Consts'; import styles from './styles'; import { Icon } from '../Icon'; import { Skeleton } from '../Skeleton'; import LottieView from 'lottie-react-native'; import Reanimated, { useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; const AnimationLinear = Animated.createAnimatedComponent(LinearGradient); export interface ButtonProps extends TouchableOpacityProps { /** * Defines the visual style of the button. */ type?: | 'primary' | 'secondary' | 'tonal' | 'outline' | 'danger' | 'text' | 'disabled'; /** * Defines the size of the button. */ size?: 'large' | 'medium' | 'small'; full?: boolean; iconRight?: string; iconLeft?: string; title: string; tintColor?: string; loading?: boolean; onPress: () => void; params?: any; } const Button: FC = ({ type = 'primary', size = 'large', tintColor = null, full = true, iconRight, iconLeft, loading = false, title = 'Button', params, accessibilityState, ...rest }) => { const { theme, config } = useContext(ApplicationContext); const context = useContext(MiniAppContext); const skeleton = useContext(SkeletonContext); const animationRef = useRef(null); const pressAnim = useSharedValue(0); const showBaseLineDebug = context?.features?.showBaseLineDebug ?? false; const componentName = 'Button'; const { componentId } = useComponentId( `${componentName}/${title}`, rest.accessibilityLabel, ); /** * animate press in (Reanimated) */ const animateIn = (event: GestureResponderEvent) => { rest?.onPressIn?.(event); pressAnim.value = withTiming(1, { duration: 100 }); }; /** * animate press out (Reanimated) */ const animateOut = (event: GestureResponderEvent) => { rest?.onPressOut?.(event); pressAnim.value = withTiming(0, { duration: 100 }); }; /** * determine loading position * @param left * @param right * @param isLeft */ const shouldShowLoading = ( left?: string, right?: string, isLeft?: boolean, ): boolean => { const hasLeft = !!left; const hasRight = !!right; const loadingOnLeft = (!hasLeft && !hasRight) || (hasLeft && !hasRight); return isLeft === loadingOnLeft; }; /** * export size style */ const getSizeStyle = () => { const styleSheet: { [key: string]: any } = styles; return styleSheet[size ?? 'small']; }; /** * export type style */ const getTypeStyle = () => { switch (type) { case 'disabled': return { backgroundColor: theme.colors.background.disable, }; case 'primary': return { backgroundColor: theme.colors.primary }; case 'secondary': return { backgroundColor: theme.colors.background.surface, borderWidth: 1, borderColor: theme.colors.border.default, }; case 'outline': return { backgroundColor: theme.colors.background.surface, borderWidth: 1, borderColor: color ?? theme.colors.primary, }; case 'tonal': return { backgroundColor: theme.colors.background.tonal, }; case 'danger': return { backgroundColor: theme.colors.error.primary, }; case 'text': return {}; default: return { backgroundColor: theme.colors.primary }; } }; /** * export icon size */ const getIconSize = () => { switch (size) { case 'large': return 24; case 'medium': case 'small': return 16; default: return 24; } }; /** * export icon space */ const getIconSpace = () => { switch (size) { case 'large': case 'medium': return 8; case 'small': return 4; default: return 8; } }; /** * export typography style */ const getTypography = (): Typography => { switch (size) { case 'large': return 'action_default_bold'; case 'medium': return 'action_s_bold'; case 'small': return 'action_xs_bold'; default: return 'action_default_bold'; } }; /** * export text color */ const getTextColor = (): string => { switch (type) { case 'disabled': return theme.colors.text.disable; case 'primary': return Colors.black_01; case 'secondary': return theme.colors.text.default; case 'outline': return color ?? theme.colors.primary; case 'tonal': return theme.colors.primary; case 'danger': return Colors.black_01; case 'text': return color ?? theme.colors.primary; default: return Colors.black_01; } }; /** * render title */ const renderTitle = () => { const typography = getTypography(); const textColor = getTextColor(); return ( {title} ); }; /** * render icon * @param position */ const renderIcon = (position: 'left' | 'right') => { const iconSize = getIconSize(); const space = getIconSpace(); const textColor = tintColor ?? getTextColor(); const isLeft = position === 'left'; const icon = isLeft ? iconLeft : iconRight; const style = [ isLeft ? styles.leading : styles.trailing, { width: iconSize, height: iconSize, marginRight: isLeft ? space : 0, marginLeft: isLeft ? 0 : space, }, ]; const showLoading = shouldShowLoading(iconLeft, iconRight, isLeft); if (loading && showLoading) { return ( ); } if (icon) { return ( ); } return null; }; const { gradient, color } = config?.navigationBar?.buttonColors ?? {}; const isDisabled = type === 'disabled' || loading; let gradientPros; if (gradient?.length > 0 && type === 'primary') { gradientPros = { colors: gradient, start: { x: 1, y: 0.5 }, end: { x: 0, y: 0.5 }, }; } const sizeStyle = getSizeStyle(); const typeStyle = getTypeStyle(); const containerStyle: ViewStyle = StyleSheet.flatten([ full && { width: '100%' }, loading && { opacity: 0.75 }, ]); const buttonStyle = StyleSheet.flatten([sizeStyle, typeStyle]); const animatedStyle = useAnimatedStyle(() => { return { marginHorizontal: pressAnim.value * 2, }; }); if (skeleton?.loading) { return ; } return ( {renderIcon('left')} {renderTitle()} {renderIcon('right')} {gradientPros && ( )} {gradientPros && } ); }; export { Button };