=> savedOptions;
diff --git a/packages/core/src/js/feedback/utils.ts b/packages/core/src/js/feedback/utils.ts
new file mode 100644
index 0000000000..b27fb3ea8d
--- /dev/null
+++ b/packages/core/src/js/feedback/utils.ts
@@ -0,0 +1,16 @@
+import { isFabricEnabled } from '../utils/environment';
+import { ReactNativeLibraries } from './../utils/rnlibraries';
+
+/**
+ * Modal is not supported in React Native < 0.71 with Fabric renderer.
+ * ref: https://github.com/facebook/react-native/issues/33652
+ */
+export function isModalSupported(): boolean {
+ const { major, minor } = ReactNativeLibraries.ReactNativeVersion?.version || {};
+ return !(isFabricEnabled() && major === 0 && minor < 71);
+}
+
+export const 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/index.ts b/packages/core/src/js/index.ts
index e7c5411613..a07e89f92a 100644
--- a/packages/core/src/js/index.ts
+++ b/packages/core/src/js/index.ts
@@ -84,3 +84,6 @@ export {
export type { TimeToDisplayProps } from './tracing';
export { Mask, Unmask } from './replay/CustomMask';
+
+export { FeedbackForm } from './feedback/FeedbackForm';
+export { showFeedbackForm } from './feedback/FeedbackFormManager';
diff --git a/packages/core/src/js/integrations/exports.ts b/packages/core/src/js/integrations/exports.ts
index dfc9e2c3e1..f5fabb397e 100644
--- a/packages/core/src/js/integrations/exports.ts
+++ b/packages/core/src/js/integrations/exports.ts
@@ -13,6 +13,7 @@ export { viewHierarchyIntegration } from './viewhierarchy';
export { expoContextIntegration } from './expocontext';
export { spotlightIntegration } from './spotlight';
export { mobileReplayIntegration } from '../replay/mobilereplay';
+export { feedbackIntegration } from '../feedback/integration';
export { browserReplayIntegration } from '../replay/browserReplay';
export { appStartIntegration } from '../tracing/integrations/appStart';
export { nativeFramesIntegration, createNativeFramesIntegrations } from '../tracing/integrations/nativeFrames';
diff --git a/packages/core/src/js/sdk.tsx b/packages/core/src/js/sdk.tsx
index 3c6fdff90c..606e2cf5ea 100644
--- a/packages/core/src/js/sdk.tsx
+++ b/packages/core/src/js/sdk.tsx
@@ -8,6 +8,7 @@ import {
import * as React from 'react';
import { ReactNativeClient } from './client';
+import { FeedbackFormProvider } from './feedback/FeedbackFormManager';
import { getDevServer } from './integrations/debugsymbolicatorutils';
import { getDefaultIntegrations } from './integrations/default';
import type { ReactNativeClientOptions, ReactNativeOptions, ReactNativeWrapperOptions } from './options';
@@ -163,7 +164,9 @@ export function wrap>(
return (
-
+
+
+
);
diff --git a/packages/core/test/feedback/FeedbackForm.test.tsx b/packages/core/test/feedback/FeedbackForm.test.tsx
new file mode 100644
index 0000000000..33cf9d5811
--- /dev/null
+++ b/packages/core/test/feedback/FeedbackForm.test.tsx
@@ -0,0 +1,323 @@
+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, FeedbackFormStyles } from '../../src/js/feedback/FeedbackForm.types';
+
+const mockOnFormClose = jest.fn();
+const mockOnAddScreenshot = jest.fn();
+const mockOnSubmitSuccess = jest.fn();
+const mockOnFormSubmitted = jest.fn();
+const mockOnSubmitError = jest.fn();
+const mockGetUser = jest.fn(() => ({
+ email: 'test@example.com',
+ name: 'Test User',
+}));
+
+jest.spyOn(Alert, 'alert');
+
+jest.mock('@sentry/core', () => ({
+ ...jest.requireActual('@sentry/core'),
+ captureFeedback: jest.fn(),
+ getCurrentScope: jest.fn(() => ({
+ getUser: mockGetUser,
+ })),
+ lastEventId: jest.fn(),
+}));
+
+const defaultProps: FeedbackFormProps = {
+ onFormClose: mockOnFormClose,
+ onAddScreenshot: mockOnAddScreenshot,
+ onSubmitSuccess: mockOnSubmitSuccess,
+ onFormSubmitted: mockOnFormSubmitted,
+ onSubmitError: mockOnSubmitError,
+ addScreenshotButtonLabel: 'Add Screenshot',
+ formTitle: 'Feedback Form',
+ nameLabel: 'Name Label',
+ namePlaceholder: 'Name Placeholder',
+ emailLabel: 'Email Label',
+ emailPlaceholder: 'Email Placeholder',
+ messageLabel: 'Message Label',
+ messagePlaceholder: 'Message Placeholder',
+ submitButtonLabel: 'Submit Button Label',
+ cancelButtonLabel: 'Cancel Button Label',
+ isRequiredLabel: '(is required label)',
+ errorTitle: 'Error',
+ formError: 'Please fill out all required fields.',
+ emailError: 'The email address is not valid.',
+ successMessageText: 'Feedback success',
+ genericError: 'Generic error',
+};
+
+const customStyles: FeedbackFormStyles = {
+ container: {
+ backgroundColor: '#ffffff',
+ },
+ title: {
+ fontSize: 20,
+ color: '#ff0000',
+ },
+ label: {
+ fontSize: 15,
+ color: '#00ff00',
+ },
+ input: {
+ height: 50,
+ borderColor: '#0000ff',
+ fontSize: 13,
+ color: '#000000',
+ },
+ textArea: {
+ height: 50,
+ color: '#00ff00',
+ },
+ submitButton: {
+ backgroundColor: '#ffff00',
+ },
+ submitText: {
+ color: '#ff0000',
+ fontSize: 12,
+ },
+ cancelButton: {
+ paddingVertical: 10,
+ },
+ cancelText: {
+ color: '#ff0000',
+ fontSize: 10,
+ },
+ screenshotButton: {
+ backgroundColor: '#00ff00',
+ },
+ screenshotText: {
+ color: '#0000ff',
+ fontSize: 13,
+ },
+};
+
+describe('FeedbackForm', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('matches the snapshot with default configuration', () => {
+ const { toJSON } = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('matches the snapshot with custom texts', () => {
+ const { toJSON } = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('matches the snapshot with custom styles', () => {
+ const customStyleProps = {styles: customStyles};
+ const { toJSON } = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('matches the snapshot with default configuration and screenshot button', () => {
+ const { toJSON } = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('matches the snapshot with custom texts and screenshot button', () => {
+ const { toJSON } = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('matches the snapshot with custom styles and screenshot button', () => {
+ const customStyleProps = {styles: customStyles};
+ const { toJSON } = render();
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('renders correctly', () => {
+ const { getByPlaceholderText, getByText, getByTestId, queryByText } = 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(queryByText(defaultProps.addScreenshotButtonLabel)).toBeNull(); // default false
+ expect(getByText(defaultProps.submitButtonLabel)).toBeTruthy();
+ expect(getByText(defaultProps.cancelButtonLabel)).toBeTruthy();
+ });
+
+ it('renders attachment button when the enableScreenshot is true', () => {
+ const { getByText } = render();
+
+ expect(getByText(defaultProps.addScreenshotButtonLabel)).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',
+ }, undefined);
+ });
+ });
+
+ 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('shows an error message when there is a an error in captureFeedback', async () => {
+ (captureFeedback as jest.Mock).mockImplementationOnce(() => {
+ throw new Error('Test error');
+ });
+
+ 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.errorTitle, defaultProps.genericError);
+ });
+ });
+
+ it('calls onSubmitError when there is an error', async () => {
+ (captureFeedback as jest.Mock).mockImplementationOnce(() => {
+ throw new Error('Test error');
+ });
+
+ 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(mockOnSubmitError).toHaveBeenCalled();
+ });
+ });
+
+ it('calls onSubmitSuccess 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(mockOnSubmitSuccess).toHaveBeenCalled();
+ });
+ });
+
+ it('calls onFormSubmitted 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(mockOnFormSubmitted).toHaveBeenCalled();
+ });
+ });
+
+ it('calls onAddScreenshot when the screenshot button is pressed', async () => {
+ const { getByText } = render();
+
+ fireEvent.press(getByText(defaultProps.addScreenshotButtonLabel));
+
+ await waitFor(() => {
+ expect(mockOnAddScreenshot).toHaveBeenCalled();
+ });
+ });
+
+ it('calls onFormClose when the cancel button is pressed', () => {
+ const { getByText } = render();
+
+ fireEvent.press(getByText(defaultProps.cancelButtonLabel));
+
+ expect(mockOnFormClose).toHaveBeenCalled();
+ });
+});
diff --git a/packages/core/test/feedback/FeedbackFormManager.test.tsx b/packages/core/test/feedback/FeedbackFormManager.test.tsx
new file mode 100644
index 0000000000..9fe53af4c6
--- /dev/null
+++ b/packages/core/test/feedback/FeedbackFormManager.test.tsx
@@ -0,0 +1,96 @@
+import { logger } from '@sentry/core';
+import { render } from '@testing-library/react-native';
+import * as React from 'react';
+import { Text } from 'react-native';
+
+import { defaultConfiguration } from '../../src/js/feedback/defaults';
+import { FeedbackFormProvider, showFeedbackForm } from '../../src/js/feedback/FeedbackFormManager';
+import { feedbackIntegration } from '../../src/js/feedback/integration';
+import { isModalSupported } from '../../src/js/feedback/utils';
+
+jest.mock('../../src/js/feedback/utils', () => ({
+ isModalSupported: jest.fn(),
+}));
+
+const mockedIsModalSupported = isModalSupported as jest.MockedFunction;
+
+beforeEach(() => {
+ logger.error = jest.fn();
+});
+
+describe('FeedbackFormManager', () => {
+ it('showFeedbackForm displays the form when FeedbackFormProvider is used', () => {
+ mockedIsModalSupported.mockReturnValue(true);
+ const { getByText, getByTestId } = render(
+
+ App Components
+
+ );
+
+ showFeedbackForm();
+
+ expect(getByTestId('feedback-form-modal')).toBeTruthy();
+ expect(getByText('App Components')).toBeTruthy();
+ });
+
+ it('showFeedbackForm does not display the form when Modal is not available', () => {
+ mockedIsModalSupported.mockReturnValue(false);
+ const { getByText, queryByTestId } = render(
+
+ App Components
+
+ );
+
+ showFeedbackForm();
+
+ expect(queryByTestId('feedback-form-modal')).toBeNull();
+ expect(getByText('App Components')).toBeTruthy();
+ expect(logger.error).toHaveBeenLastCalledWith(
+ 'FeedbackForm Modal is not supported in React Native < 0.71 with Fabric renderer.',
+ );
+ });
+
+ it('showFeedbackForm does not throw an error when FeedbackFormProvider is not used', () => {
+ expect(() => {
+ showFeedbackForm();
+ }).not.toThrow();
+ });
+
+ it('showFeedbackForm displays the form with the feedbackIntegration options', () => {
+ mockedIsModalSupported.mockReturnValue(true);
+ const { getByPlaceholderText, getByText } = render(
+
+ App Components
+
+ );
+
+ feedbackIntegration({
+ messagePlaceholder: 'Custom Message Placeholder',
+ submitButtonLabel: 'Custom Submit Button',
+ });
+
+ showFeedbackForm();
+
+ expect(getByPlaceholderText('Custom Message Placeholder')).toBeTruthy();
+ expect(getByText('Custom Submit Button')).toBeTruthy();
+ });
+
+ it('showFeedbackForm displays the form with the feedbackIntegration options merged with the defaults', () => {
+ mockedIsModalSupported.mockReturnValue(true);
+ const { getByPlaceholderText, getByText, queryByText } = render(
+
+ App Components
+
+ );
+
+ feedbackIntegration({
+ submitButtonLabel: 'Custom Submit Button',
+ }),
+
+ showFeedbackForm();
+
+ expect(queryByText(defaultConfiguration.submitButtonLabel)).toBeFalsy(); // overridden value
+ expect(getByText('Custom Submit Button')).toBeTruthy(); // overridden value
+ expect(getByPlaceholderText(defaultConfiguration.messagePlaceholder)).toBeTruthy(); // default configuration value
+ });
+});
diff --git a/packages/core/test/feedback/__snapshots__/FeedbackForm.test.tsx.snap b/packages/core/test/feedback/__snapshots__/FeedbackForm.test.tsx.snap
new file mode 100644
index 0000000000..a3c2857ac9
--- /dev/null
+++ b/packages/core/test/feedback/__snapshots__/FeedbackForm.test.tsx.snap
@@ -0,0 +1,1891 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FeedbackForm matches the snapshot with custom styles 1`] = `
+
+
+
+
+
+
+
+ Report a Bug
+
+
+
+
+ Name
+
+
+
+ Email
+
+
+
+ Description
+ (required)
+
+
+
+
+ Send Bug Report
+
+
+
+
+ Cancel
+
+
+
+
+
+
+
+`;
+
+exports[`FeedbackForm matches the snapshot with custom styles and screenshot button 1`] = `
+
+
+
+
+
+
+
+ Report a Bug
+
+
+
+
+ Name
+
+
+
+ Email
+
+
+
+ Description
+ (required)
+
+
+
+
+ Add a screenshot
+
+
+
+
+ Send Bug Report
+
+
+
+
+ Cancel
+
+
+
+
+
+
+
+`;
+
+exports[`FeedbackForm matches the snapshot with custom texts 1`] = `
+
+
+
+
+
+
+
+ Feedback Form
+
+
+
+
+ Name Label
+
+
+
+ Email Label
+
+
+
+ Message Label
+ (is required label)
+
+
+
+
+ Submit Button Label
+
+
+
+
+ Cancel Button Label
+
+
+
+
+
+
+
+`;
+
+exports[`FeedbackForm matches the snapshot with custom texts and screenshot button 1`] = `
+
+
+
+
+
+
+
+ Feedback Form
+
+
+
+
+ Name Label
+
+
+
+ Email Label
+
+
+
+ Message Label
+ (is required label)
+
+
+
+
+ Add Screenshot
+
+
+
+
+ Submit Button Label
+
+
+
+
+ Cancel Button Label
+
+
+
+
+
+
+
+`;
+
+exports[`FeedbackForm matches the snapshot with default configuration 1`] = `
+
+
+
+
+
+
+
+ Report a Bug
+
+
+
+
+ Name
+
+
+
+ Email
+
+
+
+ Description
+ (required)
+
+
+
+
+ Send Bug Report
+
+
+
+
+ Cancel
+
+
+
+
+
+
+
+`;
+
+exports[`FeedbackForm matches the snapshot with default configuration and screenshot button 1`] = `
+
+
+
+
+
+
+
+ Report a Bug
+
+
+
+
+ Name
+
+
+
+ Email
+
+
+
+ Description
+ (required)
+
+
+
+
+ Add a screenshot
+
+
+
+
+ Send Bug Report
+
+
+
+
+ Cancel
+
+
+
+
+
+
+
+`;
diff --git a/samples/react-native/ios/sentryreactnativesample/PrivacyInfo.xcprivacy b/samples/react-native/ios/sentryreactnativesample/PrivacyInfo.xcprivacy
index e9e7208ef3..2c388f904c 100644
--- a/samples/react-native/ios/sentryreactnativesample/PrivacyInfo.xcprivacy
+++ b/samples/react-native/ios/sentryreactnativesample/PrivacyInfo.xcprivacy
@@ -10,6 +10,7 @@
NSPrivacyAccessedAPITypeReasons
C617.1
+ 3B52.1
diff --git a/samples/react-native/package.json b/samples/react-native/package.json
index 97930828ab..dfa65242b8 100644
--- a/samples/react-native/package.json
+++ b/samples/react-native/package.json
@@ -30,6 +30,8 @@
"react": "18.3.1",
"react-native": "0.77.0",
"react-native-gesture-handler": "^2.22.1",
+ "react-native-image-picker": "^7.2.2",
+ "react-native-quick-base64": "^2.1.2",
"react-native-reanimated": "3.16.7",
"react-native-safe-area-context": "5.2.0",
"react-native-screens": "4.6.0",
diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx
index f6d1063736..a9e36e7fd5 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';
@@ -36,6 +37,8 @@ import { ErrorEvent } from '@sentry/core';
import HeavyNavigationScreen from './Screens/HeavyNavigationScreen';
import WebviewScreen from './Screens/WebviewScreen';
import { isTurboModuleEnabled } from '@sentry/react-native/dist/js/utils/environment';
+import { toByteArray } from 'react-native-quick-base64';
+import { launchImageLibrary } from 'react-native-image-picker';
if (typeof setImmediate === 'undefined') {
require('setimmediate');
@@ -103,6 +106,18 @@ Sentry.init({
? false
: true,
}),
+ Sentry.feedbackIntegration({
+ styles:{
+ submitButton: {
+ backgroundColor: '#6a1b9a',
+ paddingVertical: 15,
+ borderRadius: 5,
+ alignItems: 'center',
+ marginBottom: 10,
+ },
+ },
+ namePlaceholder: 'Fullname',
+ }),
);
return integrations.filter(i => i.name !== 'Dedupe');
},
@@ -138,6 +153,23 @@ const Stack = isMobileOs
: createStackNavigator();
const Tab = createBottomTabNavigator();
+const handleChooseImage = (attachFile: (filename: string, data: Uint8Array) => void): void => {
+ launchImageLibrary({ mediaType: 'photo', includeBase64: true }, (response) => {
+ if (response.didCancel) {
+ console.log('User cancelled image picker');
+ } else if (response.errorCode) {
+ console.log('ImagePicker Error: ', response.errorMessage);
+ } else if (response.assets && response.assets.length > 0) {
+ const filename = response.assets[0].fileName;
+ const base64String = response.assets[0].base64;
+ const screenShotUint8Array = toByteArray(base64String);
+ if (filename && screenShotUint8Array) {
+ attachFile(filename, screenShotUint8Array);
+ }
+ }
+ });
+};
+
const ErrorsTabNavigator = Sentry.withProfiler(
() => {
return (
@@ -149,6 +181,30 @@ 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..cc2810fd1d 100644
--- a/samples/react-native/src/Screens/ErrorsScreen.tsx
+++ b/samples/react-native/src/Screens/ErrorsScreen.tsx
@@ -220,6 +220,18 @@ const ErrorsScreen = (_props: Props) => {
}
}}
/>
+