Skip to content

Commit 03c9048

Browse files
authored
Merge 0588552 into 205982c
2 parents 205982c + 0588552 commit 03c9048

File tree

9 files changed

+636
-1
lines changed

9 files changed

+636
-1
lines changed

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,17 @@
2525
});
2626
```
2727

28-
To learn how to attach context data to the feedback visit [the documentation](https://docs.sentry.io/platforms/react-native/user-feedback/).
28+
To learn how to attach context data to the feedback visit [the documentation](https://docs.sentry.io/platforms/react-native/user-feedback/).
29+
30+
- User Feedback Form Component Beta ([#4320](https://github.com/getsentry/sentry-react-native/pull/4328))
31+
32+
To collect user feedback from inside your application add the `FeedbackForm` component.
33+
34+
```jsx
35+
import { FeedbackForm } from "@sentry/react-native";
36+
...
37+
<FeedbackForm/>
38+
```
2939

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

0 commit comments

Comments
 (0)