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 '../../../../core/platform/health_gateway.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 (Phase 3B): Apple Health / Health Connect (Samsung & Pixel) /// / SMART on FHIR rows with dynamic status indicators and Connect / /// Disconnect authorization buttons. /// /// Providers the current OS cannot host (e.g. HealthKit on Windows/Web) /// render desktop-safe disabled Connect buttons with an "Unsupported" /// badge instead of crashing into dead platform channels. class _IntegrationsSection extends ConsumerWidget { const _IntegrationsSection({required this.state}); final IntegrationsState state; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final controller = ref.read(integrationsControllerProvider.notifier); return _SettingsCard( title: 'Health Integrations', icon: Icons.cable_outlined, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _IntegrationRow( icon: Icons.apple, title: 'Apple Health', subtitle: state.nativeHealthUnsupported ? 'Weight, active energy & blood glucose (HealthKit) · ' 'Available on iOS/Android — on desktop use Log Weight ' 'on the Nutrition screen' : 'Weight, active energy & blood glucose (HealthKit)', status: state.appleHealthStatus, busy: state.isRequestingPermissions, onConnect: state.isRequestingPermissions ? null : () => controller.connectAppleHealth(), onDisconnect: state.isRequestingPermissions ? null : () => controller .disconnectProvider(HealthProviderType.appleHealth), ), const Divider(), _IntegrationRow( icon: Icons.fingerprint, title: 'Health Connect (Samsung & Pixel)', subtitle: state.nativeHealthUnsupported ? 'Federated biometrics from Samsung Health & Pixel sensors' ' · Available on Android — on desktop use Log Weight ' 'on the Nutrition screen' : 'Federated biometrics from Samsung Health & Pixel sensors', status: state.healthConnectStatus, busy: state.isRequestingPermissions, onConnect: state.isRequestingPermissions ? null : () => controller.connectHealthConnect(), onDisconnect: state.isRequestingPermissions ? null : () => controller .disconnectProvider(HealthProviderType.healthConnect), ), const Divider(), _IntegrationRow( icon: Icons.favorite_outline, title: 'Google Health', subtitle: 'Fitbit, Pixel & Wear OS data (replaces the Fitbit API)', status: state.googleHealthStatus, busy: state.isRequestingPermissions, onConnect: state.isRequestingPermissions ? null : () => controller.connectGoogleHealth(), onDisconnect: state.isRequestingPermissions ? null : () => controller .disconnectProvider(HealthProviderType.googleHealth), ), const Divider(), _IntegrationRow( icon: Icons.cloud_outlined, title: 'SMART on FHIR', subtitle: 'Enterprise lab panels (HbA1c, metabolic markers)', status: state.fhirStatus, busy: false, // The enterprise OAuth2 connector ships in a later phase; the // row surfaces live status but stays inert until provisioned. onConnect: null, onDisconnect: null, ), 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, ), ], ), ), ], ), ); } } /// A single provider row: icon, title, subtitle, dynamic status badge, and /// a Connect / Disconnect authorization button. class _IntegrationRow extends StatelessWidget { const _IntegrationRow({ required this.icon, required this.title, required this.subtitle, required this.status, required this.busy, required this.onConnect, required this.onDisconnect, }); final IconData icon; final String title; final String subtitle; final HealthConnectionStatus status; final bool busy; final VoidCallback? onConnect; final VoidCallback? onDisconnect; @override Widget build(BuildContext context) { final theme = Theme.of(context); final connected = status == HealthConnectionStatus.connected; final unsupported = status == HealthConnectionStatus.unsupportedPlatform; return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 20, color: theme.colorScheme.primary), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Flexible( child: Text(title, style: theme.textTheme.titleSmall), ), const SizedBox(width: 8), Flexible( child: _StatusBadge( label: _statusLabel(status), ok: connected, neutral: unsupported, ), ), ], ), const SizedBox(height: 2), Text( subtitle, style: theme.textTheme.bodySmall ?.copyWith(color: theme.colorScheme.outline), ), ], ), ), const SizedBox(width: 12), busy ? const SizedBox( width: 36, height: 24, child: CircularProgressIndicator(strokeWidth: 2), ) : connected ? OutlinedButton( onPressed: onDisconnect, child: const Text('Disconnect'), ) : FilledButton.tonal( // Desktop-safe disabled state: unsupported platforms // and un-provisioned connectors never fire a dead call. onPressed: unsupported ? null : onConnect, child: const Text('Connect'), ), ], ), ); } static String _statusLabel(HealthConnectionStatus status) { switch (status) { case HealthConnectionStatus.connected: return 'Connected'; case HealthConnectionStatus.notConfigured: return 'Not connected'; case HealthConnectionStatus.permissionDenied: return 'Permission denied'; case HealthConnectionStatus.unsupportedPlatform: return 'Unsupported'; } } } /// 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, this.neutral = false, }); final String label; final bool ok; /// Renders a grey "not applicable here" tone (e.g. Unsupported on /// desktop) instead of the attention-grabbing orange. final bool neutral; @override Widget build(BuildContext context) { final Color color; final Color textColor; if (neutral) { color = Colors.grey.shade200; textColor = Colors.grey.shade700; } else if (ok) { color = Colors.green.shade100; textColor = Colors.green.shade800; } else { color = Colors.orange.shade100; textColor = 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, ), ), ); } }