diff --git a/mobile/package.json b/mobile/package.json index bd7a6b9..9d66794 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -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", diff --git a/mobile/src/app/(tabs)/_layout.tsx b/mobile/src/app/(tabs)/_layout.tsx index fb729ba..4477ae2 100644 --- a/mobile/src/app/(tabs)/_layout.tsx +++ b/mobile/src/app/(tabs)/_layout.tsx @@ -7,6 +7,7 @@ export default function TabLayout() { + ); } diff --git a/mobile/src/app/(tabs)/settings.tsx b/mobile/src/app/(tabs)/settings.tsx new file mode 100644 index 0000000..db53778 --- /dev/null +++ b/mobile/src/app/(tabs)/settings.tsx @@ -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('bug'); + const [feedbackTitle, setFeedbackTitle] = useState(''); + const [feedbackBody, setFeedbackBody] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + async function submitFeedback(): Promise { + 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 ( + + Settings + Manage account and share feedback from mobile. + + + Account + Signed in as + {email ?? 'Unknown account'} + { + signOut(); + router.replace('/auth'); + }} + > + Sign out + + + + + Send feedback + + {(['bug', 'feature', 'praise', 'other'] as const).map((type) => { + const isActive = type === feedbackType; + return ( + setFeedbackType(type)} + > + {type} + + ); + })} + + + + + + { + void submitFeedback(); + }} + > + {isSubmitting ? 'Submitting…' : 'Submit feedback'} + + + + ); +} + +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, + }, +}); diff --git a/mobile/src/lib/feedback-client.ts b/mobile/src/lib/feedback-client.ts new file mode 100644 index 0000000..5464813 --- /dev/null +++ b/mobile/src/lib/feedback-client.ts @@ -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; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3802af..e1f67bb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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': {}