Skip to content

Autoinject feedback widget #4483

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jan 29, 2025
Merged
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
- Send Sentry react-native SDK version in the session replay event (#4450)
- User Feedback Form Component Beta ([#4435](https://github.com/getsentry/sentry-react-native/pull/4435))

To collect user feedback from inside your application add the `FeedbackForm` component.
To collect user feedback from inside your application call `Sentry.showFeedbackForm()` or add the `FeedbackForm` component.

```jsx
import { FeedbackForm } from "@sentry/react-native";
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/js/feedback/FeedbackForm.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ const defaultStyles: FeedbackFormStyles = {
width: 40,
height: 40,
},
modalBackground: {
flex: 1,
justifyContent: 'center',
},
};

export default defaultStyles;
1 change: 1 addition & 0 deletions packages/core/src/js/feedback/FeedbackForm.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export interface FeedbackFormStyles {
screenshotText?: TextStyle;
titleContainer?: ViewStyle;
sentryLogo?: ImageStyle;
modalBackground?: ViewStyle;
}

/**
Expand Down
99 changes: 99 additions & 0 deletions packages/core/src/js/feedback/FeedbackFormManager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { logger } from '@sentry/core';
import * as React from 'react';
import { Modal, View } from 'react-native';

import { FeedbackForm } from './FeedbackForm';
import defaultStyles from './FeedbackForm.styles';
import type { FeedbackFormStyles } from './FeedbackForm.types';
import { isModalSupported } from './utils';

class FeedbackFormManager {
private static _isVisible = false;
private static _setVisibility: (visible: boolean) => void;

public static initialize(setVisibility: (visible: boolean) => void): void {
this._setVisibility = setVisibility;
}

public static show(): void {
if (this._setVisibility) {
this._isVisible = true;
this._setVisibility(true);
}
}

public static hide(): void {
if (this._setVisibility) {
this._isVisible = false;
this._setVisibility(false);
}
}

public static isFormVisible(): boolean {
return this._isVisible;
}
}

interface FeedbackFormProviderProps {
children: React.ReactNode;
styles?: FeedbackFormStyles;
}

class FeedbackFormProvider extends React.Component<FeedbackFormProviderProps> {
public state = {
isVisible: false,
};

public constructor(props: FeedbackFormProviderProps) {
super(props);
FeedbackFormManager.initialize(this._setVisibilityFunction);
}

/**
* Renders the feedback form modal.
*/
public render(): React.ReactNode {
if (!isModalSupported()) {
logger.error('FeedbackForm Modal is not supported in React Native < 0.71 with Fabric renderer.');
return <>{this.props.children}</>;
}

const { isVisible } = this.state;
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };

// Wrapping the `Modal` component in a `View` component is necessary to avoid
// issues like https://github.com/software-mansion/react-native-reanimated/issues/6035
return (
<>
{this.props.children}
{isVisible && (
<View>
<Modal visible={isVisible} transparent animationType="slide" onRequestClose={this._handleClose} testID="feedback-form-modal">
<View style={styles.modalBackground}>
<FeedbackForm
onFormClose={this._handleClose}
onFormSubmitted={this._handleClose}
/>
</View>
</Modal>
</View>
)}
</>
);
}

private _setVisibilityFunction = (visible: boolean): void => {
this.setState({ isVisible: visible });
};

private _handleClose = (): void => {
FeedbackFormManager.hide();
this.setState({ isVisible: false });
};
}

const showFeedbackForm = (): void => {
FeedbackFormManager.show();
};

export { showFeedbackForm, FeedbackFormProvider };
12 changes: 12 additions & 0 deletions packages/core/src/js/feedback/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
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);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,4 @@ export type { TimeToDisplayProps } from './tracing';
export { Mask, Unmask } from './replay/CustomMask';

export { FeedbackForm } from './feedback/FeedbackForm';
export { showFeedbackForm } from './feedback/FeedbackFormManager';
5 changes: 4 additions & 1 deletion packages/core/src/js/sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -163,7 +164,9 @@ export function wrap<P extends Record<string, unknown>>(
return (
<TouchEventBoundary {...(options?.touchEventBoundaryProps ?? {})}>
<ReactNativeProfiler {...profilerProps}>
<RootComponent {...appProps} />
<FeedbackFormProvider>
<RootComponent {...appProps} />
</FeedbackFormProvider>
</ReactNativeProfiler>
</TouchEventBoundary>
);
Expand Down
56 changes: 56 additions & 0 deletions packages/core/test/feedback/FeedbackFormManager.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { logger } from '@sentry/core';
import { render } from '@testing-library/react-native';
import * as React from 'react';
import { Text } from 'react-native';

import { FeedbackFormProvider, showFeedbackForm } from '../../src/js/feedback/FeedbackFormManager';
import { isModalSupported } from '../../src/js/feedback/utils';

jest.mock('../../src/js/feedback/utils', () => ({
isModalSupported: jest.fn(),
}));

const mockedIsModalSupported = isModalSupported as jest.MockedFunction<typeof isModalSupported>;

beforeEach(() => {
logger.error = jest.fn();
});

describe('FeedbackFormManager', () => {
it('showFeedbackForm displays the form when FeedbackFormProvider is used', () => {
mockedIsModalSupported.mockReturnValue(true);
const { getByText, getByTestId } = render(
<FeedbackFormProvider>
<Text>App Components</Text>
</FeedbackFormProvider>
);

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(
<FeedbackFormProvider>
<Text>App Components</Text>
</FeedbackFormProvider>
);

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();
});
});
6 changes: 6 additions & 0 deletions samples/react-native/src/Screens/ErrorsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ const ErrorsScreen = (_props: Props) => {
_props.navigation.navigate('FeedbackForm');
}}
/>
<Button
title="Feedback form (auto)"
onPress={() => {
Sentry.showFeedbackForm();
}}
/>
<Button
title="Send user feedback"
onPress={() => {
Expand Down
Loading