Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 4 additions & 1 deletion app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ErrorBoundary from './src/components/ErrorBoundary';
import AppNavigation from './src/navigation';
import { AuthProvider } from './src/providers/authProvider';
import { DatabaseProvider } from './src/providers/databaseProvider';
import { FeedbackProvider } from './src/providers/feedbackProvider';
import { LoggerProvider } from './src/providers/loggerProvider';
import { NotificationTrackingProvider } from './src/providers/notificationTrackingProvider';
import { PassportProvider } from './src/providers/passportDataProvider';
Expand All @@ -31,7 +32,9 @@ function App(): React.JSX.Element {
<PassportProvider>
<DatabaseProvider>
<NotificationTrackingProvider>
<AppNavigation />
<FeedbackProvider>
<AppNavigation />
</FeedbackProvider>
</NotificationTrackingProvider>
</DatabaseProvider>
</PassportProvider>
Expand Down
45 changes: 45 additions & 0 deletions app/src/Sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,35 @@ export const captureException = (
});
};

export const captureFeedback = (
feedback: string,
context?: Record<string, any>,
) => {
if (isSentryDisabled) {
return;
}

Sentry.captureFeedback(
{
message: feedback,
name: context?.name,
email: context?.email,
tags: {
category: context?.category || 'general',
source: context?.source || 'feedback_modal',
},
},
{
captureContext: {
tags: {
category: context?.category || 'general',
source: context?.source || 'feedback_modal',
},
},
},
);
};

export const captureMessage = (
message: string,
context?: Record<string, any>,
Expand Down Expand Up @@ -54,6 +83,22 @@ export const initSentry = () => {
Sentry.consoleLoggingIntegration({
levels: ['log', 'error', 'warn', 'info', 'debug'],
}),
Sentry.feedbackIntegration({
buttonOptions: {
styles: {
triggerButton: {
position: 'absolute',
top: 20,
right: 20,
bottom: undefined,
marginTop: 100,
},
},
},
enableTakeScreenshot: true,
namePlaceholder: 'Fullname',
emailPlaceholder: 'Email',
}),
],
_experiments: {
enableLogs: true,
Expand Down
47 changes: 47 additions & 0 deletions app/src/Sentry.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,35 @@ export const captureException = (
});
};

export const captureFeedback = (
feedback: string,
context?: Record<string, any>,
) => {
if (isSentryDisabled) {
return;
}

Sentry.captureFeedback(
{
message: feedback,
name: context?.name,
email: context?.email,
tags: {
category: context?.category || 'general',
source: context?.source || 'feedback_modal',
},
},
{
captureContext: {
tags: {
category: context?.category || 'general',
source: context?.source || 'feedback_modal',
},
},
},
);
};

export const captureMessage = (
message: string,
context?: Record<string, any>,
Expand Down Expand Up @@ -49,6 +78,24 @@ export const initSentry = () => {
}
return event;
},
integrations: [
Sentry.feedbackIntegration({
buttonOptions: {
styles: {
triggerButton: {
position: 'absolute',
top: 20,
right: 20,
bottom: undefined,
marginTop: 100,
},
},
},
enableTakeScreenshot: true,
namePlaceholder: 'Fullname',
emailPlaceholder: 'Email',
}),
],
});
return Sentry;
};
Expand Down
225 changes: 225 additions & 0 deletions app/src/components/FeedbackModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11

import React, { useState } from 'react';
import { Alert, Modal, StyleSheet, Text, TextInput, View } from 'react-native';
import { Button, XStack, YStack } from 'tamagui';

import { black, slate400, white, zinc800, zinc900 } from '../utils/colors';
import { advercase, dinot } from '../utils/fonts';
import { Caption } from './typography/Caption';

interface FeedbackModalProps {
visible: boolean;
onClose: () => void;
onSubmit: (
feedback: string,
category: string,
name?: string,
email?: string,
) => void;
}

const FeedbackModal: React.FC<FeedbackModalProps> = ({
visible,
onClose,
onSubmit,
}) => {
const [feedback, setFeedback] = useState('');
const [category, setCategory] = useState('general');
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);

