Skip to content

Commit cadf235

Browse files
authored
Merge 4b5df7a into 1332acb
2 parents 1332acb + 4b5df7a commit cadf235

File tree

10 files changed

+427
-0
lines changed

10 files changed

+427
-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 { StyleSheet } from 'react-native';
2+
3+
const defaultStyles = StyleSheet.create({
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: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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 {
8+
CANCEL_BUTTON_LABEL,
9+
EMAIL_ERROR,
10+
EMAIL_LABEL,
11+
EMAIL_PLACEHOLDER,
12+
ERROR_TITLE,
13+
FORM_ERROR,
14+
FORM_TITLE,
15+
IS_REQUIRED_LABEL,
16+
MESSAGE_LABEL,
17+
MESSAGE_PLACEHOLDER,
18+
NAME_LABEL,
19+
NAME_PLACEHOLDER,
20+
SUBMIT_BUTTON_LABEL} from './constants';
21+
import defaultStyles from './FeedbackForm.styles';
22+
import type { FeedbackFormProps, FeedbackFormState } from './FeedbackForm.types';
23+
import LabelText from './LabelText';
24+
25+
/**
26+
* @beta
27+
* Implements a feedback form screen that sends feedback to Sentry using Sentry.captureFeedback.
28+
*/
29+
export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFormState> {
30+
public constructor(props: FeedbackFormProps) {
31+
super(props);
32+
this.state = {
33+
name: '',
34+
email: '',
35+
description: '',
36+
};
37+
}
38+
39+
public handleFeedbackSubmit: () => void = () => {
40+
const { name, email, description } = this.state;
41+
const { closeScreen, text } = this.props;
42+
43+
const trimmedName = name?.trim();
44+
const trimmedEmail = email?.trim();
45+
const trimmedDescription = description?.trim();
46+
47+
if (!trimmedName || !trimmedEmail || !trimmedDescription) {
48+
const errorMessage = text?.formError || FORM_ERROR;
49+
Alert.alert(text?.errorTitle || ERROR_TITLE, errorMessage);
50+
return;
51+
}
52+
53+
if (!this._isValidEmail(trimmedEmail)) {
54+
const errorMessage = text?.emailError || EMAIL_ERROR;
55+
Alert.alert(text?.errorTitle || ERROR_TITLE, errorMessage);
56+
return;
57+
}
58+
59+
const userFeedback: SendFeedbackParams = {
60+
message: trimmedDescription,
61+
name: trimmedName,
62+
email: trimmedEmail,
63+
};
64+
65+
captureFeedback(userFeedback);
66+
closeScreen();
67+
};
68+
69+
/**
70+
* Renders the feedback form screen.
71+
*/
72+
public render(): React.ReactNode {
73+
const { closeScreen, text, styles } = this.props;
74+
const { name, email, description } = this.state;
75+
76+
return (
77+
<View style={styles?.container || defaultStyles.container}>
78+
<Text style={styles?.title || defaultStyles.title}>{text?.formTitle || FORM_TITLE}</Text>
79+
80+
<LabelText
81+
label={text?.nameLabel || NAME_LABEL}
82+
isRequired={true}
83+
isRequiredLabel={text?.isRequiredLabel || IS_REQUIRED_LABEL}
84+
styles={styles?.label || defaultStyles.label}
85+
/>
86+
<TextInput
87+
style={styles?.input || defaultStyles.input}
88+
placeholder={text?.namePlaceholder || NAME_PLACEHOLDER}
89+
value={name}
90+
onChangeText={(value) => this.setState({ name: value })}
91+
/>
92+
<LabelText
93+
label={text?.emailLabel || EMAIL_LABEL}
94+
isRequired={true}
95+
isRequiredLabel={text?.isRequiredLabel || IS_REQUIRED_LABEL}
96+
styles={styles?.label || defaultStyles.label}
97+
/>
98+
<TextInput
99+
style={styles?.input || defaultStyles.input}
100+
placeholder={text?.emailPlaceholder || EMAIL_PLACEHOLDER}
101+
keyboardType={'email-address' as KeyboardTypeOptions}
102+
value={email}
103+
onChangeText={(value) => this.setState({ email: value })}
104+
/>
105+
<LabelText
106+
label={text?.descriptionLabel || MESSAGE_LABEL}
107+
isRequired={true}
108+
isRequiredLabel={text?.isRequiredLabel || IS_REQUIRED_LABEL}
109+
styles={styles?.label || defaultStyles.label}
110+
/>
111+
<TextInput
112+
style={[styles?.input || defaultStyles.input, styles?.textArea || defaultStyles.textArea]}
113+
placeholder={text?.descriptionPlaceholder || MESSAGE_PLACEHOLDER}
114+
value={description}
115+
onChangeText={(value) => this.setState({ description: value })}
116+
multiline
117+
/>
118+
119+
<TouchableOpacity style={styles?.submitButton || defaultStyles.submitButton} onPress={this.handleFeedbackSubmit}>
120+
<Text style={styles?.submitText || defaultStyles.submitText}>{text?.submitButton || SUBMIT_BUTTON_LABEL}</Text>
121+
</TouchableOpacity>
122+
123+
<TouchableOpacity style={styles?.cancelButton || defaultStyles.cancelButton} onPress={closeScreen}>
124+
<Text style={styles?.cancelText || defaultStyles.cancelText}>{text?.cancelButton || CANCEL_BUTTON_LABEL}</Text>
125+
</TouchableOpacity>
126+
</View>
127+
);
128+
}
129+
130+
private _isValidEmail = (email: string): boolean => {
131+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
132+
return emailRegex.test(email);
133+
};
134+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { TextStyle, ViewStyle } from 'react-native';
2+
3+
export interface FeedbackFormProps {
4+
closeScreen: () => void;
5+
text: FeedbackFormText;
6+
styles?: FeedbackFormStyles;
7+
}
8+
9+
export interface FeedbackFormText {
10+
formTitle?: string;
11+
nameLabel?: string;
12+
namePlaceholder?: string;
13+
emailLabel?: string;
14+
emailPlaceholder?: string;
15+
descriptionLabel?: string;
16+
descriptionPlaceholder?: string;
17+
isRequiredLabel?: string;
18+
submitButton?: string;
19+
cancelButton?: string;
20+
errorTitle?: string;
21+
formError?: string;
22+
emailError?: string;
23+
}
24+
25+
export interface FeedbackFormStyles {
26+
container?: ViewStyle;
27+
title?: TextStyle;
28+
label?: TextStyle;
29+
input?: TextStyle;
30+
textArea?: ViewStyle;
31+
submitButton?: ViewStyle;
32+
submitText?: TextStyle;
33+
cancelButton?: ViewStyle;
34+
cancelText?: TextStyle;
35+
}
36+
37+
export interface FeedbackFormState {
38+
name: string;
39+
email: string;
40+
description: string;
41+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
import type { TextStyle } from 'react-native';
3+
import { Text } from 'react-native';
4+
5+
interface LabelTextProps {
6+
label: string;
7+
isRequired: boolean;
8+
isRequiredLabel: string;
9+
styles: TextStyle;
10+
}
11+
12+
const LabelText: React.FC<LabelTextProps> = ({ label, isRequired, isRequiredLabel, styles }) => {
13+
return (
14+
<Text style={styles}>
15+
{label}
16+
{isRequired && ` ${isRequiredLabel}`}
17+
</Text>
18+
);
19+
};
20+
21+
export default LabelText;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const FORM_TITLE = 'Report a Bug';
2+
export const NAME_PLACEHOLDER = 'Your Name';
3+
export const NAME_LABEL = 'Name';
4+
export const EMAIL_PLACEHOLDER = '[email protected]';
5+
export const EMAIL_LABEL = 'Email';
6+
export const MESSAGE_PLACEHOLDER = "What's the bug? What did you expect?";
7+
export const MESSAGE_LABEL = 'Description';
8+
export const IS_REQUIRED_LABEL = '(required)';
9+
export const SUBMIT_BUTTON_LABEL = 'Send Bug Report';
10+
export const CANCEL_BUTTON_LABEL = 'Cancel';
11+
export const ERROR_TITLE = 'Error';
12+
export const FORM_ERROR = 'Please fill out all required fields.';
13+
export const EMAIL_ERROR = 'Please enter a valid email address.';

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)