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
feat: handle captureFeedback errors (#4364)
  • Loading branch information
antonis authored Jan 15, 2025
commit 4529b687f5984027606260076b6f56a21f8c4781
29 changes: 16 additions & 13 deletions packages/core/src/js/feedback/FeedbackForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SendFeedbackParams } from '@sentry/core';
import { captureFeedback, getCurrentScope, lastEventId } from '@sentry/core';
import { captureFeedback, getCurrentScope, lastEventId, logger } from '@sentry/core';
import * as React from 'react';
import type { KeyboardTypeOptions } from 'react-native';
import {
Expand All @@ -20,6 +20,7 @@ import { sentryLogo } from './branding';
import { defaultConfiguration } from './defaults';
import defaultStyles from './FeedbackForm.styles';
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles,FeedbackGeneralConfiguration, FeedbackTextConfiguration } from './FeedbackForm.types';
import { isValidEmail } from './utils';

/**
* @beta
Expand Down Expand Up @@ -50,7 +51,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor

public handleFeedbackSubmit: () => void = () => {
const { name, email, description } = this.state;
const { onFormClose } = this.props;
const { onSubmitSuccess, onSubmitError, onFormSubmitted } = this.props;
const text: FeedbackTextConfiguration = this.props;

const trimmedName = name?.trim();
Expand All @@ -62,7 +63,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
return;
}

if (this.props.shouldValidateEmail && (this.props.isEmailRequired || trimmedEmail.length > 0) && !this._isValidEmail(trimmedEmail)) {
if (this.props.shouldValidateEmail && (this.props.isEmailRequired || trimmedEmail.length > 0) && !isValidEmail(trimmedEmail)) {
Alert.alert(text.errorTitle, text.emailError);
return;
}
Expand All @@ -75,11 +76,18 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
associatedEventId: eventId,
};

onFormClose();
this.setState({ isVisible: false });

captureFeedback(userFeedback);
Alert.alert(text.successMessageText);
try {
this.setState({ isVisible: false });
captureFeedback(userFeedback);
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: undefined });
Alert.alert(text.successMessageText);
onFormSubmitted();
} catch (error) {
const errorString = `Feedback form submission failed: ${error}`;
onSubmitError(new Error(errorString));
Alert.alert(text.errorTitle, text.genericError);
logger.error(`Feedback form submission failed: ${error}`);
}
};

/**
Expand Down Expand Up @@ -174,9 +182,4 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
</SafeAreaView>
);
}

private _isValidEmail = (email: string): boolean => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return emailRegex.test(email);
};
}
28 changes: 28 additions & 0 deletions packages/core/src/js/feedback/FeedbackForm.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FeedbackFormData } from '@sentry/core';
import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';

/**
Expand Down Expand Up @@ -126,16 +127,43 @@ export interface FeedbackTextConfiguration {
* The error message when the email is invalid
*/
emailError?: string;

/**
* Message when there is a generic error
*/
genericError?: string;
}

/**
* The public callbacks available for the feedback integration
*/
export interface FeedbackCallbacks {
/**
* Callback when form is opened
*/
onFormOpen?: () => void;

/**
* Callback when form is closed and not submitted
*/
onFormClose?: () => void;

/**
* Callback when feedback is successfully submitted
*
* After this you'll see a SuccessMessage on the screen for a moment.
*/
onSubmitSuccess?: (data: FeedbackFormData) => void;

/**
* Callback when feedback is unsuccessfully submitted
*/
onSubmitError?: (error: Error) => void;

/**
* Callback when the feedback form is submitted successfully, and the SuccessMessage is complete, or dismissed
*/
onFormSubmitted?: () => void;
}

