Skip to content

Commit 3770484

Browse files
authored
Merge pull request #72 from sentry-demos/kw/add-user-feedback
Add User Feedback
2 parents 1ddcd9a + 9c200de commit 3770484

File tree

3 files changed

+281
-2
lines changed

3 files changed

+281
-2
lines changed

src/App.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ import CartScreen from './screens/CartScreen';
2323
import CheckoutScreen from './screens/CheckoutScreen';
2424
import Toast from 'react-native-toast-message';
2525

26-
import {RootState, store} from './reduxApp';
26+
import {RootState, store, showFeedbackActionButton} from './reduxApp';
2727
import {DSN} from './config';
2828
import {SE} from '@env'; // SE is undefined if no .env file is set
2929
import {RootStackParamList} from './navigation';
3030
import {GestureHandlerRootView} from 'react-native-gesture-handler';
3131
import {LogBox, Platform, StyleSheet} from 'react-native';
3232
import {SafeAreaProvider} from 'react-native-safe-area-context';
33+
import {SentryUserFeedbackActionButton} from './components/UserFeedbackModal';
3334
console.log('> SE', SE);
3435

3536
LogBox.ignoreAllLogs();
@@ -56,6 +57,12 @@ Sentry.init({
5657
// Make issue for the SE
5758
event.fingerprint = ['{{ default }}', SE];
5859
}
60+
61+
if (!event.type) {
62+
// Only show the feedback button for errors
63+
store.dispatch(showFeedbackActionButton());
64+
}
65+
5966
return event;
6067
},
6168
integrations: [
@@ -115,6 +122,7 @@ const App = () => {
115122
}}>
116123
<BottomTabNavigator />
117124
{/* <Toast /> */}
125+
<SentryUserFeedbackActionButton />
118126
</NavigationContainer>
119127
</GestureHandlerRootView>
120128
</SafeAreaProvider>
@@ -171,7 +179,7 @@ const BottomTabNavigator = () => {
171179
options={{
172180
tabBarIcon: ({focused}) => (
173181
<Icon
174-
name="bug"
182+
name="gear"
175183
size={30}
176184
color={focused ? '#f6cfb2' : '#dae3e4'}
177185
/>
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import React from 'react';
2+
import {
3+
View,
4+
StyleSheet,
5+
Text,
6+
TextInput,
7+
PressableProps,
8+
Pressable,
9+
Keyboard,
10+
ViewStyle,
11+
TextStyle,
12+
} from 'react-native';
13+
import * as Sentry from '@sentry/react-native';
14+
import {UserFeedback} from '@sentry/react-native';
15+
import Icon from 'react-native-vector-icons/FontAwesome6';
16+
import {android} from '../../utils/platform';
17+
import {useSafeAreaInsets} from 'react-native-safe-area-context';
18+
import {hideFeedbackActionButton, RootState} from '../reduxApp';
19+
import {useDispatch, useSelector} from 'react-redux';
20+
21+
export const DEFAULT_COMMENTS = "It's broken again! Please fix it.";
22+
23+
export const SentryUserFeedbackActionButton = () => {
24+
const feedbackState = useSelector((state: RootState) => state.feedback);
25+
const [isFormVisible, setFromVisibility] = React.useState(false);
26+
const style = getCloseButtonStyles({safeBottom: useSafeAreaInsets().bottom});
27+
const pressableStyle: PressableProps['style'] = ({pressed}) =>
28+
pressed
29+
? [
30+
{
31+
...style.container,
32+
backgroundColor: '#584774',
33+
},
34+
style.shadowProp,
35+
]
36+
: [style.container, style.shadowProp];
37+
38+
const onGiveFeedbackButtonPress = () => {
39+
setFromVisibility(true);
40+
};
41+
42+
return (
43+
<>
44+
{feedbackState.isActionButtonVisible && (
45+
<Pressable onPress={onGiveFeedbackButtonPress} style={pressableStyle}>
46+
<Icon name="bug" size={24} color="#fff" />
47+
<Text style={style.text}>Report a Bug</Text>
48+
</Pressable>
49+
)}
50+
{isFormVisible && (
51+
<UserFeedbackModal onDismiss={() => setFromVisibility(false)} />
52+
)}
53+
</>
54+
);
55+
};
56+
57+
const getCloseButtonStyles = ({safeBottom}: {safeBottom: number}) =>
58+
StyleSheet.create({
59+
container: {
60+
height: 50,
61+
width: 160,
62+
borderRadius: 30,
63+
backgroundColor: '#29232f',
64+
position: 'absolute',
65+
bottom: safeBottom + (android(10) || 0) + 150,
66+
left: 10,
67+
paddingLeft: 15,
68+
paddingRight: 15,
69+
display: 'flex',
70+
flexDirection: 'row',
71+
alignItems: 'center',
72+
justifyContent: 'space-between',
73+
borderColor: 'rgba(235, 230, 239, 0.15)',
74+
borderWidth: 1.5,
75+
},
76+
text: {
77+
color: '#fff',
78+
fontWeight: 'bold',
79+
},
80+
shadowProp: {
81+
shadowColor: '#171717',
82+
shadowOffset: {width: -2, height: 4},
83+
shadowOpacity: 0.3,
84+
shadowRadius: 3,
85+
},
86+
});
87+
88+
export function UserFeedbackModal(props: {onDismiss: () => void}) {
89+
const dispatch = useDispatch();
90+
const {onDismiss} = props;
91+
const [comments, onChangeComments] = React.useState(DEFAULT_COMMENTS);
92+
const clearComments = () => onChangeComments(DEFAULT_COMMENTS);
93+
const onContainerPress = () => {
94+
Keyboard.dismiss();
95+
};
96+
const onCloseButtonPress = () => {
97+
onDismiss();
98+
dispatch(hideFeedbackActionButton());
99+
};
100+
const onSendButtonPress = () => {
101+
onDismiss();
102+
103+
const sentryId =
104+
Sentry.lastEventId() ??
105+
Sentry.captureMessage('User Feedback Fallback Message');
106+
107+
const userFeedback: UserFeedback = {
108+
event_id: sentryId,
109+
name: 'Anonymous User',
110+
111+
comments,
112+
};
113+
114+
Sentry.captureUserFeedback(userFeedback);
115+
clearComments();
116+
dispatch(hideFeedbackActionButton());
117+
};
118+
119+
return (
120+
<Pressable onPress={onContainerPress} style={styles.centeredView}>
121+
<View style={styles.modalView}>
122+
<Text style={styles.modalText}>Whoops, what happened?</Text>
123+
<TextInput
124+
style={styles.input}
125+
onChangeText={onChangeComments}
126+
value={comments}
127+
multiline={true}
128+
numberOfLines={4}
129+
/>
130+
<View style={styles.actionsWrapper}>
131+
<ModalButton onPress={onSendButtonPress} text="Send Bug Report" />
132+
<View style={styles.buttonSpacer} />
133+
<ModalButton
134+
onPress={onCloseButtonPress}
135+
text="Close"
136+
wrapperStyle={modalButtonStyles.secondaryWrapper}
137+
textStyle={modalButtonStyles.secondaryText}
138+
/>
139+
</View>
140+
</View>
141+
</Pressable>
142+
);
143+
}
144+
145+
const ModalButton = ({
146+
onPress,
147+
text,
148+
wrapperStyle,
149+
textStyle,
150+
}: {
151+
onPress: () => void;
152+
text: string;
153+
wrapperStyle?: ViewStyle;
154+
textStyle?: TextStyle;
155+
}) => {
156+
return (
157+
<Pressable
158+
onPress={onPress}
159+
style={{
160+
...modalButtonStyles.wrapper,
161+
...wrapperStyle,
162+
}}>
163+
<Text
164+
style={{
165+
...modalButtonStyles.text,
166+
...textStyle,
167+
}}>
168+
{text}
169+
</Text>
170+
</Pressable>
171+
);
172+
};
173+
174+
const modalButtonStyles = StyleSheet.create({
175+
wrapper: {
176+
borderColor: 'rgba(235, 230, 239, 0.15)',
177+
borderWidth: 1.5,
178+
backgroundColor: 'rgba(88, 74, 192, 1)',
179+
padding: 10,
180+
borderRadius: 6,
181+
alignContent: 'center',
182+
alignItems: 'center',
183+
},
184+
text: {
185+
color: '#fff',
186+
fontWeight: 'bold',
187+
fontSize: 16,
188+
},
189+
secondaryWrapper: {
190+
backgroundColor: 'rgba(0, 0, 0, 0)',
191+
},
192+
secondaryText: {
193+
fontWeight: 'regular',
194+
},
195+
});
196+
197+
const styles = StyleSheet.create({
198+
centeredView: {
199+
flex: 1,
200+
height: '100%',
201+
width: '100%',
202+
position: 'absolute',
203+
display: 'flex',
204+
justifyContent: 'center',
205+
alignItems: 'center',
206+
},
207+
modalView: {
208+
margin: 5,
209+
backgroundColor: '#29232f',
210+
borderRadius: 16,
211+
padding: 25,
212+
alignItems: 'center',
213+
shadowColor: '#171717',
214+
shadowOffset: {width: -2, height: 4},
215+
shadowOpacity: 0.3,
216+
shadowRadius: 3,
217+
elevation: 5,
218+
borderColor: '#584774',
219+
borderWidth: 2,
220+
},
221+
input: {
222+
marginBottom: 20,
223+
borderWidth: 1.5,
224+
borderColor: 'rgba(235, 230, 239, 0.15)',
225+
padding: 15,
226+
borderRadius: 6,
227+
height: 100,
228+
width: 250,
229+
textAlignVertical: 'top',
230+
color: '#fff',
231+
},
232+
actionsWrapper: {
233+
width: 250,
234+
},
235+
modalText: {
236+
marginBottom: 15,
237+
textAlign: 'center',
238+
fontSize: 18,
239+
color: '#fff',
240+
fontWeight: 'bold',
241+
},
242+
modalImage: {
243+
marginBottom: 20,
244+
width: 80,
245+
height: 80,
246+
},
247+
buttonSpacer: {
248+
marginBottom: 8,
249+
},
250+
});

src/reduxApp.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ const initialState = {
1414
state: '',
1515
zipCode: '',
1616
},
17+
feedback: {
18+
isActionButtonVisible: false,
19+
},
1720
};
1821

1922
const reducer = (state = initialState, action) => {
@@ -73,11 +76,29 @@ const reducer = (state = initialState, action) => {
7376
...state,
7477
counter: 0,
7578
};
79+
case 'SHOW_FEEDBACK_ACTION_BUTTON':
80+
return {
81+
...state,
82+
feedback: {...state.feedback, isActionButtonVisible: true},
83+
};
84+
case 'HIDE_FEEDBACK_ACTION_BUTTON':
85+
return {
86+
...state,
87+
feedback: {...state.feedback, isActionButtonVisible: false},
88+
};
7689
default:
7790
return state;
7891
}
7992
};
8093

94+
export const showFeedbackActionButton = () => ({
95+
type: 'SHOW_FEEDBACK_ACTION_BUTTON',
96+
});
97+
98+
export const hideFeedbackActionButton = () => ({
99+
type: 'HIDE_FEEDBACK_ACTION_BUTTON',
100+
});
101+
81102
/*
82103
Example of how to use the Sentry redux enhancer packaged with @sentry/react:
83104
*/

0 commit comments

Comments
 (0)