Skip to content

Commit 0325426

Browse files
authored
Merge 93b770e into 0414062
2 parents 0414062 + 93b770e commit 0325426

File tree

12 files changed

+1722
-0
lines changed

12 files changed

+1722
-0
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@
88
99
## Unreleased
1010

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 add the `FeedbackForm` component.
16+
17+
```jsx
18+
import { FeedbackForm } from "@sentry/react-native";
19+
...
20+
<FeedbackForm/>
21+
```
22+
1123
### Fixes
1224

1325
- Use proper SDK name for Session Replay tags ([#4428](https://github.com/getsentry/sentry-react-native/pull/4428))
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
submitButton: {
43+
backgroundColor: PURPLE,
44+
paddingVertical: 15,
45+
borderRadius: 5,
46+
alignItems: 'center',
47+
marginBottom: 10,
48+
},
49+
submitText: {
50+
color: BACKROUND_COLOR,
51+
fontSize: 18,
52+
},
53+
cancelButton: {
54+
paddingVertical: 15,
55+
alignItems: 'center',
56+
},
57+
cancelText: {
58+
color: FORGROUND_COLOR,
59+
fontSize: 16,
60+
},
61+
titleContainer: {
62+
flexDirection: 'row',
63+
width: '100%',
64+
},
65+
sentryLogo: {
66+
width: 40,
67+
height: 40,
68+
},
69+
};
70+
71+
export default defaultStyles;
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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 eventId = lastEventId();
72+
const userFeedback: SendFeedbackParams = {
73+
message: trimmedDescription,
74+
name: trimmedName,
75+
email: trimmedEmail,
76+
associatedEventId: eventId,
77+
};
78+
79+
try {
80+
this.setState({ isVisible: false });
81+
captureFeedback(userFeedback);
82+
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: undefined });
83+
Alert.alert(text.successMessageText);
84+
onFormSubmitted();
85+
} catch (error) {
86+
const errorString = `Feedback form submission failed: ${error}`;
87+
onSubmitError(new Error(errorString));
88+
Alert.alert(text.errorTitle, text.genericError);
89+
logger.error(`Feedback form submission failed: ${error}`);
90+
}
91+
};
92+
93+
/**
94+
* Renders the feedback form screen.
95+
*/
96+
public render(): React.ReactNode {
97+
const { name, email, description } = this.state;
98+
const { onFormClose } = this.props;
99+
const config: FeedbackGeneralConfiguration = this.props;
100+
const text: FeedbackTextConfiguration = this.props;
101+
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
102+
const onCancel = (): void => {
103+
onFormClose();
104+
this.setState({ isVisible: false });
105+
}
106+
107+
if (!this.state.isVisible) {
108+
return null;
109+
}
110+
111+
return (
112+
<SafeAreaView style={[styles.container, { padding: 0 }]}>
113+
<KeyboardAvoidingView behavior={'padding'} style={[styles.container, { padding: 0 }]}>
114+
<ScrollView>
115+
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
116+
<View style={styles.container}>
117+
<View style={styles.titleContainer}>
118+
<Text style={styles.title}>{text.formTitle}</Text>
119+
{config.showBranding && (
120+
<Image
121+
source={{ uri: sentryLogo }}
122+
style={styles.sentryLogo}
123+
testID='sentry-logo'
124+
/>
125+
)}
126+
</View>
127+
128+
{config.showName && (
129+
<>
130+
<Text style={styles.label}>
131+
{text.nameLabel}
132+
{config.isNameRequired && ` ${text.isRequiredLabel}`}
133+
</Text>
134+
<TextInput
135+
style={styles.input}
136+
placeholder={text.namePlaceholder}
137+
value={name}
138+
onChangeText={(value) => this.setState({ name: value })}
139+
/>
140+
</>
141+
)}
142+
143+
{config.showEmail && (
144+
<>
145+
<Text style={styles.label}>
146+
{text.emailLabel}
147+
{config.isEmailRequired && ` ${text.isRequiredLabel}`}
148+
</Text>
149+
<TextInput
150+
style={styles.input}
151+
placeholder={text.emailPlaceholder}
152+
keyboardType={'email-address' as KeyboardTypeOptions}
153+
value={email}
154+
onChangeText={(value) => this.setState({ email: value })}
155+
/>
156+
</>
157+
)}
158+
159+
<Text style={styles.label}>
160+
{text.messageLabel}
161+
{` ${text.isRequiredLabel}`}
162+
</Text>
163+
<TextInput
164+
style={[styles.input, styles.textArea]}
165+
placeholder={text.messagePlaceholder}
166+
value={description}
167+
onChangeText={(value) => this.setState({ description: value })}
168+
multiline
169+
/>
170+
171+
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
172+
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
173+
</TouchableOpacity>
174+
175+
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
176+
<Text style={styles.cancelText}>{text.cancelButtonLabel}</Text>
177+
</TouchableOpacity>
178+
</View>
179+
</TouchableWithoutFeedback>
180+
</ScrollView>
181+
</KeyboardAvoidingView>
182+
</SafeAreaView>
183+
);
184+
}
185+
}

0 commit comments

Comments
 (0)