Skip to content

feat: Feedback Widget Beta for React Native #4435

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

Merged
merged 37 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bda351c
(1) feat: Add Feedback Form Component (#4328)
antonis Jan 10, 2025
078cb37
Merge branch 'main' into feedback-ui
antonis Jan 10, 2025
7add44c
Merge branch 'main' into feedback-ui
antonis Jan 14, 2025
8709a48
Update changelog PR reference
antonis Jan 14, 2025
08eecba
test: Adds snapshot tests (#4379)
antonis Jan 15, 2025
4529b68
feat: handle `captureFeedback` errors (#4364)
antonis Jan 15, 2025
48ff52e
Merge branch 'main' into feedback-ui
antonis Jan 15, 2025
93b770e
Merge branch 'main' into feedback-ui
antonis Jan 16, 2025
aa15c88
(2.4) feat(feedback-ui): Add screenshots (#4338)
antonis Jan 16, 2025
312116d
Merge branch 'main' into feedback-ui
antonis Jan 20, 2025
f3c3563
Merge branch 'main' into feedback-ui
antonis Jan 22, 2025
b7b36d8
Merge branch 'main' into feedback-ui
antonis Jan 27, 2025
eda1cb7
Autoinject feedback widget (#4483)
antonis Jan 29, 2025
74748f8
Adds feedbackIntegration for configuring the feedback form (#4485)
antonis Jan 30, 2025
df77091
Merge branch 'main' into feedback-ui
antonis Jan 30, 2025
dbdfceb
Merge branch 'main' into feedback-ui
antonis Jan 31, 2025
f8988bc
Merge branch 'main' into feedback-ui
antonis Feb 3, 2025
03ece25
Merge branch 'main' into feedback-ui
antonis Feb 7, 2025
07b3f54
Merge branch 'main' into feedback-ui
antonis Feb 10, 2025
7ec9441
Feedback modal UI tweaks (#4492)
antonis Feb 11, 2025
fe99425
Merge branch 'main' into feedback-ui
antonis Feb 11, 2025
22cde46
Fix changelog
antonis Feb 11, 2025
e17ab11
Feedback UI: Use Image Picker libraries from integrations (#4524)
antonis Feb 14, 2025
fcbc6c6
Merge branch 'main' into feedback-ui
antonis Feb 14, 2025
874b2a2
feat(feedback): Pull down to cancel (#4534)
antonis Feb 17, 2025
7579a06
chore(feedback): Use Widget instead of Form (#4547)
krystofwoldrich Feb 17, 2025
2135c96
chore(feedback): Improve widget animations (#4555)
krystofwoldrich Feb 17, 2025
51dc070
feat(feedback): Save form state for un-submitted data (#4538)
antonis Feb 18, 2025
b3ea2b2
feat(feedback): Show selected screenshot (#4545)
antonis Feb 18, 2025
53e13fc
feat(feedback): Use only image uri in the onAddScreenshot callback (#…
antonis Feb 18, 2025
8ff7db3
Merge branch 'main' into feedback-ui
antonis Feb 18, 2025
ef4be9e
feat(feedback): Support web environments (#4558)
antonis Feb 20, 2025
ee3aa70
misc(feedback): Improve Feedback Sheet interactions (#4571)
krystofwoldrich Feb 20, 2025
ba260a4
feat(feedback): Align secondary buttons with the web (#4572)
antonis Feb 20, 2025
58aa109
Merge branch 'main' into feedback-ui
antonis Feb 20, 2025
ba2eecd
Update changelog
antonis Feb 20, 2025
76f708d
Merge branch 'main' into feedback-ui
antonis Feb 21, 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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@
> make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first.
<!-- prettier-ignore-end -->

## Unreleased

### Features

- User Feedback Widget Beta ([#4435](https://github.com/getsentry/sentry-react-native/pull/4435))

To collect user feedback from inside your application call `Sentry.showFeedbackWidget()` or add the `FeedbackWidget` component.

```jsx
import { FeedbackWidget } from "@sentry/react-native";
...
<FeedbackWidget/>
```

## 6.8.0

### Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.net.Uri;
import android.util.SparseIntArray;
import androidx.core.app.FrameMetricsAggregator;
import androidx.fragment.app.FragmentActivity;
Expand Down Expand Up @@ -72,6 +73,7 @@
import io.sentry.vendor.Base64;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
Expand Down Expand Up @@ -970,6 +972,39 @@ public String fetchNativePackageName() {
return packageInfo.packageName;
}

public void getDataFromUri(String uri, Promise promise) {
try {
Uri contentUri = Uri.parse(uri);
try (InputStream is =
getReactApplicationContext().getContentResolver().openInputStream(contentUri)) {
if (is == null) {
String msg = "File not found for uri: " + uri;
logger.log(SentryLevel.ERROR, msg);
promise.reject(new Exception(msg));
return;
}

ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int len;
while ((len = is.read(buffer)) != -1) {
byteBuffer.write(buffer, 0, len);
}
byte[] byteArray = byteBuffer.toByteArray();
WritableArray jsArray = Arguments.createArray();
for (byte b : byteArray) {
jsArray.pushInt(b & 0xFF);
}
promise.resolve(jsArray);
}
} catch (IOException e) {
String msg = "Error reading uri: " + uri + ": " + e.getMessage();
logger.log(SentryLevel.ERROR, msg);
promise.reject(new Exception(msg));
}
}

public void crashedLastRun(Promise promise) {
promise.resolve(Sentry.isCrashedLastRun());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,9 @@ public void crashedLastRun(Promise promise) {
public void getNewScreenTimeToDisplay(Promise promise) {
this.impl.getNewScreenTimeToDisplay(promise);
}

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

@ReactMethod
public void getDataFromUri(String uri, Promise promise) {
this.impl.getDataFromUri(uri, promise);
}

@ReactMethod(isBlockingSynchronousMethod = true)
public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) {
// Not used on Android
Expand Down
29 changes: 29 additions & 0 deletions packages/core/ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,35 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
#endif
}

RCT_EXPORT_METHOD(getDataFromUri
: (NSString *_Nonnull)uri resolve
: (RCTPromiseResolveBlock)resolve rejecter
: (RCTPromiseRejectBlock)reject)
{
#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST
NSURL *fileURL = [NSURL URLWithString:uri];
if (![fileURL isFileURL]) {
reject(@"SentryReactNative", @"The provided URI is not a valid file:// URL", nil);
return;
}
NSError *error = nil;
NSData *fileData = [NSData dataWithContentsOfURL:fileURL options:0 error:&error];
if (error || !fileData) {
reject(@"SentryReactNative", @"Failed to read file data", error);
return;
}
NSMutableArray *byteArray = [NSMutableArray arrayWithCapacity:fileData.length];
const unsigned char *bytes = (const unsigned char *)fileData.bytes;

for (NSUInteger i = 0; i < fileData.length; i++) {
[byteArray addObject:@(bytes[i])];
}
resolve(byteArray);
#else
resolve(nil);
#endif
}

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getCurrentReplayId)
{
#if SENTRY_TARGET_REPLAY_SUPPORTED
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/js/NativeRNSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface Spec extends TurboModule {
captureReplay(isHardCrash: boolean): Promise<string | undefined | null>;
getCurrentReplayId(): string | undefined | null;
crashedLastRun(): Promise<boolean | undefined | null>;
getDataFromUri(uri: string): Promise<number[]>;
}

export type NativeStackFrame = {
Expand Down
128 changes: 128 additions & 0 deletions packages/core/src/js/feedback/FeedbackWidget.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type { ViewStyle } from 'react-native';

import type { FeedbackWidgetStyles } from './FeedbackWidget.types';

const PURPLE = 'rgba(88, 74, 192, 1)';
const FOREGROUND_COLOR = '#2b2233';
const BACKGROUND_COLOR = '#ffffff';
const BORDER_COLOR = 'rgba(41, 35, 47, 0.13)';

const defaultStyles: FeedbackWidgetStyles = {
container: {
flex: 1,
padding: 20,
backgroundColor: BACKGROUND_COLOR,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 20,
textAlign: 'left',
flex: 1,
color: FOREGROUND_COLOR,
},
label: {
marginBottom: 4,
fontSize: 16,
color: FOREGROUND_COLOR,
},
input: {
height: 50,
borderColor: BORDER_COLOR,
borderWidth: 1,
borderRadius: 5,
paddingHorizontal: 10,
marginBottom: 15,
fontSize: 16,
color: FOREGROUND_COLOR,
},
textArea: {
height: 100,
textAlignVertical: 'top',
color: FOREGROUND_COLOR,
},
screenshotButton: {
backgroundColor: BACKGROUND_COLOR,
padding: 15,
borderRadius: 5,
alignItems: 'center',
flex: 1,
borderWidth: 1,
borderColor: BORDER_COLOR,
},
screenshotContainer: {
flexDirection: 'row',
alignItems: 'center',
width: '100%',
marginBottom: 20,
},
screenshotThumbnail: {
width: 50,
height: 50,
borderRadius: 5,
marginRight: 10,
},
screenshotText: {
color: FOREGROUND_COLOR,
fontSize: 16,
},
submitButton: {
backgroundColor: PURPLE,
paddingVertical: 15,
borderRadius: 5,
alignItems: 'center',
marginBottom: 10,
},
submitText: {
color: BACKGROUND_COLOR,
fontSize: 18,
},
cancelButton: {
backgroundColor: BACKGROUND_COLOR,
padding: 15,
borderRadius: 5,
alignItems: 'center',
borderWidth: 1,
borderColor: BORDER_COLOR,
},
cancelText: {
color: FOREGROUND_COLOR,
fontSize: 16,
},
titleContainer: {
flexDirection: 'row',
width: '100%',
},
sentryLogo: {
width: 40,
height: 40,
},
};

export const modalWrapper: ViewStyle = {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
};

export const modalSheetContainer: ViewStyle = {
backgroundColor: '#ffffff',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
overflow: 'hidden',
alignSelf: 'stretch',
shadowColor: '#000',
shadowOffset: { width: 0, height: -3 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 5,
flex: 1,
};

export const topSpacer: ViewStyle = {
height: 64, // magic number
};

export default defaultStyles;
Loading
Loading