Skip to content

Commit 4c988a8

Browse files
antoniskrystofwoldrichlucas-zimerman
authored
feat(feedback): Screenshot button (#4714)
* Update the client implementation to use the new capture feedback js api * Updates SDK API * Adds new feedback button in the sample * Adds changelog * Removes unused mock * Update CHANGELOG.md Co-authored-by: Krystof Woldrich <[email protected]> * Directly use captureFeedback from sentry/core * Use import from core * Fixes imports order lint issue * Fixes build issue * Adds captureFeedback tests from sentry-javascript * Update CHANGELOG.md * Only deprecate client captureUserFeedback * Add simple form UI * Adds basic form functionality * Update imports * Update imports * Remove useState hook to avoid multiple react instances issues * Move types and styles in different files * Removes attachment button to be added back separately along with the implementation * Add basic field validation * Adds changelog * Updates changelog * Updates changelog * Trim whitespaces from the submitted feedback * Adds tests * Renames FeedbackFormScreen to FeedbackForm * Add beta label * Extract default text to constants * Moves constant to a separate file and aligns naming with JS * Adds input text labels * Close screen before sending the feedback to minimise wait time Co-authored-by: LucasZF <[email protected]> * Rename file for consistency * Flatten configuration hierarchy and clean up * Align required values with JS * Use Sentry user email and name when set * Simplifies email validation * Show success alert message * Aligns naming with JS and unmounts the form by default * Use the minimum config without props in the changelog * Adds development not for unimplemented function * Show email and name conditionally * Adds sentry branding (png logo) * Adds sentry logo resource * Add assets in module exports * Revert "Add assets in module exports" This reverts commit 5292475. * Revert "Adds sentry logo resource" This reverts commit d6e9229. * Revert "Adds sentry branding (png logo)" This reverts commit 8c56753. * Add last event id * Mock lastEventId * Adds beta note in the changelog * Autoinject feedback form * Updates changelog * Align colors with JS * Update CHANGELOG.md Co-authored-by: Krystof Woldrich <[email protected]> * Update CHANGELOG.md Co-authored-by: Krystof Woldrich <[email protected]> * Update CHANGELOG.md Co-authored-by: Krystof Woldrich <[email protected]> * Use regular fonts for both buttons * Handle keyboard properly * Adds an option on whether the email should be validated * Merge properties only once * Loads current user data on form construction * Remove unneeded extra padding * Fix background color issue * Adds feedback button * Updates the changelog * Fixes changelog typo * Updates styles background color Co-authored-by: Krystof Woldrich <[email protected]> * Use defaultProps * Correct defaultProps * Adds test to verify when getUser is called * Use smaller image Co-authored-by: LucasZF <[email protected]> * Add margin next to the icon * Adds bottom spacing in the ErrorScreen so that the feedback button does not hide the scrollview buttons * (2.2) feat: Add Feedback Form UI Branding logo (#4357) * Adds sentry branding logo as a base64 encoded png --------- Co-authored-by: LucasZF <[email protected]> * Autoinject feedback form (#4370) * Align changelog entry * Update changelog * Disable bouncing * Add modal ui appearance * Update snapshot tests * Fix bottom margin * Fix sheet height * Remove extra modal border * Do not expose modal styles * Animate background color * Avoid keyboard in modal * Update changelog * Fix changelog * Updates comment * Extract FeedbackButtonProps * Add public function description to satisfy lint check * Adds tests * Fix tests * Add hardcoded dark and light color themes * Rename theme options * Update snapshot tests * Include in the feedback integration * Fix circular dependency * Add theme integration options * Adds changelog * Add comment note * Align with JS api * Remove unneeded line Co-authored-by: Krystof Woldrich <[email protected]> * Place widget button below the feedback widget shadow * Expose showFeedbackButton/hideFeedbackButton methods * Add dummy integration for tracking usage * Adds button border * Fixes tests * Add accentBackground and accentForeground colors * Extract integration getter in a helper function * Adds dynamic theming support * Add snapshot tests * Show screenshot button UI * Add screenshot button integration * Add screenshot icon * Adds Take a screenshot button in FeedbackWidget * Updates snapshot tests * Fix circularDepCheck * Fix circularDepCheck * Attache captured screenshot * Hide the take screenshot button when there is a screenshot * Convert uint8Array to Base64 on the native side * Adds snapshot tests * Disable functionality on the Web * Add screenshot button in the sample expo app * Adds system theme tests * Test dynamically changed theme * Remove showScreenshotButton and hideScreenshotButton from the exposed api * Fix function name typo * Adds enableTakeScreenshot option * Adds happy flow test * Make flow tests more granular * Increate wait time out to fix flakiness on ci * Reset widget state after each test * Fix CI flakiness * Remove flaky test * Delay capture to allow the button to hide * Add comment explaining the reasoning of the call * Define defaultProps in smaller scope * Define customStyles in smaller scope * Also check remove screenshot button * Fixes the name and double negation not null --------- Co-authored-by: Krystof Woldrich <[email protected]> Co-authored-by: LucasZF <[email protected]>
1 parent 9a0ab61 commit 4c988a8

22 files changed

+870
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
- Adds the `FeedbackButton` component that shows the Feedback Widget ([#4378](https://github.com/getsentry/sentry-react-native/pull/4378))
1414
- Add Feedback Widget theming ([#4677](https://github.com/getsentry/sentry-react-native/pull/4677))
15+
- Adds the `ScreenshotButton` component that takes a screenshot ([#4714](https://github.com/getsentry/sentry-react-native/issues/4714))
1516

1617
### Fixes
1718

packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.facebook.react.bridge.Arguments;
2121
import com.facebook.react.bridge.Promise;
2222
import com.facebook.react.bridge.ReactApplicationContext;
23+
import com.facebook.react.bridge.ReadableArray;
2324
import com.facebook.react.bridge.ReadableMap;
2425
import com.facebook.react.bridge.ReadableMapKeySetIterator;
2526
import com.facebook.react.bridge.ReadableType;
@@ -1027,6 +1028,15 @@ public void getDataFromUri(String uri, Promise promise) {
10271028
}
10281029
}
10291030

1031+
public void encodeToBase64(ReadableArray array, Promise promise) {
1032+
byte[] bytes = new byte[array.size()];
1033+
for (int i = 0; i < array.size(); i++) {
1034+
bytes[i] = (byte) array.getInt(i);
1035+
}
1036+
String base64String = android.util.Base64.encodeToString(bytes, android.util.Base64.DEFAULT);
1037+
promise.resolve(base64String);
1038+
}
1039+
10301040
public void crashedLastRun(Promise promise) {
10311041
promise.resolve(Sentry.isCrashedLastRun());
10321042
}

packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ public void getDataFromUri(String uri, Promise promise) {
183183
this.impl.getDataFromUri(uri, promise);
184184
}
185185

186+
@Override
187+
public void encodeToBase64(ReadableArray array, Promise promise) {
188+
this.impl.encodeToBase64(array, promise);
189+
}
190+
186191
@Override
187192
public void popTimeToDisplayFor(String key, Promise promise) {
188193
this.impl.popTimeToDisplayFor(key, promise);

packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ public void getDataFromUri(String uri, Promise promise) {
157157
this.impl.getDataFromUri(uri, promise);
158158
}
159159

160+
@ReactMethod
161+
public void encodeToBase64(ReadableArray array, Promise promise) {
162+
this.impl.encodeToBase64(array, promise);
163+
}
164+
160165
@ReactMethod(isBlockingSynchronousMethod = true)
161166
public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) {
162167
// Not used on Android

packages/core/ios/RNSentry.mm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,4 +970,28 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
970970
return @YES; // The return ensures that the method is synchronous
971971
}
972972

973+
RCT_EXPORT_METHOD(encodeToBase64
974+
: (NSArray *)array resolver
975+
: (RCTPromiseResolveBlock)resolve rejecter
976+
: (RCTPromiseRejectBlock)reject)
977+
{
978+
NSUInteger count = array.count;
979+
uint8_t *bytes = (uint8_t *)malloc(count);
980+
981+
if (!bytes) {
982+
reject(@"encodeToBase64", @"Memory allocation failed", nil);
983+
return;
984+
}
985+
986+
for (NSUInteger i = 0; i < count; i++) {
987+
bytes[i] = (uint8_t)[array[i] unsignedCharValue];
988+
}
989+
990+
NSData *data = [NSData dataWithBytes:bytes length:count];
991+
free(bytes);
992+
993+
NSString *base64String = [data base64EncodedStringWithOptions:0];
994+
resolve(base64String);
995+
}
996+
973997
@end

packages/core/src/js/NativeRNSentry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export interface Spec extends TurboModule {
5151
getDataFromUri(uri: string): Promise<number[]>;
5252
popTimeToDisplayFor(key: string): Promise<number | undefined | null>;
5353
setActiveSpanId(spanId: string): boolean;
54+
encodeToBase64(data: number[]): Promise<string | undefined | null>;
5455
}
5556

5657
export type NativeStackFrame = {

packages/core/src/js/feedback/FeedbackWidget.styles.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ const defaultStyles = (theme: FeedbackWidgetTheme): FeedbackWidgetStyles => {
6363
color: theme.foreground,
6464
fontSize: 16,
6565
},
66+
takeScreenshotButton: {
67+
backgroundColor: theme.background,
68+
padding: 15,
69+
borderRadius: 5,
70+
alignItems: 'center',
71+
borderWidth: 1,
72+
borderColor: theme.border,
73+
marginTop: -10,
74+
marginBottom: 20,
75+
},
76+
takeScreenshotText: {
77+
color: theme.foreground,
78+
fontSize: 16,
79+
},
6680
submitButton: {
6781
backgroundColor: theme.accentBackground,
6882
paddingVertical: 15,
@@ -132,6 +146,8 @@ export const defaultButtonStyles = (theme: FeedbackWidgetTheme): FeedbackButtonS
132146
};
133147
};
134148

149+
export const defaultScreenshotButtonStyles = defaultButtonStyles;
150+
135151
export const modalWrapper: ViewStyle = {
136152
position: 'absolute',
137153
top: 0,

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

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable max-lines */
12
import type { SendFeedbackParams } from '@sentry/core';
23
import { captureFeedback, getCurrentScope, lastEventId, logger } from '@sentry/core';
34
import * as React from 'react';
@@ -15,13 +16,15 @@ import {
1516
} from 'react-native';
1617

1718
import { isWeb, notWeb } from '../utils/environment';
18-
import { getDataFromUri } from '../wrapper';
19+
import type { Screenshot } from '../wrapper';
20+
import { getDataFromUri, NATIVE } from '../wrapper';
1921
import { sentryLogo } from './branding';
2022
import { defaultConfiguration } from './defaults';
2123
import defaultStyles from './FeedbackWidget.styles';
2224
import { getTheme } from './FeedbackWidget.theme';
2325
import type { FeedbackGeneralConfiguration, FeedbackTextConfiguration, FeedbackWidgetProps, FeedbackWidgetState, FeedbackWidgetStyles, ImagePickerConfiguration } from './FeedbackWidget.types';
2426
import { lazyLoadFeedbackIntegration } from './lazy';
27+
import { getCapturedScreenshot } from './ScreenshotButton';
2528
import { base64ToUint8Array, feedbackAlertDialog, isValidEmail } from './utils';
2629

2730
/**
@@ -69,6 +72,20 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
6972
lazyLoadFeedbackIntegration();
7073
}
7174

75+
/**
76+
* For testing purposes only.
77+
*/
78+
public static reset(): void {
79+
FeedbackWidget._savedState = {
80+
name: '',
81+
email: '',
82+
description: '',
83+
filename: undefined,
84+
attachment: undefined,
85+
attachmentUri: undefined,
86+
};
87+
}
88+
7289
public handleFeedbackSubmit: () => void = () => {
7390
const { name, email, description } = this.state;
7491
const { onSubmitSuccess, onSubmitError, onFormSubmitted } = this.props;
@@ -123,7 +140,7 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
123140
};
124141

125142
public onScreenshotButtonPress: () => void = async () => {
126-
if (!this.state.filename && !this.state.attachment) {
143+
if (!this._hasScreenshot()) {
127144
const imagePickerConfiguration: ImagePickerConfiguration = this.props;
128145
if (imagePickerConfiguration.imagePicker) {
129146
const launchImageLibrary = imagePickerConfiguration.imagePicker.launchImageLibraryAsync
@@ -238,6 +255,11 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
238255
return null;
239256
}
240257

258+
const screenshot = getCapturedScreenshot();
259+
if (screenshot) {
260+
this._setCapturedScreenshot(screenshot);
261+
}
262+
241263
return (
242264
<TouchableWithoutFeedback onPress={notWeb() ? Keyboard.dismiss: undefined}>
243265
<View style={styles.container}>
@@ -294,7 +316,7 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
294316
onChangeText={(value) => this.setState({ description: value })}
295317
multiline
296318
/>
297-
{(config.enableScreenshot || imagePickerConfiguration.imagePicker) && (
319+
{(config.enableScreenshot || imagePickerConfiguration.imagePicker || this._hasScreenshot()) && (
298320
<View style={styles.screenshotContainer}>
299321
{this.state.attachmentUri && (
300322
<Image
@@ -304,13 +326,24 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
304326
)}
305327
<TouchableOpacity style={styles.screenshotButton} onPress={this.onScreenshotButtonPress}>
306328
<Text style={styles.screenshotText}>
307-
{!this.state.filename && !this.state.attachment
329+
{!this._hasScreenshot()
308330
? text.addScreenshotButtonLabel
309331
: text.removeScreenshotButtonLabel}
310332
</Text>
311333
</TouchableOpacity>
312334
</View>
313335
)}
336+
{notWeb() && config.enableTakeScreenshot && !this.state.attachmentUri && (
337+
<TouchableOpacity style={styles.takeScreenshotButton} onPress={() => {
338+
// eslint-disable-next-line @typescript-eslint/no-var-requires
339+
const { hideFeedbackButton, showScreenshotButton } = require('./FeedbackWidgetManager');
340+
hideFeedbackButton();
341+
onCancel();
342+
showScreenshotButton();
343+
}}>
344+
<Text style={styles.takeScreenshotText}>{text.captureScreenshotButtonLabel}</Text>
345+
</TouchableOpacity>
346+
)}
314347
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
315348
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
316349
</TouchableOpacity>
@@ -323,6 +356,24 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
323356
);
324357
}
325358

359+
private _setCapturedScreenshot = (screenshot: Screenshot): void => {
360+
if (screenshot.data != null) {
361+
logger.debug('Setting captured screenshot:', screenshot.filename);
362+
NATIVE.encodeToBase64(screenshot.data).then((base64String) => {
363+
if (base64String != null) {
364+
const dataUri = `data:${screenshot.contentType};base64,${base64String}`;
365+
this.setState({ filename: screenshot.filename, attachment: screenshot.data, attachmentUri: dataUri });
366+
} else {
367+
logger.error('Failed to read image data from:', screenshot.filename);
368+
}
369+
}).catch((error) => {
370+
logger.error('Failed to read image data from:', screenshot.filename, 'error: ', error);
371+
});
372+
} else {
373+
logger.error('Failed to read image data from:', screenshot.filename);
374+
}
375+
}
376+
326377
private _saveFormState = (): void => {
327378
FeedbackWidget._savedState = { ...this.state };
328379
};
@@ -337,4 +388,8 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
337388
attachmentUri: undefined,
338389
};
339390
};
391+
392+
private _hasScreenshot = (): boolean => {
393+
return this.state.filename !== undefined && this.state.attachment !== undefined && this.state.attachmentUri !== undefined;
394+
}
340395
}

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

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ export interface FeedbackGeneralConfiguration {
5454
*/
5555
enableScreenshot?: boolean;
5656

57+
/**
58+
* This flag determines whether the "Take Screenshot" button is displayed
59+
* @default false
60+
*/
61+
enableTakeScreenshot?: boolean;
62+
5763
/**
5864
* Fill in email/name input fields with Sentry user context if it exists.
5965
* The value of the email/name keys represent the properties of your user context.
@@ -124,15 +130,20 @@ export interface FeedbackTextConfiguration {
124130
isRequiredLabel?: string;
125131

126132
/**
127-
* The label for the button that adds a screenshot and renders the image editor
133+
* The label for the button that adds a screenshot
128134
*/
129135
addScreenshotButtonLabel?: string;
130136

131137
/**
132-
* The label for the button that removes a screenshot and hides the image editor
138+
* The label for the button that removes a screenshot
133139
*/
134140
removeScreenshotButtonLabel?: string;
135141

142+
/**
143+
* The label for the button that shows the capture screenshot button
144+
*/
145+
captureScreenshotButtonLabel?: string;
146+
136147
/**
137148
* The title of the error dialog
138149
*/
@@ -169,6 +180,21 @@ export interface FeedbackButtonTextConfiguration {
169180
triggerAriaLabel?: string;
170181
}
171182

183+
/**
184+
* The ScreenshotButton text labels that can be customized
185+
*/
186+
export interface ScreenshotButtonTextConfiguration {
187+
/**
188+
* The label for the Screenshot button
189+
*/
190+
triggerLabel?: string;
191+
192+
/**
193+
* The aria label for the Screenshot button
194+
*/
195+
triggerAriaLabel?: string;
196+
}
197+
172198
/**
173199
* The public callbacks available for the feedback integration
174200
*/
@@ -258,6 +284,8 @@ export interface FeedbackWidgetStyles {
258284
screenshotContainer?: ViewStyle;
259285
screenshotThumbnail?: ImageStyle;
260286
screenshotText?: TextStyle;
287+
takeScreenshotButton?: ViewStyle;
288+
takeScreenshotText?: TextStyle;
261289
titleContainer?: ViewStyle;
262290
sentryLogo?: ImageStyle;
263291
}
@@ -278,6 +306,22 @@ export interface FeedbackButtonStyles {
278306
triggerIcon?: ImageStyle;
279307
}
280308

309+
/**
310+
* The props for the screenshot button
311+
*/
312+
export interface ScreenshotButtonProps extends ScreenshotButtonTextConfiguration {
313+
styles?: ScreenshotButtonStyles;
314+
}
315+
316+
/**
317+
* The styles for the screenshot button
318+
*/
319+
export interface ScreenshotButtonStyles {
320+
triggerButton?: ViewStyle;
321+
triggerText?: TextStyle;
322+
triggerIcon?: ImageStyle;
323+
}
324+
281325
/**
282326
* The state of the feedback form
283327
*/

0 commit comments

Comments
 (0)