Skip to content

Commit 3e4cdf5

Browse files
authored
Merge b7b36d8 into 0eacc98
2 parents 0eacc98 + b7b36d8 commit 3e4cdf5

File tree

15 files changed

+2896
-0
lines changed

15 files changed

+2896
-0
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111
### Features
1212

1313
- Send Sentry react-native SDK version in the session replay event (#4450)
14+
- User Feedback Form Component Beta ([#4435](https://github.com/getsentry/sentry-react-native/pull/4435))
15+
16+
To collect user feedback from inside your application add the `FeedbackForm` component.
17+
18+
```jsx
19+
import { FeedbackForm } from "@sentry/react-native";
20+
...
21+
<FeedbackForm/>
22+
```
1423

1524
### Fixes
1625

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { FeedbackFormStyles } from './FeedbackForm.types';
2+
3+
const PURPLE = 'rgba(88, 74, 192, 1)';
4+
const FORGROUND_COLOR = '#2b2233';
5+
const BACKROUND_COLOR = '#ffffff';
6+
const BORDER_COLOR = 'rgba(41, 35, 47, 0.13)';
7+
8+
const defaultStyles: FeedbackFormStyles = {
9+
container: {
10+
flex: 1,
11+
padding: 20,
12+
backgroundColor: BACKROUND_COLOR,
13+
},
14+
title: {
15+
fontSize: 24,
16+
fontWeight: 'bold',
17+
marginBottom: 20,
18+
textAlign: 'left',
19+
flex: 1,
20+
color: FORGROUND_COLOR,
21+
},
22+
label: {
23+
marginBottom: 4,
24+
fontSize: 16,
25+
color: FORGROUND_COLOR,
26+
},
27+
input: {
28+
height: 50,
29+
borderColor: BORDER_COLOR,
30+
borderWidth: 1,
31+
borderRadius: 5,
32+
paddingHorizontal: 10,
33+
marginBottom: 15,
34+
fontSize: 16,
35+
color: FORGROUND_COLOR,
36+
},
37+
textArea: {
38+
height: 100,
39+
textAlignVertical: 'top',
40+
color: FORGROUND_COLOR,
41+
},
42+
screenshotButton: {
43+
backgroundColor: '#eee',
44+
padding: 15,
45+
borderRadius: 5,
46+
marginBottom: 20,
47+
alignItems: 'center',
48+
},
49+
screenshotText: {
50+
color: '#333',
51+
fontSize: 16,
52+
},
53+
submitButton: {
54+
backgroundColor: PURPLE,
55+
paddingVertical: 15,
56+
borderRadius: 5,
57+
alignItems: 'center',
58+
marginBottom: 10,
59+
},
60+
submitText: {
61+
color: BACKROUND_COLOR,
62+
fontSize: 18,
63+
},
64+
cancelButton: {
65+
paddingVertical: 15,
66+
alignItems: 'center',
67+
},
68+
cancelText: {
69+
color: FORGROUND_COLOR,
70+
fontSize: 16,
71+
},
72+
titleContainer: {
73+
flexDirection: 'row',
74+
width: '100%',
75+
},
76+
sentryLogo: {
77+
width: 40,
78+
height: 40,
79+
},
80+
};
81+
82+
export default defaultStyles;
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import type { SendFeedbackParams } from '@sentry/core';
2+
import { captureFeedback, getCurrentScope, lastEventId, logger } from '@sentry/core';
3+
import * as React from 'react';
4+
import type { KeyboardTypeOptions } from 'react-native';
5+
import {
6+
Alert,
7+
Image,
8+
Keyboard,
9+
KeyboardAvoidingView,
10+
SafeAreaView,
11+
ScrollView,
12+
Text,
13+
TextInput,
14+
TouchableOpacity,
15+
TouchableWithoutFeedback,
16+
View
17+
} from 'react-native';
18+
19+
import { sentryLogo } from './branding';
20+
import { defaultConfiguration } from './defaults';
21+
import defaultStyles from './FeedbackForm.styles';
22+
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles,FeedbackGeneralConfiguration, FeedbackTextConfiguration } from './FeedbackForm.types';
23+
import { isValidEmail } from './utils';
24+
25+
/**
26+
* @beta
27+
* Implements a feedback form screen that sends feedback to Sentry using Sentry.captureFeedback.
28+
*/
29+
export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFormState> {
30+
public static defaultProps: Partial<FeedbackFormProps> = {
31+
...defaultConfiguration
32+
}
33+
34+
public constructor(props: FeedbackFormProps) {
35+
super(props);
36+
37+
const currentUser = {
38+
useSentryUser: {
39+
email: this.props?.useSentryUser?.email || getCurrentScope()?.getUser()?.email || '',
40+
name: this.props?.useSentryUser?.name || getCurrentScope()?.getUser()?.name || '',
41+
}
42+
}
43+
44+
this.state = {
45+
isVisible: true,
46+
name: currentUser.useSentryUser.name,
47+
email: currentUser.useSentryUser.email,
48+
description: '',
49+
};
50+
}
51+
52+
public handleFeedbackSubmit: () => void = () => {
53+
const { name, email, description } = this.state;
54+
const { onSubmitSuccess, onSubmitError, onFormSubmitted } = this.props;
55+
const text: FeedbackTextConfiguration = this.props;
56+
57+
const trimmedName = name?.trim();
58+
const trimmedEmail = email?.trim();
59+
const trimmedDescription = description?.trim();
60+
61+
if ((this.props.isNameRequired && !trimmedName) || (this.props.isEmailRequired && !trimmedEmail) || !trimmedDescription) {
62+
Alert.alert(text.errorTitle, text.formError);
63+
return;
64+
}
65+
66+
if (this.props.shouldValidateEmail && (this.props.isEmailRequired || trimmedEmail.length > 0) && !isValidEmail(trimmedEmail)) {
67+
Alert.alert(text.errorTitle, text.emailError);
68+
return;
69+
}
70+
71+
const attachments = this.state.filename && this.state.attachment
72+
? [
73+
{
74+
filename: this.state.filename,
75+
data: this.state.attachment,
76+
},
77+
]
78+
: undefined;
79+
80+
const eventId = lastEventId();
81+
const userFeedback: SendFeedbackParams = {
82+
message: trimmedDescription,
83+
name: trimmedName,
84+
email: trimmedEmail,
85+
associatedEventId: eventId,
86+
};
87+
88+
try {
89+
this.setState({ isVisible: false });
90+
captureFeedback(userFeedback, attachments ? { attachments } : undefined);
91+
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: undefined });
92+
Alert.alert(text.successMessageText);
93+
onFormSubmitted();
94+
} catch (error) {
95+
const errorString = `Feedback form submission failed: ${error}`;
96+
onSubmitError(new Error(errorString));
97+
Alert.alert(text.errorTitle, text.genericError);
98+
logger.error(`Feedback form submission failed: ${error}`);
99+
}
100+
};
101+
102+
public onScreenshotButtonPress: () => void = () => {
103+
if (!this.state.filename && !this.state.attachment) {
104+
const { onAddScreenshot } = { ...defaultConfiguration, ...this.props };
105+
onAddScreenshot((filename: string, attachement: Uint8Array) => {
106+
this.setState({ filename, attachment: attachement });
107+
});
108+
} else {
109+
this.setState({ filename: undefined, attachment: undefined });
110+
}
111+
}
112+
113+
/**
114+
* Renders the feedback form screen.
115+
*/
116+
public render(): React.ReactNode {
117+
const { name, email, description } = this.state;
118+
const { onFormClose } = this.props;
119+
const config: FeedbackGeneralConfiguration = this.props;
120+
const text: FeedbackTextConfiguration = this.props;
121+
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
122+
const onCancel = (): void => {
123+
onFormClose();
124+
this.setState({ isVisible: false });
125+
}
126+
127+
if (!this.state.isVisible) {
128+
return null;
129+
}
130+
131+
return (
132+
<SafeAreaView style={[styles.container, { padding: 0 }]}>
133+
<KeyboardAvoidingView behavior={'padding'} style={[styles.container, { padding: 0 }]}>
134+
<ScrollView>
135+
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
136+
<View style={styles.container}>
137+
<View style={styles.titleContainer}>
138+
<Text style={styles.title}>{text.formTitle}</Text>
139+
{config.showBranding && (
140+
<Image
141+
source={{ uri: sentryLogo }}
142+
style={styles.sentryLogo}
143+
testID='sentry-logo'
144+
/>
145+
)}
146+
</View>
147+
148+
{config.showName && (
149+
<>
150+
<Text style={styles.label}>
151+
{text.nameLabel}
152+
{config.isNameRequired && ` ${text.isRequiredLabel}`}
153+
</Text>
154+
<TextInput
155+
style={styles.input}
156+
placeholder={text.namePlaceholder}
157+
value={name}
158+
onChangeText={(value) => this.setState({ name: value })}
159+
/>
160+
</>
161+
)}
162+
163+
{config.showEmail && (
164+
<>
165+
<Text style={styles.label}>
166+
{text.emailLabel}
167+
{config.isEmailRequired && ` ${text.isRequiredLabel}`}
168+
</Text>
169+
<TextInput
170+
style={styles.input}
171+
placeholder={text.emailPlaceholder}
172+
keyboardType={'email-address' as KeyboardTypeOptions}
173+
value={email}
174+
onChangeText={(value) => this.setState({ email: value })}
175+
/>
176+
</>
177+
)}
178+
179+
<Text style={styles.label}>
180+
{text.messageLabel}
181+
{` ${text.isRequiredLabel}`}
182+
</Text>
183+
<TextInput
184+
style={[styles.input, styles.textArea]}
185+
placeholder={text.messagePlaceholder}
186+
value={description}
187+
onChangeText={(value) => this.setState({ description: value })}
188+
multiline
189+
/>
190+
{config.enableScreenshot && (
191+
<TouchableOpacity style={styles.screenshotButton} onPress={this.onScreenshotButtonPress}>
192+
<Text style={styles.screenshotText}>
193+
{!this.state.filename && !this.state.attachment
194+
? text.addScreenshotButtonLabel
195+
: text.removeScreenshotButtonLabel}
196+
</Text>
197+
</TouchableOpacity>
198+
)}
199+
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
200+
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
201+
</TouchableOpacity>
202+
203+
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
204+
<Text style={styles.cancelText}>{text.cancelButtonLabel}</Text>
205+
</TouchableOpacity>
206+
</View>
207+
</TouchableWithoutFeedback>
208+
</ScrollView>
209+
</KeyboardAvoidingView>
210+
</SafeAreaView>
211+
);
212+
}
213+
}

0 commit comments

Comments
 (0)