feat(mobile): add settings tab with feedback client

This commit is contained in:
saravanakumardb1 2026-03-31 00:10:50 -07:00
parent d26d9946b3
commit 746cba74ff
5 changed files with 238 additions and 0 deletions

View File

@ -21,6 +21,7 @@
"@bytelyst/broadcast-client": "^0.1.0",
"@bytelyst/design-tokens": "^0.1.0",
"@bytelyst/diagnostics-client": "^0.1.0",
"@bytelyst/feedback-client": "^0.1.0",
"@bytelyst/feature-flag-client": "^0.1.0",
"@bytelyst/kill-switch-client": "^0.1.0",
"@bytelyst/offline-queue": "^0.1.0",

View File

@ -7,6 +7,7 @@ export default function TabLayout() {
<Tabs.Screen name="search" options={{ title: 'Search' }} />
<Tabs.Screen name="capture" options={{ title: 'Capture' }} />
<Tabs.Screen name="inbox" options={{ title: 'Inbox' }} />
<Tabs.Screen name="settings" options={{ title: 'Settings' }} />
</Tabs>
);
}

View File

@ -0,0 +1,215 @@
import { useState } from 'react';
import { Alert, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import { router } from 'expo-router';
import { useAuthStore, type AuthState } from '../../store/auth-store';
import { getFeedbackClient } from '../../lib/feedback-client';
import { colors } from '../../theme';
type FeedbackType = 'bug' | 'feature' | 'praise' | 'other';
export default function SettingsScreen() {
const email = useAuthStore((state: AuthState) => state.email);
const signOut = useAuthStore((state: AuthState) => state.signOut);
const [feedbackType, setFeedbackType] = useState<FeedbackType>('bug');
const [feedbackTitle, setFeedbackTitle] = useState('');
const [feedbackBody, setFeedbackBody] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
async function submitFeedback(): Promise<void> {
const title = feedbackTitle.trim();
if (!title || isSubmitting) {
return;
}
setIsSubmitting(true);
try {
await getFeedbackClient().submitWithScreenshot({
type: feedbackType,
title,
body: feedbackBody.trim() || undefined,
platform: 'ios',
});
setFeedbackTitle('');
setFeedbackBody('');
Alert.alert('Feedback sent', 'Thanks for sharing your feedback.');
} catch {
Alert.alert('Feedback failed', 'Unable to submit feedback right now. Please try again later.');
} finally {
setIsSubmitting(false);
}
}
return (
<ScrollView contentContainerStyle={styles.container}>
<Text style={styles.title}>Settings</Text>
<Text style={styles.subtitle}>Manage account and share feedback from mobile.</Text>
<View style={styles.card}>
<Text style={styles.cardTitle}>Account</Text>
<Text style={styles.metaLabel}>Signed in as</Text>
<Text style={styles.metaValue}>{email ?? 'Unknown account'}</Text>
<Pressable
style={styles.secondaryButton}
onPress={() => {
signOut();
router.replace('/auth');
}}
>
<Text style={styles.secondaryButtonText}>Sign out</Text>
</Pressable>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>Send feedback</Text>
<View style={styles.typeRow}>
{(['bug', 'feature', 'praise', 'other'] as const).map((type) => {
const isActive = type === feedbackType;
return (
<Pressable
key={type}
style={[styles.typeChip, isActive ? styles.typeChipActive : null]}
onPress={() => setFeedbackType(type)}
>
<Text style={[styles.typeChipText, isActive ? styles.typeChipTextActive : null]}>{type}</Text>
</Pressable>
);
})}
</View>
<TextInput
value={feedbackTitle}
onChangeText={setFeedbackTitle}
placeholder="Feedback title"
placeholderTextColor={colors.textTertiary}
style={styles.input}
/>
<TextInput
value={feedbackBody}
onChangeText={setFeedbackBody}
placeholder="Details (optional)"
placeholderTextColor={colors.textTertiary}
style={[styles.input, styles.bodyInput]}
multiline
textAlignVertical="top"
/>
<Pressable
style={[styles.primaryButton, feedbackTitle.trim().length === 0 || isSubmitting ? styles.buttonDisabled : null]}
disabled={feedbackTitle.trim().length === 0 || isSubmitting}
onPress={() => {
void submitFeedback();
}}
>
<Text style={styles.primaryButtonText}>{isSubmitting ? 'Submitting…' : 'Submit feedback'}</Text>
</Pressable>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: colors.bgCanvas,
minHeight: '100%',
padding: 20,
gap: 14,
},
title: {
color: colors.textPrimary,
fontSize: 28,
fontWeight: '700',
},
subtitle: {
color: colors.textSecondary,
fontSize: 15,
lineHeight: 21,
},
card: {
borderRadius: 16,
borderWidth: 1,
borderColor: colors.borderDefault,
backgroundColor: colors.surfaceCard,
padding: 16,
gap: 10,
},
cardTitle: {
color: colors.textPrimary,
fontSize: 17,
fontWeight: '700',
},
metaLabel: {
color: colors.textSecondary,
fontSize: 13,
textTransform: 'uppercase',
letterSpacing: 0.8,
},
metaValue: {
color: colors.textPrimary,
fontSize: 15,
fontWeight: '600',
},
typeRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
typeChip: {
borderRadius: 999,
borderWidth: 1,
borderColor: colors.borderDefault,
backgroundColor: colors.bgElevated,
paddingHorizontal: 10,
paddingVertical: 6,
},
typeChipActive: {
borderColor: colors.accentPrimary,
backgroundColor: colors.accentPrimary,
},
typeChipText: {
color: colors.textSecondary,
fontSize: 12,
fontWeight: '600',
textTransform: 'capitalize',
},
typeChipTextActive: {
color: colors.textPrimary,
},
input: {
borderRadius: 12,
borderWidth: 1,
borderColor: colors.borderDefault,
backgroundColor: colors.bgElevated,
color: colors.textPrimary,
paddingHorizontal: 12,
paddingVertical: 10,
},
bodyInput: {
minHeight: 96,
},
primaryButton: {
borderRadius: 10,
backgroundColor: colors.accentPrimary,
paddingHorizontal: 14,
paddingVertical: 10,
alignItems: 'center',
},
primaryButtonText: {
color: colors.textPrimary,
fontWeight: '700',
},
secondaryButton: {
borderRadius: 10,
borderWidth: 1,
borderColor: colors.borderDefault,
paddingHorizontal: 14,
paddingVertical: 10,
alignItems: 'center',
},
secondaryButtonText: {
color: colors.textSecondary,
fontWeight: '600',
},
buttonDisabled: {
opacity: 0.6,
},
});

View File

@ -0,0 +1,16 @@
import { createFeedbackClient, type FeedbackClient } from '@bytelyst/feedback-client';
import { API_CONFIG } from '../api/config';
import { getAuthClient } from '../api/auth';
let feedbackClient: FeedbackClient | null = null;
export function getFeedbackClient(): FeedbackClient {
if (!feedbackClient) {
feedbackClient = createFeedbackClient({
baseUrl: API_CONFIG.platformBaseUrl,
getAuthToken: () => getAuthClient().getAccessToken(),
});
}
return feedbackClient;
}

5
pnpm-lock.yaml generated
View File

@ -108,6 +108,9 @@ importers:
'@bytelyst/feature-flag-client':
specifier: ^0.1.0
version: 0.1.0
'@bytelyst/feedback-client':
specifier: ^0.1.0
version: 0.1.0(zod@4.3.6)
'@bytelyst/kill-switch-client':
specifier: ^0.1.0
version: 0.1.0
@ -8406,7 +8409,9 @@ snapshots:
metro-runtime: 0.83.5
transitivePeerDependencies:
- '@babel/core'
- bufferutil
- supports-color
- utf-8-validate
'@react-native/normalize-colors@0.83.2': {}