Skip to content

Commit

Permalink
(1) feat: Add Feedback Form Component (#4328)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Jan 10, 2025
1 parent ffd7b2b commit bda351c
Show file tree
Hide file tree
Showing 10 changed files with 688 additions and 0 deletions.
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 ([#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/>
```

### Fixes

- Use proper SDK name for Session Replay tags ([#4428](https://github.com/getsentry/sentry-react-native/pull/4428))
Expand Down
71 changes: 71 additions & 0 deletions packages/core/src/js/feedback/FeedbackForm.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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,
},
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,
},
};

export default defaultStyles;
182 changes: 182 additions & 0 deletions packages/core/src/js/feedback/FeedbackForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
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,
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';

/**
* @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}>
<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
/>

<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 bda351c

Please sign in to comment.