Skip to content

Commit 561640f

Browse files
authored
Merge b9235f2 into ddc0552
2 parents ddc0552 + b9235f2 commit 561640f

File tree

9 files changed

+533
-0
lines changed

9 files changed

+533
-0
lines changed

CHANGELOG.md

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

38+
- Adds feedback form ([#4320](https://github.com/getsentry/sentry-react-native/pull/4328))
39+
40+
You can add the form component in your UI and customise it like:
41+
```jsx
42+
import { FeedbackForm } from "@sentry/react-native";
43+
...
44+
<FeedbackForm
45+
{...props}
46+
closeScreen={props.navigation.goBack}
47+
styles={{
48+
submitButton: {
49+
backgroundColor: '#6a1b9a',
50+
paddingVertical: 15,
51+
borderRadius: 5,
52+
alignItems: 'center',
53+
marginBottom: 10,
54+
},
55+
}}
56+
text={{namePlaceholder: 'Fullname'}}
57+
/>
58+
```
59+
Check [the documentation](https://docs.sentry.io/platforms/react-native/user-feedback/) for more configuration options.
60+
3861
### Fixes
3962

4063
- Return `lastEventId` export from `@sentry/core` ([#4315](https://github.com/getsentry/sentry-react-native/pull/4315))
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { FeedbackFormStyles } from './FeedbackForm.types';
2+
3+
const defaultStyles: FeedbackFormStyles = {
4+
container: {
5+
flex: 1,
6+
padding: 20,
7+
backgroundColor: '#fff',
8+
},
9+
title: {
10+
fontSize: 24,
11+
fontWeight: 'bold',
12+
marginBottom: 20,
13+
textAlign: 'center',
14+
},
15+
label: {
16+
marginBottom: 4,
17+
fontSize: 16,
18+
},
19+
input: {
20+
height: 50,
21+
borderColor: '#ccc',
22+
borderWidth: 1,
23+
borderRadius: 5,
24+
paddingHorizontal: 10,
25+
marginBottom: 15,
26+
fontSize: 16,
27+
},
28+
textArea: {
29+
height: 100,
30+
textAlignVertical: 'top',
31+
},
32+
submitButton: {
33+
backgroundColor: '#6a1b9a',
34+
paddingVertical: 15,
35+
borderRadius: 5,
36+
alignItems: 'center',
37+
marginBottom: 10,
38+
},
39+
submitText: {
40+
color: '#fff',
41+
fontSize: 18,
42+
fontWeight: 'bold',
43+
},
44+
cancelButton: {
45+
paddingVertical: 15,
46+
alignItems: 'center',
47+
},
48+
cancelText: {
49+
color: '#6a1b9a',
50+
fontSize: 16,
51+
},
52+
};
53+
54+
export default defaultStyles;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { captureFeedback } 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 { Alert, Text, TextInput, TouchableOpacity, View } from 'react-native';
6+
7+
import { defaultConfiguration } from './defaults';
8+
import defaultStyles from './FeedbackForm.styles';
9+
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles,FeedbackGeneralConfiguration, FeedbackTextConfiguration } from './FeedbackForm.types';
10+
11+
/**
12+
* @beta
13+
* Implements a feedback form screen that sends feedback to Sentry using Sentry.captureFeedback.
14+
*/
15+
export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFormState> {
16+
public constructor(props: FeedbackFormProps) {
17+
super(props);
18+
19+
const config: FeedbackGeneralConfiguration = { ...defaultConfiguration, ...props };
20+
this.state = {
21+
name: config.useSentryUser.name,
22+
email: config.useSentryUser.email,
23+
description: '',
24+
};
25+
}
26+
27+
public handleFeedbackSubmit: () => void = () => {
28+
const { name, email, description } = this.state;
29+
const { closeScreen } = this.props;
30+
const config: FeedbackGeneralConfiguration = { ...defaultConfiguration, ...this.props };
31+
const text: FeedbackTextConfiguration = { ...defaultConfiguration, ...this.props };
32+
33+
const trimmedName = name?.trim();
34+
const trimmedEmail = email?.trim();
35+
const trimmedDescription = description?.trim();
36+
37+
if ((config.isNameRequired && !trimmedName) || (config.isEmailRequired && !trimmedEmail) || !trimmedDescription) {
38+
Alert.alert(text.errorTitle, text.formError);
39+
return;
40+
}
41+
42+
if ((config.isEmailRequired || trimmedEmail.length > 0) && !this._isValidEmail(trimmedEmail)) {
43+
Alert.alert(text.errorTitle, text.emailError);
44+
return;
45+
}
46+
47+
const userFeedback: SendFeedbackParams = {
48+
message: trimmedDescription,
49+
name: trimmedName,
50+
email: trimmedEmail,
51+
};
52+
53+
closeScreen();
54+
captureFeedback(userFeedback);
55+
};
56+
57+
/**
58+
* Renders the feedback form screen.
59+
*/
60+
public render(): React.ReactNode {
61+
const { closeScreen } = this.props;
62+
const { name, email, description } = this.state;
63+
const config: FeedbackGeneralConfiguration = { ...defaultConfiguration, ...this.props };
64+
const text: FeedbackTextConfiguration = { ...defaultConfiguration, ...this.props };
65+
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
66+
67+
return (
68+
<View style={styles.container}>
69+
<Text style={styles.title}>{text.formTitle}</Text>
70+
71+
<Text style={styles.label}>
72+
{text.nameLabel}
73+
{config.isNameRequired && ` ${text.isRequiredLabel}`}
74+
</Text>
75+
<TextInput
76+
style={styles.input}
77+
placeholder={text.namePlaceholder}
78+
value={name}
79+
onChangeText={(value) => this.setState({ name: value })}
80+
/>
81+
82+
<Text style={styles.label}>
83+
{text.emailLabel}
84+
{config.isEmailRequired && ` ${text.isRequiredLabel}`}
85+
</Text>
86+
<TextInput
87+
style={styles.input}
88+
placeholder={text.emailPlaceholder}
89+
keyboardType={'email-address' as KeyboardTypeOptions}
90+
value={email}
91+
onChangeText={(value) => this.setState({ email: value })}
92+
/>
93+
94+
<Text style={styles.label}>
95+
{text.messageLabel}
96+
{` ${text.isRequiredLabel}`}
97+
</Text>
98+
<TextInput
99+
style={[styles.input, styles.textArea]}
100+
placeholder={text.messagePlaceholder}
101+
value={description}
102+
onChangeText={(value) => this.setState({ description: value })}
103+
multiline
104+
/>
105+
106+
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
107+
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
108+
</TouchableOpacity>
109+
110+
<TouchableOpacity style={styles.cancelButton} onPress={closeScreen}>
111+
<Text style={styles.cancelText}>{text.cancelButtonLabel}</Text>
112+
</TouchableOpacity>
113+
</View>
114+
);
115+
}
116+
117+
private _isValidEmail = (email: string): boolean => {
118+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
119+
return emailRegex.test(email);
120+
};
121+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { TextStyle, ViewStyle } from 'react-native';
2+
3+
export interface FeedbackFormProps extends FeedbackGeneralConfiguration, FeedbackTextConfiguration {
4+
closeScreen: () => void;
5+
styles?: FeedbackFormStyles;
6+
}
7+
8+
/**
9+
* General feedback configuration
10+
*/
11+
export interface FeedbackGeneralConfiguration {
12+
/**
13+
* Should the email field be required?
14+
*/
15+
isEmailRequired?: boolean;
16+
17+
/**
18+
* Should the name field be required?
19+
*/
20+
isNameRequired?: boolean;
21+
22+
/**
23+
* Should the email input field be visible? Note: email will still be collected if set via `Sentry.setUser()`
24+
*/
25+
showEmail?: boolean;
26+
27+
/**
28+
* Should the name input field be visible? Note: name will still be collected if set via `Sentry.setUser()`
29+
*/
30+
showName?: boolean;
31+
32+
/**
33+
* Fill in email/name input fields with Sentry user context if it exists.
34+
* The value of the email/name keys represent the properties of your user context.
35+
*/
36+
useSentryUser?: {
37+
email: string;
38+
name: string;
39+
};
40+
}
41+
42+
/**
43+
* All of the different text labels that can be customized
44+
*/
45+
export interface FeedbackTextConfiguration {
46+
/**
47+
* The label for the Feedback form cancel button that closes dialog
48+
*/
49+
cancelButtonLabel?: string;
50+
51+
/**
52+
* The label for the Feedback form submit button that sends feedback
53+
*/
54+
submitButtonLabel?: string;
55+
56+
/**
57+
* The title of the Feedback form
58+
*/
59+
formTitle?: string;
60+
61+
/**
62+
* Label for the email input
63+
*/
64+
emailLabel?: string;
65+
66+
/**
67+
* Placeholder text for Feedback email input
68+
*/
69+
emailPlaceholder?: string;
70+
71+
/**
72+
* Label for the message input
73+
*/
74+
messageLabel?: string;
75+
76+
/**
77+
* Placeholder text for Feedback message input
78+
*/
79+
messagePlaceholder?: string;
80+
81+
/**
82+
* Label for the name input
83+
*/
84+
nameLabel?: string;
85+
86+
/**
87+
* Placeholder text for Feedback name input
88+
*/
89+
namePlaceholder?: string;
90+
91+
/**
92+
* Text which indicates that a field is required
93+
*/
94+
isRequiredLabel?: string;
95+
96+
/**
97+
* The title of the error dialog
98+
*/
99+
errorTitle?: string;
100+
101+
/**
102+
* The error message when the form is invalid
103+
*/
104+
formError?: string;
105+
106+
/**
107+
* The error message when the email is invalid
108+
*/
109+
emailError?: string;
110+
}
111+
112+
export interface FeedbackFormStyles {
113+
container?: ViewStyle;
114+
title?: TextStyle;
115+
label?: TextStyle;
116+
input?: TextStyle;
117+
textArea?: TextStyle;
118+
submitButton?: ViewStyle;
119+
submitText?: TextStyle;
120+
cancelButton?: ViewStyle;
121+
cancelText?: TextStyle;
122+
}
123+
124+
export interface FeedbackFormState {
125+
name: string;
126+
email: string;
127+
description: string;
128+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { getCurrentScope } from '@sentry/core';
2+
3+
import type { FeedbackFormProps } from './FeedbackForm.types';
4+
5+
const FORM_TITLE = 'Report a Bug';
6+
const NAME_PLACEHOLDER = 'Your Name';
7+
const NAME_LABEL = 'Name';
8+
const EMAIL_PLACEHOLDER = '[email protected]';
9+
const EMAIL_LABEL = 'Email';
10+
const MESSAGE_PLACEHOLDER = "What's the bug? What did you expect?";
11+
const MESSAGE_LABEL = 'Description';
12+
const IS_REQUIRED_LABEL = '(required)';
13+
const SUBMIT_BUTTON_LABEL = 'Send Bug Report';
14+
const CANCEL_BUTTON_LABEL = 'Cancel';
15+
const ERROR_TITLE = 'Error';
16+
const FORM_ERROR = 'Please fill out all required fields.';
17+
const EMAIL_ERROR = 'Please enter a valid email address.';
18+
19+
export const defaultConfiguration: Partial<FeedbackFormProps> = {
20+
// FeedbackGeneralConfiguration
21+
isEmailRequired: false,
22+
isNameRequired: false,
23+
showEmail: true,
24+
showName: true,
25+
useSentryUser: {
26+
email: getCurrentScope().getUser().email || '',
27+
name: getCurrentScope().getUser().name || '',
28+
},
29+
30+
// FeedbackTextConfiguration
31+
cancelButtonLabel: CANCEL_BUTTON_LABEL,
32+
emailLabel: EMAIL_LABEL,
33+
emailPlaceholder: EMAIL_PLACEHOLDER,
34+
formTitle: FORM_TITLE,
35+
isRequiredLabel: IS_REQUIRED_LABEL,
36+
messageLabel: MESSAGE_LABEL,
37+
messagePlaceholder: MESSAGE_PLACEHOLDER,
38+
nameLabel: NAME_LABEL,
39+
namePlaceholder: NAME_PLACEHOLDER,
40+
submitButtonLabel: SUBMIT_BUTTON_LABEL,
41+
errorTitle: ERROR_TITLE,
42+
formError: FORM_ERROR,
43+
emailError: EMAIL_ERROR,
44+
};

packages/core/src/js/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,5 @@ export {
8383
export type { TimeToDisplayProps } from './tracing';
8484

8585
export { Mask, Unmask } from './replay/CustomMask';
86+
87+
export { FeedbackForm } from './feedback/FeedbackForm';

0 commit comments

Comments
 (0)