feat(mobile): add settings tab with feedback client
This commit is contained in:
parent
d26d9946b3
commit
746cba74ff
@ -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",
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
215
mobile/src/app/(tabs)/settings.tsx
Normal file
215
mobile/src/app/(tabs)/settings.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
16
mobile/src/lib/feedback-client.ts
Normal file
16
mobile/src/lib/feedback-client.ts
Normal 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
5
pnpm-lock.yaml
generated
@ -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': {}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user