Skip to content

Commit bda351c

Browse files
antoniskrystofwoldrichlucas-zimerman
authored
(1) feat: Add Feedback Form Component (#4328)
* Update the client implementation to use the new capture feedback js api * Updates SDK API * Adds new feedback button in the sample * Adds changelog * Removes unused mock * Update CHANGELOG.md Co-authored-by: Krystof Woldrich <[email protected]> * Directly use captureFeedback from sentry/core * Use import from core * Fixes imports order lint issue * Fixes build issue * Adds captureFeedback tests from sentry-javascript * Update CHANGELOG.md * Only deprecate client captureUserFeedback * Add simple form UI * Adds basic form functionality * Update imports * Update imports * Remove useState hook to avoid multiple react instances issues * Move types and styles in different files * Removes attachment button to be added back separately along with the implementation * Add basic field validation * Adds changelog * Updates changelog * Updates changelog * Trim whitespaces from the submitted feedback * Adds tests * Renames FeedbackFormScreen to FeedbackForm * Add beta label * Extract default text to constants * Moves constant to a separate file and aligns naming with JS * Adds input text labels * Close screen before sending the feedback to minimise wait time Co-authored-by: LucasZF <[email protected]> * Rename file for consistency * Flatten configuration hierarchy and clean up * Align required values with JS * Use Sentry user email and name when set * Simplifies email validation * Show success alert message * Aligns naming with JS and unmounts the form by default * Use the minimum config without props in the changelog * Adds development not for unimplemented function * Show email and name conditionally * Adds sentry branding (png logo) * Adds sentry logo resource * Add assets in module exports * Revert "Add assets in module exports" This reverts commit 5292475. * Revert "Adds sentry logo resource" This reverts commit d6e9229. * Revert "Adds sentry branding (png logo)" This reverts commit 8c56753. * Add last event id * Mock lastEventId * Adds beta note in the changelog * Updates changelog * Align colors with JS * Update CHANGELOG.md Co-authored-by: Krystof Woldrich <[email protected]> * Update CHANGELOG.md Co-authored-by: Krystof Woldrich <[email protected]> * Update CHANGELOG.md Co-authored-by: Krystof Woldrich <[email protected]> * Use regular fonts for both buttons * Handle keyboard properly * Adds an option on whether the email should be validated * Merge properties only once * Loads current user data on form construction * Remove unneeded extra padding * Fix background color issue * Fixes changelog typo * Updates styles background color Co-authored-by: Krystof Woldrich <[email protected]> * Use defaultProps * Correct defaultProps * Adds test to verify when getUser is called * (2.2) feat: Add Feedback Form UI Branding logo (#4357) * Adds sentry branding logo as a base64 encoded png --------- Co-authored-by: LucasZF <[email protected]> * Autoinject feedback form (#4370) * Align changelog entry * Update changelog * Revert "Autoinject feedback form (#4370)" This reverts commit da0e3ea. --------- Co-authored-by: Krystof Woldrich <[email protected]> Co-authored-by: LucasZF <[email protected]>
1 parent ffd7b2b commit bda351c

File tree

10 files changed

+688
-0
lines changed

10 files changed

+688
-0
lines changed

CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@
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+
1123
### Fixes
1224

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

0 commit comments

Comments
 (0)