Skip to content

Commit 741efe5

Browse files
authored
Merge 91e96de into 22cde46
2 parents 22cde46 + 91e96de commit 741efe5

File tree

11 files changed

+177
-45
lines changed

11 files changed

+177
-45
lines changed

packages/core/src/js/feedback/FeedbackForm.tsx

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import {
2020
import { sentryLogo } from './branding';
2121
import { defaultConfiguration } from './defaults';
2222
import defaultStyles from './FeedbackForm.styles';
23-
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles,FeedbackGeneralConfiguration, FeedbackTextConfiguration } from './FeedbackForm.types';
24-
import { isValidEmail } from './utils';
23+
import type { FeedbackFormProps, FeedbackFormState, FeedbackFormStyles, FeedbackGeneralConfiguration, FeedbackTextConfiguration, ImagePickerConfiguration } from './FeedbackForm.types';
24+
import { getDataFromUri, isValidEmail } from './utils';
2525

2626
/**
2727
* @beta
@@ -100,12 +100,46 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
100100
}
101101
};
102102

103-
public onScreenshotButtonPress: () => void = () => {
103+
public onScreenshotButtonPress: () => void = async () => {
104104
if (!this.state.filename && !this.state.attachment) {
105-
const { onAddScreenshot } = { ...defaultConfiguration, ...this.props };
106-
onAddScreenshot((filename: string, attachement: Uint8Array) => {
107-
this.setState({ filename, attachment: attachement });
108-
});
105+
const imagePickerConfiguration: ImagePickerConfiguration = this.props;
106+
if (imagePickerConfiguration.imagePicker) {
107+
const launchImageLibrary = imagePickerConfiguration.imagePicker.launchImageLibraryAsync
108+
// expo-image-picker library is available
109+
? () => imagePickerConfiguration.imagePicker.launchImageLibraryAsync({ mediaTypes: ["images"] })
110+
// react-native-image-picker library is available
111+
: imagePickerConfiguration.imagePicker.launchImageLibrary
112+
? () => imagePickerConfiguration.imagePicker.launchImageLibrary({ mediaType: "photo" })
113+
: null;
114+
if (!launchImageLibrary) {
115+
logger.warn('No compatible image picker library found. Please provide a valid image picker library.');
116+
if (__DEV__) {
117+
Alert.alert(
118+
'Development note',
119+
'No compatible image picker library found. Please provide a compatible version of `expo-image-picker` or `react-native-image-picker`.',
120+
);
121+
}
122+
return;
123+
}
124+
125+
const result = await launchImageLibrary();
126+
if (result.assets && result.assets.length > 0) {
127+
const filename = result.assets[0].fileName;
128+
const imageUri = result.assets[0].uri;
129+
getDataFromUri(imageUri).then((data) => {
130+
this.setState({ filename, attachment: data });
131+
})
132+
.catch((error) => {
133+
logger.error("Error:", error);
134+
});
135+
}
136+
} else {
137+
// Defaulting to the onAddScreenshot callback
138+
const { onAddScreenshot } = { ...defaultConfiguration, ...this.props };
139+
onAddScreenshot((filename: string, attachement: Uint8Array) => {
140+
this.setState({ filename, attachment: attachement });
141+
});
142+
}
109143
} else {
110144
this.setState({ filename: undefined, attachment: undefined });
111145
}
@@ -118,6 +152,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
118152
const { name, email, description } = this.state;
119153
const { onFormClose } = this.props;
120154
const config: FeedbackGeneralConfiguration = this.props;
155+
const imagePickerConfiguration: ImagePickerConfiguration = this.props;
121156
const text: FeedbackTextConfiguration = this.props;
122157
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
123158
const onCancel = (): void => {
@@ -191,7 +226,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
191226
onChangeText={(value) => this.setState({ description: value })}
192227
multiline
193228
/>
194-
{config.enableScreenshot && (
229+
{(config.enableScreenshot || imagePickerConfiguration.imagePicker) && (
195230
<TouchableOpacity style={styles.screenshotButton} onPress={this.onScreenshotButtonPress}>
196231
<Text style={styles.screenshotText}>
197232
{!this.state.filename && !this.state.attachment

packages/core/src/js/feedback/FeedbackForm.types.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';
44
/**
55
* The props for the feedback form
66
*/
7-
export interface FeedbackFormProps extends FeedbackGeneralConfiguration, FeedbackTextConfiguration, FeedbackCallbacks {
7+
export interface FeedbackFormProps
8+
extends FeedbackGeneralConfiguration,
9+
FeedbackTextConfiguration,
10+
FeedbackCallbacks,
11+
ImagePickerConfiguration {
812
styles?: FeedbackFormStyles;
913
}
1014

@@ -187,6 +191,36 @@ export interface FeedbackCallbacks {
187191
onFormSubmitted?: () => void;
188192
}
189193

194+
/**
195+
* Image Picker configuration interfact matching `expo-image-picker` and `react-native-image-picker`
196+
*/
197+
export interface ImagePickerConfiguration {
198+
imagePicker?: ImagePicker;
199+
}
200+
201+
interface ImagePickerResponse {
202+
assets?: ImagePickerAsset[];
203+
}
204+
205+
interface ImagePickerAsset {
206+
fileName?: string;
207+
uri?: string;
208+
}
209+
210+
interface ExpoImageLibraryOptions {
211+
mediaTypes?: any[];
212+
}
213+
214+
interface ReactNativeImageLibraryOptions {
215+
mediaType: any;
216+
}
217+
218+
export interface ImagePicker {
219+
launchImageLibraryAsync?: (options?: ExpoImageLibraryOptions) => Promise<ImagePickerResponse>;
220+
221+
launchImageLibrary?: (options: ReactNativeImageLibraryOptions) => Promise<ImagePickerResponse>;
222+
}
223+
190224
/**
191225
* The styles for the feedback form
192226
*/

packages/core/src/js/feedback/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,26 @@ export const isValidEmail = (email: string): boolean => {
1414
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
1515
return emailRegex.test(email);
1616
};
17+
18+
/**
19+
* Reads a file from a URI and returns a UInt8Array of its data.
20+
* @param uri The file URI.
21+
* @returns A Promise resolving to a UInt8Array.
22+
*/
23+
export async function getDataFromUri(uri: string): Promise<Uint8Array> {
24+
const response = await fetch(uri);
25+
const blob = await response.blob();
26+
27+
return new Promise((resolve, reject) => {
28+
const reader = new FileReader();
29+
reader.onloadend = () => {
30+
if (reader.result instanceof ArrayBuffer) {
31+
resolve(new Uint8Array(reader.result));
32+
} else {
33+
reject(new Error('Failed to read file as UInt8Array'));
34+
}
35+
};
36+
reader.onerror = reject;
37+
reader.readAsArrayBuffer(blob);
38+
});
39+
}

packages/core/test/feedback/FeedbackForm.test.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as React from 'react';
44
import { Alert } from 'react-native';
55

66
import { FeedbackForm } from '../../src/js/feedback/FeedbackForm';
7-
import type { FeedbackFormProps, FeedbackFormStyles } from '../../src/js/feedback/FeedbackForm.types';
7+
import type { FeedbackFormProps, FeedbackFormStyles, ImagePicker } from '../../src/js/feedback/FeedbackForm.types';
88

99
const mockOnFormClose = jest.fn();
1010
const mockOnAddScreenshot = jest.fn();
@@ -303,7 +303,7 @@ describe('FeedbackForm', () => {
303303
});
304304
});
305305

