import 'dart:io'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; class AppConfig { // ─── API Configuration ──────────────────────────────────────────────────── // // Dual-Mode Configuration: // - If public ngrok is running and active, the app will use it automatically. // - If ngrok is down/inactive or unreachable, it falls back to the LAN/Local URL. // // LAN / WiFi / Emulator URL (port 8000 = nginx gateway) // - Physical Device on same Wi-Fi: use 'http://192.168.70.7:8000/api/v1' // - Android Emulator: use 'http://10.0.2.2:8000/api/v1' // - iOS Simulator: use 'http://localhost:8000/api/v1' static const String _lanBaseUrl = 'http://192.168.70.7:8000/api/v1'; // Ngrok public tunnel URL (update ini setiap ngrok di-restart) static const String _ngrokBaseUrl = 'https://unlocated-waylon-potently.ngrok-free.dev/api/v1'; // Dynamic Base URL determined at startup static String apiBaseUrl = _lanBaseUrl; /// Dynamic URL resolver: Checks if the Ngrok tunnel is active. /// If it returns a valid response (not 502/503/504), we use Ngrok. /// Otherwise, we fall back to the LAN/Local base URL. static Future initializeApiBaseUrl() async { final client = HttpClient(); client.connectionTimeout = const Duration(seconds: 2); try { debugPrint('AppConfig: Checking public Ngrok tunnel at $_ngrokBaseUrl/auth/login...'); final uri = Uri.parse('$_ngrokBaseUrl/auth/login'); final request = await client.postUrl(uri); // Add headers to bypass the ngrok browser warning page request.headers.add('ngrok-skip-browser-warning', 'true'); request.headers.contentType = ContentType.json; request.write('{}'); final response = await request.close(); final contentType = response.headers.contentType?.mimeType ?? ''; final ngrokErrorCode = response.headers.value('ngrok-error-code'); debugPrint('AppConfig: Ngrok response status: ${response.statusCode}, Content-Type: $contentType, Ngrok-Error-Code: $ngrokErrorCode'); // An active tunnel connected to our backend: // 1. Should not return an ngrok-error-code header. // 2. The response content-type should be JSON (our login API returns JSON). // 3. Status code should not be 502, 503, or 504. if (ngrokErrorCode == null && contentType.contains('json') && response.statusCode != 502 && response.statusCode != 503 && response.statusCode != 504) { apiBaseUrl = _ngrokBaseUrl; debugPrint('AppConfig: Ngrok tunnel is ACTIVE and backend responded. API Base URL: $apiBaseUrl'); return; } else { debugPrint('AppConfig: Ngrok is offline, inactive, or returned an error page.'); } } catch (e) { debugPrint('AppConfig: Ngrok connection failed or timed out: $e'); } finally { client.close(); } apiBaseUrl = _lanBaseUrl; debugPrint('AppConfig: Using Local/LAN API Base URL: $apiBaseUrl'); } static const String loginEndpoint = '/auth/login'; static const String uploadDocumentEndpoint = '/documents/upload'; static const String fetchDocumentsEndpoint = '/documents'; // App Constants static const String appName = 'Prima Mart Scanner'; static const String appVersion = '1.0.0'; // Colors - Prima Fresh Mart Theme static const Color primaryColor = Color(0xFF6EBE4A); // Prima Green static const Color secondaryColor = Color(0xFF005DAA); // Prima Blue static const Color accentColor = Color(0xFFFFCC00); // Prima Yellow static const Color backgroundColor = Color(0xFFF8F9FA); // Off-white static const Color surfaceColor = Colors.white; static const Color errorColor = Color(0xFFD32F2F); static const Color successColor = Color(0xFF388E3C); // Text Colors static const Color textPrimary = Color(0xFF212121); static const Color textSecondary = Color(0xFF757575); static const Color textOnPrimary = Colors.white; // Typography Config // Using Google Fonts - Inter for clean, modern look static TextStyle get h1 => GoogleFonts.inter( fontSize: 32, fontWeight: FontWeight.bold, color: textPrimary, ); static TextStyle get h2 => GoogleFonts.inter( fontSize: 24, fontWeight: FontWeight.w600, color: textPrimary, ); static TextStyle get h3 => GoogleFonts.inter( fontSize: 20, fontWeight: FontWeight.w600, color: textPrimary, ); static TextStyle get bodyLarge => GoogleFonts.inter( fontSize: 16, fontWeight: FontWeight.normal, color: textPrimary, ); static TextStyle get bodyMedium => GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.normal, color: textPrimary, ); static TextStyle get bodySmall => GoogleFonts.inter( fontSize: 12, fontWeight: FontWeight.normal, color: textSecondary, ); // Theme Data Builder static ThemeData get lightTheme { return ThemeData( primaryColor: primaryColor, scaffoldBackgroundColor: backgroundColor, colorScheme: const ColorScheme.light( primary: primaryColor, secondary: secondaryColor, surface: surfaceColor, error: errorColor, ), appBarTheme: AppBarTheme( backgroundColor: surfaceColor, elevation: 0, centerTitle: true, iconTheme: const IconThemeData(color: textPrimary), titleTextStyle: h2.copyWith(color: textPrimary), ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: primaryColor, foregroundColor: textOnPrimary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), textStyle: bodyLarge.copyWith(fontWeight: FontWeight.w600), ), ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: surfaceColor, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: primaryColor, width: 2), ), labelStyle: bodyMedium.copyWith(color: textSecondary), ), ); } }