Skip to content

Commit

Permalink
Merge 28738bf into e5a9bcc
Browse files Browse the repository at this point in the history
  • Loading branch information
antonis authored Feb 18, 2025
2 parents e5a9bcc + 28738bf commit d7f56e5
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 25 deletions.
44 changes: 28 additions & 16 deletions packages/core/src/js/feedback/FeedbackWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -75,12 +75,12 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
const trimmedDescription = description?.trim();

if ((this.props.isNameRequired && !trimmedName) || (this.props.isEmailRequired && !trimmedEmail) || !trimmedDescription) {
Alert.alert(text.errorTitle, text.formError);
feedbackAlertDialog(text.errorTitle, text.formError);
return;
}

if (this.props.shouldValidateEmail && (this.props.isEmailRequired || trimmedEmail.length > 0) && !isValidEmail(trimmedEmail)) {
Alert.alert(text.errorTitle, text.emailError);
feedbackAlertDialog(text.errorTitle, text.emailError);
return;
}

Expand All @@ -107,13 +107,13 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
}
captureFeedback(userFeedback, attachments ? { attachments } : undefined);
onSubmitSuccess({ name: trimmedName, email: trimmedEmail, message: trimmedDescription, attachments: attachments });
Alert.alert(text.successMessageText);
feedbackAlertDialog(text.successMessageText , '');
onFormSubmitted();
this._didSubmitForm = true;
} catch (error) {
const errorString = `Feedback form submission failed: ${error}`;
onSubmitError(new Error(errorString));
Alert.alert(text.errorTitle, text.genericError);
feedbackAlertDialog(text.errorTitle, text.genericError);
logger.error(`Feedback form submission failed: ${error}`);
}
};
Expand All @@ -124,15 +124,15 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
if (imagePickerConfiguration.imagePicker) {
const launchImageLibrary = imagePickerConfiguration.imagePicker.launchImageLibraryAsync
// expo-image-picker library is available
? () => 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`.',
);
Expand All @@ -142,18 +142,29 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac

const result = await launchImageLibrary();
if (result.assets && result.assets.length > 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
Expand Down Expand Up @@ -215,9 +226,10 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={[styles.container, { padding: 0 }]}
enabled={notWeb()}
>
<ScrollView bounces={false}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<TouchableWithoutFeedback onPress={notWeb() ? Keyboard.dismiss: undefined}>
<View style={styles.container}>
<View style={styles.titleContainer}>
<Text style={styles.title}>{text.formTitle}</Text>
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/js/feedback/FeedbackWidget.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/js/feedback/FeedbackWidgetManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -61,10 +62,10 @@ class FeedbackWidgetProvider extends React.Component<FeedbackWidgetProviderProps
private _panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, _gestureState) => {
// 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) {
Expand Down Expand Up @@ -147,6 +148,7 @@ class FeedbackWidgetProvider extends React.Component<FeedbackWidgetProviderProps
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={modalBackground}
enabled={notWeb()}
>
<Animated.View
style={[modalSheetContainer, { transform: [{ translateY: this.state.panY }] }]}
Expand Down
9 changes: 4 additions & 5 deletions packages/core/src/js/feedback/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Alert } from 'react-native';

import type { FeedbackWidgetProps } from './FeedbackWidget.types';
import { feedbackAlertDialog } from './utils';

const FORM_TITLE = 'Report a Bug';
const NAME_PLACEHOLDER = 'Your Name';
Expand All @@ -27,15 +26,15 @@ export const defaultConfiguration: Partial<FeedbackWidgetProps> = {
},
onFormClose: () => {
if (__DEV__) {
Alert.alert(
feedbackAlertDialog(
'Development note',
'onFormClose callback is not implemented. By default the form is just unmounted.',
);
}
},
onAddScreenshot: (_: (uri: string) => void) => {
if (__DEV__) {
Alert.alert('Development note', 'onAddScreenshot callback is not implemented.');
feedbackAlertDialog('Development note', 'onAddScreenshot callback is not implemented.');
}
},
onSubmitSuccess: () => {
Expand All @@ -46,7 +45,7 @@ export const defaultConfiguration: Partial<FeedbackWidgetProps> = {
},
onFormSubmitted: () => {
if (__DEV__) {
Alert.alert(
feedbackAlertDialog(
'Development note',
'onFormSubmitted callback is not implemented. By default the form is just unmounted.',
);
Expand Down
32 changes: 31 additions & 1 deletion packages/core/src/js/feedback/utils.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
}
};
1 change: 1 addition & 0 deletions packages/core/src/js/utils/worldwide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface ReactNativeInternalGlobal extends InternalGlobal {
__BUNDLE_START_TIME__?: number;
nativePerformanceNow?: () => number;
TextEncoder?: TextEncoder;
alert?: (message: string) => void;
}

type TextEncoder = {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/feedback/FeedbackWidget.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, '');
});
});

Expand Down

0 comments on commit d7f56e5

Please sign in to comment.