diff --git a/src/App.tsx b/src/App.tsx index 50517d5..2478f12 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,13 +23,14 @@ import CartScreen from './screens/CartScreen'; import CheckoutScreen from './screens/CheckoutScreen'; import Toast from 'react-native-toast-message'; -import {RootState, store} from './reduxApp'; +import {RootState, store, showFeedbackActionButton} from './reduxApp'; import {DSN} from './config'; import {SE} from '@env'; // SE is undefined if no .env file is set import {RootStackParamList} from './navigation'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {LogBox, Platform, StyleSheet} from 'react-native'; import {SafeAreaProvider} from 'react-native-safe-area-context'; +import {SentryUserFeedbackActionButton} from './components/UserFeedbackModal'; console.log('> SE', SE); LogBox.ignoreAllLogs(); @@ -56,6 +57,12 @@ Sentry.init({ // Make issue for the SE event.fingerprint = ['{{ default }}', SE]; } + + if (!event.type) { + // Only show the feedback button for errors + store.dispatch(showFeedbackActionButton()); + } + return event; }, integrations: [ @@ -115,6 +122,7 @@ const App = () => { }}> {/* */} + @@ -170,7 +178,7 @@ const BottomTabNavigator = () => { options={{ tabBarIcon: ({focused}) => ( diff --git a/src/components/UserFeedbackModal.tsx b/src/components/UserFeedbackModal.tsx new file mode 100644 index 0000000..497a7f5 --- /dev/null +++ b/src/components/UserFeedbackModal.tsx @@ -0,0 +1,250 @@ +import React from 'react'; +import { + View, + StyleSheet, + Text, + TextInput, + PressableProps, + Pressable, + Keyboard, + ViewStyle, + TextStyle, +} from 'react-native'; +import * as Sentry from '@sentry/react-native'; +import {UserFeedback} from '@sentry/react-native'; +import Icon from 'react-native-vector-icons/FontAwesome6'; +import {android} from '../../utils/platform'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; +import {hideFeedbackActionButton, RootState} from '../reduxApp'; +import {useDispatch, useSelector} from 'react-redux'; + +export const DEFAULT_COMMENTS = "It's broken again! Please fix it."; + +export const SentryUserFeedbackActionButton = () => { + const feedbackState = useSelector((state: RootState) => state.feedback); + const [isFormVisible, setFromVisibility] = React.useState(false); + const style = getCloseButtonStyles({safeBottom: useSafeAreaInsets().bottom}); + const pressableStyle: PressableProps['style'] = ({pressed}) => + pressed + ? [ + { + ...style.container, + backgroundColor: '#584774', + }, + style.shadowProp, + ] + : [style.container, style.shadowProp]; + + const onGiveFeedbackButtonPress = () => { + setFromVisibility(true); + }; + + return ( + <> + {feedbackState.isActionButtonVisible && ( + + + Report a Bug + + )} + {isFormVisible && ( + setFromVisibility(false)} /> + )} + + ); +}; + +const getCloseButtonStyles = ({safeBottom}: {safeBottom: number}) => + StyleSheet.create({ + container: { + height: 50, + width: 160, + borderRadius: 30, + backgroundColor: '#29232f', + position: 'absolute', + bottom: safeBottom + (android(10) || 0) + 150, + left: 10, + paddingLeft: 15, + paddingRight: 15, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderColor: 'rgba(235, 230, 239, 0.15)', + borderWidth: 1.5, + }, + text: { + color: '#fff', + fontWeight: 'bold', + }, + shadowProp: { + shadowColor: '#171717', + shadowOffset: {width: -2, height: 4}, + shadowOpacity: 0.3, + shadowRadius: 3, + }, + }); + +export function UserFeedbackModal(props: {onDismiss: () => void}) { + const dispatch = useDispatch(); + const {onDismiss} = props; + const [comments, onChangeComments] = React.useState(DEFAULT_COMMENTS); + const clearComments = () => onChangeComments(DEFAULT_COMMENTS); + const onContainerPress = () => { + Keyboard.dismiss(); + }; + const onCloseButtonPress = () => { + onDismiss(); + dispatch(hideFeedbackActionButton()); + }; + const onSendButtonPress = () => { + onDismiss(); + + const sentryId = + Sentry.lastEventId() ?? + Sentry.captureMessage('User Feedback Fallback Message'); + + const userFeedback: UserFeedback = { + event_id: sentryId, + name: 'Anonymous User', + email: 'example@example.com', + comments, + }; + + Sentry.captureUserFeedback(userFeedback); + clearComments(); + dispatch(hideFeedbackActionButton()); + }; + + return ( + + + Whoops, what happened? + + + + + + + + + ); +} + +const ModalButton = ({ + onPress, + text, + wrapperStyle, + textStyle, +}: { + onPress: () => void; + text: string; + wrapperStyle?: ViewStyle; + textStyle?: TextStyle; +}) => { + return ( + + + {text} + + + ); +}; + +const modalButtonStyles = StyleSheet.create({ + wrapper: { + borderColor: 'rgba(235, 230, 239, 0.15)', + borderWidth: 1.5, + backgroundColor: 'rgba(88, 74, 192, 1)', + padding: 10, + borderRadius: 6, + alignContent: 'center', + alignItems: 'center', + }, + text: { + color: '#fff', + fontWeight: 'bold', + fontSize: 16, + }, + secondaryWrapper: { + backgroundColor: 'rgba(0, 0, 0, 0)', + }, + secondaryText: { + fontWeight: 'regular', + }, +}); + +const styles = StyleSheet.create({ + centeredView: { + flex: 1, + height: '100%', + width: '100%', + position: 'absolute', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + modalView: { + margin: 5, + backgroundColor: '#29232f', + borderRadius: 16, + padding: 25, + alignItems: 'center', + shadowColor: '#171717', + shadowOffset: {width: -2, height: 4}, + shadowOpacity: 0.3, + shadowRadius: 3, + elevation: 5, + borderColor: '#584774', + borderWidth: 2, + }, + input: { + marginBottom: 20, + borderWidth: 1.5, + borderColor: 'rgba(235, 230, 239, 0.15)', + padding: 15, + borderRadius: 6, + height: 100, + width: 250, + textAlignVertical: 'top', + color: '#fff', + }, + actionsWrapper: { + width: 250, + }, + modalText: { + marginBottom: 15, + textAlign: 'center', + fontSize: 18, + color: '#fff', + fontWeight: 'bold', + }, + modalImage: { + marginBottom: 20, + width: 80, + height: 80, + }, + buttonSpacer: { + marginBottom: 8, + }, +}); diff --git a/src/reduxApp.ts b/src/reduxApp.ts index d6b2973..3fbfdfc 100644 --- a/src/reduxApp.ts +++ b/src/reduxApp.ts @@ -14,6 +14,9 @@ const initialState = { state: '', zipCode: '', }, + feedback: { + isActionButtonVisible: false, + }, }; const reducer = (state = initialState, action) => { @@ -73,11 +76,29 @@ const reducer = (state = initialState, action) => { ...state, counter: 0, }; + case 'SHOW_FEEDBACK_ACTION_BUTTON': + return { + ...state, + feedback: {...state.feedback, isActionButtonVisible: true}, + }; + case 'HIDE_FEEDBACK_ACTION_BUTTON': + return { + ...state, + feedback: {...state.feedback, isActionButtonVisible: false}, + }; default: return state; } }; +export const showFeedbackActionButton = () => ({ + type: 'SHOW_FEEDBACK_ACTION_BUTTON', +}); + +export const hideFeedbackActionButton = () => ({ + type: 'HIDE_FEEDBACK_ACTION_BUTTON', +}); + /* Example of how to use the Sentry redux enhancer packaged with @sentry/react: */