Skip to content

Commit d33790a

Browse files
authored
Merge 78e412c into 2fb5240
2 parents 2fb5240 + 78e412c commit d33790a

File tree

9 files changed

+613
-0
lines changed

9 files changed

+613
-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: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { captureFeedback, 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+
public constructor(props: FeedbackFormProps) {
28+
super(props);
29+
30+
const config: FeedbackGeneralConfiguration = { ...defaultConfiguration, ...props };
31+
this.state = {
32+
isVisible: true,
33+
name: config.useSentryUser.name,
34+
email: config.useSentryUser.email,
35+
description: '',
36+
};
37+
}
38+
39+
public handleFeedbackSubmit: () => void = () => {
40+
const { name, email, description } = this.state;
41+
const { onFormClose } = { ...defaultConfiguration, ...this.props };
42+
const config: FeedbackGeneralConfiguration = { ...defaultConfiguration, ...this.props };
43+
const text: FeedbackTextConfiguration = { ...defaultConfiguration, ...this.props };
44+
45+
const trimmedName = name?.trim();
46+
const trimmedEmail = email?.trim();
47+
const trimmedDescription = description?.trim();
48+
49+
if ((config.isNameRequired && !trimmedName) || (config.isEmailRequired && !trimmedEmail) || !trimmedDescription) {
50+
Alert.alert(text.errorTitle, text.formError);
51+
return;
52+
}
53+
54+
if ((config.isEmailRequired || trimmedEmail.length > 0) && !this._isValidEmail(trimmedEmail)) {
55+
Alert.alert(text.errorTitle, text.emailError);
56+
return;
57+
}
58+
59+
const eventId = lastEventId();
60+
const userFeedback: SendFeedbackParams = {
61+
message: trimmedDescription,
62+
name: trimmedName,
63+
email: trimmedEmail,
64+
associatedEventId: eventId,
65+
};
66+
67+
onFormClose();
68+
this.setState({ isVisible: false });
69+
70+
captureFeedback(userFeedback);
71+
Alert.alert(text.successMessageText);
72+
};
73+
74+
/**
75+
* Renders the feedback form screen.
76+
*/
77+
public render(): React.ReactNode {
78+
const { name, email, description } = this.state;
79+
const { onFormClose } = { ...defaultConfiguration, ...this.props };
80+
const config: FeedbackGeneralConfiguration = { ...defaultConfiguration, ...this.props };
81+
const text: FeedbackTextConfiguration = { ...defaultConfiguration, ...this.props };
82+
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
83+
const onCancel = (): void => {
84+
onFormClose();
85+
this.setState({ isVisible: false });
86+
}
87+
88+
if (!this.state.isVisible) {
89+
return null;
90+
}
91+
92+
return (
93+
<SafeAreaView style={styles.container}>
94+
<KeyboardAvoidingView behavior={'padding'} style={styles.container}>
95+
<ScrollView>
96+
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
97+
<View style={styles.container}>
98+
<Text style={styles.title}>{text.formTitle}</Text>
99+
100+
{config.showName && (
101+
<>
102+
<Text style={styles.label}>
103+
{text.nameLabel}
104+
{config.isNameRequired && ` ${text.isRequiredLabel}`}
105+
</Text>
106+
<TextInput
107+
style={styles.input}
108+
placeholder={text.namePlaceholder}
109+
value={name}
110+
onChangeText={(value) => this.setState({ name: value })}
111+
/>
112+
</>
113+
)}
114+
115+
{config.showEmail && (
116+
<>
117+
<Text style={styles.label}>
118+
{text.emailLabel}
119+
{config.isEmailRequired && ` ${text.isRequiredLabel}`}
120+
</Text>
121+
<TextInput
122+
style={styles.input}
123+
placeholder={text.emailPlaceholder}
124+
keyboardType={'email-address' as KeyboardTypeOptions}
125+
value={email}
126+
onChangeText={(value) => this.setState({ email: value })}
127+
/>
128+
</>
129+
)}
130+
131+
<Text style={styles.label}>
132+
{text.messageLabel}
133+
{` ${text.isRequiredLabel}`}
134+
</Text>
135+
<TextInput
136+
style={[styles.input, styles.textArea]}
137+
placeholder={text.messagePlaceholder}
138+
value={description}
139+
onChangeText={(value) => this.setState({ description: value })}
140+
multiline
141+
/>
142+
143+
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
144+
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
145+
</TouchableOpacity>
146+
147+
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
148+
<Text style={styles.cancelText}>{text.cancelButtonLabel}</Text>
149+
</TouchableOpacity>
150+
</View>
151+
</TouchableWithoutFeedback>
152+
</ScrollView>
153+
</KeyboardAvoidingView>
154+
</SafeAreaView>
155+
);
156+
}
157+
158+
private _isValidEmail = (email: string): boolean => {
159+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
160+
return emailRegex.test(email);
161+
};
162+
}
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)