Skip to content

Commit

Permalink
Merge 4b5df7a into 1332acb
Browse files Browse the repository at this point in the history
  • Loading branch information
antonis authored Dec 10, 2024
2 parents 1332acb + 4b5df7a commit cadf235
Show file tree
Hide file tree
Showing 10 changed files with 427 additions and 0 deletions.
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,29 @@
});
```

- Adds feedback form ([#4320](https://github.com/getsentry/sentry-react-native/pull/4328))

You can add the form component in your UI and customise it like:
```jsx
import { FeedbackForm } from "@sentry/react-native";
...
<FeedbackForm
{...props}
closeScreen={props.navigation.goBack}
styles={{
submitButton: {
backgroundColor: '#6a1b9a',
paddingVertical: 15,
borderRadius: 5,
alignItems: 'center',
marginBottom: 10,
},
}}
text={{namePlaceholder: 'Fullname'}}
/>
```
Check [the documentation](https://docs.sentry.io/platforms/react-native/user-feedback/) for more configuration options.

### Fixes

- Return `lastEventId` export from `@sentry/core` ([#4315](https://github.com/getsentry/sentry-react-native/pull/4315))
Expand Down
54 changes: 54 additions & 0 deletions packages/core/src/js/feedback/FeedbackForm.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { StyleSheet } from 'react-native';

const defaultStyles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#fff',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'center',
},
label: {
marginBottom: 4,
fontSize: 16,
},
input: {
height: 50,
borderColor: '#ccc',
borderWidth: 1,
borderRadius: 5,
paddingHorizontal: 10,
marginBottom: 15,
fontSize: 16,
},
textArea: {
height: 100,
textAlignVertical: 'top',
},
submitButton: {
backgroundColor: '#6a1b9a',
paddingVertical: 15,
borderRadius: 5,
alignItems: 'center',
marginBottom: 10,
},
submitText: {
color: '#fff',
fontSize: 18,
fontWeight: 'bold',
},
cancelButton: {
paddingVertical: 15,
alignItems: 'center',
},
cancelText: {
color: '#6a1b9a',
fontSize: 16,
},
});

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

import {
CANCEL_BUTTON_LABEL,
EMAIL_ERROR,
EMAIL_LABEL,
EMAIL_PLACEHOLDER,
ERROR_TITLE,
FORM_ERROR,
FORM_TITLE,
IS_REQUIRED_LABEL,
MESSAGE_LABEL,
MESSAGE_PLACEHOLDER,
NAME_LABEL,
NAME_PLACEHOLDER,
SUBMIT_BUTTON_LABEL} from './constants';
import defaultStyles from './FeedbackForm.styles';
import type { FeedbackFormProps, FeedbackFormState } from './FeedbackForm.types';
import LabelText from './LabelText';

/**
* @beta
* Implements a feedback form screen that sends feedback to Sentry using Sentry.captureFeedback.
*/
export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFormState> {
public constructor(props: FeedbackFormProps) {
super(props);
this.state = {
name: '',
email: '',
description: '',
};
}

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

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

if (!trimmedName || !trimmedEmail || !trimmedDescription) {
const errorMessage = text?.formError || FORM_ERROR;
Alert.alert(text?.errorTitle || ERROR_TITLE, errorMessage);
return;
}

if (!this._isValidEmail(trimmedEmail)) {
const errorMessage = text?.emailError || EMAIL_ERROR;
Alert.alert(text?.errorTitle || ERROR_TITLE, errorMessage);
return;
}

const userFeedback: SendFeedbackParams = {
message: trimmedDescription,
name: trimmedName,
email: trimmedEmail,
};

captureFeedback(userFeedback);
closeScreen();
};

/**
* Renders the feedback form screen.
*/
public render(): React.ReactNode {
const { closeScreen, text, styles } = this.props;
const { name, email, description } = this.state;

return (
<View style={styles?.container || defaultStyles.container}>
<Text style={styles?.title || defaultStyles.title}>{text?.formTitle || FORM_TITLE}</Text>

<LabelText
label={text?.nameLabel || NAME_LABEL}
isRequired={true}
isRequiredLabel={text?.isRequiredLabel || IS_REQUIRED_LABEL}
styles={styles?.label || defaultStyles.label}
/>
<TextInput
style={styles?.input || defaultStyles.input}
placeholder={text?.namePlaceholder || NAME_PLACEHOLDER}
value={name}
onChangeText={(value) => this.setState({ name: value })}
/>
<LabelText
label={text?.emailLabel || EMAIL_LABEL}
isRequired={true}
isRequiredLabel={text?.isRequiredLabel || IS_REQUIRED_LABEL}
styles={styles?.label || defaultStyles.label}
/>
<TextInput
style={styles?.input || defaultStyles.input}
placeholder={text?.emailPlaceholder || EMAIL_PLACEHOLDER}
keyboardType={'email-address' as KeyboardTypeOptions}
value={email}
onChangeText={(value) => this.setState({ email: value })}
/>
<LabelText
label={text?.descriptionLabel || MESSAGE_LABEL}
isRequired={true}
isRequiredLabel={text?.isRequiredLabel || IS_REQUIRED_LABEL}
styles={styles?.label || defaultStyles.label}
/>
<TextInput
style={[styles?.input || defaultStyles.input, styles?.textArea || defaultStyles.textArea]}
placeholder={text?.descriptionPlaceholder || MESSAGE_PLACEHOLDER}
value={description}
onChangeText={(value) => this.setState({ description: value })}
multiline
/>

<TouchableOpacity style={styles?.submitButton || defaultStyles.submitButton} onPress={this.handleFeedbackSubmit}>
<Text style={styles?.submitText || defaultStyles.submitText}>{text?.submitButton || SUBMIT_BUTTON_LABEL}</Text>
</TouchableOpacity>

<TouchableOpacity style={styles?.cancelButton || defaultStyles.cancelButton} onPress={closeScreen}>
<Text style={styles?.cancelText || defaultStyles.cancelText}>{text?.cancelButton || CANCEL_BUTTON_LABEL}</Text>
</TouchableOpacity>
</View>
);
}

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

export interface FeedbackFormProps {
closeScreen: () => void;
text: FeedbackFormText;
styles?: FeedbackFormStyles;
}

export interface FeedbackFormText {
formTitle?: string;
nameLabel?: string;
namePlaceholder?: string;
emailLabel?: string;
emailPlaceholder?: string;
descriptionLabel?: string;
descriptionPlaceholder?: string;
isRequiredLabel?: string;
submitButton?: string;
cancelButton?: string;
errorTitle?: string;
formError?: string;
emailError?: string;
}

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

export interface FeedbackFormState {
name: string;
email: string;
description: string;
}
21 changes: 21 additions & 0 deletions packages/core/src/js/feedback/LabelText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import type { TextStyle } from 'react-native';
import { Text } from 'react-native';

interface LabelTextProps {
label: string;
isRequired: boolean;
isRequiredLabel: string;
styles: TextStyle;
}

const LabelText: React.FC<LabelTextProps> = ({ label, isRequired, isRequiredLabel, styles }) => {
return (
<Text style={styles}>
{label}
{isRequired && ` ${isRequiredLabel}`}
</Text>
);
};

export default LabelText;
13 changes: 13 additions & 0 deletions packages/core/src/js/feedback/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const FORM_TITLE = 'Report a Bug';
export const NAME_PLACEHOLDER = 'Your Name';
export const NAME_LABEL = 'Name';
export const EMAIL_PLACEHOLDER = '[email protected]';
export const EMAIL_LABEL = 'Email';
export const MESSAGE_PLACEHOLDER = "What's the bug? What did you expect?";
export const MESSAGE_LABEL = 'Description';
export const IS_REQUIRED_LABEL = '(required)';
export const SUBMIT_BUTTON_LABEL = 'Send Bug Report';
export const CANCEL_BUTTON_LABEL = 'Cancel';
export const ERROR_TITLE = 'Error';
export const FORM_ERROR = 'Please fill out all required fields.';
export const EMAIL_ERROR = 'Please enter a valid email address.';
2 changes: 2 additions & 0 deletions packages/core/src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,5 @@ export {
export type { TimeToDisplayProps } from './tracing';

export { Mask, Unmask } from './replay/CustomMask';

export { FeedbackForm } from './feedback/FeedbackForm';
Loading

0 comments on commit cadf235

Please sign in to comment.