Skip to content

Commit d8992c6

Browse files
feat: Feedback Widget Beta for React Native (#4435)
2 parents 5852d77 + 76f708d commit d8992c6

32 files changed

+3492
-1
lines changed

CHANGELOG.md

+14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
> make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first.
77
<!-- prettier-ignore-end -->
88
9+
## Unreleased
10+
11+
### Features
12+
13+
- User Feedback Widget Beta ([#4435](https://github.com/getsentry/sentry-react-native/pull/4435))
14+
15+
To collect user feedback from inside your application call `Sentry.showFeedbackWidget()` or add the `FeedbackWidget` component.
16+
17+
```jsx
18+
import { FeedbackWidget } from "@sentry/react-native";
19+
...
20+
<FeedbackWidget/>
21+
```
22+
923
## 6.8.0
1024

1125
### Features

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

+35
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import android.content.pm.PackageInfo;
1111
import android.content.pm.PackageManager;
1212
import android.content.res.AssetManager;
13+
import android.net.Uri;
1314
import android.util.SparseIntArray;
1415
import androidx.core.app.FrameMetricsAggregator;
1516
import androidx.fragment.app.FragmentActivity;
@@ -72,6 +73,7 @@
7273
import io.sentry.vendor.Base64;
7374
import java.io.BufferedInputStream;
7475
import java.io.BufferedReader;
76+
import java.io.ByteArrayOutputStream;
7577
import java.io.File;
7678
import java.io.FileNotFoundException;
7779
import java.io.FileReader;
@@ -970,6 +972,39 @@ public String fetchNativePackageName() {
970972
return packageInfo.packageName;
971973
}
972974

975+
public void getDataFromUri(String uri, Promise promise) {
976+
try {
977+
Uri contentUri = Uri.parse(uri);
978+
try (InputStream is =
979+
getReactApplicationContext().getContentResolver().openInputStream(contentUri)) {
980+
if (is == null) {
981+
String msg = "File not found for uri: " + uri;
982+
logger.log(SentryLevel.ERROR, msg);
983+
promise.reject(new Exception(msg));
984+
return;
985+
}
986+
987+
ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
988+
int bufferSize = 1024;
989+
byte[] buffer = new byte[bufferSize];
990+
int len;
991+
while ((len = is.read(buffer)) != -1) {
992+
byteBuffer.write(buffer, 0, len);
993+
}
994+
byte[] byteArray = byteBuffer.toByteArray();
995+
WritableArray jsArray = Arguments.createArray();
996+
for (byte b : byteArray) {
997+
jsArray.pushInt(b & 0xFF);
998+
}
999+
promise.resolve(jsArray);
1000+
}
1001+
} catch (IOException e) {
1002+
String msg = "Error reading uri: " + uri + ": " + e.getMessage();
1003+
logger.log(SentryLevel.ERROR, msg);
1004+
promise.reject(new Exception(msg));
1005+
}
1006+
}
1007+
9731008
public void crashedLastRun(Promise promise) {
9741009
promise.resolve(Sentry.isCrashedLastRun());
9751010
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,9 @@ public void crashedLastRun(Promise promise) {
177177
public void getNewScreenTimeToDisplay(Promise promise) {
178178
this.impl.getNewScreenTimeToDisplay(promise);
179179
}
180+
181+
@Override
182+
public void getDataFromUri(String uri, Promise promise) {
183+
this.impl.getDataFromUri(uri, promise);
184+
}
180185
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ public String fetchNativePackageName() {
152152
return this.impl.fetchNativePackageName();
153153
}
154154

155+
@ReactMethod
156+
public void getDataFromUri(String uri, Promise promise) {
157+
this.impl.getDataFromUri(uri, promise);
158+
}
159+
155160
@ReactMethod(isBlockingSynchronousMethod = true)
156161
public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) {
157162
// Not used on Android

packages/core/ios/RNSentry.mm

+29
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,35 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
769769
#endif
770770
}
771771

772+
RCT_EXPORT_METHOD(getDataFromUri
773+
: (NSString *_Nonnull)uri resolve
774+
: (RCTPromiseResolveBlock)resolve rejecter
775+
: (RCTPromiseRejectBlock)reject)
776+
{
777+
#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST
778+
NSURL *fileURL = [NSURL URLWithString:uri];
779+
if (![fileURL isFileURL]) {
780+
reject(@"SentryReactNative", @"The provided URI is not a valid file:// URL", nil);
781+
return;
782+
}
783+
NSError *error = nil;
784+
NSData *fileData = [NSData dataWithContentsOfURL:fileURL options:0 error:&error];
785+
if (error || !fileData) {
786+
reject(@"SentryReactNative", @"Failed to read file data", error);
787+
return;
788+
}
789+
NSMutableArray *byteArray = [NSMutableArray arrayWithCapacity:fileData.length];
790+
const unsigned char *bytes = (const unsigned char *)fileData.bytes;
791+
792+
for (NSUInteger i = 0; i < fileData.length; i++) {
793+
[byteArray addObject:@(bytes[i])];
794+
}
795+
resolve(byteArray);
796+
#else
797+
resolve(nil);
798+
#endif
799+
}
800+
772801
RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getCurrentReplayId)
773802
{
774803
#if SENTRY_TARGET_REPLAY_SUPPORTED

packages/core/src/js/NativeRNSentry.ts

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface Spec extends TurboModule {
4848
captureReplay(isHardCrash: boolean): Promise<string | undefined | null>;
4949
getCurrentReplayId(): string | undefined | null;
5050
crashedLastRun(): Promise<boolean | undefined | null>;
51+
getDataFromUri(uri: string): Promise<number[]>;
5152
}
5253

5354
export type NativeStackFrame = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { ViewStyle } from 'react-native';
2+
3+
import type { FeedbackWidgetStyles } from './FeedbackWidget.types';
4+
5+
const PURPLE = 'rgba(88, 74, 192, 1)';
6+
const FOREGROUND_COLOR = '#2b2233';
7+
const BACKGROUND_COLOR = '#ffffff';
8+
const BORDER_COLOR = 'rgba(41, 35, 47, 0.13)';
9+
10+
const defaultStyles: FeedbackWidgetStyles = {
11+
container: {
12+
flex: 1,
13+
padding: 20,
14+
backgroundColor: BACKGROUND_COLOR,
15+
},
16+
title: {
17+
fontSize: 24,
18+
fontWeight: 'bold',
19+
marginBottom: 20,
20+
textAlign: 'left',
21+
flex: 1,
22+
color: FOREGROUND_COLOR,
23+
},
24+
label: {
25+
marginBottom: 4,
26+
fontSize: 16,
27+
color: FOREGROUND_COLOR,
28+
},
29+
input: {
30+
height: 50,
31+
borderColor: BORDER_COLOR,
32+
borderWidth: 1,
33+
borderRadius: 5,
34+
paddingHorizontal: 10,
35+
marginBottom: 15,
36+
fontSize: 16,
37+
color: FOREGROUND_COLOR,
38+
},
39+
textArea: {
40+
height: 100,
41+
textAlignVertical: 'top',
42+
color: FOREGROUND_COLOR,
43+
},
44+
screenshotButton: {
45+
backgroundColor: BACKGROUND_COLOR,
46+
padding: 15,
47+
borderRadius: 5,
48+
alignItems: 'center',
49+
flex: 1,
50+
borderWidth: 1,
51+
borderColor: BORDER_COLOR,
52+
},
53+
screenshotContainer: {
54+
flexDirection: 'row',
55+
alignItems: 'center',
56+
width: '100%',
57+
marginBottom: 20,
58+
},
59+
screenshotThumbnail: {
60+
width: 50,
61+
height: 50,
62+
borderRadius: 5,
63+
marginRight: 10,
64+
},
65+
screenshotText: {
66+
color: FOREGROUND_COLOR,
67+
fontSize: 16,
68+
},
69+
submitButton: {
70+
backgroundColor: PURPLE,
71+
paddingVertical: 15,
72+
borderRadius: 5,
73+
alignItems: 'center',
74+
marginBottom: 10,
75+
},
76+
submitText: {
77+
color: BACKGROUND_COLOR,
78+
fontSize: 18,
79+
},
80+
cancelButton: {
81+
backgroundColor: BACKGROUND_COLOR,
82+
padding: 15,
83+
borderRadius: 5,
84+
alignItems: 'center',
85+
borderWidth: 1,
86+
borderColor: BORDER_COLOR,
87+
},
88+
cancelText: {
89+
color: FOREGROUND_COLOR,
90+
fontSize: 16,
91+
},
92+
titleContainer: {
93+
flexDirection: 'row',
94+
width: '100%',
95+
},
96+
sentryLogo: {
97+
width: 40,
98+
height: 40,
99+
},
100+
};
101+
102+
export const modalWrapper: ViewStyle = {
103+
position: 'absolute',
104+
top: 0,
105+
left: 0,
106+
right: 0,
107+
bottom: 0,
108+
};
109+
110+
export const modalSheetContainer: ViewStyle = {
111+
backgroundColor: '#ffffff',
112+
borderTopLeftRadius: 16,
113+
borderTopRightRadius: 16,
114+
overflow: 'hidden',
115+
alignSelf: 'stretch',
116+
shadowColor: '#000',
117+
shadowOffset: { width: 0, height: -3 },
118+
shadowOpacity: 0.1,
119+
shadowRadius: 4,
120+
elevation: 5,
121+
flex: 1,
122+
};
123+
124+
export const topSpacer: ViewStyle = {
125+
height: 64, // magic number
126+
};
127+
128+
export default defaultStyles;

0 commit comments

Comments
 (0)