Skip to content

Commit dbf8c90

Browse files
committed
Merge branch 'antonis/feedback-save-state' into antonis/feedback-show-screenshot
# Conflicts: # packages/core/src/js/feedback/FeedbackWidget.tsx
2 parents 56a113e + 797611f commit dbf8c90

15 files changed

+207
-137
lines changed

CHANGELOG.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010

1111
### Features
1212

13-
- User Feedback Form Component Beta ([#4435](https://github.com/getsentry/sentry-react-native/pull/4435))
13+
- User Feedback Widget Beta ([#4435](https://github.com/getsentry/sentry-react-native/pull/4435))
1414

15-
To collect user feedback from inside your application call `Sentry.showFeedbackForm()` or add the `FeedbackForm` component.
15+
To collect user feedback from inside your application call `Sentry.showFeedbackWidget()` or add the `FeedbackWidget` component.
1616

1717
```jsx
18-
import { FeedbackForm } from "@sentry/react-native";
18+
import { FeedbackWidget } from "@sentry/react-native";
1919
...
20-
<FeedbackForm/>
20+
<FeedbackWidget/>
2121
```
2222

2323
### Fixes

packages/core/src/js/feedback/FeedbackForm.styles.ts renamed to packages/core/src/js/feedback/FeedbackWidget.styles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { ViewStyle } from 'react-native';
22

3-
import type { FeedbackFormStyles } from './FeedbackForm.types';
3+
import type { FeedbackWidgetStyles } from './FeedbackWidget.types';
44

55
const PURPLE = 'rgba(88, 74, 192, 1)';
66
const FORGROUND_COLOR = '#2b2233';
77
const BACKROUND_COLOR = '#ffffff';
88
const BORDER_COLOR = 'rgba(41, 35, 47, 0.13)';
99

10-
const defaultStyles: FeedbackFormStyles = {
10+
const defaultStyles: FeedbackWidgetStyles = {
1111
container: {
1212
flex: 1,
1313
padding: 20,

packages/core/src/js/feedback/FeedbackForm.tsx renamed to packages/core/src/js/feedback/FeedbackWidget.tsx

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,24 @@ import {
1717
View
1818
} from 'react-native';
1919

20-
import { NATIVE } from './../wrapper';
20+
import { NATIVE } from '../wrapper';
2121
import { sentryLogo } from './branding';
2222
import { defaultConfiguration } from './defaults';
23-
import defaultStyles from './FeedbackForm.styles';
24-
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles, FeedbackGeneralConfiguration, FeedbackTextConfiguration, ImagePickerConfiguration } from './FeedbackForm.types';
23+
import defaultStyles from './FeedbackWidget.styles';
24+
import type { FeedbackGeneralConfiguration, FeedbackTextConfiguration, FeedbackWidgetProps, FeedbackWidgetState, FeedbackWidgetStyles, ImagePickerConfiguration } from './FeedbackWidget.types';
2525
import { isValidEmail } from './utils';
2626

2727
/**
2828
* @beta
2929
* Implements a feedback form screen that sends feedback to Sentry using Sentry.captureFeedback.
3030
*/
31-
export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFormState> {
32-
public static defaultProps: Partial<FeedbackFormProps> = {
31+
export class FeedbackWidget extends React.Component<FeedbackWidgetProps, FeedbackWidgetState> {
32+
public static defaultProps: Partial<FeedbackWidgetProps> = {
3333
...defaultConfiguration
3434
}
3535

36-
private static _savedState: FeedbackFormState = {
37-
isVisible: false,
36+
private static _didSubmitForm: boolean = false;
37+
private static _savedState: Omit<FeedbackWidgetState, 'isVisible'> = {
3838
name: '',
3939
email: '',
4040
description: '',
@@ -43,7 +43,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
4343
attachmentUri: undefined,
4444
};
4545

46-
public constructor(props: FeedbackFormProps) {
46+
public constructor(props: FeedbackWidgetProps) {
4747
super(props);
4848

4949
const currentUser = {
@@ -55,12 +55,12 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
5555

5656
this.state = {
5757
isVisible: true,
58-
name: FeedbackForm._savedState.name || currentUser.useSentryUser.name,
59-
email: FeedbackForm._savedState.email || currentUser.useSentryUser.email,
60-
description: FeedbackForm._savedState.description || '',
61-
filename: FeedbackForm._savedState.filename || undefined,
62-
attachment: FeedbackForm._savedState.attachment || undefined,
63-
attachmentUri: FeedbackForm._savedState.attachmentUri || undefined,
58+
name: FeedbackWidget._savedState.name || currentUser.useSentryUser.name,
59+
email: FeedbackWidget._savedState.email || currentUser.useSentryUser.email,
60+
description: FeedbackWidget._savedState.description || '',
61+
filename: FeedbackWidget._savedState.filename || undefined,
62+
attachment: FeedbackWidget._savedState.attachment || undefined,
63+
attachmentUri: FeedbackWidget._savedState.attachmentUri || undefined,
6464
};
6565
}
6666

@@ -103,10 +103,10 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
103103
try {
104104
this.setState({ isVisible: false });
105105
captureFeedback(userFeedback, attachments ? { attachments } : undefined);
106-
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: undefined });
106+
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: attachments });
107107
Alert.alert(text.successMessageText);
108108
onFormSubmitted();
109-
this._clearFormState();
109+
FeedbackWidget._didSubmitForm = true;
110110
} catch (error) {
111111
const errorString = `Feedback form submission failed: ${error}`;
112112
onSubmitError(new Error(errorString));
@@ -143,25 +143,37 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
143143
const imageUri = result.assets[0].uri;
144144
NATIVE.getDataFromUri(imageUri).then((data) => {
145145
if (data != null) {
146-
this.setState({ filename, attachment: data, attachmentUri: imageUri }, this._saveFormState);
146+
this.setState({ filename, attachment: data, attachmentUri: imageUri });
147147
} else {
148148
logger.error('Failed to read image data from uri:', imageUri);
149149
}
150150
})
151-
.catch((error) => {
152-
logger.error('Failed to read image data from uri:', imageUri, 'error: ', error);
153-
});
151+
.catch((error) => {
152+
logger.error('Failed to read image data from uri:', imageUri, 'error: ', error);
153+
});
154154
}
155155
} else {
156156
// Defaulting to the onAddScreenshot callback
157157
const { onAddScreenshot } = { ...defaultConfiguration, ...this.props };
158158
onAddScreenshot((filename: string, attachement: Uint8Array) => {
159159
// TODO: Add support for image uri when using onAddScreenshot
160-
this.setState({ filename, attachment: attachement, attachmentUri: undefined }, this._saveFormState);
160+
this.setState({ filename, attachment: attachement, attachmentUri: undefined });
161161
});
162162
}
163163
} else {
164-
this.setState({ filename: undefined, attachment: undefined, attachmentUri: undefined }, this._saveFormState);
164+
this.setState({ filename: undefined, attachment: undefined, attachmentUri: undefined });
165+
}
166+
}
167+
168+
/**
169+
* Save the state before unmounting the component.
170+
*/
171+
public componentWillUnmount(): void {
172+
if (FeedbackWidget._didSubmitForm) {
173+
this._clearFormState();
174+
FeedbackWidget._didSubmitForm = false;
175+
} else {
176+
this._saveFormState();
165177
}
166178
}
167179

@@ -174,7 +186,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
174186
const config: FeedbackGeneralConfiguration = this.props;
175187
const imagePickerConfiguration: ImagePickerConfiguration = this.props;
176188
const text: FeedbackTextConfiguration = this.props;
177-
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
189+
const styles: FeedbackWidgetStyles = { ...defaultStyles, ...this.props.styles };
178190
const onCancel = (): void => {
179191
onFormClose();
180192
this.setState({ isVisible: false });
@@ -214,7 +226,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
214226
style={styles.input}
215227
placeholder={text.namePlaceholder}
216228
value={name}
217-
onChangeText={(value) => this.setState({ name: value }, this._saveFormState)}
229+
onChangeText={(value) => this.setState({ name: value })}
218230
/>
219231
</>
220232
)}
@@ -230,7 +242,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
230242
placeholder={text.emailPlaceholder}
231243
keyboardType={'email-address' as KeyboardTypeOptions}
232244
value={email}
233-
onChangeText={(value) => this.setState({ email: value }, this._saveFormState)}
245+
onChangeText={(value) => this.setState({ email: value })}
234246
/>
235247
</>
236248
)}
@@ -243,7 +255,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
243255
style={[styles.input, styles.textArea]}
244256
placeholder={text.messagePlaceholder}
245257
value={description}
246-
onChangeText={(value) => this.setState({ description: value }, this._saveFormState)}
258+
onChangeText={(value) => this.setState({ description: value })}
247259
multiline
248260
/>
249261
{(config.enableScreenshot || imagePickerConfiguration.imagePicker) && (
@@ -279,12 +291,11 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
279291
}
280292

281293
private _saveFormState = (): void => {
282-
FeedbackForm._savedState = { ...this.state };
294+
FeedbackWidget._savedState = { ...this.state };
283295
};
284296

285297
private _clearFormState = (): void => {
286-
FeedbackForm._savedState = {
287-
isVisible: false,
298+
FeedbackWidget._savedState = {
288299
name: '',
289300
email: '',
290301
description: '',

packages/core/src/js/feedback/FeedbackForm.types.ts renamed to packages/core/src/js/feedback/FeedbackWidget.types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';
44
/**
55
* The props for the feedback form
66
*/
7-
export interface FeedbackFormProps
7+
export interface FeedbackWidgetProps
88
extends FeedbackGeneralConfiguration,
99
FeedbackTextConfiguration,
1010
FeedbackCallbacks,
1111
ImagePickerConfiguration {
12-
styles?: FeedbackFormStyles;
12+
styles?: FeedbackWidgetStyles;
1313
}
1414

1515
/**
@@ -226,7 +226,7 @@ export interface ImagePicker {
226226
/**
227227
* The styles for the feedback form
228228
*/
229-
export interface FeedbackFormStyles {
229+
export interface FeedbackWidgetStyles {
230230
container?: ViewStyle;
231231
title?: TextStyle;
232232
label?: TextStyle;
@@ -247,7 +247,7 @@ export interface FeedbackFormStyles {
247247
/**
248248
* The state of the feedback form
249249
*/
250-
export interface FeedbackFormState {
250+
export interface FeedbackWidgetState {
251251
isVisible: boolean;
252252
name: string;
253253
email: string;

packages/core/src/js/feedback/FeedbackFormManager.tsx renamed to packages/core/src/js/feedback/FeedbackWidgetManager.tsx

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { logger } from '@sentry/core';
22
import * as React from 'react';
3-
import { Animated, KeyboardAvoidingView, Modal, Platform, View } from 'react-native';
3+
import { Animated, KeyboardAvoidingView, Modal, PanResponder, Platform } from 'react-native';
44

5-
import { FeedbackForm } from './FeedbackForm';
6-
import { modalBackground, modalSheetContainer, modalWrapper } from './FeedbackForm.styles';
7-
import type { FeedbackFormStyles } from './FeedbackForm.types';
5+
import { FeedbackWidget } from './FeedbackWidget';
6+
import { modalBackground, modalSheetContainer, modalWrapper } from './FeedbackWidget.styles';
7+
import type { FeedbackWidgetStyles } from './FeedbackWidget.types';
88
import { getFeedbackOptions } from './integration';
99
import { isModalSupported } from './utils';
1010

11-
class FeedbackFormManager {
11+
const PULL_DOWN_CLOSE_THREESHOLD = 200;
12+
const PULL_DOWN_ANDROID_ACTIVATION_HEIGHT = 150;
13+
14+
class FeedbackWidgetManager {
1215
private static _isVisible = false;
1316
private static _setVisibility: (visible: boolean) => void;
1417

@@ -35,31 +38,64 @@ class FeedbackFormManager {
3538
}
3639
}
3740

38-
interface FeedbackFormProviderProps {
41+
interface FeedbackWidgetProviderProps {
3942
children: React.ReactNode;
40-
styles?: FeedbackFormStyles;
43+
styles?: FeedbackWidgetStyles;
4144
}
4245

43-
interface FeedbackFormProviderState {
46+
interface FeedbackWidgetProviderState {
4447
isVisible: boolean;
4548
backgroundOpacity: Animated.Value;
49+
panY: Animated.Value;
4650
}
4751

48-
class FeedbackFormProvider extends React.Component<FeedbackFormProviderProps> {
49-
public state: FeedbackFormProviderState = {
52+
class FeedbackWidgetProvider extends React.Component<FeedbackWidgetProviderProps> {
53+
public state: FeedbackWidgetProviderState = {
5054
isVisible: false,
5155
backgroundOpacity: new Animated.Value(0),
56+
panY: new Animated.Value(0),
5257
};
5358

54-
public constructor(props: FeedbackFormProviderProps) {
59+
private _panResponder = PanResponder.create({
60+
onStartShouldSetPanResponder: (evt, _gestureState) => {
61+
// On Android allow pulling down only from the top to avoid breaking native gestures
62+
return Platform.OS !== 'android' || evt.nativeEvent.pageY < PULL_DOWN_ANDROID_ACTIVATION_HEIGHT;
63+
},
64+
onMoveShouldSetPanResponder: (evt, _gestureState) => {
65+
return Platform.OS !== 'android' || evt.nativeEvent.pageY < PULL_DOWN_ANDROID_ACTIVATION_HEIGHT;
66+
},
67+
onPanResponderMove: (_, gestureState) => {
68+
if (gestureState.dy > 0) {
69+
this.state.panY.setValue(gestureState.dy);
70+
}
71+
},
72+
onPanResponderRelease: (_, gestureState) => {
73+
if (gestureState.dy > PULL_DOWN_CLOSE_THREESHOLD) { // Close on swipe below a certain threshold
74+
Animated.timing(this.state.panY, {
75+
toValue: 600,
76+
duration: 200,
77+
useNativeDriver: true,
78+
}).start(() => {
79+
this._handleClose();
80+
});
81+
} else { // Animate it back to the original position
82+
Animated.spring(this.state.panY, {
83+
toValue: 0,
84+
useNativeDriver: true,
85+
}).start();
86+
}
87+
},
88+
});
89+
90+
public constructor(props: FeedbackWidgetProviderProps) {
5591
super(props);
56-
FeedbackFormManager.initialize(this._setVisibilityFunction);
92+
FeedbackWidgetManager.initialize(this._setVisibilityFunction);
5793
}
5894

5995
/**
6096
* Animates the background opacity when the modal is shown.
6197
*/
62-
public componentDidUpdate(_prevProps: any, prevState: FeedbackFormProviderState): void {
98+
public componentDidUpdate(_prevProps: any, prevState: FeedbackWidgetProviderState): void {
6399
if (!prevState.isVisible && this.state.isVisible) {
64100
Animated.timing(this.state.backgroundOpacity, {
65101
toValue: 1,
@@ -76,7 +112,7 @@ class FeedbackFormProvider extends React.Component<FeedbackFormProviderProps> {
76112
*/
77113
public render(): React.ReactNode {
78114
if (!isModalSupported()) {
79-
logger.error('FeedbackForm Modal is not supported in React Native < 0.71 with Fabric renderer.');
115+
logger.error('FeedbackWidget Modal is not supported in React Native < 0.71 with Fabric renderer.');
80116
return <>{this.props.children}</>;
81117
}
82118

@@ -99,12 +135,15 @@ class FeedbackFormProvider extends React.Component<FeedbackFormProviderProps> {
99135
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
100136
style={modalBackground}
101137
>
102-
<View style={modalSheetContainer}>
103-
<FeedbackForm {...getFeedbackOptions()}
138+
<Animated.View
139+
style={[modalSheetContainer, { transform: [{ translateY: this.state.panY }] }]}
140+
{...this._panResponder.panHandlers}
141+
>
142+
<FeedbackWidget {...getFeedbackOptions()}
104143
onFormClose={this._handleClose}
105144
onFormSubmitted={this._handleClose}
106145
/>
107-
</View>
146+
</Animated.View>
108147
</KeyboardAvoidingView>
109148
</Modal>
110149
</Animated.View>
@@ -115,16 +154,19 @@ class FeedbackFormProvider extends React.Component<FeedbackFormProviderProps> {
115154

116155
private _setVisibilityFunction = (visible: boolean): void => {
117156
this.setState({ isVisible: visible });
157+
if (visible) {
158+
this.state.panY.setValue(0);
159+
}
118160
};
119161

120162
private _handleClose = (): void => {
121-
FeedbackFormManager.hide();
163+
FeedbackWidgetManager.hide();
122164
this.setState({ isVisible: false });
123165
};
124166
}
125167

126-
const showFeedbackForm = (): void => {
127-
FeedbackFormManager.show();
168+
const showFeedbackWidget = (): void => {
169+
FeedbackWidgetManager.show();
128170
};
129171

130-
export { showFeedbackForm, FeedbackFormProvider };
172+
export { showFeedbackWidget, FeedbackWidgetProvider };

0 commit comments

Comments
 (0)