306-
it('calls onAddScreenshot when the screenshot button is pressed', async () => {
306+
it('calls onAddScreenshot when the screenshot button is pressed and no image picker library is integrated', async () => {
307307
const { getByText } = render(<FeedbackForm {...defaultProps} enableScreenshot={true} />);
308308

309309
fireEvent.press(getByText(defaultProps.addScreenshotButtonLabel));
@@ -313,6 +313,40 @@ describe('FeedbackForm', () => {
313313
});
314314
});
315315

316+
it('calls launchImageLibraryAsync when the expo-image-picker library is integrated', async () => {
317+
const mockLaunchImageLibrary = jest.fn().mockResolvedValue({
318+
assets: [{ fileName: "mock-image.jpg", uri: "file:///mock/path/image.jpg" }],
319+
});
320+
const mockImagePicker: jest.Mocked<ImagePicker> = {
321+
launchImageLibraryAsync: mockLaunchImageLibrary,
322+
};
323+
324+
const { getByText } = render(<FeedbackForm {...defaultProps} imagePicker={ mockImagePicker } />);
325+
326+
fireEvent.press(getByText(defaultProps.addScreenshotButtonLabel));
327+
328+
await waitFor(() => {
329+
expect(mockLaunchImageLibrary).toHaveBeenCalled();
330+
});
331+
});
332+
333+
it('calls launchImageLibrary when the react-native-image-picker library is integrated', async () => {
334+
const mockLaunchImageLibrary = jest.fn().mockResolvedValue({
335+
assets: [{ fileName: "mock-image.jpg", uri: "file:///mock/path/image.jpg" }],
336+
});
337+
const mockImagePicker: jest.Mocked<ImagePicker> = {
338+
launchImageLibrary: mockLaunchImageLibrary,
339+
};
340+
341+
const { getByText } = render(<FeedbackForm {...defaultProps} imagePicker={ mockImagePicker } />);
342+
343+
fireEvent.press(getByText(defaultProps.addScreenshotButtonLabel));
344+
345+
await waitFor(() => {
346+
expect(mockLaunchImageLibrary).toHaveBeenCalled();
347+
});
348+
});
349+
316350
it('calls onFormClose when the cancel button is pressed', () => {
317351
const { getByText } = render(<FeedbackForm {...defaultProps} />);
318352

samples/expo/app.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
"organization": "sentry-sdks"
4848
}
4949
],
50+
[
51+
"expo-image-picker",
52+
{
53+
"photosPermission": "The app accesses your photos to let you share them with your friends."
54+
}
55+
],
5056
[
5157
"expo-router",
5258
{

samples/expo/app/(tabs)/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ export default function TabOneScreen() {
5757
Sentry.nativeCrash();
5858
}}
5959
/>
60+
<Button
61+
title="Show feedback form"
62+
onPress={() => {
63+
Sentry.showFeedbackForm();
64+
}}
65+
/>
6066
<Button
6167
title="Set Scope Properties"
6268
onPress={() => {

samples/expo/app/_layout.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ErrorEvent } from '@sentry/core';
1111
import { isExpoGo } from '../utils/isExpoGo';
1212
import { LogBox } from 'react-native';
1313
import { isWeb } from '../utils/isWeb';
14+
import * as ImagePicker from 'expo-image-picker';
1415

1516
export {
1617
// Catch any errors thrown by the Layout component.
@@ -57,6 +58,9 @@ Sentry.init({
5758
}),
5859
navigationIntegration,
5960
Sentry.reactNativeTracingIntegration(),
61+
Sentry.feedbackIntegration({
62+
imagePicker: ImagePicker,
63+
}),
6064
);
6165
if (isWeb()) {
6266
integrations.push(Sentry.browserReplayIntegration());

samples/expo/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@types/react": "~18.3.12",
2121
"expo": "^52.0.0",
2222
"expo-constants": "~17.0.3",
23+
"expo-image-picker": "~16.0.5",
2324
"expo-linking": "~7.0.2",
2425
"expo-router": "~4.0.5",
2526
"expo-status-bar": "~2.0.0",

samples/react-native/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
"react-native": "0.77.0",
3232
"react-native-gesture-handler": "^2.22.1",
3333
"react-native-image-picker": "^7.2.2",
34-
"react-native-quick-base64": "^2.1.2",
3534
"react-native-reanimated": "3.16.7",
3635
"react-native-safe-area-context": "5.2.0",
3736
"react-native-screens": "4.6.0",

samples/react-native/src/App.tsx

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ import { ErrorEvent } from '@sentry/core';
3737
import HeavyNavigationScreen from './Screens/HeavyNavigationScreen';
3838
import WebviewScreen from './Screens/WebviewScreen';
3939
import { isTurboModuleEnabled } from '@sentry/react-native/dist/js/utils/environment';
40-
import { toByteArray } from 'react-native-quick-base64';
41-
import { launchImageLibrary } from 'react-native-image-picker';
40+
import * as ImagePicker from 'react-native-image-picker';
4241

4342
if (typeof setImmediate === 'undefined') {
4443
require('setimmediate');
@@ -107,6 +106,7 @@ Sentry.init({
107106
: true,
108107
}),
109108
Sentry.feedbackIntegration({
109+
imagePicker: ImagePicker,
110110
styles:{
111111
submitButton: {
112112
backgroundColor: '#6a1b9a',
@@ -153,23 +153,6 @@ const Stack = isMobileOs
153153
: createStackNavigator();
154154
const Tab = createBottomTabNavigator();
155155

156-
const handleChooseImage = (attachFile: (filename: string, data: Uint8Array) => void): void => {
157-
launchImageLibrary({ mediaType: 'photo', includeBase64: true }, (response) => {
158-
if (response.didCancel) {
159-
console.log('User cancelled image picker');
160-
} else if (response.errorCode) {
161-
console.log('ImagePicker Error: ', response.errorMessage);
162-
} else if (response.assets && response.assets.length > 0) {
163-
const filename = response.assets[0].fileName;
164-
const base64String = response.assets[0].base64;
165-
const screenShotUint8Array = toByteArray(base64String);
166-
if (filename && screenShotUint8Array) {
167-
attachFile(filename, screenShotUint8Array);
168-
}
169-
}
170-
});
171-
};
172-
173156
const ErrorsTabNavigator = Sentry.withProfiler(
174157
() => {
175158
return (
@@ -189,7 +172,6 @@ const ErrorsTabNavigator = Sentry.withProfiler(
189172
<FeedbackForm
190173
{...props}
191174
enableScreenshot={true}
192-
onAddScreenshot={handleChooseImage}
193175
onFormClose={props.navigation.goBack}
194176
onFormSubmitted={props.navigation.goBack}
195177
styles={{

yarn.lock

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14686,6 +14686,26 @@ __metadata:
1468614686
languageName: node
1468714687
linkType: hard
1468814688

14689+
"expo-image-loader@npm:~5.0.0":
14690+
version: 5.0.0
14691+
resolution: "expo-image-loader@npm:5.0.0"
14692+
peerDependencies:
14693+
expo: "*"
14694+
checksum: 7741b4b926124a1f85e51b0b8c8c9adc37fccf0654eaa0e715cb55cffc716bdc149c0421120f9bec69a69643d3328ec8562899b4aa37a0fdcecfb6fe3cf6f985
14695+
languageName: node
14696+
linkType: hard
14697+
14698+
"expo-image-picker@npm:~16.0.5":
14699+
version: 16.0.6
14700+
resolution: "expo-image-picker@npm:16.0.6"
14701+
dependencies:
14702+
expo-image-loader: ~5.0.0
14703+
peerDependencies:
14704+
expo: "*"
14705+
checksum: 448097075ad08c99a9c2857547e36f07e5e491fe5264fe03e77418271f077478a89f1ad462febedb9070013bf90f826b75ad4b2855d98a93593f76ae20c121de
14706+
languageName: node
14707+
linkType: hard
14708+
1468914709
"expo-keep-awake@npm:~14.0.1":
1469014710
version: 14.0.1
1469114711
resolution: "expo-keep-awake@npm:14.0.1"
@@ -22988,18 +23008,6 @@ __metadata:
2298823008
languageName: node
2298923009
linkType: hard
2299023010

22991-
"react-native-quick-base64@npm:^2.1.2":
22992-
version: 2.1.2
22993-
resolution: "react-native-quick-base64@npm:2.1.2"
22994-
dependencies:
22995-
base64-js: ^1.5.1
22996-
peerDependencies:
22997-
react: "*"
22998-
react-native: "*"
22999-
checksum: 46f3b26f48b26978686b0c043336220d681e6a02af5abcf3eb4ab7b9216251d1eb2fac5c559e984d963e93f54bd9f323651daac09762196815558abbd551729b
23000-
languageName: node
23001-
linkType: hard
23002-
2300323011
"react-native-reanimated@npm:3.16.7":
2300423012
version: 3.16.7
2300523013
resolution: "react-native-reanimated@npm:3.16.7"
@@ -24599,6 +24607,7 @@ __metadata:
2459924607
"@types/react": ~18.3.12
2460024608
expo: ^52.0.0
2460124609
expo-constants: ~17.0.3
24610+
expo-image-picker: ~16.0.5
2460224611
expo-linking: ~7.0.2
2460324612
expo-router: ~4.0.5
2460424613
expo-status-bar: ~2.0.0
@@ -24695,7 +24704,6 @@ __metadata:
2469524704
react-native: 0.77.0
2469624705
react-native-gesture-handler: ^2.22.1
2469724706
react-native-image-picker: ^7.2.2
24698-
react-native-quick-base64: ^2.1.2
2469924707
react-native-reanimated: 3.16.7
2470024708
react-native-safe-area-context: 5.2.0
2470124709
react-native-screens: 4.6.0

0 commit comments

Comments
 (0)