Skip to content

Commit df05370

Browse files
authored
Merge 22cde46 into 21a0abb
2 parents 21a0abb + 22cde46 commit df05370

20 files changed

+3230
-1
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
> make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first.
77
<!-- prettier-ignore-end -->
88
9+
## Unreleased
10+
11+
### Features
12+
13+
- User Feedback Form Component Beta ([#4435](https://github.com/getsentry/sentry-react-native/pull/4435))
14+
15+
To collect user feedback from inside your application call `Sentry.showFeedbackForm()` or add the `FeedbackForm` component.
16+
17+
```jsx
18+
import { FeedbackForm } from "@sentry/react-native";
19+
...
20+
<FeedbackForm/>
21+
```
22+
923
## 6.7.0
1024

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

0 commit comments

Comments
 (0)