Skip to content

Commit

Permalink
Merge b209ad6 into 7f6950b
Browse files Browse the repository at this point in the history
  • Loading branch information
antonis authored Dec 16, 2024
2 parents 7f6950b + b209ad6 commit f4a5053
Show file tree
Hide file tree
Showing 9 changed files with 622 additions and 0 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@
});
```

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

To collect user feedback from inside your application add the `FeedbackForm` component.

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

- Export `Span` type from `@sentry/types` ([#4345](https://github.com/getsentry/sentry-react-native/pull/4345))

### Fixes
Expand Down
62 changes: 62 additions & 0 deletions packages/core/src/js/feedback/FeedbackForm.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { FeedbackFormStyles } from './FeedbackForm.types';

const PURPLE = 'rgba(88, 74, 192, 1)';
const FORGROUND_COLOR = '#2b2233';
const BACKROUND_COLOR = '#fff';
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: 'center',
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,
},
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,
},
};

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

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

/**
* @beta
* Implements a feedback form screen that sends feedback to Sentry using Sentry.captureFeedback.
*/
export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFormState> {
private _config: FeedbackFormProps;

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

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

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

public handleFeedbackSubmit: () => void = () => {
const { name, email, description } = this.state;
const { onFormClose } = this._config;
const text: FeedbackTextConfiguration = this._config;

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

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

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

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

onFormClose();
this.setState({ isVisible: false });

captureFeedback(userFeedback);
Alert.alert(text.successMessageText);
};

/**
* Renders the feedback form screen.
*/
public render(): React.ReactNode {
const { name, email, description } = this.state;
const { onFormClose } = this._config;
const config: FeedbackGeneralConfiguration = this._config;
const text: FeedbackTextConfiguration = this._config;
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}>
<Text style={styles.title}>{text.formTitle}</Text>

{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
/>

<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>
);
}

private _isValidEmail = (email: string): boolean => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return emailRegex.test(email);
};
}
148 changes: 148 additions & 0 deletions packages/core/src/js/feedback/FeedbackForm.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import type { TextStyle, ViewStyle } from 'react-native';

export interface FeedbackFormProps extends FeedbackGeneralConfiguration, FeedbackTextConfiguration, FeedbackCallbacks {
styles?: FeedbackFormStyles;
}

/**
* General feedback configuration
*/
export interface FeedbackGeneralConfiguration {
/**
* Should the email field be required?
*/
isEmailRequired?: boolean;

/**
* Should the email field be validated?
*/
shouldValidateEmail?: boolean;

/**
* Should the name field be required?
*/
isNameRequired?: boolean;

/**
* Should the email input field be visible? Note: email will still be collected if set via `Sentry.setUser()`
*/
showEmail?: boolean;

/**
* Should the name input field be visible? Note: name will still be collected if set via `Sentry.setUser()`
*/
showName?: boolean;

/**
* Fill in email/name input fields with Sentry user context if it exists.
* The value of the email/name keys represent the properties of your user context.
*/
useSentryUser?: {
email: string;
name: string;
};
}

/**
* All of the different text labels that can be customized
*/
export interface FeedbackTextConfiguration {
/**
* The label for the Feedback form cancel button that closes dialog
*/
cancelButtonLabel?: string;

/**
* The label for the Feedback form submit button that sends feedback
*/
submitButtonLabel?: string;

/**
* The title of the Feedback form
*/
formTitle?: string;

/**
* Label for the email input
*/
emailLabel?: string;

/**
* Placeholder text for Feedback email input
*/
emailPlaceholder?: string;

/**
* Label for the message input
*/
messageLabel?: string;

/**
* Placeholder text for Feedback message input
*/
messagePlaceholder?: string;

/**
* Label for the name input
*/
nameLabel?: string;

/**
* Message after feedback was sent successfully
*/
successMessageText?: string;

/**
* Placeholder text for Feedback name input
*/
namePlaceholder?: string;

/**
* Text which indicates that a field is required
*/
isRequiredLabel?: string;

/**
* The title of the error dialog
*/
errorTitle?: string;

/**
* The error message when the form is invalid
*/
formError?: string;

/**
* The error message when the email is invalid
*/
emailError?: string;
}

/**
* The public callbacks available for the feedback integration
*/
export interface FeedbackCallbacks {
/**
* Callback when form is closed and not submitted
*/
onFormClose?: () => void;
}

export interface FeedbackFormStyles {
container?: ViewStyle;
title?: TextStyle;
label?: TextStyle;
input?: TextStyle;
textArea?: TextStyle;
submitButton?: ViewStyle;
submitText?: TextStyle;
cancelButton?: ViewStyle;
cancelText?: TextStyle;
}

export interface FeedbackFormState {
isVisible: boolean;
name: string;
email: string;
description: string;
}
Loading

0 comments on commit f4a5053

Please sign in to comment.