From eda1cb74846c08cf1ac324592a5d7631a68f71f9 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 29 Jan 2025 10:55:03 +0200 Subject: [PATCH] Autoinject feedback widget (#4483) * Auto-inject feedback form * Temporarily disable sample rotating indicator * Revert "Temporarily disable sample rotating indicator" This reverts commit db407ce2fb0035ea4d729e07834eae25811d9917. * Wrap Modal in a View * Handles Android back button * Make modal style configurable * Print an error when the modal is not supported * Add changelog * Adds tests * Get major, minor version with deconstruct declaration Co-authored-by: LucasZF * Remove if condition Co-authored-by: LucasZF * Prettier * Fix test import --------- Co-authored-by: LucasZF --- CHANGELOG.md | 2 +- .../src/js/feedback/FeedbackForm.styles.ts | 4 + .../src/js/feedback/FeedbackForm.types.ts | 1 + .../src/js/feedback/FeedbackFormManager.tsx | 99 +++++++++++++++++++ packages/core/src/js/feedback/utils.ts | 12 +++ packages/core/src/js/index.ts | 1 + packages/core/src/js/sdk.tsx | 5 +- .../feedback/FeedbackFormManager.test.tsx | 56 +++++++++++ .../react-native/src/Screens/ErrorsScreen.tsx | 6 ++ 9 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/js/feedback/FeedbackFormManager.tsx create mode 100644 packages/core/test/feedback/FeedbackFormManager.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 7095dc52fc..300f993cbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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"; diff --git a/packages/core/src/js/feedback/FeedbackForm.styles.ts b/packages/core/src/js/feedback/FeedbackForm.styles.ts index 9cbbfab7f7..57d407adf3 100644 --- a/packages/core/src/js/feedback/FeedbackForm.styles.ts +++ b/packages/core/src/js/feedback/FeedbackForm.styles.ts @@ -77,6 +77,10 @@ const defaultStyles: FeedbackFormStyles = { width: 40, height: 40, }, + modalBackground: { + flex: 1, + justifyContent: 'center', + }, }; export default defaultStyles; diff --git a/packages/core/src/js/feedback/FeedbackForm.types.ts b/packages/core/src/js/feedback/FeedbackForm.types.ts index cffe54447a..fbeee76896 100644 --- a/packages/core/src/js/feedback/FeedbackForm.types.ts +++ b/packages/core/src/js/feedback/FeedbackForm.types.ts @@ -204,6 +204,7 @@ export interface FeedbackFormStyles { screenshotText?: TextStyle; titleContainer?: ViewStyle; sentryLogo?: ImageStyle; + modalBackground?: ViewStyle; } /** diff --git a/packages/core/src/js/feedback/FeedbackFormManager.tsx b/packages/core/src/js/feedback/FeedbackFormManager.tsx new file mode 100644 index 0000000000..e69ac11e6c --- /dev/null +++ b/packages/core/src/js/feedback/FeedbackFormManager.tsx @@ -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 { + 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 && ( + + + + + + + + )} + + ); + } + + 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 }; diff --git a/packages/core/src/js/feedback/utils.ts b/packages/core/src/js/feedback/utils.ts index 4d4efff42b..b27fb3ea8d 100644 --- a/packages/core/src/js/feedback/utils.ts +++ b/packages/core/src/js/feedback/utils.ts @@ -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); diff --git a/packages/core/src/js/index.ts b/packages/core/src/js/index.ts index 53abd065b8..a07e89f92a 100644 --- a/packages/core/src/js/index.ts +++ b/packages/core/src/js/index.ts @@ -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'; 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/FeedbackFormManager.test.tsx b/packages/core/test/feedback/FeedbackFormManager.test.tsx new file mode 100644 index 0000000000..7c93b9eb5a --- /dev/null +++ b/packages/core/test/feedback/FeedbackFormManager.test.tsx @@ -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; + +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(); + }); +}); diff --git a/samples/react-native/src/Screens/ErrorsScreen.tsx b/samples/react-native/src/Screens/ErrorsScreen.tsx index 4788fa407a..cc2810fd1d 100644 --- a/samples/react-native/src/Screens/ErrorsScreen.tsx +++ b/samples/react-native/src/Screens/ErrorsScreen.tsx @@ -226,6 +226,12 @@ const ErrorsScreen = (_props: Props) => { _props.navigation.navigate('FeedbackForm'); }} /> +