Skip to content

Commit

Permalink
Merge 03ece25 into e99226d
Browse files Browse the repository at this point in the history
  • Loading branch information
antonis authored Feb 7, 2025
2 parents e99226d + 03ece25 commit 6b1624f
Show file tree
Hide file tree
Showing 20 changed files with 3,158 additions and 1 deletion.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@
## Unreleased

### Features

- User Feedback Form Component Beta ([#4435](https://github.com/getsentry/sentry-react-native/pull/4435))

To collect user feedback from inside your application call `Sentry.showFeedbackForm()` or add the `FeedbackForm` component.

```jsx
import { FeedbackForm } from "@sentry/react-native";
...
<FeedbackForm/>
```

### Dependencies

- Bump JavaScript SDK from v8.53.0 to v8.54.0 ([#4503](https://github.com/getsentry/sentry-react-native/pull/4503))
Expand Down
86 changes: 86 additions & 0 deletions packages/core/src/js/feedback/FeedbackForm.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { FeedbackFormStyles } from './FeedbackForm.types';

const PURPLE = 'rgba(88, 74, 192, 1)';
const FORGROUND_COLOR = '#2b2233';
const BACKROUND_COLOR = '#ffffff';
const BORDER_COLOR = 'rgba(41, 35, 47, 0.13)';

const defaultStyles: FeedbackFormStyles = {
container: {
flex: 1,
padding: 20,
backgroundColor: BACKROUND_COLOR,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'left',
flex: 1,
color: FORGROUND_COLOR,
},
label: {
marginBottom: 4,
fontSize: 16,
color: FORGROUND_COLOR,
},
input: {
height: 50,
borderColor: BORDER_COLOR,
borderWidth: 1,
borderRadius: 5,
paddingHorizontal: 10,
marginBottom: 15,
fontSize: 16,
color: FORGROUND_COLOR,
},
textArea: {
height: 100,
textAlignVertical: 'top',
color: FORGROUND_COLOR,
},
screenshotButton: {
backgroundColor: '#eee',
padding: 15,
borderRadius: 5,
marginBottom: 20,
alignItems: 'center',
},
screenshotText: {
color: '#333',
fontSize: 16,
},
submitButton: {
backgroundColor: PURPLE,
paddingVertical: 15,
borderRadius: 5,
alignItems: 'center',
marginBottom: 10,
},
submitText: {
color: BACKROUND_COLOR,
fontSize: 18,
},
cancelButton: {
paddingVertical: 15,
alignItems: 'center',
},
cancelText: {
color: FORGROUND_COLOR,
fontSize: 16,
},
titleContainer: {
flexDirection: 'row',
width: '100%',
},
sentryLogo: {
width: 40,
height: 40,
},
modalBackground: {
flex: 1,
justifyContent: 'center',
},
};

export default defaultStyles;
213 changes: 213 additions & 0 deletions packages/core/src/js/feedback/FeedbackForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import type { SendFeedbackParams } from '@sentry/core';
import { captureFeedback, getCurrentScope, lastEventId, logger } from '@sentry/core';
import * as React from 'react';
import type { KeyboardTypeOptions } from 'react-native';
import {
Alert,
Image,
Keyboard,
KeyboardAvoidingView,
SafeAreaView,
ScrollView,
Text,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
View
} from 'react-native';

import { sentryLogo } from './branding';
import { defaultConfiguration } from './defaults';
import defaultStyles from './FeedbackForm.styles';
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles,FeedbackGeneralConfiguration, FeedbackTextConfiguration } from './FeedbackForm.types';
import { isValidEmail } from './utils';

/**
* @beta
* Implements a feedback form screen that sends feedback to Sentry using Sentry.captureFeedback.
*/
export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFormState> {
public static defaultProps: Partial<FeedbackFormProps> = {
...defaultConfiguration
}

public constructor(props: FeedbackFormProps) {
super(props);

const currentUser = {
useSentryUser: {
email: this.props?.useSentryUser?.email || getCurrentScope()?.getUser()?.email || '',
name: this.props?.useSentryUser?.name || getCurrentScope()?.getUser()?.name || '',
}
}

this.state = {
isVisible: true,
name: currentUser.useSentryUser.name,
email: currentUser.useSentryUser.email,
description: '',
};
}

public handleFeedbackSubmit: () => void = () => {
const { name, email, description } = this.state;
const { onSubmitSuccess, onSubmitError, onFormSubmitted } = this.props;
const text: FeedbackTextConfiguration = this.props;

const trimmedName = name?.trim();
const trimmedEmail = email?.trim();
const trimmedDescription = description?.trim();

if ((this.props.isNameRequired && !trimmedName) || (this.props.isEmailRequired && !trimmedEmail) || !trimmedDescription) {
Alert.alert(text.errorTitle, text.formError);
return;
}

if (this.props.shouldValidateEmail && (this.props.isEmailRequired || trimmedEmail.length > 0) && !isValidEmail(trimmedEmail)) {
Alert.alert(text.errorTitle, text.emailError);
return;
}

const attachments = this.state.filename && this.state.attachment
? [
{
filename: this.state.filename,
data: this.state.attachment,
},
]
: undefined;

const eventId = lastEventId();
const userFeedback: SendFeedbackParams = {
message: trimmedDescription,
name: trimmedName,
email: trimmedEmail,
associatedEventId: eventId,
};

try {
this.setState({ isVisible: false });
captureFeedback(userFeedback, attachments ? { attachments } : undefined);
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: undefined });
Alert.alert(text.successMessageText);
onFormSubmitted();
} catch (error) {
const errorString = `Feedback form submission failed: ${error}`;
onSubmitError(new Error(errorString));
Alert.alert(text.errorTitle, text.genericError);
logger.error(`Feedback form submission failed: ${error}`);
}
};

public onScreenshotButtonPress: () => void = () => {
if (!this.state.filename && !this.state.attachment) {
const { onAddScreenshot } = { ...defaultConfiguration, ...this.props };
onAddScreenshot((filename: string, attachement: Uint8Array) => {
this.setState({ filename, attachment: attachement });
});
} else {
this.setState({ filename: undefined, attachment: undefined });
}
}

/**
* Renders the feedback form screen.
*/
public render(): React.ReactNode {
const { name, email, description } = this.state;
const { onFormClose } = this.props;
const config: FeedbackGeneralConfiguration = this.props;
const text: FeedbackTextConfiguration = this.props;
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
const onCancel = (): void => {
onFormClose();
this.setState({ isVisible: false });
}

if (!this.state.isVisible) {
return null;
}

return (
<SafeAreaView style={[styles.container, { padding: 0 }]}>
<KeyboardAvoidingView behavior={'padding'} style={[styles.container, { padding: 0 }]}>
<ScrollView>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.container}>
<View style={styles.titleContainer}>
<Text style={styles.title}>{text.formTitle}</Text>
{config.showBranding && (
<Image
source={{ uri: sentryLogo }}
style={styles.sentryLogo}
testID='sentry-logo'
/>
)}
</View>

{config.showName && (
<>
<Text style={styles.label}>
{text.nameLabel}
{config.isNameRequired && ` ${text.isRequiredLabel}`}
</Text>
<TextInput
style={styles.input}
placeholder={text.namePlaceholder}
value={name}
onChangeText={(value) => this.setState({ name: value })}
/>
</>
)}

{config.showEmail && (
<>
<Text style={styles.label}>
{text.emailLabel}
{config.isEmailRequired && ` ${text.isRequiredLabel}`}
</Text>
<TextInput
style={styles.input}
placeholder={text.emailPlaceholder}
keyboardType={'email-address' as KeyboardTypeOptions}
value={email}
onChangeText={(value) => this.setState({ email: value })}
/>
</>
)}

<Text style={styles.label}>
{text.messageLabel}
{` ${text.isRequiredLabel}`}
</Text>
<TextInput
style={[styles.input, styles.textArea]}
placeholder={text.messagePlaceholder}
value={description}
onChangeText={(value) => this.setState({ description: value })}
multiline
/>
{config.enableScreenshot && (
<TouchableOpacity style={styles.screenshotButton} onPress={this.onScreenshotButtonPress}>
<Text style={styles.screenshotText}>
{!this.state.filename && !this.state.attachment
? text.addScreenshotButtonLabel
: text.removeScreenshotButtonLabel}
</Text>
</TouchableOpacity>
)}
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
</TouchableOpacity>

<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
<Text style={styles.cancelText}>{text.cancelButtonLabel}</Text>
</TouchableOpacity>
</View>
</TouchableWithoutFeedback>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
}
Loading

0 comments on commit 6b1624f

Please sign in to comment.