/**
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/js/feedback/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ const ERROR_TITLE = 'Error';
const FORM_ERROR = 'Please fill out all required fields.';
const EMAIL_ERROR = 'Please enter a valid email address.';
const SUCCESS_MESSAGE_TEXT = 'Thank you for your report!';
const GENERIC_ERROR_TEXT = 'Unable to send feedback due to an unexpected error.';

export const defaultConfiguration: Partial<FeedbackFormProps> = {
// FeedbackCallbacks
onFormOpen: () => {
// Does nothing by default
},
onFormClose: () => {
if (__DEV__) {
Alert.alert(
Expand All @@ -27,6 +31,20 @@ export const defaultConfiguration: Partial<FeedbackFormProps> = {
);
}
},
onSubmitSuccess: () => {
// Does nothing by default
},
onSubmitError: () => {
// Does nothing by default
},
onFormSubmitted: () => {
if (__DEV__) {
Alert.alert(
'Development note',
'onFormSubmitted callback is not implemented. By default the form is just unmounted.',
);
}
},

// FeedbackGeneralConfiguration
showBranding: true,
Expand All @@ -51,4 +69,5 @@ export const defaultConfiguration: Partial<FeedbackFormProps> = {
formError: FORM_ERROR,
emailError: EMAIL_ERROR,
successMessageText: SUCCESS_MESSAGE_TEXT,
genericError: GENERIC_ERROR_TEXT,
};
4 changes: 4 additions & 0 deletions packages/core/src/js/feedback/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const isValidEmail = (email: string): boolean => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
};
62 changes: 60 additions & 2 deletions packages/core/test/feedback/FeedbackForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { FeedbackForm } from '../../src/js/feedback/FeedbackForm';
import type { FeedbackFormProps, FeedbackFormStyles } from '../../src/js/feedback/FeedbackForm.types';

const mockOnFormClose = jest.fn();
const mockOnSubmitSuccess = jest.fn();
const mockOnFormSubmitted = jest.fn();
const mockOnSubmitError = jest.fn();
const mockGetUser = jest.fn(() => ({
email: '[email protected]',
name: 'Test User',
Expand All @@ -15,6 +18,7 @@ const mockGetUser = jest.fn(() => ({
jest.spyOn(Alert, 'alert');

jest.mock('@sentry/core', () => ({
...jest.requireActual('@sentry/core'),
captureFeedback: jest.fn(),
getCurrentScope: jest.fn(() => ({
getUser: mockGetUser,
Expand All @@ -24,6 +28,9 @@ jest.mock('@sentry/core', () => ({

const defaultProps: FeedbackFormProps = {
onFormClose: mockOnFormClose,
onSubmitSuccess: mockOnSubmitSuccess,
onFormSubmitted: mockOnFormSubmitted,
onSubmitError: mockOnSubmitError,
formTitle: 'Feedback Form',
nameLabel: 'Name Label',
namePlaceholder: 'Name Placeholder',
Expand All @@ -38,6 +45,7 @@ const defaultProps: FeedbackFormProps = {
formError: 'Please fill out all required fields.',
emailError: 'The email address is not valid.',
successMessageText: 'Feedback success',
genericError: 'Generic error',
};

const customStyles: FeedbackFormStyles = {
Expand Down Expand Up @@ -198,7 +206,57 @@ describe('FeedbackForm', () => {
});
});

it('calls onFormClose when the form is submitted successfully', async () => {
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(<FeedbackForm {...defaultProps} />);

fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), '[email protected]');
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(<FeedbackForm {...defaultProps} />);

fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), '[email protected]');
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(<FeedbackForm {...defaultProps} />);

fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), '[email protected]');
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(<FeedbackForm {...defaultProps} />);

fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe');
Expand All @@ -208,7 +266,7 @@ describe('FeedbackForm', () => {
fireEvent.press(getByText(defaultProps.submitButtonLabel));

await waitFor(() => {
expect(mockOnFormClose).toHaveBeenCalled();
expect(mockOnFormSubmitted).toHaveBeenCalled();
});
});

Expand Down
1 change: 1 addition & 0 deletions samples/react-native/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ const ErrorsTabNavigator = Sentry.withProfiler(
<FeedbackForm
{...props}
onFormClose={props.navigation.goBack}
onFormSubmitted={props.navigation.goBack}
styles={{
submitButton: {
backgroundColor: '#6a1b9a',
Expand Down
Loading