diff --git a/packages/core/src/js/feedback/FeedbackFormManager.tsx b/packages/core/src/js/feedback/FeedbackFormManager.tsx index b7b9f9fe55..3cf1c114d0 100644 --- a/packages/core/src/js/feedback/FeedbackFormManager.tsx +++ b/packages/core/src/js/feedback/FeedbackFormManager.tsx @@ -1,6 +1,6 @@ import { logger } from '@sentry/core'; import * as React from 'react'; -import { Animated, KeyboardAvoidingView, Modal, Platform, View } from 'react-native'; +import { Animated, KeyboardAvoidingView, Modal, PanResponder, Platform } from 'react-native'; import { FeedbackForm } from './FeedbackForm'; import { modalBackground, modalSheetContainer, modalWrapper } from './FeedbackForm.styles'; @@ -8,6 +8,9 @@ import type { FeedbackFormStyles } from './FeedbackForm.types'; import { getFeedbackOptions } from './integration'; import { isModalSupported } from './utils'; +const PULL_DOWN_CLOSE_THREESHOLD = 200; +const PULL_DOWN_ANDROID_ACTIVATION_HEIGHT = 150; + class FeedbackFormManager { private static _isVisible = false; private static _setVisibility: (visible: boolean) => void; @@ -43,14 +46,48 @@ interface FeedbackFormProviderProps { interface FeedbackFormProviderState { isVisible: boolean; backgroundOpacity: Animated.Value; + panY: Animated.Value; } class FeedbackFormProvider extends React.Component { public state: FeedbackFormProviderState = { isVisible: false, backgroundOpacity: new Animated.Value(0), + panY: new Animated.Value(0), }; + + private _panResponder = PanResponder.create({ + onStartShouldSetPanResponder: (evt, _gestureState) => { + // On Android allow pulling down only from the top to avoid breaking native gestures + return Platform.OS !== 'android' || evt.nativeEvent.pageY < PULL_DOWN_ANDROID_ACTIVATION_HEIGHT; + }, + onMoveShouldSetPanResponder: (evt, _gestureState) => { + return Platform.OS !== 'android' || evt.nativeEvent.pageY < PULL_DOWN_ANDROID_ACTIVATION_HEIGHT; + }, + onPanResponderMove: (_, gestureState) => { + if (gestureState.dy > 0) { + this.state.panY.setValue(gestureState.dy); + } + }, + onPanResponderRelease: (_, gestureState) => { + if (gestureState.dy > PULL_DOWN_CLOSE_THREESHOLD) { // Close on swipe below a certain threshold + Animated.timing(this.state.panY, { + toValue: 600, + duration: 200, + useNativeDriver: true, + }).start(() => { + this._handleClose(); + }); + } else { // Animate it back to the original position + Animated.spring(this.state.panY, { + toValue: 0, + useNativeDriver: true, + }).start(); + } + }, + }); + public constructor(props: FeedbackFormProviderProps) { super(props); FeedbackFormManager.initialize(this._setVisibilityFunction); @@ -99,12 +136,15 @@ class FeedbackFormProvider extends React.Component { behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={modalBackground} > - + - + @@ -115,6 +155,9 @@ class FeedbackFormProvider extends React.Component { private _setVisibilityFunction = (visible: boolean): void => { this.setState({ isVisible: visible }); + if (visible) { + this.state.panY.setValue(0); + } }; private _handleClose = (): void => {