Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1bb4e34
feat(feedback): Report a Bug button (#4378)
antonis Apr 7, 2025
cd6bf1f
Merge branch 'main' into feedback-ui-2
antonis Apr 7, 2025
9a0ab61
feat(feedback): Theming (#4677)
antonis Apr 14, 2025
4c988a8
feat(feedback): Screenshot button (#4714)
antonis Apr 14, 2025
7f8e673
ref(feedback): Extracts FeedbackWidgetProvider in a separate file (#4…
antonis Apr 14, 2025
ad7d3e3
fix(feedback): Fixes accessibility issue on iOS (#4739)
antonis Apr 14, 2025
e7ce2ce
Merge branch 'main' into feedback-ui-2
antonis Apr 14, 2025
3cae215
Merge branch 'main' into feedback-ui-2
antonis Apr 16, 2025
4610d7f
Merge branch 'main' into feedback-ui-2
antonis Apr 16, 2025
bd5bd30
feat(feedback): Screenshot button error flow (#4757)
antonis Apr 17, 2025
b07bf20
Merge branch 'main' into feedback-ui-2
antonis Apr 24, 2025
98f9b1d
Merge branch 'main' into feedback-ui-2
antonis Apr 25, 2025
68bafa1
Increase iOS binary size diff by 100KB (#4784)
antonis Apr 28, 2025
69fc805
Merge branch 'main' into feedback-ui-2
antonis Apr 29, 2025
14dec8c
Merge branch 'main' into feedback-ui-2
antonis May 1, 2025
6cf7905
Fix changelog
antonis May 1, 2025
4492611
Update changelog
antonis May 1, 2025
7d5aba8
Merge branch 'main' into feedback-ui-2
antonis May 7, 2025
f7675d9
Merge branch 'main' into feedback-ui-2
antonis May 8, 2025
92c4d2d
test(e2e): Adds Feedback Widget Maestro E2E tests (#4604)
antonis May 9, 2025
1728356
Merge branch 'main' into feedback-ui-2
antonis May 9, 2025
83c070c
Merge branch 'main' into feedback-ui-2
lucas-zimerman May 13, 2025
469134d
Merge branch 'main' into feedback-ui-2
antonis May 15, 2025
e7a016f
Merge branch 'main' into feedback-ui-2
antonis May 15, 2025
776747f
Merge branch 'main' into feedback-ui-2
antonis May 20, 2025
5c627e6
Merge branch 'main' into feedback-ui-2
antonis May 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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]>
  • Loading branch information
3 people authored Apr 14, 2025
commit 4c988a88fc78eb1250d988cd940c0240b4adb7fa
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

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

### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReadableType;
Expand Down Expand Up @@ -1027,6 +1028,15 @@ public void getDataFromUri(String uri, Promise promise) {
}
}

public void encodeToBase64(ReadableArray array, Promise promise) {
byte[] bytes = new byte[array.size()];
for (int i = 0; i < array.size(); i++) {
bytes[i] = (byte) array.getInt(i);
}
String base64String = android.util.Base64.encodeToString(bytes, android.util.Base64.DEFAULT);
promise.resolve(base64String);
}

public void crashedLastRun(Promise promise) {
promise.resolve(Sentry.isCrashedLastRun());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ public void getDataFromUri(String uri, Promise promise) {
this.impl.getDataFromUri(uri, promise);
}

@Override
public void encodeToBase64(ReadableArray array, Promise promise) {
this.impl.encodeToBase64(array, promise);
}

@Override
public void popTimeToDisplayFor(String key, Promise promise) {
this.impl.popTimeToDisplayFor(key, promise);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ public void getDataFromUri(String uri, Promise promise) {
this.impl.getDataFromUri(uri, promise);
}

@ReactMethod
public void encodeToBase64(ReadableArray array, Promise promise) {
this.impl.encodeToBase64(array, promise);
}

@ReactMethod(isBlockingSynchronousMethod = true)
public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) {
// Not used on Android
Expand Down
24 changes: 24 additions & 0 deletions packages/core/ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -970,4 +970,28 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
return @YES; // The return ensures that the method is synchronous
}

RCT_EXPORT_METHOD(encodeToBase64
: (NSArray *)array resolver
: (RCTPromiseResolveBlock)resolve rejecter
: (RCTPromiseRejectBlock)reject)
{
NSUInteger count = array.count;
uint8_t *bytes = (uint8_t *)malloc(count);

if (!bytes) {
reject(@"encodeToBase64", @"Memory allocation failed", nil);
return;
}

for (NSUInteger i = 0; i < count; i++) {
bytes[i] = (uint8_t)[array[i] unsignedCharValue];
}

NSData *data = [NSData dataWithBytes:bytes length:count];
free(bytes);

NSString *base64String = [data base64EncodedStringWithOptions:0];
resolve(base64String);
}

@end
1 change: 1 addition & 0 deletions packages/core/src/js/NativeRNSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface Spec extends TurboModule {
getDataFromUri(uri: string): Promise<number[]>;
popTimeToDisplayFor(key: string): Promise<number | undefined | null>;
setActiveSpanId(spanId: string): boolean;
encodeToBase64(data: number[]): Promise<string | undefined | null>;
}

export type NativeStackFrame = {
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/js/feedback/FeedbackWidget.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ const defaultStyles = (theme: FeedbackWidgetTheme): FeedbackWidgetStyles => {
color: theme.foreground,
fontSize: 16,
},
takeScreenshotButton: {
backgroundColor: theme.background,
padding: 15,
borderRadius: 5,
alignItems: 'center',
borderWidth: 1,
borderColor: theme.border,
marginTop: -10,
marginBottom: 20,
},
takeScreenshotText: {
color: theme.foreground,
fontSize: 16,
},
submitButton: {
backgroundColor: theme.accentBackground,
paddingVertical: 15,
Expand Down Expand Up @@ -132,6 +146,8 @@ export const defaultButtonStyles = (theme: FeedbackWidgetTheme): FeedbackButtonS
};
};

export const defaultScreenshotButtonStyles = defaultButtonStyles;

export const modalWrapper: ViewStyle = {
position: 'absolute',
top: 0,
Expand Down
63 changes: 59 additions & 4 deletions packages/core/src/js/feedback/FeedbackWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import type { SendFeedbackParams } from '@sentry/core';
import { captureFeedback, getCurrentScope, lastEventId, logger } from '@sentry/core';
import * as React from 'react';
Expand All @@ -15,13 +16,15 @@ import {
} from 'react-native';

import { isWeb, notWeb } from '../utils/environment';
import { getDataFromUri } from '../wrapper';
import type { Screenshot } from '../wrapper';
import { getDataFromUri, NATIVE } from '../wrapper';
import { sentryLogo } from './branding';
import { defaultConfiguration } from './defaults';
import defaultStyles from './FeedbackWidget.styles';
import { getTheme } from './FeedbackWidget.theme';
import type { FeedbackGeneralConfiguration, FeedbackTextConfiguration, FeedbackWidgetProps, FeedbackWidgetState, FeedbackWidgetStyles, ImagePickerConfiguration } from './FeedbackWidget.types';
import { lazyLoadFeedbackIntegration } from './lazy';
import { getCapturedScreenshot } from './ScreenshotButton';
import { base64ToUint8Array, feedbackAlertDialog, isValidEmail } from './utils';

/**
Expand Down Expand Up @@ -69,6 +72,20 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
lazyLoadFeedbackIntegration();
}

/**
* For testing purposes only.
*/
public static reset(): void {
FeedbackWidget._savedState = {
name: '',
email: '',
description: '',
filename: undefined,
attachment: undefined,
attachmentUri: undefined,
};
}

public handleFeedbackSubmit: () => void = () => {
const { name, email, description } = this.state;
const { onSubmitSuccess, onSubmitError, onFormSubmitted } = this.props;
Expand Down Expand Up @@ -123,7 +140,7 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
};

public onScreenshotButtonPress: () => void = async () => {
if (!this.state.filename && !this.state.attachment) {
if (!this._hasScreenshot()) {
const imagePickerConfiguration: ImagePickerConfiguration = this.props;
if (imagePickerConfiguration.imagePicker) {
const launchImageLibrary = imagePickerConfiguration.imagePicker.launchImageLibraryAsync
Expand Down Expand Up @@ -238,6 +255,11 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
return null;
}

const screenshot = getCapturedScreenshot();
if (screenshot) {
this._setCapturedScreenshot(screenshot);
}

return (
<TouchableWithoutFeedback onPress={notWeb() ? Keyboard.dismiss: undefined}>
<View style={styles.container}>
Expand Down Expand Up @@ -294,7 +316,7 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
onChangeText={(value) => this.setState({ description: value })}
multiline
/>
{(config.enableScreenshot || imagePickerConfiguration.imagePicker) && (
{(config.enableScreenshot || imagePickerConfiguration.imagePicker || this._hasScreenshot()) && (
<View style={styles.screenshotContainer}>
{this.state.attachmentUri && (
<Image
Expand All @@ -304,13 +326,24 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
)}
<TouchableOpacity style={styles.screenshotButton} onPress={this.onScreenshotButtonPress}>
<Text style={styles.screenshotText}>
{!this.state.filename && !this.state.attachment
{!this._hasScreenshot()
? text.addScreenshotButtonLabel
: text.removeScreenshotButtonLabel}
</Text>
</TouchableOpacity>
</View>
)}
{notWeb() && config.enableTakeScreenshot && !this.state.attachmentUri && (
<TouchableOpacity style={styles.takeScreenshotButton} onPress={() => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { hideFeedbackButton, showScreenshotButton } = require('./FeedbackWidgetManager');
hideFeedbackButton();
onCancel();
showScreenshotButton();
}}>
<Text style={styles.takeScreenshotText}>{text.captureScreenshotButtonLabel}</Text>
</TouchableOpacity>
)}
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
</TouchableOpacity>
Expand All @@ -323,6 +356,24 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
);
}

private _setCapturedScreenshot = (screenshot: Screenshot): void => {
if (screenshot.data != null) {
logger.debug('Setting captured screenshot:', screenshot.filename);
NATIVE.encodeToBase64(screenshot.data).then((base64String) => {
if (base64String != null) {
const dataUri = `data:${screenshot.contentType};base64,${base64String}`;
this.setState({ filename: screenshot.filename, attachment: screenshot.data, attachmentUri: dataUri });
} else {
logger.error('Failed to read image data from:', screenshot.filename);
}
}).catch((error) => {
logger.error('Failed to read image data from:', screenshot.filename, 'error: ', error);
});
} else {
logger.error('Failed to read image data from:', screenshot.filename);
}
}

private _saveFormState = (): void => {
FeedbackWidget._savedState = { ...this.state };
};
Expand All @@ -337,4 +388,8 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
attachmentUri: undefined,
};
};

private _hasScreenshot = (): boolean => {
return this.state.filename !== undefined && this.state.attachment !== undefined && this.state.attachmentUri !== undefined;
}
}
48 changes: 46 additions & 2 deletions packages/core/src/js/feedback/FeedbackWidget.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ export interface FeedbackGeneralConfiguration {
*/
enableScreenshot?: boolean;

/**
* This flag determines whether the "Take Screenshot" button is displayed
* @default false
*/
enableTakeScreenshot?: boolean;

/**
* Fill in email/name input fields with Sentry user context if it exists.
* The value of the email/name keys represent the properties of your user context.
Expand Down Expand Up @@ -124,15 +130,20 @@ export interface FeedbackTextConfiguration {
isRequiredLabel?: string;

/**
* The label for the button that adds a screenshot and renders the image editor
* The label for the button that adds a screenshot
*/
addScreenshotButtonLabel?: string;

/**
* The label for the button that removes a screenshot and hides the image editor
* The label for the button that removes a screenshot
*/
removeScreenshotButtonLabel?: string;

/**
* The label for the button that shows the capture screenshot button
*/
captureScreenshotButtonLabel?: string;

/**
* The title of the error dialog
*/
Expand Down Expand Up @@ -169,6 +180,21 @@ export interface FeedbackButtonTextConfiguration {
triggerAriaLabel?: string;
}

/**
* The ScreenshotButton text labels that can be customized
*/
export interface ScreenshotButtonTextConfiguration {
/**
* The label for the Screenshot button
*/
triggerLabel?: string;

/**
* The aria label for the Screenshot button
*/
triggerAriaLabel?: string;
}

/**
* The public callbacks available for the feedback integration
*/
Expand Down Expand Up @@ -258,6 +284,8 @@ export interface FeedbackWidgetStyles {
screenshotContainer?: ViewStyle;
screenshotThumbnail?: ImageStyle;
screenshotText?: TextStyle;
takeScreenshotButton?: ViewStyle;
takeScreenshotText?: TextStyle;
titleContainer?: ViewStyle;
sentryLogo?: ImageStyle;
}
Expand All @@ -278,6 +306,22 @@ export interface FeedbackButtonStyles {
triggerIcon?: ImageStyle;
}

/**
* The props for the screenshot button
*/
export interface ScreenshotButtonProps extends ScreenshotButtonTextConfiguration {
styles?: ScreenshotButtonStyles;
}

/**
* The styles for the screenshot button
*/
export interface ScreenshotButtonStyles {
triggerButton?: ViewStyle;
triggerText?: TextStyle;
triggerIcon?: ImageStyle;
}

/**
* The state of the feedback form
*/
Expand Down
Loading
Loading