Skip to content

Commit

Permalink
Merge 0588552 into 205982c
Browse files Browse the repository at this point in the history
  • Loading branch information
antonis authored Dec 17, 2024
2 parents 205982c + 0588552 commit 03c9048
Show file tree
Hide file tree
Showing 9 changed files with 636 additions and 1 deletion.
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@
});
```

To learn how to attach context data to the feedback visit [the documentation](https://docs.sentry.io/platforms/react-native/user-feedback/).
To learn how to attach context data to the feedback visit [the documentation](https://docs.sentry.io/platforms/react-native/user-feedback/).

- 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))
- Add RN SDK package to `sdk.packages` on Android ([#4380](https://github.com/getsentry/sentry-react-native/pull/4380))
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 = '#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: '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;
171 changes: 171 additions & 0 deletions packages/core/src/js/feedback/FeedbackForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import type { SendFeedbackParams } from '@sentry/core';
import { captureFeedback, getCurrentScope, lastEventId } from '@sentry/core';
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> {
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 { onFormClose } = 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) && !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.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}>
<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);
};
}
Loading

0 comments on commit 03c9048

Please sign in to comment.