import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gimnasio_soma/core/config/app_env.dart'; import 'package:gimnasio_soma/core/config/env_banner.dart'; import 'package:gimnasio_soma/core/config/supabase_config.dart'; import 'package:gimnasio_soma/core/router/app_router.dart'; import 'package:gimnasio_soma/core/services/soma_logger.dart'; import 'package:gimnasio_soma/core/theme/soma_theme.dart'; /// Si se compiló para prod y no vino marcada por `soma_pro_updater`, relanza /// a través de él en vez de arrancar directo. Evita que alguien se quede en /// una versión vieja por abrir el .exe de la app sin pasar por el updater. /// Se salta en dev porque ahí no existe `soma_pro_updater` al lado. Future _redirectToUpdaterIfNeeded(List args) async { if (!AppEnv.isProd || args.contains('--from-updater')) return false; // En Windows/Linux esta app vive en una subcarpeta `app/` al lado del // updater (evita que sus carpetas `data/` se pisen); en macOS son // hermanos porque cada `.app` ya es autocontenido. final exeDir = File(Platform.resolvedExecutable).parent; // En macOS el exe vive en .app/Contents/MacOS/ — subimos 3 niveles para llegar // a la carpeta que contiene ambos .app bundles (donde está soma_pro_updater.app). // En Windows/Linux la app vive en app/ — subimos 1 nivel para llegar al updater. final updaterDir = Platform.isMacOS ? exeDir.parent.parent.parent : exeDir.parent; final sep = Platform.pathSeparator; final updaterName = Platform.isMacOS ? 'soma_pro_updater.app' : 'soma_pro_updater.exe'; final updaterPath = '${updaterDir.path}$sep$updaterName'; final exists = Platform.isMacOS ? Directory(updaterPath).existsSync() : File(updaterPath).existsSync(); if (!exists) return false; // Freno de rebote: si ya redirigimos hace poco, el updater ya nos relanzó // pero este proceso arrancó de nuevo sin `--from-updater` (en macOS, `open` // sin `-n` puede reactivar una instancia existente en vez de crear una // nueva, y ahí se pierde el `--args`). En vez de rebotar para siempre, // cortamos el ciclo y arrancamos directo. final bounceFile = File('${updaterDir.path}$sep.soma_redirect_bounce'); final nowMs = DateTime.now().millisecondsSinceEpoch; if (bounceFile.existsSync()) { final lastMs = int.tryParse(bounceFile.readAsStringSync().trim()) ?? 0; if (nowMs - lastMs < 8000) { try { bounceFile.deleteSync(); } catch (_) {} return false; } } bounceFile.writeAsStringSync('$nowMs'); if (Platform.isMacOS) { // `-n` fuerza una instancia nueva del updater en vez de reactivar una // existente, así el `open` de abajo siempre entrega los args frescos. await Process.start('open', ['-n', updaterPath], mode: ProcessStartMode.detached); } else { await Process.start(updaterPath, const [], mode: ProcessStartMode.detached); } // Mismo respiro que del lado del updater: si este proceso sale demasiado // rápido, macOS puede no llegar a transferirle el foco al proceso recién // lanzado y su ventana queda sin renderizar el primer frame. await Future.delayed(const Duration(milliseconds: 600)); return true; } void main(List args) async { if (await _redirectToUpdaterIfNeeded(args)) { exit(0); } runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); final log = SomaLogger.instance; log.detectPlatform(); log.info('App', 'Iniciando SOMA PRO...'); // Config de entorno horneada al compilar (--dart-define-from-file=config/.json). // Corta el arranque con un mensaje claro si se compiló sin pasar el archivo. AppEnv.assertConfigured(); log.info('App', 'Entorno: ${AppEnv.env}'); try { await SupabaseConfig.initialize(); log.info('App', 'Supabase inicializado'); } catch (e) { log.error('App', 'Error inicializando Supabase', detail: e.toString()); } // Captura errores de Flutter (widgets, rendering, etc.) FlutterError.onError = (details) { log.error('Flutter', details.exceptionAsString(), detail: details.stack?.toString().split('\n').take(5).join('\n')); }; runApp(const ProviderScope(child: MainApp())); }, (error, stack) { // Captura errores no manejados fuera del framework SomaLogger.instance.error('Zone', error.toString(), detail: stack.toString().split('\n').take(5).join('\n')); }); } class MainApp extends ConsumerWidget { const MainApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(routerProvider); return MaterialApp.router( title: 'SOMA PRO', debugShowCheckedModeBanner: false, theme: SomaTheme.dark, routerConfig: router, // Cinta "DEV" en cualquier build que no sea prod (ver EnvBanner). builder: (context, child) => EnvBanner(child: child ?? const SizedBox.shrink()), ); } }