Skip to content

Commit 41e9784

Browse files
authored
Merge 9853630 into 5f03ae9
2 parents 5f03ae9 + 9853630 commit 41e9784

File tree

10 files changed

+738
-0
lines changed

10 files changed

+738
-0
lines changed

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@
88
99
## Unreleased
1010

11+
### Features
12+
13+
- User Feedback Form Component Beta ([#4320](https://github.com/getsentry/sentry-react-native/pull/4328))
14+
15+
To collect user feedback from inside your application add the `FeedbackForm` component.
16+
17+
```jsx
18+
import { FeedbackForm } from "@sentry/react-native";
19+
...
20+
<FeedbackForm/>
21+
```
22+
or auto-inject it by calling the `showFeedbackForm`:
23+
```jsx
24+
import { showFeedbackForm } from '@sentry/react-native';
25+
...
26+
<Button
27+
title="Show feedback form"
28+
onPress={() => {
29+
showFeedbackForm(_props.navigation);
30+
}}
31+
/>
32+
```
33+
1134
### Changes
1235

1336
- Rename `navigation.processing` span to more expressive `Navigation dispatch to screen A mounted/navigation cancelled` ([#4423](https://github.com/getsentry/sentry-react-native/pull/4423))
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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: 'left',
19+
flex: 1,
20+
color: FORGROUND_COLOR,
21+
},
22+
label: {
23+
marginBottom: 4,
24+
fontSize: 16,
25+
color: FORGROUND_COLOR,
26+
},
27+
input: {
28+
height: 50,
29+
borderColor: BORDER_COLOR,
30+
borderWidth: 1,
31+
borderRadius: 5,
32+
paddingHorizontal: 10,
33+
marginBottom: 15,
34+
fontSize: 16,
35+
color: FORGROUND_COLOR,
36+
},
37+
textArea: {
38+
height: 100,
39+
textAlignVertical: 'top',
40+
color: FORGROUND_COLOR,
41+
},
42+
submitButton: {
43+
backgroundColor: PURPLE,
44+
paddingVertical: 15,
45+
borderRadius: 5,
46+
alignItems: 'center',
47+
marginBottom: 10,
48+
},
49+
submitText: {
50+
color: BACKROUND_COLOR,
51+
fontSize: 18,
52+
},
53+
cancelButton: {
54+
paddingVertical: 15,
55+
alignItems: 'center',
56+
},
57+
cancelText: {
58+
color: FORGROUND_COLOR,
59+
fontSize: 16,
60+
},
61+
titleContainer: {
62+
flexDirection: 'row',
63+
width: '100%',
64+
},
65+
sentryLogo: {
66+
width: 40,
67+
height: 40,
68+
},
69+
};
70+
71+
export default defaultStyles;
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import type { SendFeedbackParams } from '@sentry/core';
2+
import { captureFeedback, getCurrentScope, lastEventId, logger } from '@sentry/core';
3+
import * as React from 'react';
4+
import type { KeyboardTypeOptions } from 'react-native';
5+
import {
6+
Alert,
7+
Image,
8+
Keyboard,
9+
KeyboardAvoidingView,
10+
SafeAreaView,
11+
ScrollView,
12+
Text,
13+
TextInput,
14+
TouchableOpacity,
15+
TouchableWithoutFeedback,
16+
View
17+
} from 'react-native';
18+
19+
import { sentryLogo } from './branding';
20+
import { defaultConfiguration } from './defaults';
21+
import defaultStyles from './FeedbackForm.styles';
22+
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles,FeedbackGeneralConfiguration, FeedbackTextConfiguration } from './FeedbackForm.types';
23+
24+
let feedbackFormHandler: (() => void) | null = null;
25+
26+
const setFeedbackFormHandler = (handler: () => void): void => {
27+
feedbackFormHandler = handler;
28+
};
29+
30+
const clearFeedbackFormHandler = (): void => {
31+
feedbackFormHandler = null;
32+
};
33+
34+
type Navigation = {
35+
navigate: (screen: string, params?: Record<string, unknown>) => void;
36+
};
37+
38+
export const showFeedbackForm = (navigation: Navigation): void => {
39+
setFeedbackFormHandler(() => {
40+
navigation?.navigate?.('FeedbackForm');
41+
});
42+
if (feedbackFormHandler) {
43+
feedbackFormHandler();
44+
} else {
45+
logger.error('FeedbackForm handler is not set. Please ensure it is initialized.');
46+
}
47+
};
48+
49+
/**
50+
* @beta
51+
* Implements a feedback form screen that sends feedback to Sentry using Sentry.captureFeedback.
52+
*/
53+
export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFormState> {
54+
public static defaultProps: Partial<FeedbackFormProps> = {
55+
...defaultConfiguration
56+
}
57+
58+
public constructor(props: FeedbackFormProps) {
59+
super(props);
60+
61+
const currentUser = {
62+
useSentryUser: {
63+
email: this.props?.useSentryUser?.email || getCurrentScope()?.getUser()?.email || '',
64+
name: this.props?.useSentryUser?.name || getCurrentScope()?.getUser()?.name || '',
65+
}
66+
}
67+
68+
this.state = {
69+
isVisible: true,
70+
name: currentUser.useSentryUser.name,
71+
email: currentUser.useSentryUser.email,
72+
description: '',
73+
};
74+
}
75+
76+
/**
77+
* Clear the handler when the component unmounts
78+
*/
79+
public componentWillUnmount(): void {
80+
clearFeedbackFormHandler();
81+
}
82+
83+
public handleFeedbackSubmit: () => void = () => {
84+
const { name, email, description } = this.state;
85+
const { onFormClose } = this.props;
86+
const text: FeedbackTextConfiguration = this.props;
87+
88+
const trimmedName = name?.trim();
89+
const trimmedEmail = email?.trim();
90+
const trimmedDescription = description?.trim();
91+
92+
if ((this.props.isNameRequired && !trimmedName) || (this.props.isEmailRequired && !trimmedEmail) || !trimmedDescription) {
93+
Alert.alert(text.errorTitle, text.formError);
94+
return;
95+
}
96+
97+
if (this.props.shouldValidateEmail && (this.props.isEmailRequired || trimmedEmail.length > 0) && !this._isValidEmail(trimmedEmail)) {
98+
Alert.alert(text.errorTitle, text.emailError);
99+
return;
100+
}
101+
102+
const eventId = lastEventId();
103+
const userFeedback: SendFeedbackParams = {
104+
message: trimmedDescription,
105+
name: trimmedName,
106+
email: trimmedEmail,
107+
associatedEventId: eventId,
108+
};
109+
110+
onFormClose();
111+
this.setState({ isVisible: false });
112+
113+
captureFeedback(userFeedback);
114+
Alert.alert(text.successMessageText);
115+
};
116+
117+
/**
118+
* Renders the feedback form screen.
119+
*/
120+
public render(): React.ReactNode {
121+
const { name, email, description } = this.state;
122+
const { onFormClose } = this.props;
123+
const config: FeedbackGeneralConfiguration = this.props;
124+
const text: FeedbackTextConfiguration = this.props;
125+
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
126+
const onCancel = (): void => {
127+
onFormClose();
128+
this.setState({ isVisible: false });
129+
}
130+
131+
if (!this.state.isVisible) {
132+
return null;
133+
}
134+
135+
return (
136+
<SafeAreaView style={[styles.container, { padding: 0 }]}>
137+
<KeyboardAvoidingView behavior={'padding'} style={[styles.container, { padding: 0 }]}>
138+
<ScrollView>
139+
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
140+
<View style={styles.container}>
141+
<View style={styles.titleContainer}>
142+
<Text style={styles.title}>{text.formTitle}</Text>
143+
{config.showBranding && (
144+
<Image
145+
source={{ uri: sentryLogo }}
146+
style={styles.sentryLogo}
147+
testID='sentry-logo'
148+
/>
149+
)}
150+
</View>
151+
152+
{config.showName && (
153+
<>
154+
<Text style={styles.label}>
155+
{text.nameLabel}
156+
{config.isNameRequired && ` ${text.isRequiredLabel}`}
157+
</Text>
158+
<TextInput
159+
style={styles.input}
160+
placeholder={text.namePlaceholder}
161+
value={name}
162+
onChangeText={(value) => this.setState({ name: value })}
163+
/>
164+
</>
165+
)}
166+
167+
{config.showEmail && (
168+
<>
169+
<Text style={styles.label}>
170+
{text.emailLabel}
171+
{config.isEmailRequired && ` ${text.isRequiredLabel}`}
172+
</Text>
173+
<TextInput
174+
style={styles.input}
175+
placeholder={text.emailPlaceholder}
176+
keyboardType={'email-address' as KeyboardTypeOptions}
177+
value={email}
178+
onChangeText={(value) => this.setState({ email: value })}
179+
/>
180+
</>
181+
)}
182+
183+
<Text style={styles.label}>
184+
{text.messageLabel}
185+
{` ${text.isRequiredLabel}`}
186+
</Text>
187+
<TextInput
188+
style={[styles.input, styles.textArea]}
189+
placeholder={text.messagePlaceholder}
190+
value={description}
191+
onChangeText={(value) => this.setState({ description: value })}
192+
multiline
193+
/>
194+
195+
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
196+
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
197+
</TouchableOpacity>
198+
199+
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
200+
<Text style={styles.cancelText}>{text.cancelButtonLabel}</Text>
201+
</TouchableOpacity>
202+
</View>
203+
</TouchableWithoutFeedback>
204+
</ScrollView>
205+
</KeyboardAvoidingView>
206+
</SafeAreaView>
207+
);
208+
}
209+
210+
private _isValidEmail = (email: string): boolean => {
211+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
212+
return emailRegex.test(email);
213+
};
214+
}

0 commit comments

Comments
 (0)