From 9bf49987848b6a49362e9bda5dfdbea781f6232a Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:40:12 -0500 Subject: [PATCH] Add react-native-turbo-haptics (#6264) --- ios/Podfile.lock | 25 +++++++++++++++ package.json | 1 + .../Swap/components/SearchInputButton.tsx | 32 ++++++++----------- .../Swap/components/SwapBottomPanel.tsx | 14 ++------ .../screens/Swap/components/SwapSlider.tsx | 8 ++--- .../Swap/hooks/useSwapInputsController.ts | 4 +-- .../points/components/AnimatedText.tsx | 8 ++--- src/screens/points/constants.ts | 4 --- yarn.lock | 11 +++++++ 9 files changed, 64 insertions(+), 43 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index af7eb5f5407..626b2054726 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1876,6 +1876,27 @@ PODS: - TOCropViewController (2.7.4) - ToolTipMenu (5.2.1): - React + - TurboHaptics (1.0.4): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - VisionCamera (4.4.2): - VisionCamera/Core (= 4.4.2) - VisionCamera/React (= 4.4.2) @@ -2035,6 +2056,7 @@ DEPENDENCIES: - TcpSockets (from `../node_modules/react-native-tcp`) - TOCropViewController (~> 2.7.4) - ToolTipMenu (from `../node_modules/react-native-tooltip`) + - TurboHaptics (from `../node_modules/react-native-turbo-haptics`) - VisionCamera (from `../node_modules/react-native-vision-camera`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -2345,6 +2367,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-tcp" ToolTipMenu: :path: "../node_modules/react-native-tooltip" + TurboHaptics: + :path: "../node_modules/react-native-turbo-haptics" VisionCamera: :path: "../node_modules/react-native-vision-camera" Yoga: @@ -2524,6 +2548,7 @@ SPEC CHECKSUMS: TcpSockets: bd31674146c0931a064fc254a59812dfd1a73ae0 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 ToolTipMenu: 8ac61aded0fbc4acfe7e84a7d0c9479d15a9a382 + TurboHaptics: 6381613d33ab97aeb30d9b15c3df94dc616a25e4 VisionCamera: 2af28201c3de77245f8c58b7a5274d5979df70df Yoga: 04f1db30bb810187397fa4c37dd1868a27af229c diff --git a/package.json b/package.json index bf21af4bac6..ec053d9d264 100644 --- a/package.json +++ b/package.json @@ -272,6 +272,7 @@ "react-native-text-size": "rainbow-me/react-native-text-size#15b21c9f88c6df0d1b5e0f2ba792fe59b5dc255a", "react-native-tooltip": "rainbow-me/react-native-tooltip#e0e88d212b5b7f350e5eabba87f588a32e0f2590", "react-native-tooltips": "rainbow-me/react-native-tooltips#fdafbc7ba33ee231229f5d3f58b29d0d1c55ddfa", + "react-native-turbo-haptics": "1.0.4", "react-native-udp": "2.7.0", "react-native-url-polyfill": "2.0.0", "react-native-version-number": "0.3.6", diff --git a/src/__swaps__/screens/Swap/components/SearchInputButton.tsx b/src/__swaps__/screens/Swap/components/SearchInputButton.tsx index aa89ce65023..9e2b328fa64 100644 --- a/src/__swaps__/screens/Swap/components/SearchInputButton.tsx +++ b/src/__swaps__/screens/Swap/components/SearchInputButton.tsx @@ -2,6 +2,7 @@ import React, { useCallback } from 'react'; import { GestureHandlerButton } from './GestureHandlerButton'; import { AnimatedText, Box } from '@/design-system'; import Animated, { SharedValue, runOnJS, useAnimatedStyle, useDerivedValue, withTiming } from 'react-native-reanimated'; +import { triggerHaptics } from 'react-native-turbo-haptics'; import { NavigationSteps, useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; import Clipboard from '@react-native-clipboard/clipboard'; import { useSwapsStore } from '@/state/swaps/swapsStore'; @@ -9,7 +10,6 @@ import * as i18n from '@/languages'; import { THICK_BORDER_WIDTH } from '../constants'; import { useClipboard } from '@/hooks'; import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; -import { triggerHapticFeedback } from '@/screens/points/constants'; import { IS_ANDROID } from '@/env'; const CANCEL_LABEL = i18n.t(i18n.l.button.cancel); @@ -54,22 +54,14 @@ export const SearchInputButton = ({ return PASTE_LABEL; }); - const onPaste = useCallback( - (isPasteDisabled: boolean) => { - if (isPasteDisabled) { - triggerHapticFeedback('notificationError'); - return; - } - - Clipboard.getString().then(text => { - // Slice the pasted text to the length of an ETH address - const v = text.trim().slice(0, 42); - pastedSearchInputValue.value = v; - useSwapsStore.setState({ outputSearchQuery: v }); - }); - }, - [pastedSearchInputValue] - ); + const onPaste = useCallback(() => { + Clipboard.getString().then(text => { + // Slice the pasted text to the length of an ETH address + const v = text.trim().slice(0, 42); + pastedSearchInputValue.value = v; + useSwapsStore.setState({ outputSearchQuery: v }); + }); + }, [pastedSearchInputValue]); const buttonInfo = useDerivedValue(() => { const isInputSearchFocused = inputProgress.value === NavigationSteps.SEARCH_FOCUSED; @@ -106,7 +98,11 @@ export const SearchInputButton = ({ onPressWorklet={() => { 'worklet'; if (output && outputProgress.value === NavigationSteps.TOKEN_LIST_FOCUSED && !internalSelectedOutputAsset.value) { - runOnJS(onPaste)(buttonInfo.value.isPasteDisabled); + if (buttonInfo.value.isPasteDisabled) { + triggerHaptics('notificationError'); + } else { + runOnJS(onPaste)(); + } } if (isSearchFocused.value || (output && internalSelectedOutputAsset.value) || (!output && internalSelectedInputAsset.value)) { diff --git a/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx b/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx index 1bb554d9617..5f85f146be7 100644 --- a/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx +++ b/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx @@ -5,15 +5,8 @@ import { Box, Separator, globalColors, useColorMode } from '@/design-system'; import React from 'react'; import { StyleSheet } from 'react-native'; import { PanGestureHandler } from 'react-native-gesture-handler'; -import Animated, { - Easing, - runOnJS, - useAnimatedStyle, - useDerivedValue, - useSharedValue, - withSpring, - withTiming, -} from 'react-native-reanimated'; +import Animated, { Easing, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring, withTiming } from 'react-native-reanimated'; +import { triggerHaptics } from 'react-native-turbo-haptics'; import { useBottomPanelGestureHandler } from '../hooks/useBottomPanelGestureHandler'; import { GasButton } from './GasButton'; import { GasPanel } from './GasPanel'; @@ -21,7 +14,6 @@ import { ReviewPanel } from './ReviewPanel'; import { SwapActionButton } from './SwapActionButton'; import { SettingsPanel } from './SettingsPanel'; import { SPRING_CONFIGS } from '@/components/animations/animationConfigs'; -import { triggerHapticFeedback } from '@/screens/points/constants'; const HOLD_TO_SWAP_DURATION_MS = 400; @@ -123,7 +115,7 @@ export function SwapBottomPanel() { onLongPressWorklet={() => { 'worklet'; if (type.value === 'hold') { - runOnJS(triggerHapticFeedback)('notificationSuccess'); + triggerHaptics('notificationSuccess'); SwapNavigation.handleSwapAction(); } }} diff --git a/src/__swaps__/screens/Swap/components/SwapSlider.tsx b/src/__swaps__/screens/Swap/components/SwapSlider.tsx index a845fb8b25c..572f9eb6e80 100644 --- a/src/__swaps__/screens/Swap/components/SwapSlider.tsx +++ b/src/__swaps__/screens/Swap/components/SwapSlider.tsx @@ -18,10 +18,10 @@ import Animated, { withSpring, withTiming, } from 'react-native-reanimated'; +import { triggerHaptics } from 'react-native-turbo-haptics'; import { SPRING_CONFIGS, TIMING_CONFIGS } from '@/components/animations/animationConfigs'; import { AnimatedText, Bleed, Box, Column, Columns, Inline, globalColors, useColorMode, useForegroundColor } from '@/design-system'; import { IS_IOS } from '@/env'; -import { triggerHapticFeedback } from '@/screens/points/constants'; import { greaterThanWorklet } from '@/safe-math/SafeMath'; import { SCRUBBER_WIDTH, @@ -124,10 +124,10 @@ export const SwapSlider = ({ (current, previous) => { if (current !== previous && SwapInputController.inputMethod.value === 'slider') { if (current.x >= width * 0.995 && previous?.x && previous?.x < width * 0.995) { - runOnJS(triggerHapticFeedback)('impactMedium'); + triggerHaptics('impactMedium'); } if (current.x < width * 0.005 && previous?.x && previous?.x >= width * 0.005) { - runOnJS(triggerHapticFeedback)('impactLight'); + triggerHaptics('impactLight'); } } }, @@ -160,7 +160,7 @@ export const SwapSlider = ({ ctx.exceedsMax = true; isQuoteStale.value = 1; sliderXPosition.value = width * 0.999; - runOnJS(triggerHapticFeedback)('impactMedium'); + triggerHaptics('impactMedium'); } } diff --git a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts index 6472a89fbf7..68be42a67f5 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts @@ -32,11 +32,11 @@ import { externalTokenQueryKey, fetchExternalToken, } from '@/resources/assets/externalAssetsQuery'; -import { triggerHapticFeedback } from '@/screens/points/constants'; import { swapsStore } from '@/state/swaps/swapsStore'; import { CrosschainQuote, Quote, QuoteError, getCrosschainQuote, getQuote } from '@rainbow-me/swaps'; import { useCallback } from 'react'; import { SharedValue, runOnJS, runOnUI, useAnimatedReaction, useDerivedValue, useSharedValue, withSpring } from 'react-native-reanimated'; +import { triggerHaptics } from 'react-native-turbo-haptics'; import { useDebouncedCallback } from 'use-debounce'; import { NavigationSteps } from './useSwapNavigation'; import { deepEqualWorklet } from '@/worklets/comparisons'; @@ -584,7 +584,7 @@ export function useSwapInputsController({ const exceedsMax = maxSwappableAmount ? greaterThanWorklet(currentInputValue, maxSwappableAmount) : false; if (isAlreadyMax) { - runOnJS(triggerHapticFeedback)('impactMedium'); + triggerHaptics('impactMedium'); } else { quoteFetchingInterval.stop(); diff --git a/src/screens/points/components/AnimatedText.tsx b/src/screens/points/components/AnimatedText.tsx index 87aa1e7a2fc..4e1980357ce 100644 --- a/src/screens/points/components/AnimatedText.tsx +++ b/src/screens/points/components/AnimatedText.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { HapticFeedbackType } from '@/utils/haptics'; import { Bleed } from '@/design-system'; import { Text as RNText, StyleSheet } from 'react-native'; +import { HapticType, triggerHaptics } from 'react-native-turbo-haptics'; import { useTheme } from '@/theme'; import { useAnimationContext } from '../contexts/AnimationContext'; import { @@ -14,7 +14,7 @@ import { withSequence, withTiming, } from 'react-native-reanimated'; -import { generateRainbowColors, triggerHapticFeedback } from '../constants'; +import { generateRainbowColors } from '../constants'; import { fonts } from '@/styles'; type AnimatedTextProps = { @@ -22,7 +22,7 @@ type AnimatedTextProps = { delayStart?: number; disableShadow?: boolean; enableHapticTyping?: boolean; - hapticType?: HapticFeedbackType; + hapticType?: HapticType; multiline?: boolean; onComplete?: () => void; opacity?: number; @@ -145,7 +145,7 @@ export const AnimatedText = ({ runOnJS(setDisplayedText)(newText); if (enableHapticTyping && Math.round(current.displayedValue) && newText[newText.length - 1] !== ' ') { - runOnJS(triggerHapticFeedback)(hapticType); + triggerHaptics(hapticType); } } } diff --git a/src/screens/points/constants.ts b/src/screens/points/constants.ts index 2c83b2f3800..b1bd9e0b52b 100644 --- a/src/screens/points/constants.ts +++ b/src/screens/points/constants.ts @@ -1,5 +1,3 @@ -import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; -import { HapticFeedbackType } from '@/utils/haptics'; import { safeAreaInsetValues } from '@/utils'; import { OnboardPointsMutation, PointsOnboardingCategory } from '@/graphql/__generated__/metadata'; import * as i18n from '@/languages'; @@ -69,8 +67,6 @@ export const generateRainbowColors = (text: string): Array<{ text: string; shado return colors; }; -export const triggerHapticFeedback = (hapticType: HapticFeedbackType) => ReactNativeHapticFeedback?.trigger(hapticType); - const BASE_URL = `https://twitter.com/intent/tweet?text=`; const NEWLINE_OR_SPACE = IS_IOS ? '\n\n' : ' '; export const buildTwitterIntentMessage = ( diff --git a/yarn.lock b/yarn.lock index cb5669f2082..e584dfb1e0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9404,6 +9404,7 @@ __metadata: react-native-text-size: "rainbow-me/react-native-text-size#15b21c9f88c6df0d1b5e0f2ba792fe59b5dc255a" react-native-tooltip: "rainbow-me/react-native-tooltip#e0e88d212b5b7f350e5eabba87f588a32e0f2590" react-native-tooltips: "rainbow-me/react-native-tooltips#fdafbc7ba33ee231229f5d3f58b29d0d1c55ddfa" + react-native-turbo-haptics: "npm:1.0.4" react-native-udp: "npm:2.7.0" react-native-url-polyfill: "npm:2.0.0" react-native-version-number: "npm:0.3.6" @@ -23734,6 +23735,16 @@ react-native-safe-area-view@rainbow-me/react-native-safe-area-view: languageName: node linkType: hard +"react-native-turbo-haptics@npm:1.0.4": + version: 1.0.4 + resolution: "react-native-turbo-haptics@npm:1.0.4" + peerDependencies: + react: ^18.2.0 + react-native: ">=0.72.0" + checksum: 10c0/1c65d3c6380a438e55540cbdc82d865f26aa2b6c24364d2be5092c87f347115471162f8a61c8f9c71324d88bf392da7fbe19c4a489e74a8394bbbdf0664da24e + languageName: node + linkType: hard + "react-native-udp@npm:2.7.0": version: 2.7.0 resolution: "react-native-udp@npm:2.7.0"