Skip to content

Commit 26fc306

Browse files
authored
Merge 39a67bd into 9385d74
2 parents 9385d74 + 39a67bd commit 26fc306

File tree

9 files changed

+556
-0
lines changed

9 files changed

+556
-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: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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+
Alert.alert(text.successMessageText);
56+
};
57+
58+
/**
59+
* Renders the feedback form screen.
60+
*/
61+
public render(): React.ReactNode {
62+
const { closeScreen } = this.props;
63+
const { name, email, description } = this.state;
64+
const config: FeedbackGeneralConfiguration = { ...defaultConfiguration, ...this.props };
65+
const text: FeedbackTextConfiguration = { ...defaultConfiguration, ...this.props };
66+
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
67+
68+
return (
69+
<View style={styles.container}>
70+
<Text style={styles.title}>{text.formTitle}</Text>
71+
72+
<Text style={styles.label}>
73+
{text.nameLabel}
74+
{config.isNameRequired && ` ${text.isRequiredLabel}`}
75+
</Text>
76+
<TextInput
77+
style={styles.input}
78+
placeholder={text.namePlaceholder}
79+
value={name}
80+
onChangeText={(value) => this.setState({ name: value })}
81+
/>
82+
83+
<Text style={styles.label}>
84+
{text.emailLabel}
85+
{config.isEmailRequired && ` ${text.isRequiredLabel}`}
86+
</Text>
87+
<TextInput
88+
style={styles.input}
89+
placeholder={text.emailPlaceholder}
90+
keyboardType={'email-address' as KeyboardTypeOptions}
91+
value={email}
92+
onChangeText={(value) => this.setState({ email: value })}
93+
/>
94+
95+
<Text style={styles.label}>
96+
{text.messageLabel}
97+
{` ${text.isRequiredLabel}`}
98+
</Text>
99+
<TextInput
100+
style={[styles.input, styles.textArea]}
101+
placeholder={text.messagePlaceholder}
102+
value={description}
103+
onChangeText={(value) => this.setState({ description: value })}
104+
multiline
105+
/>
106+
107+
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
108+
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
109+
</TouchableOpacity>
110+
111+
<TouchableOpacity style={styles.cancelButton} onPress={closeScreen}>
112+
<Text style={styles.cancelText}>{text.cancelButtonLabel}</Text>
113+
</TouchableOpacity>
114+
</View>
115+
);
116+
}
117+
118+
private _isValidEmail = (email: string): boolean => {
119+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
120+
return emailRegex.test(email);
121+
};
122+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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+
* Message after feedback was sent successfully
88+
*/
89+
successMessageText?: string;
90+
91+
/**
92+
* Placeholder text for Feedback name input
93+
*/
94+
namePlaceholder?: string;
95+
96+
/**
97+
* Text which indicates that a field is required
98+
*/
99+
isRequiredLabel?: string;
100+
101+
/**
102+
* The title of the error dialog
103+
*/
104+
errorTitle?: string;
105+
106+
/**
107+
* The error message when the form is invalid
108+
*/
109+
formError?: string;
110+
111+
/**
112+
* The error message when the email is invalid
113+
*/
114+
emailError?: string;
115+
}
116+
117+
export interface FeedbackFormStyles {
118+
container?: ViewStyle;
119+
title?: TextStyle;
120+
label?: TextStyle;
121+
input?: TextStyle;
122+
textArea?: TextStyle;
123+
submitButton?: ViewStyle;
124+
submitText?: TextStyle;
125+
cancelButton?: ViewStyle;
126+
cancelText?: TextStyle;
127+
}
128+
129+
export interface FeedbackFormState {
130+
name: string;
131+
email: string;
132+
description: string;
133+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!';
19+
20+
export const defaultConfiguration: Partial<FeedbackFormProps> = {
21+
// FeedbackGeneralConfiguration
22+
isEmailRequired: false,
23+
isNameRequired: false,
24+
showEmail: true,
25+
showName: true,
26+
useSentryUser: {
27+
email: getCurrentScope().getUser().email || '',
28+
name: getCurrentScope().getUser().name || '',
29+
},
30+
31+
// FeedbackTextConfiguration
32+
cancelButtonLabel: CANCEL_BUTTON_LABEL,
33+
emailLabel: EMAIL_LABEL,
34+
emailPlaceholder: EMAIL_PLACEHOLDER,
35+
formTitle: FORM_TITLE,
36+
isRequiredLabel: IS_REQUIRED_LABEL,
37+
messageLabel: MESSAGE_LABEL,
38+
messagePlaceholder: MESSAGE_PLACEHOLDER,
39+
nameLabel: NAME_LABEL,
40+
namePlaceholder: NAME_PLACEHOLDER,
41+
submitButtonLabel: SUBMIT_BUTTON_LABEL,
42+
errorTitle: ERROR_TITLE,
43+
formError: FORM_ERROR,
44+
emailError: EMAIL_ERROR,
45+
successMessageText: SUCCESS_MESSAGE_TEXT,
46+
};

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)