From 38a6085e0bb534ea1621d9a4d4c356a461887d1d Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 14 Feb 2025 13:47:36 +0200 Subject: [PATCH 01/14] Save form state for unsubmitted data --- .../core/src/js/feedback/FeedbackForm.tsx | 45 +++++++++++++++---- .../core/test/feedback/FeedbackForm.test.tsx | 30 +++++++++++++ 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/packages/core/src/js/feedback/FeedbackForm.tsx b/packages/core/src/js/feedback/FeedbackForm.tsx index b6aac2f412..8f5103854c 100644 --- a/packages/core/src/js/feedback/FeedbackForm.tsx +++ b/packages/core/src/js/feedback/FeedbackForm.tsx @@ -33,6 +33,15 @@ export class FeedbackForm extends React.Component { if (data != null) { - this.setState({ filename, attachment: data }); + this.setState({ filename, attachment: data }, this._saveFormState); } else { logger.error('Failed to read image data from uri:', imageUri); } @@ -142,11 +154,11 @@ export class FeedbackForm extends React.Component { - this.setState({ filename, attachment: attachement }); + this.setState({ filename, attachment: attachement }, this._saveFormState); }); } } else { - this.setState({ filename: undefined, attachment: undefined }); + this.setState({ filename: undefined, attachment: undefined }, this._saveFormState); } } @@ -199,7 +211,7 @@ export class FeedbackForm extends React.Component this.setState({ name: value })} + onChangeText={(value) => this.setState({ name: value }, this._saveFormState)} /> )} @@ -215,7 +227,7 @@ export class FeedbackForm extends React.Component this.setState({ email: value })} + onChangeText={(value) => this.setState({ email: value }, this._saveFormState)} /> )} @@ -228,7 +240,7 @@ export class FeedbackForm extends React.Component this.setState({ description: value })} + onChangeText={(value) => this.setState({ description: value }, this._saveFormState)} multiline /> {(config.enableScreenshot || imagePickerConfiguration.imagePicker) && ( @@ -254,4 +266,19 @@ export class FeedbackForm extends React.Component ); } + + private _saveFormState = (): void => { + FeedbackForm._savedState = { ...this.state }; + }; + + private _clearFormState = (): void => { + FeedbackForm._savedState = { + isVisible: false, + name: '', + email: '', + description: '', + filename: undefined, + attachment: undefined, + }; + }; } diff --git a/packages/core/test/feedback/FeedbackForm.test.tsx b/packages/core/test/feedback/FeedbackForm.test.tsx index 92ed48fe4a..15918799ca 100644 --- a/packages/core/test/feedback/FeedbackForm.test.tsx +++ b/packages/core/test/feedback/FeedbackForm.test.tsx @@ -354,4 +354,34 @@ describe('FeedbackForm', () => { expect(mockOnFormClose).toHaveBeenCalled(); }); + + it('onCancel the input is saved and restored when the form reopens', async () => { + const { getByPlaceholderText, getByText } = render(); + + fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe'); + fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), 'john.doe@example.com'); + fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.'); + + fireEvent.press(getByText(defaultProps.cancelButtonLabel)); + const { queryByPlaceholderText } = render(); + + expect(queryByPlaceholderText(defaultProps.namePlaceholder).props.value).toBe('John Doe'); + expect(queryByPlaceholderText(defaultProps.emailPlaceholder).props.value).toBe('john.doe@example.com'); + expect(queryByPlaceholderText(defaultProps.messagePlaceholder).props.value).toBe('This is a feedback message.'); + }); + + it('onSubmit the saved input is cleared and not restored when the form reopens', async () => { + const { getByPlaceholderText, getByText } = render(); + + fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe'); + fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), 'john.doe@example.com'); + fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.'); + + fireEvent.press(getByText(defaultProps.submitButtonLabel)); + const { queryByPlaceholderText } = render(); + + expect(queryByPlaceholderText(defaultProps.namePlaceholder).props.value).toBe('Test User'); + expect(queryByPlaceholderText(defaultProps.emailPlaceholder).props.value).toBe('test@example.com'); + expect(queryByPlaceholderText(defaultProps.messagePlaceholder).props.value).toBe(''); + }); }); From 56a113e15e7f1c25af6b982956b7e08ea7169d95 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 14 Feb 2025 17:55:15 +0200 Subject: [PATCH 02/14] Show selected screenshot --- .../src/js/feedback/FeedbackForm.styles.ts | 14 +- .../core/src/js/feedback/FeedbackForm.tsx | 32 ++- .../src/js/feedback/FeedbackForm.types.ts | 3 + .../__snapshots__/FeedbackForm.test.tsx.snap | 245 ++++++++++-------- 4 files changed, 177 insertions(+), 117 deletions(-) diff --git a/packages/core/src/js/feedback/FeedbackForm.styles.ts b/packages/core/src/js/feedback/FeedbackForm.styles.ts index 1e90af07c6..e1373f6c8c 100644 --- a/packages/core/src/js/feedback/FeedbackForm.styles.ts +++ b/packages/core/src/js/feedback/FeedbackForm.styles.ts @@ -45,8 +45,20 @@ const defaultStyles: FeedbackFormStyles = { backgroundColor: '#eee', padding: 15, borderRadius: 5, - marginBottom: 20, alignItems: 'center', + flex: 1, + }, + screenshotContainer: { + flexDirection: 'row', + alignItems: 'center', + width: '100%', + marginBottom: 20, + }, + screenshotThumbnail: { + width: 50, + height: 50, + borderRadius: 5, + marginRight: 10, }, screenshotText: { color: '#333', diff --git a/packages/core/src/js/feedback/FeedbackForm.tsx b/packages/core/src/js/feedback/FeedbackForm.tsx index 8f5103854c..19a3a96f8d 100644 --- a/packages/core/src/js/feedback/FeedbackForm.tsx +++ b/packages/core/src/js/feedback/FeedbackForm.tsx @@ -40,6 +40,7 @@ export class FeedbackForm extends React.Component { if (data != null) { - this.setState({ filename, attachment: data }, this._saveFormState); + this.setState({ filename, attachment: data, attachmentUri: imageUri }, this._saveFormState); } else { logger.error('Failed to read image data from uri:', imageUri); } @@ -154,11 +156,12 @@ export class FeedbackForm extends React.Component { - this.setState({ filename, attachment: attachement }, this._saveFormState); + // TODO: Add support for image uri when using onAddScreenshot + this.setState({ filename, attachment: attachement, attachmentUri: undefined }, this._saveFormState); }); } } else { - this.setState({ filename: undefined, attachment: undefined }, this._saveFormState); + this.setState({ filename: undefined, attachment: undefined, attachmentUri: undefined }, this._saveFormState); } } @@ -244,13 +247,21 @@ export class FeedbackForm extends React.Component {(config.enableScreenshot || imagePickerConfiguration.imagePicker) && ( - - - {!this.state.filename && !this.state.attachment - ? text.addScreenshotButtonLabel - : text.removeScreenshotButtonLabel} - - + + {this.state.attachmentUri && ( + + )} + + + {!this.state.filename && !this.state.attachment + ? text.addScreenshotButtonLabel + : text.removeScreenshotButtonLabel} + + + )} {text.submitButtonLabel} @@ -279,6 +290,7 @@ export class FeedbackForm extends React.Component - - Add a screenshot - + + Add a screenshot + + - - Add Screenshot - + + Add Screenshot + + - - Add a screenshot - + + Add a screenshot + + Date: Fri, 14 Feb 2025 18:09:03 +0200 Subject: [PATCH 03/14] Use image uri instead of UInt8Array in onAddScreenshot callback --- packages/core/src/js/feedback/FeedbackForm.tsx | 14 +++++++++++--- .../core/src/js/feedback/FeedbackForm.types.ts | 2 +- packages/core/src/js/feedback/defaults.ts | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/core/src/js/feedback/FeedbackForm.tsx b/packages/core/src/js/feedback/FeedbackForm.tsx index 19a3a96f8d..ae7ce25088 100644 --- a/packages/core/src/js/feedback/FeedbackForm.tsx +++ b/packages/core/src/js/feedback/FeedbackForm.tsx @@ -155,9 +155,17 @@ export class FeedbackForm extends React.Component { - // TODO: Add support for image uri when using onAddScreenshot - this.setState({ filename, attachment: attachement, attachmentUri: undefined }, this._saveFormState); + onAddScreenshot((filename: string, fileUri: string) => { + NATIVE.getDataFromUri(fileUri).then((data) => { + if (data != null) { + this.setState({ filename, attachment: data, attachmentUri: fileUri }, this._saveFormState); + } else { + logger.error('Failed to read image data from uri:', fileUri); + } + }) + .catch((error) => { + logger.error('Failed to read image data from uri:', fileUri, 'error: ', error); + }); }); } } else { diff --git a/packages/core/src/js/feedback/FeedbackForm.types.ts b/packages/core/src/js/feedback/FeedbackForm.types.ts index e5ee456f7e..bb53e5b8df 100644 --- a/packages/core/src/js/feedback/FeedbackForm.types.ts +++ b/packages/core/src/js/feedback/FeedbackForm.types.ts @@ -171,7 +171,7 @@ export interface FeedbackCallbacks { /** * Callback when a screenshot is added */ - onAddScreenshot?: (attachFile: (filename: string, data: Uint8Array) => void) => void; + onAddScreenshot?: (attachFile: (filename: string, fileUri: string) => void) => void; /** * Callback when feedback is successfully submitted diff --git a/packages/core/src/js/feedback/defaults.ts b/packages/core/src/js/feedback/defaults.ts index a1d8e74b62..af4311b072 100644 --- a/packages/core/src/js/feedback/defaults.ts +++ b/packages/core/src/js/feedback/defaults.ts @@ -33,7 +33,7 @@ export const defaultConfiguration: Partial = { ); } }, - onAddScreenshot: (_: (filename: string, data: Uint8Array) => void) => { + onAddScreenshot: (_: (filename: string, fileUri: string) => void) => { if (__DEV__) { Alert.alert('Development note', 'onAddScreenshot callback is not implemented.'); } From 1efb23e290f3aeef6977e8e714d1c191a1e8342c Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 17 Feb 2025 12:06:56 +0200 Subject: [PATCH 04/14] Omit isVisible from state --- packages/core/src/js/feedback/FeedbackWidget.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/src/js/feedback/FeedbackWidget.tsx b/packages/core/src/js/feedback/FeedbackWidget.tsx index 839617beeb..e992f08d37 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.tsx +++ b/packages/core/src/js/feedback/FeedbackWidget.tsx @@ -33,8 +33,7 @@ export class FeedbackWidget extends React.Component = { name: '', email: '', description: '', @@ -273,7 +272,6 @@ export class FeedbackWidget extends React.Component { FeedbackWidget._savedState = { - isVisible: false, name: '', email: '', description: '', From 59f2f570ae0e6bb6744bb59566cdc13e549add75 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 17 Feb 2025 15:02:07 +0200 Subject: [PATCH 05/14] Save/clear form state on unmount --- .../core/src/js/feedback/FeedbackWidget.tsx | 27 ++++++++++++++----- .../test/feedback/FeedbackWidget.test.tsx | 21 +++++++++++++-- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/packages/core/src/js/feedback/FeedbackWidget.tsx b/packages/core/src/js/feedback/FeedbackWidget.tsx index e992f08d37..8ad7f85ded 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.tsx +++ b/packages/core/src/js/feedback/FeedbackWidget.tsx @@ -33,6 +33,7 @@ export class FeedbackWidget extends React.Component = { name: '', email: '', @@ -103,7 +104,7 @@ export class FeedbackWidget extends React.Component { if (data != null) { - this.setState({ filename, attachment: data }, this._saveFormState); + this.setState({ filename, attachment: data }); } else { logger.error('Failed to read image data from uri:', imageUri); } @@ -153,11 +154,23 @@ export class FeedbackWidget extends React.Component { - this.setState({ filename, attachment: attachement }, this._saveFormState); + this.setState({ filename, attachment: attachement }); }); } } else { - this.setState({ filename: undefined, attachment: undefined }, this._saveFormState); + this.setState({ filename: undefined, attachment: undefined }); + } + } + + /** + * Save the state before unmounting the component. + */ + public componentWillUnmount(): void { + if (FeedbackWidget._didSubmitForm) { + this._clearFormState(); + FeedbackWidget._didSubmitForm = false; + } else { + this._saveFormState(); } } @@ -210,7 +223,7 @@ export class FeedbackWidget extends React.Component this.setState({ name: value }, this._saveFormState)} + onChangeText={(value) => this.setState({ name: value })} /> )} @@ -226,7 +239,7 @@ export class FeedbackWidget extends React.Component this.setState({ email: value }, this._saveFormState)} + onChangeText={(value) => this.setState({ email: value })} /> )} @@ -239,7 +252,7 @@ export class FeedbackWidget extends React.Component this.setState({ description: value }, this._saveFormState)} + onChangeText={(value) => this.setState({ description: value })} multiline /> {(config.enableScreenshot || imagePickerConfiguration.imagePicker) && ( diff --git a/packages/core/test/feedback/FeedbackWidget.test.tsx b/packages/core/test/feedback/FeedbackWidget.test.tsx index c3fcc8fefe..0274651a31 100644 --- a/packages/core/test/feedback/FeedbackWidget.test.tsx +++ b/packages/core/test/feedback/FeedbackWidget.test.tsx @@ -355,14 +355,30 @@ describe('FeedbackWidget', () => { expect(mockOnFormClose).toHaveBeenCalled(); }); + it('onUnmount the input is saved and restored when the form reopens', async () => { + const { getByPlaceholderText, unmount } = render(); + + fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe'); + fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), 'john.doe@example.com'); + fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.'); + + unmount(); + const { queryByPlaceholderText } = render(); + + expect(queryByPlaceholderText(defaultProps.namePlaceholder).props.value).toBe('John Doe'); + expect(queryByPlaceholderText(defaultProps.emailPlaceholder).props.value).toBe('john.doe@example.com'); + expect(queryByPlaceholderText(defaultProps.messagePlaceholder).props.value).toBe('This is a feedback message.'); + }); + it('onCancel the input is saved and restored when the form reopens', async () => { - const { getByPlaceholderText, getByText } = render(); + const { getByPlaceholderText, getByText, unmount } = render(); fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe'); fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), 'john.doe@example.com'); fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.'); fireEvent.press(getByText(defaultProps.cancelButtonLabel)); + unmount(); const { queryByPlaceholderText } = render(); expect(queryByPlaceholderText(defaultProps.namePlaceholder).props.value).toBe('John Doe'); @@ -371,13 +387,14 @@ describe('FeedbackWidget', () => { }); it('onSubmit the saved input is cleared and not restored when the form reopens', async () => { - const { getByPlaceholderText, getByText } = render(); + const { getByPlaceholderText, getByText, unmount } = render(); fireEvent.changeText(getByPlaceholderText(defaultProps.namePlaceholder), 'John Doe'); fireEvent.changeText(getByPlaceholderText(defaultProps.emailPlaceholder), 'john.doe@example.com'); fireEvent.changeText(getByPlaceholderText(defaultProps.messagePlaceholder), 'This is a feedback message.'); fireEvent.press(getByText(defaultProps.submitButtonLabel)); + unmount(); const { queryByPlaceholderText } = render(); expect(queryByPlaceholderText(defaultProps.namePlaceholder).props.value).toBe('Test User'); From 797611f898676086b4e9f3570244cf8783245055 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 17 Feb 2025 15:02:42 +0200 Subject: [PATCH 06/14] Pass the missing attachment parameter in the onSubmitSuccess --- packages/core/src/js/feedback/FeedbackWidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/js/feedback/FeedbackWidget.tsx b/packages/core/src/js/feedback/FeedbackWidget.tsx index 8ad7f85ded..b1da634aef 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.tsx +++ b/packages/core/src/js/feedback/FeedbackWidget.tsx @@ -101,7 +101,7 @@ export class FeedbackWidget extends React.Component Date: Mon, 17 Feb 2025 15:43:46 +0200 Subject: [PATCH 07/14] Use only the uri parameter for the onAddScreenshot callback --- packages/core/src/js/feedback/FeedbackWidget.tsx | 10 +++++----- packages/core/src/js/feedback/FeedbackWidget.types.ts | 2 +- packages/core/src/js/feedback/defaults.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/src/js/feedback/FeedbackWidget.tsx b/packages/core/src/js/feedback/FeedbackWidget.tsx index ad91d7890b..7cc28cdc9f 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.tsx +++ b/packages/core/src/js/feedback/FeedbackWidget.tsx @@ -155,16 +155,16 @@ export class FeedbackWidget extends React.Component { - NATIVE.getDataFromUri(fileUri).then((data) => { + onAddScreenshot((uri: string) => { + NATIVE.getDataFromUri(uri).then((data) => { if (data != null) { - this.setState({ filename, attachment: data, attachmentUri: fileUri }); + this.setState({ filename: 'feedback_screenshot', attachment: data, attachmentUri: uri }); } else { - logger.error('Failed to read image data from uri:', fileUri); + logger.error('Failed to read image data from uri:', uri); } }) .catch((error) => { - logger.error('Failed to read image data from uri:', fileUri, 'error: ', error); + logger.error('Failed to read image data from uri:', uri, 'error: ', error); }); }); } diff --git a/packages/core/src/js/feedback/FeedbackWidget.types.ts b/packages/core/src/js/feedback/FeedbackWidget.types.ts index 2f369f9b1b..2c231c0594 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.types.ts +++ b/packages/core/src/js/feedback/FeedbackWidget.types.ts @@ -171,7 +171,7 @@ export interface FeedbackCallbacks { /** * Callback when a screenshot is added */ - onAddScreenshot?: (attachFile: (filename: string, fileUri: string) => void) => void; + onAddScreenshot?: (screenshotAdded: (uri: string) => void) => void; /** * Callback when feedback is successfully submitted diff --git a/packages/core/src/js/feedback/defaults.ts b/packages/core/src/js/feedback/defaults.ts index a17e5d8541..1b7ba84ce9 100644 --- a/packages/core/src/js/feedback/defaults.ts +++ b/packages/core/src/js/feedback/defaults.ts @@ -33,7 +33,7 @@ export const defaultConfiguration: Partial = { ); } }, - onAddScreenshot: (_: (filename: string, fileUri: string) => void) => { + onAddScreenshot: (_: (uri: string) => void) => { if (__DEV__) { Alert.alert('Development note', 'onAddScreenshot callback is not implemented.'); } From 41cb1cab3fca8306d98fd36c967cc38ced8a8763 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 17 Feb 2025 18:19:49 +0200 Subject: [PATCH 08/14] Handle attachments on the web --- .../core/src/js/feedback/FeedbackWidget.tsx | 30 +++++++++++++------ .../src/js/feedback/FeedbackWidget.types.ts | 3 ++ packages/core/src/js/feedback/utils.ts | 21 ++++++++++++- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/packages/core/src/js/feedback/FeedbackWidget.tsx b/packages/core/src/js/feedback/FeedbackWidget.tsx index 1136ef7e7b..6a96ffe524 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.tsx +++ b/packages/core/src/js/feedback/FeedbackWidget.tsx @@ -17,12 +17,13 @@ import { View } from 'react-native'; +import { isWeb } from '../utils/environment'; import { NATIVE } from '../wrapper'; import { sentryLogo } from './branding'; import { defaultConfiguration } from './defaults'; import defaultStyles from './FeedbackWidget.styles'; import type { FeedbackGeneralConfiguration, FeedbackTextConfiguration, FeedbackWidgetProps, FeedbackWidgetState, FeedbackWidgetStyles, ImagePickerConfiguration } from './FeedbackWidget.types'; -import { isValidEmail } from './utils'; +import { base64ToUint8Array, isValidEmail } from './utils'; /** * @beta @@ -123,10 +124,10 @@ export class FeedbackWidget extends React.Component imagePickerConfiguration.imagePicker.launchImageLibraryAsync({ mediaTypes: ['images'] }) + ? () => imagePickerConfiguration.imagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], base64: isWeb() }) // react-native-image-picker library is available : imagePickerConfiguration.imagePicker.launchImageLibrary - ? () => imagePickerConfiguration.imagePicker.launchImageLibrary({ mediaType: 'photo' }) + ? () => imagePickerConfiguration.imagePicker.launchImageLibrary({ mediaType: 'photo', includeBase64: isWeb() }) : null; if (!launchImageLibrary) { logger.warn('No compatible image picker library found. Please provide a valid image picker library.'); @@ -141,18 +142,29 @@ export class FeedbackWidget extends React.Component 0) { - const filename = result.assets[0].fileName; - const imageUri = result.assets[0].uri; - NATIVE.getDataFromUri(imageUri).then((data) => { + if (isWeb()) { + const filename = result.assets[0].fileName; + const imageUri = result.assets[0].uri; + const base64 = result.assets[0].base64; + const data = base64ToUint8Array(base64); if (data != null) { this.setState({ filename, attachment: data, attachmentUri: imageUri }); } else { - logger.error('Failed to read image data from uri:', imageUri); + logger.error('Failed to read image data on the web'); } - }) - .catch((error) => { + } else { + const filename = result.assets[0].fileName; + const imageUri = result.assets[0].uri; + NATIVE.getDataFromUri(imageUri).then((data) => { + if (data != null) { + this.setState({ filename, attachment: data, attachmentUri: imageUri }); + } else { + logger.error('Failed to read image data from uri:', imageUri); + } + }).catch((error) => { logger.error('Failed to read image data from uri:', imageUri, 'error: ', error); }); + } } } else { // Defaulting to the onAddScreenshot callback diff --git a/packages/core/src/js/feedback/FeedbackWidget.types.ts b/packages/core/src/js/feedback/FeedbackWidget.types.ts index 2c231c0594..f3349c9bcc 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.types.ts +++ b/packages/core/src/js/feedback/FeedbackWidget.types.ts @@ -207,14 +207,17 @@ interface ImagePickerResponse { interface ImagePickerAsset { fileName?: string; uri?: string; + base64?: string; } interface ExpoImageLibraryOptions { mediaTypes?: 'images'[]; + base64?: boolean; } interface ReactNativeImageLibraryOptions { mediaType: 'photo'; + includeBase64?: boolean; } export interface ImagePicker { diff --git a/packages/core/src/js/feedback/utils.ts b/packages/core/src/js/feedback/utils.ts index b27fb3ea8d..80291211b9 100644 --- a/packages/core/src/js/feedback/utils.ts +++ b/packages/core/src/js/feedback/utils.ts @@ -1,6 +1,11 @@ -import { isFabricEnabled } from '../utils/environment'; +import { isFabricEnabled, isWeb } from '../utils/environment'; import { ReactNativeLibraries } from './../utils/rnlibraries'; +declare global { + // Declaring atob function to be used in web environment + function atob(encodedString: string): string; +} + /** * Modal is not supported in React Native < 0.71 with Fabric renderer. * ref: https://github.com/facebook/react-native/issues/33652 @@ -14,3 +19,17 @@ export const isValidEmail = (email: string): boolean => { const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; return emailRegex.test(email); }; + +/** + * Converts base64 string to Uint8Array on the web + * @param base64 base64 string + * @returns Uint8Array data + */ +export const base64ToUint8Array = (base64: string): Uint8Array => { + if (typeof atob !== 'function' || !isWeb()) { + throw new Error('atob is not available in this environment.'); + } + + const binaryString = atob(base64); + return new Uint8Array([...binaryString].map(char => char.charCodeAt(0))); +}; From f5c306f3009bcad75e9c04555c03236ad1835920 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 17 Feb 2025 19:15:06 +0200 Subject: [PATCH 09/14] Use window for showing alerts on the web --- packages/core/src/js/feedback/FeedbackWidget.tsx | 13 ++++++------- packages/core/src/js/feedback/defaults.ts | 9 ++++----- packages/core/src/js/feedback/utils.ts | 14 ++++++++++++++ .../core/test/feedback/FeedbackWidget.test.tsx | 2 +- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/core/src/js/feedback/FeedbackWidget.tsx b/packages/core/src/js/feedback/FeedbackWidget.tsx index 6a96ffe524..9a505393eb 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.tsx +++ b/packages/core/src/js/feedback/FeedbackWidget.tsx @@ -3,7 +3,6 @@ import { captureFeedback, getCurrentScope, lastEventId, logger } from '@sentry/c import * as React from 'react'; import type { KeyboardTypeOptions } from 'react-native'; import { - Alert, Image, Keyboard, KeyboardAvoidingView, @@ -23,7 +22,7 @@ import { sentryLogo } from './branding'; import { defaultConfiguration } from './defaults'; import defaultStyles from './FeedbackWidget.styles'; import type { FeedbackGeneralConfiguration, FeedbackTextConfiguration, FeedbackWidgetProps, FeedbackWidgetState, FeedbackWidgetStyles, ImagePickerConfiguration } from './FeedbackWidget.types'; -import { base64ToUint8Array, isValidEmail } from './utils'; +import { base64ToUint8Array, feedbackAlertDialog, isValidEmail } from './utils'; /** * @beta @@ -75,12 +74,12 @@ export class FeedbackWidget extends React.Component 0) && !isValidEmail(trimmedEmail)) { - Alert.alert(text.errorTitle, text.emailError); + feedbackAlertDialog(text.errorTitle, text.emailError); return; } @@ -107,13 +106,13 @@ export class FeedbackWidget extends React.Component = { }, onFormClose: () => { if (__DEV__) { - Alert.alert( + feedbackAlertDialog( 'Development note', 'onFormClose callback is not implemented. By default the form is just unmounted.', ); @@ -35,7 +34,7 @@ export const defaultConfiguration: Partial = { }, onAddScreenshot: (_: (uri: string) => void) => { if (__DEV__) { - Alert.alert('Development note', 'onAddScreenshot callback is not implemented.'); + feedbackAlertDialog('Development note', 'onAddScreenshot callback is not implemented.'); } }, onSubmitSuccess: () => { @@ -46,7 +45,7 @@ export const defaultConfiguration: Partial = { }, onFormSubmitted: () => { if (__DEV__) { - Alert.alert( + feedbackAlertDialog( 'Development note', 'onFormSubmitted callback is not implemented. By default the form is just unmounted.', ); diff --git a/packages/core/src/js/feedback/utils.ts b/packages/core/src/js/feedback/utils.ts index 80291211b9..8b05731980 100644 --- a/packages/core/src/js/feedback/utils.ts +++ b/packages/core/src/js/feedback/utils.ts @@ -1,3 +1,5 @@ +import { Alert } from 'react-native'; + import { isFabricEnabled, isWeb } from '../utils/environment'; import { ReactNativeLibraries } from './../utils/rnlibraries'; @@ -33,3 +35,15 @@ export const base64ToUint8Array = (base64: string): Uint8Array => { const binaryString = atob(base64); return new Uint8Array([...binaryString].map(char => char.charCodeAt(0))); }; + +export const feedbackAlertDialog = (title: string, message: string): void => { + /* eslint-disable @typescript-eslint/ban-ts-comment, no-restricted-globals, no-alert, @typescript-eslint/no-unsafe-member-access */ + // @ts-ignore + if (isWeb() && typeof window !== 'undefined') { + // @ts-ignore + window.alert(`${title}\n${message}`); + /* eslint-enable @typescript-eslint/ban-ts-comment, no-restricted-globals, no-alert, @typescript-eslint/no-unsafe-member-access */ + } else { + Alert.alert(title, message); + } +}; diff --git a/packages/core/test/feedback/FeedbackWidget.test.tsx b/packages/core/test/feedback/FeedbackWidget.test.tsx index 0274651a31..fb5a394fa3 100644 --- a/packages/core/test/feedback/FeedbackWidget.test.tsx +++ b/packages/core/test/feedback/FeedbackWidget.test.tsx @@ -235,7 +235,7 @@ describe('FeedbackWidget', () => { fireEvent.press(getByText(defaultProps.submitButtonLabel)); await waitFor(() => { - expect(Alert.alert).toHaveBeenCalledWith(defaultProps.successMessageText); + expect(Alert.alert).toHaveBeenCalledWith(defaultProps.successMessageText, ''); }); }); From 42ab16c16b74bd7f236b3d252beb1b1c4c4b4e10 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 17 Feb 2025 19:18:02 +0200 Subject: [PATCH 10/14] Disable keyboard handling on the web --- packages/core/src/js/feedback/FeedbackWidget.tsx | 5 +++-- packages/core/src/js/feedback/FeedbackWidgetManager.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core/src/js/feedback/FeedbackWidget.tsx b/packages/core/src/js/feedback/FeedbackWidget.tsx index 9a505393eb..79c77a3664 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.tsx +++ b/packages/core/src/js/feedback/FeedbackWidget.tsx @@ -16,7 +16,7 @@ import { View } from 'react-native'; -import { isWeb } from '../utils/environment'; +import { isWeb, notWeb } from '../utils/environment'; import { NATIVE } from '../wrapper'; import { sentryLogo } from './branding'; import { defaultConfiguration } from './defaults'; @@ -225,9 +225,10 @@ export class FeedbackWidget extends React.Component - + {text.formTitle} diff --git a/packages/core/src/js/feedback/FeedbackWidgetManager.tsx b/packages/core/src/js/feedback/FeedbackWidgetManager.tsx index 11a7c3f8cc..48439a07f1 100644 --- a/packages/core/src/js/feedback/FeedbackWidgetManager.tsx +++ b/packages/core/src/js/feedback/FeedbackWidgetManager.tsx @@ -2,6 +2,7 @@ import { logger } from '@sentry/core'; import * as React from 'react'; import { Animated, Dimensions, Easing, KeyboardAvoidingView, Modal, PanResponder, Platform } from 'react-native'; +import { notWeb } from '../utils/environment'; import { FeedbackWidget } from './FeedbackWidget'; import { modalBackground, modalSheetContainer, modalWrapper } from './FeedbackWidget.styles'; import type { FeedbackWidgetStyles } from './FeedbackWidget.types'; @@ -61,10 +62,10 @@ class FeedbackWidgetProvider extends React.Component { // On Android allow pulling down only from the top to avoid breaking native gestures - return Platform.OS !== 'android' || evt.nativeEvent.pageY < PULL_DOWN_ANDROID_ACTIVATION_HEIGHT; + return notWeb() && (Platform.OS !== 'android' || evt.nativeEvent.pageY < PULL_DOWN_ANDROID_ACTIVATION_HEIGHT); }, onMoveShouldSetPanResponder: (evt, _gestureState) => { - return Platform.OS !== 'android' || evt.nativeEvent.pageY < PULL_DOWN_ANDROID_ACTIVATION_HEIGHT; + return notWeb() && (Platform.OS !== 'android' || evt.nativeEvent.pageY < PULL_DOWN_ANDROID_ACTIVATION_HEIGHT); }, onPanResponderMove: (_, gestureState) => { if (gestureState.dy > 0) { @@ -147,6 +148,7 @@ class FeedbackWidgetProvider extends React.Component Date: Tue, 18 Feb 2025 11:38:18 +0200 Subject: [PATCH 11/14] Use instance variable for _didSubmitForm --- packages/core/src/js/feedback/FeedbackWidget.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/js/feedback/FeedbackWidget.tsx b/packages/core/src/js/feedback/FeedbackWidget.tsx index 92d2f6d3b8..c5415f4ddc 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.tsx +++ b/packages/core/src/js/feedback/FeedbackWidget.tsx @@ -33,7 +33,7 @@ export class FeedbackWidget extends React.Component = { name: '', email: '', @@ -106,7 +106,7 @@ export class FeedbackWidget extends React.Component Date: Tue, 18 Feb 2025 11:41:49 +0200 Subject: [PATCH 12/14] Fixed callback function parameter name for clarity Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> --- packages/core/src/js/feedback/FeedbackWidget.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/js/feedback/FeedbackWidget.types.ts b/packages/core/src/js/feedback/FeedbackWidget.types.ts index 2c231c0594..e821f886b8 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.types.ts +++ b/packages/core/src/js/feedback/FeedbackWidget.types.ts @@ -171,7 +171,7 @@ export interface FeedbackCallbacks { /** * Callback when a screenshot is added */ - onAddScreenshot?: (screenshotAdded: (uri: string) => void) => void; + onAddScreenshot?: (addScreenshot: (uri: string) => void) => void; /** * Callback when feedback is successfully submitted From 6e2d6eca1561cb052f088cf1736e3b2f0a45f86d Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 18 Feb 2025 11:45:18 +0200 Subject: [PATCH 13/14] Fixes lint issue --- packages/core/src/js/feedback/FeedbackWidget.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/js/feedback/FeedbackWidget.tsx b/packages/core/src/js/feedback/FeedbackWidget.tsx index c5415f4ddc..f0026751d3 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.tsx +++ b/packages/core/src/js/feedback/FeedbackWidget.tsx @@ -33,7 +33,6 @@ export class FeedbackWidget extends React.Component = { name: '', email: '', @@ -42,6 +41,8 @@ export class FeedbackWidget extends React.Component Date: Tue, 18 Feb 2025 12:08:24 +0200 Subject: [PATCH 14/14] Use RN_GLOBAL_OBJ for web alert --- packages/core/src/js/feedback/utils.ts | 9 +++------ packages/core/src/js/utils/worldwide.ts | 1 + 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/core/src/js/feedback/utils.ts b/packages/core/src/js/feedback/utils.ts index 8b05731980..9c2826981d 100644 --- a/packages/core/src/js/feedback/utils.ts +++ b/packages/core/src/js/feedback/utils.ts @@ -1,6 +1,7 @@ import { Alert } from 'react-native'; import { isFabricEnabled, isWeb } from '../utils/environment'; +import { RN_GLOBAL_OBJ } from '../utils/worldwide'; import { ReactNativeLibraries } from './../utils/rnlibraries'; declare global { @@ -37,12 +38,8 @@ export const base64ToUint8Array = (base64: string): Uint8Array => { }; export const feedbackAlertDialog = (title: string, message: string): void => { - /* eslint-disable @typescript-eslint/ban-ts-comment, no-restricted-globals, no-alert, @typescript-eslint/no-unsafe-member-access */ - // @ts-ignore - if (isWeb() && typeof window !== 'undefined') { - // @ts-ignore - window.alert(`${title}\n${message}`); - /* eslint-enable @typescript-eslint/ban-ts-comment, no-restricted-globals, no-alert, @typescript-eslint/no-unsafe-member-access */ + if (isWeb() && typeof RN_GLOBAL_OBJ.alert !== 'undefined') { + RN_GLOBAL_OBJ.alert(`${title}\n${message}`); } else { Alert.alert(title, message); } diff --git a/packages/core/src/js/utils/worldwide.ts b/packages/core/src/js/utils/worldwide.ts index c1a4ae5dbb..03327bac36 100644 --- a/packages/core/src/js/utils/worldwide.ts +++ b/packages/core/src/js/utils/worldwide.ts @@ -25,6 +25,7 @@ export interface ReactNativeInternalGlobal extends InternalGlobal { __BUNDLE_START_TIME__?: number; nativePerformanceNow?: () => number; TextEncoder?: TextEncoder; + alert?: (message: string) => void; } type TextEncoder = {