Skip to content

Commit 7f1934e

Browse files
authored
Merge 1d8a0db into 7ec9441
2 parents 7ec9441 + 1d8a0db commit 7f1934e

File tree

9 files changed

+143
-43
lines changed

9 files changed

+143
-43
lines changed

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

Lines changed: 35 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 { base64ToUint8Array, isValidEmail } from './utils';
2525

2626
/**
2727
* @beta
@@ -100,12 +100,38 @@ 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 && imagePickerConfiguration.imagePicker.launchImageLibraryAsync) {
107+
// expo-image-picker library is available
108+
const result = await imagePickerConfiguration.imagePicker.launchImageLibraryAsync({
109+
mediaTypes: ['images'], base64: true
110+
});
111+
if (!result.canceled) {
112+
const filename = result.assets[0].fileName;
113+
const attachement = base64ToUint8Array(result.assets[0].base64);
114+
this.setState({ filename, attachment: attachement });
115+
}
116+
} else if (imagePickerConfiguration.imagePicker && imagePickerConfiguration.imagePicker.launchImageLibrary) {
117+
// react-native-image-picker library is available
118+
const result = await imagePickerConfiguration.imagePicker.launchImageLibrary({
119+
mediaType: 'photo', includeBase64: true
120+
});
121+
if (!result.didCancel && !result.errorCode) {
122+
const filename = result.assets[0].fileName;
123+
const attachement = base64ToUint8Array(result.assets[0].base64);
124+
this.setState({ filename, attachment: attachement });
125+
}
126+
} else {
127+
logger.warn('No image picker library found. Please provide an image picker library to use this feature.');
128+
129+
// Defaulting to the onAddScreenshot callback
130+
const { onAddScreenshot } = { ...defaultConfiguration, ...this.props };
131+
onAddScreenshot((filename: string, attachement: Uint8Array) => {
132+
this.setState({ filename, attachment: attachement });
133+
});
134+
}
109135
} else {
110136
this.setState({ filename: undefined, attachment: undefined });
111137
}
@@ -118,6 +144,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
118144
const { name, email, description } = this.state;
119145
const { onFormClose } = this.props;
120146
const config: FeedbackGeneralConfiguration = this.props;
147+
const imagePickerConfiguration: ImagePickerConfiguration = this.props;
121148
const text: FeedbackTextConfiguration = this.props;
122149
const styles: FeedbackFormStyles = { ...defaultStyles, ...this.props.styles };
123150
const onCancel = (): void => {
@@ -191,7 +218,7 @@ export class FeedbackForm extends React.Component<FeedbackFormProps, FeedbackFor
191218
onChangeText={(value) => this.setState({ description: value })}
192219
multiline
193220
/>
194-
{config.enableScreenshot && (
221+
{(config.enableScreenshot || imagePickerConfiguration.imagePicker) && (
195222
<TouchableOpacity style={styles.screenshotButton} onPress={this.onScreenshotButtonPress}>
196223
<Text style={styles.screenshotText}>
197224
{!this.state.filename && !this.state.attachment

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

Lines changed: 44 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,45 @@ 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 ExpoImagePickerResponse {
202+
canceled?: boolean;
203+
assets?: ImagePickerAsset[];
204+
}
205+
206+
interface ReactNativeImagePickerResponse {
207+
didCancel?: boolean;
208+
errorCode?: string;
209+
assets?: ImagePickerAsset[];
210+
}
211+
212+
interface ImagePickerAsset {
213+
fileName?: string;
214+
base64?: string;
215+
}
216+
217+
interface ExpoImageLibraryOptions {
218+
mediaTypes?: any[];
219+
base64?: boolean;
220+
}
221+
222+
interface ReactNativeImageLibraryOptions {
223+
mediaType: any;
224+
includeBase64?: boolean;
225+
}
226+
227+
interface ImagePicker {
228+
launchImageLibraryAsync?: (options?: ExpoImageLibraryOptions) => Promise<ExpoImagePickerResponse>;
229+
230+
launchImageLibrary?: (options: ReactNativeImageLibraryOptions) => Promise<ReactNativeImagePickerResponse>;
231+
}
232+
190233
/**
191234
* The styles for the feedback form
192235
*/

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,33 @@ 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+
/* eslint-disable no-bitwise */
19+
export const base64ToUint8Array = (base64?: string): Uint8Array | undefined => {
20+
if (!base64) return undefined;
21+
22+
const cleanedBase64 = base64.replace(/^data:.*;base64,/, ''); // Remove any prefix before the base64 string
23+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
24+
const bytes: number[] = [];
25+
26+
let buffer = 0;
27+
let bits = 0;
28+
29+
for (const char of cleanedBase64) {
30+
if (char === '=') break;
31+
32+
const value = chars.indexOf(char); // Validate each character
33+
if (value === -1) return undefined;
34+
35+
buffer = (buffer << 6) | value; // Shift 6 bits to the left and add the value
36+
bits += 6;
37+
38+
if (bits >= 8) {
39+
// Add a byte when we have 8 or more bits
40+
bits -= 8;
41+
bytes.push((buffer >> bits) & 0xff);
42+
}
43+
}
44+
45+
return new Uint8Array(bytes);
46+
};

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.5
14700+
resolution: "expo-image-picker@npm:16.0.5"
14701+
dependencies:
14702+
expo-image-loader: ~5.0.0
14703+
peerDependencies:
14704+
expo: "*"
14705+
checksum: 8391455480d07a2fa785a0246b8eb892d393e64173f4663d77ee26bc497a0f52d9e1690b0fa04094d03ada8cc78aa5477505d870bdcbdb05d5c48c791ddac168
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)