diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index eebd6ad532d4..0fac30a26430 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -777,10 +777,35 @@ PODS:
- React-Core
- RNReactNativeHapticFeedback (1.14.0):
- React-Core
- - RNReanimated (3.6.1):
- - RCT-Folly (= 2021.07.22.00)
+ - RNReanimated (3.5.4):
+ - DoubleConversion
+ - FBLazyVector
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCTRequired
+ - RCTTypeSafety
+ - React-callinvoker
- React-Core
+ - React-Core/DevSupport
+ - React-Core/RCTWebSocket
+ - React-CoreModules
+ - React-cxxreact
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-RCTActionSheet
+ - React-RCTAnimation
+ - React-RCTAppDelegate
+ - React-RCTBlob
+ - React-RCTImage
+ - React-RCTLinking
+ - React-RCTNetwork
+ - React-RCTSettings
+ - React-RCTText
- ReactCommon/turbomodule/core
+ - Yoga
- RNScreens (3.21.0):
- React-Core
- React-RCTImage
@@ -1255,7 +1280,7 @@ SPEC CHECKSUMS:
rnmapbox-maps: 6f638ec002aa6e906a6f766d69cd45f968d98e64
RNPermissions: 9b086c8f05b2e2faa587fdc31f4c5ab4509728aa
RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c
- RNReanimated: fdbaa9c964bbab7fac50c90862b6cc5f041679b9
+ RNReanimated: ab2e96c6d5591c3dfbb38a464f54c8d17fb34a87
RNScreens: d037903436160a4b039d32606668350d2a808806
RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
diff --git a/package-lock.json b/package-lock.json
index 1f6496c08566..2d58e7cc7f4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -100,7 +100,7 @@
"react-native-plaid-link-sdk": "10.8.0",
"react-native-qrcode-svg": "^6.2.0",
"react-native-quick-sqlite": "^8.0.0-beta.2",
- "react-native-reanimated": "^3.6.1",
+ "react-native-reanimated": "3.5.4",
"react-native-render-html": "6.3.1",
"react-native-safe-area-context": "4.4.1",
"react-native-screens": "3.21.0",
@@ -44555,9 +44555,9 @@
}
},
"node_modules/react-native-reanimated": {
- "version": "3.6.1",
- "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.1.tgz",
- "integrity": "sha512-F4vG9Yf9PKmE3GaWtVGUpzj3SM6YY2cx1yRHCwiMd1uY7W0gU017LfcVUorboJnj0y5QZqEriEK1Usq2Y8YZqg==",
+ "version": "3.5.4",
+ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.5.4.tgz",
+ "integrity": "sha512-8we9LLDO1o4Oj9/DICeEJ2K1tjfqkJagqQUglxeUAkol/HcEJ6PGxIrpBcNryLqCDYEcu6FZWld/FzizBIw6bg==",
"dependencies": {
"@babel/plugin-transform-object-assign": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
@@ -84872,9 +84872,9 @@
"requires": {}
},
"react-native-reanimated": {
- "version": "3.6.1",
- "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.6.1.tgz",
- "integrity": "sha512-F4vG9Yf9PKmE3GaWtVGUpzj3SM6YY2cx1yRHCwiMd1uY7W0gU017LfcVUorboJnj0y5QZqEriEK1Usq2Y8YZqg==",
+ "version": "3.5.4",
+ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.5.4.tgz",
+ "integrity": "sha512-8we9LLDO1o4Oj9/DICeEJ2K1tjfqkJagqQUglxeUAkol/HcEJ6PGxIrpBcNryLqCDYEcu6FZWld/FzizBIw6bg==",
"requires": {
"@babel/plugin-transform-object-assign": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
diff --git a/package.json b/package.json
index d11a8c69f8f9..d4726491e36e 100644
--- a/package.json
+++ b/package.json
@@ -148,7 +148,7 @@
"react-native-plaid-link-sdk": "10.8.0",
"react-native-qrcode-svg": "^6.2.0",
"react-native-quick-sqlite": "^8.0.0-beta.2",
- "react-native-reanimated": "^3.6.1",
+ "react-native-reanimated": "3.5.4",
"react-native-render-html": "6.3.1",
"react-native-safe-area-context": "4.4.1",
"react-native-screens": "3.21.0",
diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js
new file mode 100644
index 000000000000..791eb150f8c9
--- /dev/null
+++ b/src/components/FloatingActionButton.js
@@ -0,0 +1,132 @@
+import PropTypes from 'prop-types';
+import React, {PureComponent} from 'react';
+import {Animated, Easing, View} from 'react-native';
+import compose from '@libs/compose';
+import Icon from './Icon';
+import * as Expensicons from './Icon/Expensicons';
+import PressableWithFeedback from './Pressable/PressableWithFeedback';
+import Tooltip from './Tooltip/PopoverAnchorTooltip';
+import withLocalize, {withLocalizePropTypes} from './withLocalize';
+import withStyleUtils, {withStyleUtilsPropTypes} from './withStyleUtils';
+import withTheme, {withThemePropTypes} from './withTheme';
+import withThemeStyles, {withThemeStylesPropTypes} from './withThemeStyles';
+
+const AnimatedIcon = Animated.createAnimatedComponent(Icon);
+AnimatedIcon.displayName = 'AnimatedIcon';
+
+const AnimatedPressable = Animated.createAnimatedComponent(PressableWithFeedback);
+AnimatedPressable.displayName = 'AnimatedPressable';
+
+const propTypes = {
+ // Callback to fire on request to toggle the FloatingActionButton
+ onPress: PropTypes.func.isRequired,
+
+ // Current state (active or not active) of the component
+ isActive: PropTypes.bool.isRequired,
+
+ // Ref for the button
+ buttonRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+
+ ...withLocalizePropTypes,
+ ...withThemePropTypes,
+ ...withThemeStylesPropTypes,
+ ...withStyleUtilsPropTypes,
+};
+
+const defaultProps = {
+ buttonRef: () => {},
+};
+
+class FloatingActionButton extends PureComponent {
+ constructor(props) {
+ super(props);
+ this.animatedValue = new Animated.Value(props.isActive ? 1 : 0);
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.isActive === this.props.isActive) {
+ return;
+ }
+
+ this.animateFloatingActionButton();
+ }
+
+ /**
+ * Animates the floating action button
+ * Method is called when the isActive prop changes
+ */
+ animateFloatingActionButton() {
+ const animationFinalValue = this.props.isActive ? 1 : 0;
+
+ Animated.timing(this.animatedValue, {
+ toValue: animationFinalValue,
+ duration: 340,
+ easing: Easing.inOut(Easing.ease),
+ useNativeDriver: false,
+ }).start();
+ }
+
+ render() {
+ const rotate = this.animatedValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: ['0deg', '135deg'],
+ });
+
+ const backgroundColor = this.animatedValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [this.props.theme.success, this.props.theme.buttonDefaultBG],
+ });
+
+ const fill = this.animatedValue.interpolate({
+ inputRange: [0, 1],
+ outputRange: [this.props.theme.textLight, this.props.theme.textDark],
+ });
+
+ return (
+
+
+ {
+ this.fabPressable = el;
+ if (this.props.buttonRef) {
+ this.props.buttonRef.current = el;
+ }
+ }}
+ accessibilityLabel={this.props.accessibilityLabel}
+ role={this.props.role}
+ pressDimmingValue={1}
+ onPress={(e) => {
+ // Drop focus to avoid blue focus ring.
+ this.fabPressable.blur();
+ this.props.onPress(e);
+ }}
+ onLongPress={() => {}}
+ style={[this.props.themeStyles.floatingActionButton, this.props.StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]}
+ >
+
+
+
+
+ );
+ }
+}
+
+FloatingActionButton.propTypes = propTypes;
+FloatingActionButton.defaultProps = defaultProps;
+
+const FloatingActionButtonWithLocalize = withLocalize(FloatingActionButton);
+
+const FloatingActionButtonWithLocalizeWithRef = React.forwardRef((props, ref) => (
+
+));
+
+FloatingActionButtonWithLocalizeWithRef.displayName = 'FloatingActionButtonWithLocalizeWithRef';
+
+export default compose(withThemeStyles, withTheme, withStyleUtils)(FloatingActionButtonWithLocalizeWithRef);
diff --git a/src/components/FloatingActionButton/FabPlusIcon.js b/src/components/FloatingActionButton/FabPlusIcon.js
deleted file mode 100644
index 09afa00f119d..000000000000
--- a/src/components/FloatingActionButton/FabPlusIcon.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import PropTypes from 'prop-types';
-import React, {useEffect} from 'react';
-import Animated, {Easing, interpolateColor, useAnimatedProps, useSharedValue, withTiming} from 'react-native-reanimated';
-import Svg, {Path} from 'react-native-svg';
-import useTheme from '@hooks/useTheme';
-
-const AnimatedPath = Animated.createAnimatedComponent(Path);
-
-const propTypes = {
- /* Current state (active or not active) of the component */
- isActive: PropTypes.bool.isRequired,
-};
-
-function FabPlusIcon({isActive}) {
- const theme = useTheme();
- const animatedValue = useSharedValue(isActive ? 1 : 0);
-
- useEffect(() => {
- animatedValue.value = withTiming(isActive ? 1 : 0, {
- duration: 340,
- easing: Easing.inOut(Easing.ease),
- });
- }, [isActive, animatedValue]);
-
- const animatedProps = useAnimatedProps(() => {
- const fill = interpolateColor(animatedValue.value, [0, 1], [theme.textLight, theme.textDark]);
-
- return {
- fill,
- };
- });
-
- return (
-
- );
-}
-
-FabPlusIcon.propTypes = propTypes;
-FabPlusIcon.displayName = 'FabPlusIcon';
-
-export default FabPlusIcon;
diff --git a/src/components/FloatingActionButton/index.js b/src/components/FloatingActionButton/index.js
deleted file mode 100644
index d341396c44b7..000000000000
--- a/src/components/FloatingActionButton/index.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import PropTypes from 'prop-types';
-import React, {useEffect, useRef} from 'react';
-import {View} from 'react-native';
-import Animated, {Easing, interpolateColor, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
-import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
-import Tooltip from '@components/Tooltip/PopoverAnchorTooltip';
-import useLocalize from '@hooks/useLocalize';
-import useTheme from '@hooks/useTheme';
-import useThemeStyles from '@hooks/useThemeStyles';
-import FabPlusIcon from './FabPlusIcon';
-
-const AnimatedPressable = Animated.createAnimatedComponent(PressableWithFeedback);
-AnimatedPressable.displayName = 'AnimatedPressable';
-
-const propTypes = {
- /* Callback to fire on request to toggle the FloatingActionButton */
- onPress: PropTypes.func.isRequired,
-
- /* Current state (active or not active) of the component */
- isActive: PropTypes.bool.isRequired,
-
- /* An accessibility label for the button */
- accessibilityLabel: PropTypes.string.isRequired,
-
- /* An accessibility role for the button */
- role: PropTypes.string.isRequired,
-};
-
-const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibilityLabel, role}, ref) => {
- const theme = useTheme();
- const styles = useThemeStyles();
- const {translate} = useLocalize();
- const fabPressable = useRef(null);
- const animatedValue = useSharedValue(isActive ? 1 : 0);
- const buttonRef = ref;
-
- useEffect(() => {
- animatedValue.value = withTiming(isActive ? 1 : 0, {
- duration: 340,
- easing: Easing.inOut(Easing.ease),
- });
- }, [isActive, animatedValue]);
-
- const animatedStyle = useAnimatedStyle(() => {
- const backgroundColor = interpolateColor(animatedValue.value, [0, 1], [theme.success, theme.buttonDefaultBG]);
-
- return {
- transform: [{rotate: `${animatedValue.value * 135}deg`}],
- backgroundColor,
- borderRadius: styles.floatingActionButton.borderRadius,
- };
- });
-
- return (
-
-
- {
- fabPressable.current = el;
- if (buttonRef) {
- buttonRef.current = el;
- }
- }}
- accessibilityLabel={accessibilityLabel}
- role={role}
- pressDimmingValue={1}
- onPress={(e) => {
- // Drop focus to avoid blue focus ring.
- fabPressable.current.blur();
- onPress(e);
- }}
- onLongPress={() => {}}
- style={[styles.floatingActionButton, animatedStyle]}
- >
-
-
-
-
- );
-});
-
-FloatingActionButton.propTypes = propTypes;
-FloatingActionButton.displayName = 'FloatingActionButton';
-
-export default FloatingActionButton;
diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx
index 932ea17541b9..59b1639dce33 100644
--- a/src/components/Icon/index.tsx
+++ b/src/components/Icon/index.tsx
@@ -1,8 +1,8 @@
-import React from 'react';
+import React, {PureComponent} from 'react';
import {StyleProp, View, ViewStyle} from 'react-native';
-import useStyleUtils from '@hooks/useStyleUtils';
-import useTheme from '@hooks/useTheme';
-import useThemeStyles from '@hooks/useThemeStyles';
+import withStyleUtils, {WithStyleUtilsProps} from '@components/withStyleUtils';
+import withTheme, {WithThemeProps} from '@components/withTheme';
+import withThemeStyles, {type WithThemeStylesProps} from '@components/withThemeStyles';
import variables from '@styles/variables';
import IconWrapperStyles from './IconWrapperStyles';
@@ -41,65 +41,67 @@ type IconProps = {
/** Additional styles to add to the Icon */
additionalStyles?: StyleProp;
-};
+} & WithThemeStylesProps &
+ WithThemeProps &
+ WithStyleUtilsProps;
+
+// We must use a class component to create an animatable component with the Animated API
+// eslint-disable-next-line react/prefer-stateless-function
+class Icon extends PureComponent {
+ // eslint-disable-next-line react/static-property-placement
+ public static defaultProps = {
+ width: variables.iconSizeNormal,
+ height: variables.iconSizeNormal,
+ fill: undefined,
+ small: false,
+ inline: false,
+ additionalStyles: [],
+ hovered: false,
+ pressed: false,
+ };
+
+ render() {
+ const width = this.props.small ? variables.iconSizeSmall : this.props.width;
+ const height = this.props.small ? variables.iconSizeSmall : this.props.height;
+ const iconStyles = [this.props.StyleUtils.getWidthAndHeightStyle(width ?? 0, height), IconWrapperStyles, this.props.themeStyles.pAbsolute, this.props.additionalStyles];
+ const fill = this.props.fill ?? this.props.theme.icon;
+
+ if (this.props.inline) {
+ return (
+
+
+
+
+
+ );
+ }
-function Icon({
- src,
- width = variables.iconSizeNormal,
- height = variables.iconSizeNormal,
- fill = undefined,
- small = false,
- inline = false,
- hovered = false,
- pressed = false,
- additionalStyles = [],
-}: IconProps) {
- const theme = useTheme();
- const StyleUtils = useStyleUtils();
- const styles = useThemeStyles();
- const iconWidth = small ? variables.iconSizeSmall : width;
- const iconHeight = small ? variables.iconSizeSmall : height;
- const iconStyles = [StyleUtils.getWidthAndHeightStyle(width ?? 0, height), IconWrapperStyles, styles.pAbsolute, additionalStyles];
- const iconFill = fill ?? theme.icon;
- const IconComponent = src;
-
- if (inline) {
return (
-
-
-
+
);
}
-
- return (
-
-
-
- );
}
-Icon.displayName = 'Icon';
-
export type {SrcProps};
-export default Icon;
+export default withTheme(withThemeStyles(withStyleUtils(Icon)));
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index 6f3d55e5acb0..de87d2b5dd59 100644
--- a/src/styles/utils/index.ts
+++ b/src/styles/utils/index.ts
@@ -446,6 +446,13 @@ function getBackgroundColorWithOpacityStyle(backgroundColor: string, opacity: nu
return {};
}
+function getAnimatedFABStyle(rotate: Animated.Value, backgroundColor: Animated.Value): Animated.WithAnimatedValue {
+ return {
+ transform: [{rotate}],
+ backgroundColor,
+ };
+}
+
function getWidthAndHeightStyle(width: number, height?: number): ViewStyle {
return {
width,
@@ -1008,6 +1015,7 @@ const staticStyleUtils = {
combineStyles,
displayIfTrue,
getAmountFontSizeAndLineHeight,
+ getAnimatedFABStyle,
getAutoCompleteSuggestionContainerStyle,
getAvatarBorderRadius,
getAvatarBorderStyle,