Skip to content

Commit a06f6ba

Browse files
authored
Merge c80c5cb into 9385d74
2 parents 9385d74 + c80c5cb commit a06f6ba

File tree

9 files changed

+583
-0
lines changed

9 files changed

+583
-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+
- 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+
```
46+
Check [the documentation](https://docs.sentry.io/platforms/react-native/user-feedback/) for more configuration options.
47+
3848
### Fixes
3949

4050
- 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: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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+
isVisible: true,
22+
name: config.useSentryUser.name,
23+
email: config.useSentryUser.email,
24+
description: '',
25+
};
26+
}
27+
28+
public handleFeedbackSubmit: () => void = () => {
29+
const { name, email, description } = this.state;
30+
const { onFormClose } = { ...defaultConfiguration, ...this.props };
31+
const config: FeedbackGeneralConfiguration = { ...defaultConfiguration, ...this.props };
32+
const text: FeedbackTextConfiguration = { ...defaultConfiguration, ...this.props };
33+
34+
const trimmedName = name?.trim();
35+
const trimmedEmail = email?.trim();
36+
const trimmedDescription = description?.trim();
37+
38+
if ((config.isNameRequired && !trimmedName) || (config.isEmailRequired && !trimmedEmail) || !trimmedDescription) {
39+
Alert.alert(text.errorTitle, text.formError);
40+
return;
41+
}
42+
43+
if ((config.isEmailRequired || trimmedEmail.length > 0) && !this._isValidEmail(trimmedEmail)) {
44+
Alert.alert(text.errorTitle, text.emailError);
45+
return;
46+
}
47+
48+
const userFeedback: SendFeedbackParams = {
49+
message: trimmedDescription,
50+
name: trimmedName,
51+
email: trimmedEmail,
52+
};
53+
54+
onFormClose();
55+
this.setState({ isVisible: false });
56+
57+
captureFeedback(userFeedback);
58+
Alert.alert(text.successMessageText);
59+
};
60+
61+
/**
62+
* Renders the feedback form screen.
63+
*/
64+
public render(): React.ReactNode {
65+
const { name, email, description } = this.state;
66+
const { onFormClose } = { ...defaultConfiguration, ...this.props };
67+
const config: FeedbackGeneralConfiguration = { ...defaultConfiguration, ...this.props };
68+
const text: FeedbackTextConfiguration = { ...defaultConfiguration, ...this.props };
69+
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
70+
const onCancel = (): void => {
71+
onFormClose();
72+
this.setState({ isVisible: false });
73+
}
74+
75+
if (!this.state.isVisible) {
76+
return null;
77+
}
78+
79+
return (
80+
<View style={styles.container}>
81+
<Text style={styles.title}>{text.formTitle}</Text>
82+
83+
{config.showName && (
84+
<>
85+
<Text style={styles.label}>
86+
{text.nameLabel}
87+
{config.isNameRequired && ` ${text.isRequiredLabel}`}
88+
</Text>
89+
<TextInput
90+
style={styles.input}
91+
placeholder={text.namePlaceholder}
92+
value={name}
93+
onChangeText={(value) => this.setState({ name: value })}
94+
/>
95+
</>
96+
)}
97+
98+
{config.showEmail && (
99+
<>
100+
<Text style={styles.label}>
101+
{text.emailLabel}
102+
{config.isEmailRequired && ` ${text.isRequiredLabel}`}
103+
</Text>
104+
<TextInput
105+
style={styles.input}
106+
placeholder={text.emailPlaceholder}
107+
keyboardType={'email-address' as KeyboardTypeOptions}
108+
value={email}
109+
onChangeText={(value) => this.setState({ email: value })}
110+
/>
111+
</>
112+
)}
113+
114+
<Text style={styles.label}>
115+
{text.messageLabel}
116+
{` ${text.isRequiredLabel}`}
117+
</Text>
118+
<TextInput
119+
style={[styles.input, styles.textArea]}
120+
placeholder={text.messagePlaceholder}
121+
value={description}
122+
onChangeText={(value) => this.setState({ description: value })}
123+
multiline
124+
/>
125+
126+
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
127+
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
128+
</TouchableOpacity>
129+
130+
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
131+
<Text style={styles.cancelText}>{text.cancelButtonLabel}</Text>
132+
</TouchableOpacity>
133+
</View>
134+
);
135+
}
136+
137+
private _isValidEmail = (email: string): boolean => {
138+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
139+
return emailRegex.test(email);
140+
};
141+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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 name field be required?
18+
*/
19+
isNameRequired?: boolean;
20+
21+
/**
22+
* Should the email input field be visible? Note: email will still be collected if set via `Sentry.setUser()`
23+
*/
24+
showEmail?: boolean;
25+
26+
/**
27+
* Should the name input field be visible? Note: name will still be collected if set via `Sentry.setUser()`
28+
*/
29+
showName?: boolean;
30+
31+
/**
32+
* Fill in email/name input fields with Sentry user context if it exists.
33+
* The value of the email/name keys represent the properties of your user context.
34+
*/
35+
useSentryUser?: {
36+
email: string;
37+
name: string;
38+
};
39+
}
40+
41+
/**
42+
* All of the different text labels that can be customized
43+
*/
44+
export interface FeedbackTextConfiguration {
45+
/**
46+
* The label for the Feedback form cancel button that closes dialog
47+
*/
48+
cancelButtonLabel?: string;
49+
50+
/**
51+
* The label for the Feedback form submit button that sends feedback
52+
*/
53+
submitButtonLabel?: string;
54+
55+
/**
56+
* The title of the Feedback form
57+
*/
58+
formTitle?: string;
59+
60+
/**
61+
* Label for the email input
62+
*/
63+
emailLabel?: string;
64+
65+
/**
66+
* Placeholder text for Feedback email input
67+
*/
68+
emailPlaceholder?: string;
69+
70+
/**
71+
* Label for the message input
72+
*/
73+
messageLabel?: string;
74+
75+
/**
76+
* Placeholder text for Feedback message input
77+
*/
78+
messagePlaceholder?: string;
79+
80+
/**
81+
* Label for the name input
82+
*/
83+
nameLabel?: string;
84+
85+
/**
86+
* Message after feedback was sent successfully
87+
*/
88+
successMessageText?: string;
89+
90+
/**
91+
* Placeholder text for Feedback name input
92+
*/
93+
namePlaceholder?: string;
94+
95+
/**
96+
* Text which indicates that a field is required
97+
*/
98+
isRequiredLabel?: string;
99+
100+
/**
101+
* The title of the error dialog
102+
*/
103+
errorTitle?: string;
104+
105+
/**
106+
* The error message when the form is invalid
107+
*/
108+
formError?: string;
109+
110+
/**
111+
* The error message when the email is invalid
112+
*/
113+
emailError?: string;
114+
}
115+
116+
/**
117+
* The public callbacks available for the feedback integration
118+
*/
119+
export interface FeedbackCallbacks {
120+
/**
121+
* Callback when form is closed and not submitted
122+
*/
123+
onFormClose?: () => void;
124+
}
125+
126+
export interface FeedbackFormStyles {
127+
container?: ViewStyle;
128+
title?: TextStyle;
129+
label?: TextStyle;
130+
input?: TextStyle;
131+
textArea?: TextStyle;
132+
submitButton?: ViewStyle;
133+
submitText?: TextStyle;
134+
cancelButton?: ViewStyle;
135+
cancelText?: TextStyle;
136+
}
137+
138+
export interface FeedbackFormState {
139+
isVisible: boolean;
140+
name: string;
141+
email: string;
142+
description: string;
143+
}

0 commit comments

Comments
 (0)