Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bda351c
(1) feat: Add Feedback Form Component (#4328)
antonis Jan 10, 2025
078cb37
Merge branch 'main' into feedback-ui
antonis Jan 10, 2025
7add44c
Merge branch 'main' into feedback-ui
antonis Jan 14, 2025
8709a48
Update changelog PR reference
antonis Jan 14, 2025
08eecba
test: Adds snapshot tests (#4379)
antonis Jan 15, 2025
4529b68
feat: handle `captureFeedback` errors (#4364)
antonis Jan 15, 2025
48ff52e
Merge branch 'main' into feedback-ui
antonis Jan 15, 2025
93b770e
Merge branch 'main' into feedback-ui
antonis Jan 16, 2025
aa15c88
(2.4) feat(feedback-ui): Add screenshots (#4338)
antonis Jan 16, 2025
312116d
Merge branch 'main' into feedback-ui
antonis Jan 20, 2025
f3c3563
Merge branch 'main' into feedback-ui
antonis Jan 22, 2025
b7b36d8
Merge branch 'main' into feedback-ui
antonis Jan 27, 2025
eda1cb7
Autoinject feedback widget (#4483)
antonis Jan 29, 2025
74748f8
Adds feedbackIntegration for configuring the feedback form (#4485)
antonis Jan 30, 2025
df77091
Merge branch 'main' into feedback-ui
antonis Jan 30, 2025
dbdfceb
Merge branch 'main' into feedback-ui
antonis Jan 31, 2025
f8988bc
Merge branch 'main' into feedback-ui
antonis Feb 3, 2025
03ece25
Merge branch 'main' into feedback-ui
antonis Feb 7, 2025
07b3f54
Merge branch 'main' into feedback-ui
antonis Feb 10, 2025
7ec9441
Feedback modal UI tweaks (#4492)
antonis Feb 11, 2025
fe99425
Merge branch 'main' into feedback-ui
antonis Feb 11, 2025
22cde46
Fix changelog
antonis Feb 11, 2025
e17ab11
Feedback UI: Use Image Picker libraries from integrations (#4524)
antonis Feb 14, 2025
fcbc6c6
Merge branch 'main' into feedback-ui
antonis Feb 14, 2025
874b2a2
feat(feedback): Pull down to cancel (#4534)
antonis Feb 17, 2025
7579a06
chore(feedback): Use Widget instead of Form (#4547)
krystofwoldrich Feb 17, 2025
2135c96
chore(feedback): Improve widget animations (#4555)
krystofwoldrich Feb 17, 2025
51dc070
feat(feedback): Save form state for un-submitted data (#4538)
antonis Feb 18, 2025
b3ea2b2
feat(feedback): Show selected screenshot (#4545)
antonis Feb 18, 2025
53e13fc
feat(feedback): Use only image uri in the onAddScreenshot callback (#…
antonis Feb 18, 2025
8ff7db3
Merge branch 'main' into feedback-ui
antonis Feb 18, 2025
ef4be9e
feat(feedback): Support web environments (#4558)
antonis Feb 20, 2025
ee3aa70
misc(feedback): Improve Feedback Sheet interactions (#4571)
krystofwoldrich Feb 20, 2025
ba260a4
feat(feedback): Align secondary buttons with the web (#4572)
antonis Feb 20, 2025
58aa109
Merge branch 'main' into feedback-ui
antonis Feb 20, 2025
ba2eecd
Update changelog
antonis Feb 20, 2025
76f708d
Merge branch 'main' into feedback-ui
antonis Feb 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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 <[email protected]>

* Remove if condition

Co-authored-by: LucasZF <[email protected]>

* Prettier

* Fix test import

---------

Co-authored-by: LucasZF <[email protected]>
  • Loading branch information
antonis and lucas-zimerman authored Jan 29, 2025
commit eda1cb74846c08cf1ac324592a5d7631a68f71f9
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