diff --git a/packages/core/src/js/feedback/FeedbackWidget.tsx b/packages/core/src/js/feedback/FeedbackWidget.tsx index ed49d7b139..bb6931e759 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, @@ -17,12 +16,13 @@ import { View } from 'react-native'; +import { isWeb, notWeb } 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, feedbackAlertDialog, isValidEmail } from './utils'; /** * @beta @@ -75,12 +75,12 @@ export class FeedbackWidget extends React.Component 0) && !isValidEmail(trimmedEmail)) { - Alert.alert(text.errorTitle, text.emailError); + feedbackAlertDialog(text.errorTitle, text.emailError); return; } @@ -107,13 +107,13 @@ 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.'); if (__DEV__) { - Alert.alert( + feedbackAlertDialog( 'Development note', 'No compatible image picker library found. Please provide a compatible version of `expo-image-picker` or `react-native-image-picker`.', ); @@ -142,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 @@ -215,9 +226,10 @@ export class FeedbackWidget extends React.Component - + {text.formTitle} diff --git a/packages/core/src/js/feedback/FeedbackWidget.types.ts b/packages/core/src/js/feedback/FeedbackWidget.types.ts index e821f886b8..af08c2ffc3 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/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 = { }, 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 b27fb3ea8d..9c2826981d 100644 --- a/packages/core/src/js/feedback/utils.ts +++ b/packages/core/src/js/feedback/utils.ts @@ -1,6 +1,14 @@ -import { isFabricEnabled } from '../utils/environment'; +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 { + // 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 +22,25 @@ 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))); +}; + +export const feedbackAlertDialog = (title: string, message: string): void => { + 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 = { 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, ''); }); });