Skip to content

Commit eda1cb7

Browse files
Autoinject feedback widget (#4483)
* Auto-inject feedback form * Temporarily disable sample rotating indicator * Revert "Temporarily disable sample rotating indicator" This reverts commit db407ce. * 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 <lucas-zimerman1@hotmail.com> * Remove if condition Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com> * Prettier * Fix test import --------- Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>
1 parent b7b36d8 commit eda1cb7

File tree

9 files changed

+184
-2
lines changed

9 files changed

+184
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
- Send Sentry react-native SDK version in the session replay event (#4450)
1414
- User Feedback Form Component Beta ([#4435](https://github.com/getsentry/sentry-react-native/pull/4435))
1515

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

1818
```jsx
1919
import { FeedbackForm } from "@sentry/react-native";

packages/core/src/js/feedback/FeedbackForm.styles.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ const defaultStyles: FeedbackFormStyles = {
7777
width: 40,
7878
height: 40,
7979
},
80+
modalBackground: {
81+
flex: 1,
82+
justifyContent: 'center',
83+
},
8084
};
8185

8286
export default defaultStyles;

packages/core/src/js/feedback/FeedbackForm.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ export interface FeedbackFormStyles {
204204
screenshotText?: TextStyle;
205205
titleContainer?: ViewStyle;
206206
sentryLogo?: ImageStyle;
207+
modalBackground?: ViewStyle;
207208
}
208209

209210
/**
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { logger } from '@sentry/core';
2+
import * as React from 'react';
3+
import { Modal, View } from 'react-native';
4+
5+
import { FeedbackForm } from './FeedbackForm';
6+
import defaultStyles from './FeedbackForm.styles';
7+
import type { FeedbackFormStyles } from './FeedbackForm.types';
8+
import { isModalSupported } from './utils';
9+
10+
class FeedbackFormManager {
11+
private static _isVisible = false;
12+
private static _setVisibility: (visible: boolean) => void;
13+
14+
public static initialize(setVisibility: (visible: boolean) => void): void {
15+
this._setVisibility = setVisibility;
16+
}
17+
18+
public static show(): void {
19+
if (this._setVisibility) {
20+
this._isVisible = true;
21+
this._setVisibility(true);
22+
}
23+
}
24+
25+
public static hide(): void {
26+
if (this._setVisibility) {
27+
this._isVisible = false;
28+
this._setVisibility(false);
29+
}
30+
}
31+
32+
public static isFormVisible(): boolean {
33+
return this._isVisible;
34+
}
35+
}
36+
37+
interface FeedbackFormProviderProps {
38+
children: React.ReactNode;
39+
styles?: FeedbackFormStyles;
40+
}
41+
42+
class FeedbackFormProvider extends React.Component<FeedbackFormProviderProps> {
43+
public state = {
44+
isVisible: false,
45+
};
46+
47+
public constructor(props: FeedbackFormProviderProps) {
48+
super(props);
49+
FeedbackFormManager.initialize(this._setVisibilityFunction);
50+
}
51+
52+
/**
53+
* Renders the feedback form modal.
54+
*/
55+
public render(): React.ReactNode {
56+
if (!isModalSupported()) {
57+
logger.error('FeedbackForm Modal is not supported in React Native < 0.71 with Fabric renderer.');
58+
return <>{this.props.children}</>;
59+
}
60+
61+
const { isVisible } = this.state;
62+
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
63+
64+
// Wrapping the `Modal` component in a `View` component is necessary to avoid
65+
// issues like https://github.com/software-mansion/react-native-reanimated/issues/6035
66+
return (
67+
<>
68+
{this.props.children}
69+
{isVisible && (
70+
<View>
71+
<Modal visible={isVisible} transparent animationType="slide" onRequestClose={this._handleClose} testID="feedback-form-modal">
72+
<View style={styles.modalBackground}>
73+
<FeedbackForm
74+
onFormClose={this._handleClose}
75+
onFormSubmitted={this._handleClose}
76+
/>
77+
</View>
78+
</Modal>
79+
</View>
80+
)}
81+
</>
82+
);
83+
}
84+
85+
private _setVisibilityFunction = (visible: boolean): void => {
86+
this.setState({ isVisible: visible });
87+
};
88+
89+
private _handleClose = (): void => {
90+
FeedbackFormManager.hide();
91+
this.setState({ isVisible: false });
92+
};
93+
}
94+
95+
const showFeedbackForm = (): void => {
96+
FeedbackFormManager.show();
97+
};
98+
99+
export { showFeedbackForm, FeedbackFormProvider };

packages/core/src/js/feedback/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
import { isFabricEnabled } from '../utils/environment';
2+
import { ReactNativeLibraries } from './../utils/rnlibraries';
3+
4+
/**
5+
* Modal is not supported in React Native < 0.71 with Fabric renderer.
6+
* ref: https://github.com/facebook/react-native/issues/33652
7+
*/
8+
export function isModalSupported(): boolean {
9+
const { major, minor } = ReactNativeLibraries.ReactNativeVersion?.version || {};
10+
return !(isFabricEnabled() && major === 0 && minor < 71);
11+
}
12+
113
export const isValidEmail = (email: string): boolean => {
214
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
315
return emailRegex.test(email);

packages/core/src/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,4 @@ export type { TimeToDisplayProps } from './tracing';
8686
export { Mask, Unmask } from './replay/CustomMask';
8787

8888
export { FeedbackForm } from './feedback/FeedbackForm';
89+
export { showFeedbackForm } from './feedback/FeedbackFormManager';

packages/core/src/js/sdk.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import * as React from 'react';
99

1010
import { ReactNativeClient } from './client';
11+
import { FeedbackFormProvider } from './feedback/FeedbackFormManager';
1112
import { getDevServer } from './integrations/debugsymbolicatorutils';
1213
import { getDefaultIntegrations } from './integrations/default';
1314
import type { ReactNativeClientOptions, ReactNativeOptions, ReactNativeWrapperOptions } from './options';
@@ -163,7 +164,9 @@ export function wrap<P extends Record<string, unknown>>(
163164
return (
164165
<TouchEventBoundary {...(options?.touchEventBoundaryProps ?? {})}>
165166
<ReactNativeProfiler {...profilerProps}>
166-
<RootComponent {...appProps} />
167+
<FeedbackFormProvider>
168+
<RootComponent {...appProps} />
169+
</FeedbackFormProvider>
167170
</ReactNativeProfiler>
168171
</TouchEventBoundary>
169172
);
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { logger } from '@sentry/core';
2+
import { render } from '@testing-library/react-native';
3+
import * as React from 'react';
4+
import { Text } from 'react-native';
5+
6+
import { FeedbackFormProvider, showFeedbackForm } from '../../src/js/feedback/FeedbackFormManager';
7+
import { isModalSupported } from '../../src/js/feedback/utils';
8+
9+
jest.mock('../../src/js/feedback/utils', () => ({
10+
isModalSupported: jest.fn(),
11+
}));
12+
13+
const mockedIsModalSupported = isModalSupported as jest.MockedFunction<typeof isModalSupported>;
14+
15+
beforeEach(() => {
16+
logger.error = jest.fn();
17+
});
18+
19+
describe('FeedbackFormManager', () => {
20+
it('showFeedbackForm displays the form when FeedbackFormProvider is used', () => {
21+
mockedIsModalSupported.mockReturnValue(true);
22+
const { getByText, getByTestId } = render(
23+
<FeedbackFormProvider>
24+
<Text>App Components</Text>
25+
</FeedbackFormProvider>
26+
);
27+
28+
showFeedbackForm();
29+
30+
expect(getByTestId('feedback-form-modal')).toBeTruthy();
31+
expect(getByText('App Components')).toBeTruthy();
32+
});
33+
34+
it('showFeedbackForm does not display the form when Modal is not available', () => {
35+
mockedIsModalSupported.mockReturnValue(false);
36+
const { getByText, queryByTestId } = render(
37+
<FeedbackFormProvider>
38+
<Text>App Components</Text>
39+
</FeedbackFormProvider>
40+
);
41+
42+
showFeedbackForm();
43+
44+
expect(queryByTestId('feedback-form-modal')).toBeNull();
45+
expect(getByText('App Components')).toBeTruthy();
46+
expect(logger.error).toHaveBeenLastCalledWith(
47+
'FeedbackForm Modal is not supported in React Native < 0.71 with Fabric renderer.',
48+
);
49+
});
50+
51+
it('showFeedbackForm does not throw an error when FeedbackFormProvider is not used', () => {
52+
expect(() => {
53+
showFeedbackForm();
54+
}).not.toThrow();
55+
});
56+
});

samples/react-native/src/Screens/ErrorsScreen.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,12 @@ const ErrorsScreen = (_props: Props) => {
226226
_props.navigation.navigate('FeedbackForm');
227227
}}
228228
/>
229+
<Button
230+
title="Feedback form (auto)"
231+
onPress={() => {
232+
Sentry.showFeedbackForm();
233+
}}
234+
/>
229235
<Button
230236
title="Send user feedback"
231237
onPress={() => {

0 commit comments

Comments
 (0)