diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0580a1b851..c97c86296c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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";
+ ...
+
+ ```
+
### Fixes
- Use proper SDK name for Session Replay tags ([#4428](https://github.com/getsentry/sentry-react-native/pull/4428))
diff --git a/packages/core/src/js/feedback/FeedbackForm.styles.ts b/packages/core/src/js/feedback/FeedbackForm.styles.ts
new file mode 100644
index 0000000000..836f4e1629
--- /dev/null
+++ b/packages/core/src/js/feedback/FeedbackForm.styles.ts
@@ -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;
diff --git a/packages/core/src/js/feedback/FeedbackForm.tsx b/packages/core/src/js/feedback/FeedbackForm.tsx
new file mode 100644
index 0000000000..6e741b655d
--- /dev/null
+++ b/packages/core/src/js/feedback/FeedbackForm.tsx
@@ -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 {
+ public static defaultProps: Partial = {
+ ...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 (
+
+
+
+
+
+
+ {text.formTitle}
+ {config.showBranding && (
+
+ )}
+
+
+ {config.showName && (
+ <>
+
+ {text.nameLabel}
+ {config.isNameRequired && ` ${text.isRequiredLabel}`}
+
+ this.setState({ name: value })}
+ />
+ >
+ )}
+
+ {config.showEmail && (
+ <>
+
+ {text.emailLabel}
+ {config.isEmailRequired && ` ${text.isRequiredLabel}`}
+
+ this.setState({ email: value })}
+ />
+ >
+ )}
+
+
+ {text.messageLabel}
+ {` ${text.isRequiredLabel}`}
+
+ this.setState({ description: value })}
+ multiline
+ />
+
+
+ {text.submitButtonLabel}
+
+
+
+ {text.cancelButtonLabel}
+
+
+
+
+
+
+ );
+ }
+
+ private _isValidEmail = (email: string): boolean => {
+ const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
+ return emailRegex.test(email);
+ };
+}
diff --git a/packages/core/src/js/feedback/FeedbackForm.types.ts b/packages/core/src/js/feedback/FeedbackForm.types.ts
new file mode 100644
index 0000000000..9805e166c1
--- /dev/null
+++ b/packages/core/src/js/feedback/FeedbackForm.types.ts
@@ -0,0 +1,166 @@
+import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';
+
+/**
+ * The props for the feedback form
+ */
+export interface FeedbackFormProps extends FeedbackGeneralConfiguration, FeedbackTextConfiguration, FeedbackCallbacks {
+ styles?: FeedbackFormStyles;
+}
+
+/**
+ * General feedback configuration
+ */
+export interface FeedbackGeneralConfiguration {
+ /**
+ * Show the Sentry branding
+ *
+ * @default true
+ */
+ showBranding?: boolean;
+
+ /**
+ * 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;
+}
+
+/**
+ * The styles for the feedback form
+ */
+export interface FeedbackFormStyles {
+ container?: ViewStyle;
+ title?: TextStyle;
+ label?: TextStyle;
+ input?: TextStyle;
+ textArea?: TextStyle;
+ submitButton?: ViewStyle;
+ submitText?: TextStyle;
+ cancelButton?: ViewStyle;
+ cancelText?: TextStyle;
+ titleContainer?: ViewStyle;
+ sentryLogo?: ImageStyle;
+}
+
+/**
+ * The state of the feedback form
+ */
+export interface FeedbackFormState {
+ isVisible: boolean;
+ name: string;
+ email: string;
+ description: string;
+}
diff --git a/packages/core/src/js/feedback/branding.ts b/packages/core/src/js/feedback/branding.ts
new file mode 100644
index 0000000000..e69dd1c79f
--- /dev/null
+++ b/packages/core/src/js/feedback/branding.ts
@@ -0,0 +1,5 @@
+/**
+ * Base64 encoded PNG image of the Sentry logo (source https://sentry.io/branding/)
+ */
+export const sentryLogo =
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAC4BAMAAABUVL5sAAAAD1BMVEUAAAA2Llo3LVk4LFg2LVmgcvrUAAAABHRSTlMAgL9ARyeO/QAAA7pJREFUeNrtWltynDAQFBIHIFUcgCQ6AIk5AAjd/0ypxFk3ouUCNJofl/rHy75a09MzI7Q2DQ0NDQ0NDQ0NDQ0N9eF+xPh9MKpwPv6FLss7R9yMImz8j8nowb9IglFDF6N+KCNIdqMFcGjpBbVUXdzHqKsXUhJGNb2g1vz6azRgX8XulUwMtVZjFsWkvFz10s0ooEO+9Vpxj/6rp9eI5VsdE6eJcDCxilqICiZWMHBSMkBVA2vq1SVrVyr6Ea5VMjHqjytTwcAgRVJ0DAy9VoWUTNT3tQysZGIYmGNTKHfK0q6oFkysYWCuHB0DaxX9yHLx5JKrhVC0TGwjWNRMvNAuW6PoR4TCepnqLLuiicEyKJq4g2C6Rc+hsInlgtHCF41OrD65INisObksbKy2/YJg9U08sGBTbRN3YaBQQu2i74/mcQil6vZr5C0dQpGbGKJvdOA1VDVxd5LHooPR5BJPdwql5vaLeq9FQZKJpa1kOoUSKpq45+m+ZK93aUpSwRyuafslUguCIZRZamKoBYQT80QmlqnF38p6bSIDcyg2q5fw/uo8dx0upZMLLdadQ1kgkNDEOOGiYYXjVKGJ8V00rEggcErurxAKCwQTi9RCAW7UFcUm5vPAOFNXFBY9DggQyp76jnIkOyBwaeqtyMScEu7w4JRNLnyQOjyvACcWpR145g6PV1fp9mvE0jMd3tWZXDR3/Ud2cSXZfmHhvNpEoFlgYsjFHX4AJc3kXXSTyEfDTrz94ptE1qvS9ouG1Ud2sQT5PVcHg3FL78FIYUpqxWK1yLzMxNzhHVaLzItMzB0eB/S4CDRHC+AzFTjhAiSSHx9tpgJXqnmhXi7VizM/F5v4V5oVqOIp81PpEW4Xt7PUA0kEe5WZ2PLt7ZopDg8Seue9GpxoU0WrHyFPgYlzmyrKPDxcpFeX3YRS5mGvxybmsC2tPhLJQxPzdsfliwMeLjAx9wcujoFIaEAX/KSYXz0s+9TE/E7LX0yF8lQvitl99sVjSgITl/yk6Lk48JjfGadnanHml8xjMvFTA+eL42CRwDKEZwbm4rBMyAmdH6UEz8HDTPj4d4ie1EJxJCQg56DXaxKOl0iGz0jcdebZluzhbFSA1yEZ2JzbHZKQe3I/EK4CErTHbwn84ZP+8Poxqrd/+I2cXJAw0v9VAkBiI3DhLryZEe6SXNeJk5HcHFu+Aom5wiIn2a7niZiE1WKMUhIOhNFJSQZzh0VG8tPcQufLSQI46sO9vcM0NDQ0NDQ0NDQ0NHxF/AFGJOBYBWrb5gAAAABJRU5ErkJggg==';
diff --git a/packages/core/src/js/feedback/defaults.ts b/packages/core/src/js/feedback/defaults.ts
new file mode 100644
index 0000000000..f184c62634
--- /dev/null
+++ b/packages/core/src/js/feedback/defaults.ts
@@ -0,0 +1,54 @@
+import { Alert } from 'react-native';
+
+import type { FeedbackFormProps } from './FeedbackForm.types';
+
+const FORM_TITLE = 'Report a Bug';
+const NAME_PLACEHOLDER = 'Your Name';
+const NAME_LABEL = 'Name';
+const EMAIL_PLACEHOLDER = 'your.email@example.org';
+const EMAIL_LABEL = 'Email';
+const MESSAGE_PLACEHOLDER = "What's the bug? What did you expect?";
+const MESSAGE_LABEL = 'Description';
+const IS_REQUIRED_LABEL = '(required)';
+const SUBMIT_BUTTON_LABEL = 'Send Bug Report';
+const CANCEL_BUTTON_LABEL = 'Cancel';
+const ERROR_TITLE = 'Error';
+const FORM_ERROR = 'Please fill out all required fields.';
+const EMAIL_ERROR = 'Please enter a valid email address.';
+const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!';
+
+export const defaultConfiguration: Partial = {
+ // FeedbackCallbacks
+ onFormClose: () => {
+ if (__DEV__) {
+ Alert.alert(
+ 'Development note',
+ 'onFormClose callback is not implemented. By default the form is just unmounted.',
+ );
+ }
+ },
+
+ // FeedbackGeneralConfiguration
+ showBranding: true,
+ isEmailRequired: false,
+ shouldValidateEmail: true,
+ isNameRequired: false,
+ showEmail: true,
+ showName: true,
+
+ // FeedbackTextConfiguration
+ cancelButtonLabel: CANCEL_BUTTON_LABEL,
+ emailLabel: EMAIL_LABEL,
+ emailPlaceholder: EMAIL_PLACEHOLDER,
+ formTitle: FORM_TITLE,
+ isRequiredLabel: IS_REQUIRED_LABEL,
+ messageLabel: MESSAGE_LABEL,
+ messagePlaceholder: MESSAGE_PLACEHOLDER,
+ nameLabel: NAME_LABEL,
+ namePlaceholder: NAME_PLACEHOLDER,
+ submitButtonLabel: SUBMIT_BUTTON_LABEL,
+ errorTitle: ERROR_TITLE,
+ formError: FORM_ERROR,
+ emailError: EMAIL_ERROR,
+ successMessageText: SUCCESS_MESSAGE_TEXT,
+};
diff --git a/packages/core/src/js/index.ts b/packages/core/src/js/index.ts
index e7c5411613..53abd065b8 100644
--- a/packages/core/src/js/index.ts
+++ b/packages/core/src/js/index.ts
@@ -84,3 +84,5 @@ export {
export type { TimeToDisplayProps } from './tracing';
export { Mask, Unmask } from './replay/CustomMask';
+
+export { FeedbackForm } from './feedback/FeedbackForm';
diff --git a/packages/core/test/feedback/FeedbackForm.test.tsx b/packages/core/test/feedback/FeedbackForm.test.tsx
new file mode 100644
index 0000000000..a97250441b
--- /dev/null
+++ b/packages/core/test/feedback/FeedbackForm.test.tsx
@@ -0,0 +1,168 @@
+import { captureFeedback } from '@sentry/core';
+import { fireEvent, render, waitFor } from '@testing-library/react-native';
+import * as React from 'react';
+import { Alert } from 'react-native';
+
+import { FeedbackForm } from '../../src/js/feedback/FeedbackForm';
+import type { FeedbackFormProps } from '../../src/js/feedback/FeedbackForm.types';
+
+const mockOnFormClose = jest.fn();
+const mockGetUser = jest.fn(() => ({
+ email: 'test@example.com',
+ name: 'Test User',
+}));
+
+jest.spyOn(Alert, 'alert');
+
+jest.mock('@sentry/core', () => ({
+ captureFeedback: jest.fn(),
+ getCurrentScope: jest.fn(() => ({
+ getUser: mockGetUser,
+ })),
+ lastEventId: jest.fn(),
+}));
+
+const defaultProps: FeedbackFormProps = {
+ onFormClose: mockOnFormClose,
+ formTitle: 'Feedback Form',
+ nameLabel: 'Name',
+ namePlaceholder: 'Name Placeholder',
+ emailLabel: 'Email',
+ emailPlaceholder: 'Email Placeholder',
+ messageLabel: 'Description',
+ messagePlaceholder: 'Description Placeholder',
+ submitButtonLabel: 'Submit',
+ cancelButtonLabel: 'Cancel',
+ isRequiredLabel: '(required)',
+ errorTitle: 'Error',
+ formError: 'Please fill out all required fields.',
+ emailError: 'The email address is not valid.',
+ successMessageText: 'Feedback success',
+};
+
+describe('FeedbackForm', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders correctly', () => {
+ const { getByPlaceholderText, getByText, getByTestId } = render();
+
+ expect(getByText(defaultProps.formTitle)).toBeTruthy();
+ expect(getByTestId('sentry-logo')).toBeTruthy(); // default showBranding is true
+ expect(getByText(defaultProps.nameLabel)).toBeTruthy();
+ expect(getByPlaceholderText(defaultProps.namePlaceholder)).toBeTruthy();
+ expect(getByText(defaultProps.emailLabel)).toBeTruthy();
+ expect(getByPlaceholderText(defaultProps.emailPlaceholder)).toBeTruthy();
+ expect(getByText(`${defaultProps.messageLabel } ${ defaultProps.isRequiredLabel}`)).toBeTruthy();
+ expect(getByPlaceholderText(defaultProps.messagePlaceholder)).toBeTruthy();
+ expect(getByText(defaultProps.submitButtonLabel)).toBeTruthy();
+ expect(getByText(defaultProps.cancelButtonLabel)).toBeTruthy();
+ });
+
+ it('does not render the sentry logo when showBranding is false', () => {
+ const { queryByTestId } = render();
+
+ expect(queryByTestId('sentry-logo')).toBeNull();
+ });
+
+ it('name and email are prefilled when sentry user is set', () => {
+ const { getByPlaceholderText } = render();
+
+ const nameInput = getByPlaceholderText(defaultProps.namePlaceholder);
+ const emailInput = getByPlaceholderText(defaultProps.emailPlaceholder);
+
+ expect(nameInput.props.value).toBe('Test User');
+ expect(emailInput.props.value).toBe('test@example.com');
+ });
+
+ it('ensure getUser is called only after the component is rendered', () => {
+ // Ensure getUser is not called before render
+ expect(mockGetUser).not.toHaveBeenCalled();
+
+ // Render the component
+ render();
+
+ // After rendering, check that getUser was called twice (email and name)
+ expect(mockGetUser).toHaveBeenCalledTimes(2);
+ });
+
+ it('shows an error message if required fields are empty', async () => {
+ const { getByText } = render();
+
+ fireEvent.press(getByText(defaultProps.submitButtonLabel));
+
+ await waitFor(() => {
+ expect(Alert.alert).toHaveBeenCalledWith(defaultProps.errorTitle, defaultProps.formError);
+ });
+ });
+
+ it('shows an error message if the email is not valid and the email is required', async () => {
+ const withEmailProps = {...defaultProps, ...{isEmailRequired: true}};
+ const { getByPlaceholderText, getByText } = render();
+
+ fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
+ fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), 'not-an-email');
+ fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.');
+
+ fireEvent.press(getByText(defaultProps.submitButtonLabel));
+
+ await waitFor(() => {
+ expect(Alert.alert).toHaveBeenCalledWith(defaultProps.errorTitle, defaultProps.emailError);
+ });
+ });
+
+ it('calls captureFeedback when the form is submitted successfully', async () => {
+ const { getByPlaceholderText, getByText } = render();
+
+ fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
+ fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), 'john.doe@example.com');
+ fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.');
+
+ fireEvent.press(getByText(defaultProps.submitButtonLabel));
+
+ await waitFor(() => {
+ expect(captureFeedback).toHaveBeenCalledWith({
+ message: 'This is a feedback message.',
+ name: 'John Doe',
+ email: 'john.doe@example.com',
+ });
+ });
+ });
+
+ it('shows success message when the form is submitted successfully', async () => {
+ const { getByPlaceholderText, getByText } = render();
+
+ fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
+ fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), 'john.doe@example.com');
+ fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.');
+
+ fireEvent.press(getByText(defaultProps.submitButtonLabel));
+
+ await waitFor(() => {
+ expect(Alert.alert).toHaveBeenCalledWith(defaultProps.successMessageText);
+ });
+ });
+
+ it('calls onFormClose when the form is submitted successfully', async () => {
+ const { getByPlaceholderText, getByText } = render();
+
+ fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
+ fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), 'john.doe@example.com');
+ fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.');
+
+ fireEvent.press(getByText(defaultProps.submitButtonLabel));
+
+ await waitFor(() => {
+ expect(mockOnFormClose).toHaveBeenCalled();
+ });
+ });
+
+ it('calls onFormClose when the cancel button is pressed', () => {
+ const { getByText } = render();
+
+ fireEvent.press(getByText(defaultProps.cancelButtonLabel));
+
+ expect(mockOnFormClose).toHaveBeenCalled();
+ });
+});
diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx
index f6d1063736..07afc5a214 100644
--- a/samples/react-native/src/App.tsx
+++ b/samples/react-native/src/App.tsx
@@ -16,6 +16,7 @@ import Animated, {
// Import the Sentry React Native SDK
import * as Sentry from '@sentry/react-native';
+import { FeedbackForm } from '@sentry/react-native';
import { SENTRY_INTERNAL_DSN } from './dsn';
import ErrorsScreen from './Screens/ErrorsScreen';
@@ -149,6 +150,27 @@ const ErrorsTabNavigator = Sentry.withProfiler(
component={ErrorsScreen}
options={{ title: 'Errors' }}
/>
+
+ {(props) => (
+
+ )}
+
diff --git a/samples/react-native/src/Screens/ErrorsScreen.tsx b/samples/react-native/src/Screens/ErrorsScreen.tsx
index 5f2f405677..4788fa407a 100644
--- a/samples/react-native/src/Screens/ErrorsScreen.tsx
+++ b/samples/react-native/src/Screens/ErrorsScreen.tsx
@@ -220,6 +220,12 @@ const ErrorsScreen = (_props: Props) => {
}
}}
/>
+