From 41cb1cab3fca8306d98fd36c967cc38ced8a8763 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 17 Feb 2025 18:19:49 +0200 Subject: [PATCH 1/4] 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 1136ef7e7..6a96ffe52 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 2c231c059..f3349c9bc 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 b27fb3ea8..80291211b 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 2/4] 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 6a96ffe52..9a505393e 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 80291211b..8b0573198 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 0274651a3..fb5a394fa 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 3/4] 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 9a505393e..79c77a366 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 11a7c3f8c..48439a07f 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 12:08:24 +0200 Subject: [PATCH 4/4] 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 8b0573198..9c2826981 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 c1a4ae5db..03327bac3 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 = {