const categories = [
{ value: 'general', label: 'General Feedback' },
{ value: 'bug', label: 'Bug Report' },
{ value: 'feature', label: 'Feature Request' },
{ value: 'ui', label: 'UI/UX Issue' },
];

const handleSubmit = async () => {
if (!feedback.trim()) {
Alert.alert('Error', 'Please enter your feedback');
return;
}

setIsSubmitting(true);
try {
await onSubmit(
feedback.trim(),
category,
name.trim() || undefined,
email.trim() || undefined,
);
setFeedback('');
setCategory('general');
setName('');
setEmail('');
onClose();
Alert.alert('Success', 'Thank you for your feedback!');
} catch (error) {
console.error('Error submitting feedback:', error);
Alert.alert('Error', 'Failed to submit feedback. Please try again.');
} finally {
setIsSubmitting(false);
}
};

const handleClose = () => {
if (feedback.trim() || name.trim() || email.trim()) {
Alert.alert(
'Discard Feedback?',
'You have unsaved feedback. Are you sure you want to close?',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Discard', style: 'destructive', onPress: onClose },
],
);
} else {
onClose();
}
};

return (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={handleClose}
>
<View style={styles.overlay}>
<View style={styles.modalContainer}>
<YStack gap="$4" padding="$4">
<XStack justifyContent="space-between" alignItems="center">
<Text style={styles.title}>Send Feedback</Text>
<Button
size="$2"
variant="outlined"
onPress={handleClose}
disabled={isSubmitting}
>
</Button>
</XStack>

<YStack gap="$2">
<Caption style={styles.label}>Category</Caption>
<XStack gap="$2" flexWrap="wrap">
{categories.map(cat => (
<Button
key={cat.value}
size="$2"
backgroundColor={
category === cat.value ? white : 'transparent'
}
color={category === cat.value ? black : white}
borderColor={white}
onPress={() => setCategory(cat.value)}
disabled={isSubmitting}
>
{cat.label}
</Button>
))}
</XStack>
</YStack>

<YStack gap="$2">
<Caption style={styles.label}>
Contact Information (Optional)
</Caption>
<XStack gap="$2">
<TextInput
style={[styles.textInput, { flex: 1, minHeight: 48 }]}
placeholder="Name"
placeholderTextColor={slate400}
value={name}
onChangeText={setName}
editable={!isSubmitting}
/>
<TextInput
style={[styles.textInput, { flex: 1, minHeight: 48 }]}
placeholder="Email"
placeholderTextColor={slate400}
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
editable={!isSubmitting}
/>
</XStack>
</YStack>

<YStack gap="$2">
<Caption style={styles.label}>Your Feedback</Caption>
<TextInput
style={styles.textInput}
placeholder="Tell us what you think, report a bug, or suggest a feature..."
placeholderTextColor={slate400}
value={feedback}
onChangeText={setFeedback}
multiline
numberOfLines={6}
textAlignVertical="top"
editable={!isSubmitting}
/>
</YStack>

<Button
size="$4"
backgroundColor={white}
color={black}
onPress={handleSubmit}
disabled={isSubmitting || !feedback.trim()}
>
{isSubmitting ? 'Submitting...' : 'Submit Feedback'}
</Button>
</YStack>
</View>
</View>
</Modal>
);
};

const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
modalContainer: {
backgroundColor: zinc900,
borderRadius: 16,
width: '100%',
maxWidth: 400,
maxHeight: '80%',
borderWidth: 1,
borderColor: zinc800,
},
title: {
fontFamily: advercase,
fontSize: 24,
fontWeight: '600',
color: white,
},
label: {
fontFamily: dinot,
color: white,
fontSize: 14,
fontWeight: '500',
},
textInput: {
backgroundColor: black,
borderWidth: 1,
borderColor: zinc800,
borderRadius: 8,
padding: 12,
color: white,
fontSize: 16,
fontFamily: dinot,
minHeight: 120,
},
});

export default FeedbackModal;
Loading
Loading