import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../../../../core/network/supabase_status.dart'; import '../../../auth/presentation/controllers/auth_controller.dart'; import '../../../nutrition/presentation/controllers/nutrition_controller.dart'; import '../controllers/integrations_controller.dart'; /// Application settings hub: identity & session, daily tracking targets, /// encrypted-storage status, and health integrations. class SettingsScreen extends ConsumerWidget { const SettingsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final integrations = ref.watch(integrationsControllerProvider); ref.listen(integrationsControllerProvider, (previous, next) { if (next.errorMessage != null && next.errorMessage != previous?.errorMessage) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(next.errorMessage!), backgroundColor: Theme.of(context).colorScheme.error, ), ); ref.read(integrationsControllerProvider.notifier).clearError(); } }); return Scaffold( appBar: AppBar(title: const Text('Settings')), body: ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), children: [ const _IdentitySection(), const SizedBox(height: 16), const _TargetsSection(), const SizedBox(height: 16), const _SecuritySection(), const SizedBox(height: 16), _IntegrationsSection(state: integrations), ], ), ); } } /// Identity: signed-in email, Supabase connection status, and sign-out. class _IdentitySection extends ConsumerWidget { const _IdentitySection(); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final email = _currentEmail(); final authState = ref.watch(authControllerProvider); final isSigningOut = authState.isLoading; return _SettingsCard( title: 'Identity', icon: Icons.person_outline, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.email_outlined, size: 18, color: theme.colorScheme.outline), const SizedBox(width: 8), Expanded( child: Text( email ?? 'Not signed in', style: theme.textTheme.bodyMedium, ), ), ], ), const SizedBox(height: 8), Row( children: [ Icon(Icons.cloud_outlined, size: 18, color: theme.colorScheme.outline), const SizedBox(width: 8), Flexible( child: _StatusBadge( label: isSupabaseAvailable ? 'Supabase connected' : 'Offline', ok: isSupabaseAvailable, ), ), ], ), const SizedBox(height: 12), Align( alignment: Alignment.centerRight, child: FilledButton.tonalIcon( onPressed: email == null || isSigningOut ? null : () => _signOut(ref), icon: isSigningOut ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.logout), label: const Text('Sign Out'), ), ), ], ), ); } /// Authenticated user's email, or `null` when the SDK is unavailable or no /// session exists (e.g. debug dev bypass). String? _currentEmail() { if (!isSupabaseAvailable) return null; return Supabase.instance.client.auth.currentUser?.email; } Future _signOut(WidgetRef ref) => ref.read(authControllerProvider.notifier).signOut(); } /// Targets: editable daily protein (g) and water (oz) goals driving the /// nutrition progress meters and meal-plan rollups. class _TargetsSection extends ConsumerStatefulWidget { const _TargetsSection(); @override ConsumerState<_TargetsSection> createState() => _TargetsSectionState(); } class _TargetsSectionState extends ConsumerState<_TargetsSection> { late final TextEditingController _proteinController; late final TextEditingController _waterController; final GlobalKey _formKey = GlobalKey(); @override void initState() { super.initState(); final nutrition = ref.read(nutritionControllerProvider).valueOrNull; _proteinController = TextEditingController( text: _fmt(nutrition?.proteinTargetGrams ?? NutritionController.defaultProteinTargetGrams), ); _waterController = TextEditingController( text: _fmt(nutrition?.hydrationTargetOz ?? NutritionController.defaultHydrationTargetOz), ); } @override void dispose() { _proteinController.dispose(); _waterController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final nutrition = ref.watch(nutritionControllerProvider); final loaded = nutrition.valueOrNull != null; return _SettingsCard( title: 'Targets', icon: Icons.flag_outlined, child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextFormField( controller: _proteinController, enabled: loaded, decoration: const InputDecoration( labelText: 'Daily protein goal (g)', border: OutlineInputBorder(), isDense: true, prefixIcon: Icon(Icons.fitness_center), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: _positiveDouble, ), const SizedBox(height: 12), TextFormField( controller: _waterController, enabled: loaded, decoration: const InputDecoration( labelText: 'Daily water goal (oz)', border: OutlineInputBorder(), isDense: true, prefixIcon: Icon(Icons.water_drop_outlined), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: _positiveDouble, ), const SizedBox(height: 12), Align( alignment: Alignment.centerRight, child: FilledButton.icon( onPressed: loaded ? _save : null, icon: const Icon(Icons.save_outlined), label: const Text('Save Targets'), ), ), ], ), ), ); } String? _positiveDouble(String? value) { final parsed = double.tryParse((value ?? '').trim()); if (parsed == null || parsed <= 0) return 'Must be a positive number'; return null; } void _save() { if (!(_formKey.currentState?.validate() ?? false)) return; ref.read(nutritionControllerProvider.notifier).updateTargets( proteinTargetGrams: double.parse(_proteinController.text.trim()), hydrationTargetOz: double.parse(_waterController.text.trim()), ); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Targets updated.')), ); } static String _fmt(double value) { final rounded = value.roundToDouble(); return rounded == value ? rounded.toStringAsFixed(0) : value.toStringAsFixed(1); } } /// Security & Storage: SQLCipher encryption status badge. /// /// Native builds open the drift database through SQLCipher with a /// vault-managed AES-256 key; web builds fall back to unencrypted WASM /// SQLite (SQLCipher cannot run in the browser). class _SecuritySection extends StatelessWidget { const _SecuritySection(); @override Widget build(BuildContext context) { final theme = Theme.of(context); const encrypted = !kIsWeb; return _SettingsCard( title: 'Security & Storage', icon: Icons.shield_outlined, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( encrypted ? Icons.lock_outline : Icons.lock_open_outlined, size: 18, color: encrypted ? Colors.green.shade700 : Colors.orange, ), const SizedBox(width: 8), const Flexible( child: _StatusBadge( label: encrypted ? 'SQLCipher AES-256 active' : 'Web storage (unencrypted)', ok: encrypted, ), ), ], ), const SizedBox(height: 8), Text( encrypted ? 'Health markers are persisted in a SQLCipher-encrypted ' 'database; the master key lives in your platform keystore ' '(Keychain / EncryptedSharedPreferences / DPAPI).' : 'SQLCipher cannot run inside the browser — web builds ' 'store data in OPFS/IndexedDB without at-rest ' 'encryption. Use a native app for encrypted storage.', style: theme.textTheme.bodySmall ?.copyWith(color: theme.colorScheme.outline), ), ], ), ); } } /// Integrations: Apple Health / Health Connect / HealthGateway toggles. class _IntegrationsSection extends ConsumerWidget { const _IntegrationsSection({required this.state}); final IntegrationsState state; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); return _SettingsCard( title: 'Integrations', icon: Icons.sync_outlined, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile( contentPadding: EdgeInsets.zero, secondary: const Icon(Icons.apple), title: const Text('Apple Health'), subtitle: const Text('Sync weight & active energy (iOS)'), value: state.appleHealthEnabled, onChanged: state.isRequestingPermissions ? null : (enabled) => ref .read(integrationsControllerProvider.notifier) .setAppleHealthEnabled(enabled), ), SwitchListTile( contentPadding: EdgeInsets.zero, secondary: const Icon(Icons.fingerprint), title: const Text('Health Connect'), subtitle: const Text('Sync weight & active energy (Android)'), value: state.healthConnectEnabled, onChanged: state.isRequestingPermissions ? null : (enabled) => ref .read(integrationsControllerProvider.notifier) .setHealthConnectEnabled(enabled), ), SwitchListTile( contentPadding: EdgeInsets.zero, secondary: const Icon(Icons.cloud_sync_outlined), title: const Text('HealthGateway'), subtitle: const Text( 'Cloud partner sync (MyFitnessPal / Withings) with ' 'manual-entry fallback', ), value: state.healthGatewayEnabled, onChanged: (enabled) => ref .read(integrationsControllerProvider.notifier) .setHealthGatewayEnabled(enabled), ), if (state.isRequestingPermissions) Padding( padding: const EdgeInsets.only(top: 4), child: Row( children: [ const SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2), ), const SizedBox(width: 8), Text( 'Requesting health permissions…', style: theme.textTheme.labelSmall, ), ], ), ), ], ), ); } } /// Shared card chrome for a settings section. class _SettingsCard extends StatelessWidget { const _SettingsCard({ required this.title, required this.icon, required this.child, }); final String title; final IconData icon; final Widget child; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, size: 20, color: theme.colorScheme.primary), const SizedBox(width: 8), Text( title, style: theme.textTheme.titleSmall ?.copyWith(fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 12), child, ], ), ), ); } } /// Compact colored badge used for capability/status rows. class _StatusBadge extends StatelessWidget { const _StatusBadge({required this.label, required this.ok}); final String label; final bool ok; @override Widget build(BuildContext context) { final color = ok ? Colors.green.shade100 : Colors.orange.shade100; final textColor = ok ? Colors.green.shade800 : Colors.orange.shade900; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(8), ), child: Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: textColor, ), ), ); } }