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': {}