Skip to content

Commit

Permalink
Merge f79621d into 9831482
Browse files Browse the repository at this point in the history
  • Loading branch information
antonis authored Dec 13, 2024
2 parents 9831482 + f79621d commit 06ab320
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 12 deletions.
28 changes: 16 additions & 12 deletions packages/core/src/js/feedback/FeedbackForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { captureFeedback, lastEventId } from '@sentry/core';
import { captureFeedback, lastEventId, logger } from '@sentry/core';
import type { SendFeedbackParams } from '@sentry/types';
import * as React from 'react';
import type { KeyboardTypeOptions } from 'react-native';
Expand All @@ -7,6 +7,7 @@ import { Alert, Text, TextInput, TouchableOpacity, View } from 'react-native';
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 @@ -40,7 +41,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
return;
}

if ((config.isEmailRequired || trimmedEmail.length > 0) && !this._isValidEmail(trimmedEmail)) {
if ((config.isEmailRequired || trimmedEmail.length > 0) && !isValidEmail(trimmedEmail)) {
Alert.alert(text.errorTitle, text.emailError);
return;
}
Expand All @@ -53,11 +54,19 @@ 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
onFormClose();
this.setState({ isVisible: false });
captureFeedback(userFeedback);
Alert.alert(text.successMessageText);
}, () => { // onDisconnected
Alert.alert(text.errorTitle, text.networkError);
logger.error(`Feedback form submission failed: ${text.networkError}`);
}, () => { // onError
Alert.alert(text.errorTitle, text.genericError);
logger.error(`Feedback form submission failed: ${text.genericError}`);
});
};

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

private _isValidEmail = (email: string): boolean => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return emailRegex.test(email);
};
}
10 changes: 10 additions & 0 deletions packages/core/src/js/feedback/FeedbackForm.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ 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;
}

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/js/feedback/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ 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
Expand Down Expand Up @@ -54,4 +56,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);
};
49 changes: 49 additions & 0 deletions packages/core/test/feedback/FeedbackForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ 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();

jest.spyOn(Alert, 'alert');

jest.mock('@sentry/core', () => ({
...jest.requireActual('@sentry/core'),
captureFeedback: jest.fn(),
getCurrentScope: jest.fn(() => ({
getUser: jest.fn(() => ({
Expand All @@ -20,6 +22,10 @@ jest.mock('@sentry/core', () => ({
})),
lastEventId: jest.fn(),
}));
jest.mock('../../src/js/feedback/utils', () => ({
...jest.requireActual('../../src/js/feedback/utils'),
checkInternetConnection: jest.fn(),
}));

const defaultProps: FeedbackFormProps = {
onFormClose: mockOnFormClose,
Expand All @@ -37,9 +43,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 @@ -125,6 +138,42 @@ describe('FeedbackForm', () => {
});
});

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

Expand Down

0 comments on commit 06ab320

Please sign in to comment.