Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
6 changes: 3 additions & 3 deletions app/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1148.0)
aws-sdk-core (3.229.0)
aws-partitions (1.1150.0)
aws-sdk-core (3.230.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
Expand Down Expand Up @@ -99,7 +99,7 @@ GEM
drb (2.2.3)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.16.0)
ethon (0.17.0)
ffi (>= 1.15.0)
excon (0.112.0)
faraday (1.10.4)
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 { Caption } from '@/components/typography/Caption';
import { black, slate400, white, zinc800, zinc900 } from '@/utils/colors';
import { advercase, dinot } from '@/utils/fonts';

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