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/broadcast-client": "^0.1.0",
|
||||||
"@bytelyst/design-tokens": "^0.1.0",
|
"@bytelyst/design-tokens": "^0.1.0",
|
||||||
"@bytelyst/diagnostics-client": "^0.1.0",
|
"@bytelyst/diagnostics-client": "^0.1.0",
|
||||||
|
"@bytelyst/feedback-client": "^0.1.0",
|
||||||
"@bytelyst/feature-flag-client": "^0.1.0",
|
"@bytelyst/feature-flag-client": "^0.1.0",
|
||||||
"@bytelyst/kill-switch-client": "^0.1.0",
|
"@bytelyst/kill-switch-client": "^0.1.0",
|
||||||
"@bytelyst/offline-queue": "^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="search" options={{ title: 'Search' }} />
|
||||||
<Tabs.Screen name="capture" options={{ title: 'Capture' }} />
|
<Tabs.Screen name="capture" options={{ title: 'Capture' }} />
|
||||||
<Tabs.Screen name="inbox" options={{ title: 'Inbox' }} />
|
<Tabs.Screen name="inbox" options={{ title: 'Inbox' }} />
|
||||||
|
<Tabs.Screen name="settings" options={{ title: 'Settings' }} />
|
||||||
</Tabs>
|
</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':
|
'@bytelyst/feature-flag-client':
|
||||||
specifier: ^0.1.0
|
specifier: ^0.1.0
|
||||||
version: 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':
|
'@bytelyst/kill-switch-client':
|
||||||
specifier: ^0.1.0
|
specifier: ^0.1.0
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
@ -8406,7 +8409,9 @@ snapshots:
|
|||||||
metro-runtime: 0.83.5
|
metro-runtime: 0.83.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
|
- bufferutil
|
||||||
- supports-color
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
'@react-native/normalize-colors@0.83.2': {}
|
'@react-native/normalize-colors@0.83.2': {}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user