Skip to content

feat(feedback): Feedback Widget Drop 2 #4726

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -8,9 +8,16 @@

## Unreleased

### Features

- 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

- Disable native driver for Feedback Widget `backgroundColor` animation in unsupported React Native versions ([#4794](https://github.com/getsentry/sentry-react-native/pull/4794))
- Fixes Feedback Widget accessibility issue on iOS ([#4739](https://github.com/getsentry/sentry-react-native/pull/4739))

## 6.13.0

Original file line number Diff line number Diff line change
@@ -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;
@@ -1038,6 +1039,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());
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -183,6 +183,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
public void popTimeToDisplayFor(String key, Promise promise) {
this.impl.popTimeToDisplayFor(key, promise);
24 changes: 24 additions & 0 deletions packages/core/ios/RNSentry.mm
Original file line number Diff line number Diff line change
@@ -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
@@ -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 = {
66 changes: 66 additions & 0 deletions packages/core/src/js/feedback/FeedbackButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';
import type { NativeEventSubscription} from 'react-native';
import { Appearance, Image, Text, TouchableOpacity } from 'react-native';

import { defaultButtonConfiguration } from './defaults';
import { defaultButtonStyles } from './FeedbackWidget.styles';
import { getTheme } from './FeedbackWidget.theme';
import type { FeedbackButtonProps, FeedbackButtonStyles, FeedbackButtonTextConfiguration } from './FeedbackWidget.types';
import { showFeedbackWidget } from './FeedbackWidgetManager';
import { feedbackIcon } from './icons';
import { lazyLoadFeedbackIntegration } from './lazy';

/**
* @beta
* Implements a feedback button that opens the FeedbackForm.
*/
export class FeedbackButton extends React.Component<FeedbackButtonProps> {
private _themeListener: NativeEventSubscription;

public constructor(props: FeedbackButtonProps) {
super(props);
lazyLoadFeedbackIntegration();
}

/**
* Adds a listener for theme changes.
*/
public componentDidMount(): void {
this._themeListener = Appearance.addChangeListener(() => {
this.forceUpdate();
});
}

/**
* Removes the theme listener.
*/
public componentWillUnmount(): void {
if (this._themeListener) {
this._themeListener.remove();
}
}

/**
* Renders the feedback button.
*/
public render(): React.ReactNode {
const theme = getTheme();
const text: FeedbackButtonTextConfiguration = { ...defaultButtonConfiguration, ...this.props };
const styles: FeedbackButtonStyles = {
triggerButton: { ...defaultButtonStyles(theme).triggerButton, ...this.props.styles?.triggerButton },
triggerText: { ...defaultButtonStyles(theme).triggerText, ...this.props.styles?.triggerText },
triggerIcon: { ...defaultButtonStyles(theme).triggerIcon, ...this.props.styles?.triggerIcon },
};

return (
<TouchableOpacity
style={styles.triggerButton}
onPress={showFeedbackWidget}
accessibilityLabel={text.triggerAriaLabel}
>
<Image source={{ uri: feedbackIcon }} style={styles.triggerIcon}/>
<Text style={styles.triggerText}>{text.triggerLabel}</Text>
</TouchableOpacity>
);
}
}
Loading