Skip to content

Commit e0624b6

Browse files
authored
Merge fd2e317 into 2fb5240
2 parents 2fb5240 + fd2e317 commit e0624b6

File tree

9 files changed

+622
-0
lines changed

9 files changed

+622
-0
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@
3535
});
3636
```
3737

38+
- User Feedback From Component Beta ([#4320](https://github.com/getsentry/sentry-react-native/pull/4328))
39+
40+
To collect user feedback from inside your application add the `FeedbackFrom` component.
41+
42+
```jsx
43+
import { FeedbackForm } from "@sentry/react-native";
44+
...
45+
<FeedbackForm/>
46+
```
47+
3848
- Export `Span` type from `@sentry/types` ([#4345](https://github.com/getsentry/sentry-react-native/pull/4345))
3949

4050
### Fixes
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 = '#fff';
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: 'center',
19+
color: FORGROUND_COLOR,
20+
},
21+
label: {
22+
marginBottom: 4,
23+
fontSize: 16,
24+
color: FORGROUND_COLOR,
25+
},
26+
input: {
27+
height: 50,
28+
borderColor: BORDER_COLOR,
29+
borderWidth: 1,
30+
borderRadius: 5,
31+
paddingHorizontal: 10,
32+
marginBottom: 15,
33+
fontSize: 16,
34+
color: FORGROUND_COLOR,
35+
},
36+
textArea: {
37+
height: 100,
38+
textAlignVertical: 'top',
39+
color: FORGROUND_COLOR,
40+
},
41+
submitButton: {
42+
backgroundColor: PURPLE,
43+
paddingVertical: 15,
44+
borderRadius: 5,
45+
alignItems: 'center',
46+
marginBottom: 10,
47+
},
48+
submitText: {
49+
color: BACKROUND_COLOR,
50+
fontSize: 18,
51+
},
52+
cancelButton: {
53+
paddingVertical: 15,
54+
alignItems: 'center',
55+
},
56+
cancelText: {
57+
color: FORGROUND_COLOR,
58+
fontSize: 16,
59+
},
60+
};
61+
62+
export default defaultStyles;
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { captureFeedback, getCurrentScope, lastEventId } from '@sentry/core';
2+
import type { SendFeedbackParams } from '@sentry/types';
3+
import * as React from 'react';
4+
import type { KeyboardTypeOptions } from 'react-native';
5+
import {
6+
Alert,
7+
Keyboard,
8+
KeyboardAvoidingView,
9+
SafeAreaView,
10+
ScrollView,
11+
Text,
12+
TextInput,
13+
TouchableOpacity,
14+
TouchableWithoutFeedback,
15+
View
16+
} from 'react-native';
17+
18+
import { defaultConfiguration } from './defaults';
19+
import defaultStyles from './FeedbackForm.styles';
20+
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles,FeedbackGeneralConfiguration, FeedbackTextConfiguration } from './FeedbackForm.types';
21+
22+
/**
23+
* @beta
24+
* Implements a feedback form screen that sends feedback to Sentry using Sentry.captureFeedback.
25+
*/
26+
export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFormState> {
27+
private _config: FeedbackFormProps;
28+
29+
public constructor(props: FeedbackFormProps) {
30+
super(props);
31+
32+
const currentUser = {
33+
useSentryUser: {
34+
email: getCurrentScope().getUser().email || '',
35+
name: getCurrentScope().getUser().name || '',
36+
}
37+
}
38+
39+
this._config = { ...defaultConfiguration, ...currentUser, ...props };
40+
this.state = {
41+
isVisible: true,
42+
name: this._config.useSentryUser.name,
43+
email: this._config.useSentryUser.email,
44+
description: '',
45+
};
46+
}
47+
48+
public handleFeedbackSubmit: () => void = () => {
49+
const { name, email, description } = this.state;
50+
const { onFormClose } = this._config;
51+
const text: FeedbackTextConfiguration = this._config;
52+
53+
const trimmedName = name?.trim();
54+
const trimmedEmail = email?.trim();
55+
const trimmedDescription = description?.trim();
56+
57+
if ((this._config.isNameRequired && !trimmedName) || (this._config.isEmailRequired && !trimmedEmail) || !trimmedDescription) {
58+
Alert.alert(text.errorTitle, text.formError);
59+
return;
60+
}
61+
62+
if (this._config.shouldValidateEmail && (this._config.isEmailRequired || trimmedEmail.length > 0) && !this._isValidEmail(trimmedEmail)) {
63+
Alert.alert(text.errorTitle, text.emailError);
64+
return;
65+
}
66+
67+
const eventId = lastEventId();
68+
const userFeedback: SendFeedbackParams = {
69+
message: trimmedDescription,
70+
name: trimmedName,
71+
email: trimmedEmail,
72+
associatedEventId: eventId,
73+
};
74+
75+
onFormClose();
76+
this.setState({ isVisible: false });
77+
78+
captureFeedback(userFeedback);
79+
Alert.alert(text.successMessageText);
80+
};
81+
82+
/**
83+
* Renders the feedback form screen.
84+
*/
85+
public render(): React.ReactNode {
86+
const { name, email, description } = this.state;
87+
const { onFormClose } = this._config;
88+
const config: FeedbackGeneralConfiguration = this._config;
89+
const text: FeedbackTextConfiguration = this._config;
90+
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
91+
const onCancel = (): void => {
92+
onFormClose();
93+
this.setState({ isVisible: false });
94+
}
95+
96+
if (!this.state.isVisible) {
97+
return null;
98+
}
99+
100+
return (
101+
<SafeAreaView style={[styles.container, { padding: 0 }]}>
102+
<KeyboardAvoidingView behavior={'padding'} style={[styles.container, { padding: 0 }]}>
103+
<ScrollView>
104+
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
105+
<View style={styles.container}>
106+
<Text style={styles.title}>{text.formTitle}</Text>
107+
108+
{config.showName && (
109+
<>
110+
<Text style={styles.label}>
111+
{text.nameLabel}
112+
{config.isNameRequired && ` ${text.isRequiredLabel}`}
113+
</Text>
114+
<TextInput
115+
style={styles.input}
116+
placeholder={text.namePlaceholder}
117+
value={name}
118+
onChangeText={(value) => this.setState({ name: value })}
119+
/>
120+
</>
121+
)}
122+
123+
{config.showEmail && (
124+
<>
125+
<Text style={styles.label}>
126+
{text.emailLabel}
127+
{config.isEmailRequired && ` ${text.isRequiredLabel}`}
128+
</Text>
129+
<TextInput
130+
style={styles.input}
131+
placeholder={text.emailPlaceholder}
132+
keyboardType={'email-address' as KeyboardTypeOptions}
133+
value={email}
134+
onChangeText={(value) => this.setState({ email: value })}
135+
/>
136+
</>
137+
)}
138+
139+
<Text style={styles.label}>
140+
{text.messageLabel}
141+
{` ${text.isRequiredLabel}`}
142+
</Text>
143+
<TextInput
144+
style={[styles.input, styles.textArea]}
145+
placeholder={text.messagePlaceholder}
146+
value={description}
147+
onChangeText={(value) => this.setState({ description: value })}
148+
multiline
149+
/>
150+
151+
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
152+
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
153+
</TouchableOpacity>
154+
155+
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
156+
<Text style={styles.cancelText}>{text.cancelButtonLabel}</Text>
157+
</TouchableOpacity>
158+
</View>
159+
</TouchableWithoutFeedback>
160+
</ScrollView>
161+
</KeyboardAvoidingView>
162+
</SafeAreaView>
163+
);
164+
}
165+
166+
private _isValidEmail = (email: string): boolean => {
167+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
168+
return emailRegex.test(email);
169+
};
170+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import type { TextStyle, ViewStyle } from 'react-native';
2+
3+
export interface FeedbackFormProps extends FeedbackGeneralConfiguration, FeedbackTextConfiguration, FeedbackCallbacks {
4+
styles?: FeedbackFormStyles;
5+
}
6+
7+
/**
8+
* General feedback configuration
9+
*/
10+
export interface FeedbackGeneralConfiguration {
11+
/**
12+
* Should the email field be required?
13+
*/
14+
isEmailRequired?: boolean;
15+
16+
/**
17+
* Should the email field be validated?
18+
*/
19+
shouldValidateEmail?: boolean;
20+
21+
/**
22+
* Should the name field be required?
23+
*/
24+
isNameRequired?: boolean;
25+
26+
/**
27+
* Should the email input field be visible? Note: email will still be collected if set via `Sentry.setUser()`
28+
*/
29+
showEmail?: boolean;
30+
31+
/**
32+
* Should the name input field be visible? Note: name will still be collected if set via `Sentry.setUser()`
33+
*/
34+
showName?: boolean;
35+
36+
/**
37+
* Fill in email/name input fields with Sentry user context if it exists.
38+
* The value of the email/name keys represent the properties of your user context.
39+
*/
40+
useSentryUser?: {
41+
email: string;
42+
name: string;
43+
};
44+
}
45+
46+
/**
47+
* All of the different text labels that can be customized
48+
*/
49+
export interface FeedbackTextConfiguration {
50+
/**
51+
* The label for the Feedback form cancel button that closes dialog
52+
*/
53+
cancelButtonLabel?: string;
54+
55+
/**
56+
* The label for the Feedback form submit button that sends feedback
57+
*/
58+
submitButtonLabel?: string;
59+
60+
/**
61+
* The title of the Feedback form
62+
*/
63+
formTitle?: string;
64+
65+
/**
66+
* Label for the email input
67+
*/
68+
emailLabel?: string;
69+
70+
/**
71+
* Placeholder text for Feedback email input
72+
*/
73+
emailPlaceholder?: string;
74+
75+
/**
76+
* Label for the message input
77+
*/
78+
messageLabel?: string;
79+
80+
/**
81+
* Placeholder text for Feedback message input
82+
*/
83+
messagePlaceholder?: string;
84+
85+
/**
86+
* Label for the name input
87+
*/
88+
nameLabel?: string;
89+
90+
/**
91+
* Message after feedback was sent successfully
92+
*/
93+
successMessageText?: string;
94+
95+
/**
96+
* Placeholder text for Feedback name input
97+
*/
98+
namePlaceholder?: string;
99+
100+
/**
101+
* Text which indicates that a field is required
102+
*/
103+
isRequiredLabel?: string;
104+
105+
/**
106+
* The title of the error dialog
107+
*/
108+
errorTitle?: string;
109+
110+
/**
111+
* The error message when the form is invalid
112+
*/
113+
formError?: string;
114+
115+
/**
116+
* The error message when the email is invalid
117+
*/
118+
emailError?: string;
119+
}
120+
121+
/**
122+
* The public callbacks available for the feedback integration
123+
*/
124+
export interface FeedbackCallbacks {
125+
/**
126+
* Callback when form is closed and not submitted
127+
*/
128+
onFormClose?: () => void;
129+
}
130+
131+
export interface FeedbackFormStyles {
132+
container?: ViewStyle;
133+
title?: TextStyle;
134+
label?: TextStyle;
135+
input?: TextStyle;
136+
textArea?: TextStyle;
137+
submitButton?: ViewStyle;
138+
submitText?: TextStyle;
139+
cancelButton?: ViewStyle;
140+
cancelText?: TextStyle;
141+
}
142+
143+
export interface FeedbackFormState {
144+
isVisible: boolean;
145+
name: string;
146+
email: string;
147+
description: string;
148+
}

0 commit comments

Comments
 (0)