Skip to content

Commit

Permalink
Merge e4e23a6 into 0588552
Browse files Browse the repository at this point in the history
  • Loading branch information
antonis authored Dec 17, 2024
2 parents 0588552 + e4e23a6 commit 25507c5
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 15 deletions.
33 changes: 20 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 @@ -18,6 +18,7 @@ import {
import { defaultConfiguration } from './defaults';
import defaultStyles from './FeedbackForm.styles';
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles,FeedbackGeneralConfiguration, FeedbackTextConfiguration } from './FeedbackForm.types';
import { checkInternetConnection, isValidEmail } from './utils';

/**
* @beta
Expand Down Expand Up @@ -48,7 +49,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 @@ -60,7 +61,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 @@ -73,11 +74,22 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
associatedEventId: eventId,
};

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

captureFeedback(userFeedback);
Alert.alert(text.successMessageText);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
checkInternetConnection(() => { // onConnected
this.setState({ isVisible: false });
captureFeedback(userFeedback);
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: undefined });
Alert.alert(text.successMessageText);
onFormSubmitted();
}, () => { // onDisconnected
Alert.alert(text.errorTitle, text.networkError);
logger.error(`Feedback form submission failed: ${text.networkError}`);
}, () => { // onError
const errorString = `Feedback form submission failed: ${text.genericError}`;
onSubmitError(new Error(errorString));
Alert.alert(text.errorTitle, text.genericError);
logger.error(errorString);
});
};

/**
Expand Down Expand Up @@ -163,9 +175,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);
};
}
33 changes: 33 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 { TextStyle, ViewStyle } from 'react-native';

export interface FeedbackFormProps extends FeedbackGeneralConfiguration, FeedbackTextConfiguration, FeedbackCallbacks {
Expand Down Expand Up @@ -116,16 +117,48 @@ export interface FeedbackTextConfiguration {
* The error message when the email is invalid
*/
emailError?: string;

/**
* Message when there is a network error
*/
networkError?: string;

/**
* Message when there is a network 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;
}

export interface FeedbackFormStyles {
Expand Down
16 changes: 16 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,14 @@ 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 CONNECTIONS_ERROR_TEXT = 'Unable to send Feedback due to network issues.';
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 +32,15 @@ export const defaultConfiguration: Partial<FeedbackFormProps> = {
);
}
},
onSubmitSuccess: () => {
// Does nothing by default
},
onSubmitError: () => {
// Does nothing by default
},
onFormSubmitted: () => {
// Does nothing by default
},

// FeedbackGeneralConfiguration
isEmailRequired: false,
Expand All @@ -50,4 +64,6 @@ export const defaultConfiguration: Partial<FeedbackFormProps> = {
formError: FORM_ERROR,
emailError: EMAIL_ERROR,
successMessageText: SUCCESS_MESSAGE_TEXT,
networkError: CONNECTIONS_ERROR_TEXT,
genericError: GENERIC_ERROR_TEXT,
};
21 changes: 21 additions & 0 deletions packages/core/src/js/feedback/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const checkInternetConnection = async (
onConnected: () => void,
onDisconnected: () => void,
onError: () => void,
): Promise<void> => {
try {
const response = await fetch('https://sentry.io', { method: 'HEAD' });
if (response.ok) {
onConnected();
} else {
onDisconnected();
}
} catch (error) {
onError();
}
};

export const isValidEmail = (email: string): boolean => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
};
91 changes: 89 additions & 2 deletions packages/core/test/feedback/FeedbackForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import { Alert } from 'react-native';

import { FeedbackForm } from '../../src/js/feedback/FeedbackForm';
import type { FeedbackFormProps } from '../../src/js/feedback/FeedbackForm.types';
import { checkInternetConnection } from '../../src/js/feedback/utils';

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,15 +19,23 @@ const mockGetUser = jest.fn(() => ({
jest.spyOn(Alert, 'alert');

jest.mock('@sentry/core', () => ({
...jest.requireActual('@sentry/core'),
captureFeedback: jest.fn(),
getCurrentScope: jest.fn(() => ({
getUser: mockGetUser,
})),
lastEventId: jest.fn(),
}));
jest.mock('../../src/js/feedback/utils', () => ({
...jest.requireActual('../../src/js/feedback/utils'),
checkInternetConnection: jest.fn(),
}));

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

describe('FeedbackForm', () => {
beforeEach(() => {
(checkInternetConnection as jest.Mock).mockImplementation((onConnected, _onDisconnected, _onError) => {
onConnected();
});
});
afterEach(() => {
jest.clearAllMocks();
});
Expand Down Expand Up @@ -137,7 +156,75 @@ describe('FeedbackForm', () => {
});
});

it('calls onFormClose when the form is submitted successfully', async () => {
it('shows an error message when there is no network connection', async () => {
(checkInternetConnection as jest.Mock).mockImplementationOnce((_onConnected, onDisconnected, _onError) => {
onDisconnected();
});

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.networkError);
});
});

it('shows an error message when there is a generic connection', async () => {
(checkInternetConnection as jest.Mock).mockImplementationOnce((_onConnected, _onDisconnected, onError) => {
onError();
});

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 () => {
(checkInternetConnection as jest.Mock).mockImplementationOnce((_onConnected, _onDisconnected, onError) => {
onError();
});

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 @@ -147,7 +234,7 @@ describe('FeedbackForm', () => {
fireEvent.press(getByText(defaultProps.submitButtonLabel));

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

Expand Down

0 comments on commit 25507c5

Please sign in to comment.