diff --git a/sojorn_app/lib/services/network_service.dart b/sojorn_app/lib/services/network_service.dart new file mode 100644 index 0000000..af1580d --- /dev/null +++ b/sojorn_app/lib/services/network_service.dart @@ -0,0 +1,41 @@ +import 'dart:async'; +import 'package:connectivity_plus/connectivity_plus.dart'; + +/// Service for monitoring network connectivity status +class NetworkService { + static final NetworkService _instance = NetworkService._internal(); + factory NetworkService() => _instance; + NetworkService._internal(); + + final Connectivity _connectivity = Connectivity(); + final StreamController _connectionController = + StreamController.broadcast(); + + Stream get connectionStream => _connectionController.stream; + bool _isConnected = true; + + bool get isConnected => _isConnected; + + /// Initialize the network service and start monitoring + void initialize() { + _connectivity.onConnectivityChanged.listen((List results) { + final result = results.isNotEmpty ? results.first : ConnectivityResult.none; + _isConnected = result != ConnectivityResult.none; + _connectionController.add(_isConnected); + }); + + // Check initial state + _checkConnection(); + } + + Future _checkConnection() async { + final results = await _connectivity.checkConnectivity(); + final result = results.isNotEmpty ? results.first : ConnectivityResult.none; + _isConnected = result != ConnectivityResult.none; + _connectionController.add(_isConnected); + } + + void dispose() { + _connectionController.close(); + } +} diff --git a/sojorn_app/lib/utils/error_handler.dart b/sojorn_app/lib/utils/error_handler.dart new file mode 100644 index 0000000..c8549ef --- /dev/null +++ b/sojorn_app/lib/utils/error_handler.dart @@ -0,0 +1,87 @@ +import 'dart:io'; +import 'dart:async'; +import 'package:flutter/material.dart'; + +/// Global error handler for consistent error messaging and logging +class ErrorHandler { + /// Handle an error and optionally show a snackbar to the user + static void handleError( + dynamic error, { + required BuildContext context, + String? userMessage, + bool showSnackbar = true, + }) { + final displayMessage = _getDisplayMessage(error, userMessage); + + // Log to console (in production, send to analytics/crash reporting) + _logError(error, displayMessage); + + if (showSnackbar && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(displayMessage), + action: SnackBarAction( + label: 'Dismiss', + onPressed: () {}, + ), + duration: const Duration(seconds: 4), + backgroundColor: Colors.red[700], + ), + ); + } + } + + /// Get user-friendly error message + static String _getDisplayMessage(dynamic error, String? userMessage) { + if (userMessage != null) return userMessage; + + if (error is SocketException) { + return 'No internet connection. Please check your network.'; + } else if (error is TimeoutException) { + return 'Request timed out. Please try again.'; + } else if (error is FormatException) { + return 'Invalid data format received.'; + } else if (error.toString().contains('401')) { + return 'Authentication error. Please sign in again.'; + } else if (error.toString().contains('403')) { + return 'You don\'t have permission to do that.'; + } else if (error.toString().contains('404')) { + return 'Resource not found.'; + } else if (error.toString().contains('500')) { + return 'Server error. Please try again later.'; + } else { + return 'Something went wrong. Please try again.'; + } + } + + /// Log error for debugging/analytics + static void _logError(dynamic error, String message) { + // In production, send to Sentry, Firebase Crashlytics, etc. + debugPrint('ERROR: $message'); + debugPrint('Details: ${error.toString()}'); + if (error is Error) { + debugPrint('Stack trace: ${error.stackTrace}'); + } + } +} + +/// Wrapper for async operations with automatic error handling +Future safeExecute({ + required Future Function() operation, + required BuildContext context, + String? errorMessage, + bool showError = true, +}) async { + try { + return await operation(); + } catch (e) { + if (showError) { + ErrorHandler.handleError( + e, + context: context, + userMessage: errorMessage, + ); + } + return null; + } +} diff --git a/sojorn_app/lib/utils/retry_helper.dart b/sojorn_app/lib/utils/retry_helper.dart new file mode 100644 index 0000000..326a0c5 --- /dev/null +++ b/sojorn_app/lib/utils/retry_helper.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +/// Helper for retrying failed operations with exponential backoff +class RetryHelper { + /// Retry an operation with exponential backoff + static Future retry({ + required Future Function() operation, + int maxAttempts = 3, + Duration initialDelay = const Duration(seconds: 1), + double backoffMultiplier = 2.0, + bool Function(dynamic error)? retryIf, + }) async { + int attempt = 0; + Duration delay = initialDelay; + + while (true) { + try { + return await operation(); + } catch (e) { + attempt++; + + // Check if we should retry this error + if (retryIf != null && !retryIf(e)) { + rethrow; + } + + if (attempt >= maxAttempts) { + rethrow; // Give up after max attempts + } + + // Wait before retrying with exponential backoff + await Future.delayed(delay); + delay = Duration( + milliseconds: (delay.inMilliseconds * backoffMultiplier).round(), + ); + } + } + } + + /// Retry specifically for network operations + static Future retryNetwork({ + required Future Function() operation, + int maxAttempts = 3, + }) async { + return retry( + operation: operation, + maxAttempts: maxAttempts, + retryIf: (error) { + // Retry on network errors, timeouts, and 5xx server errors + final errorStr = error.toString().toLowerCase(); + return errorStr.contains('socket') || + errorStr.contains('timeout') || + errorStr.contains('500') || + errorStr.contains('502') || + errorStr.contains('503') || + errorStr.contains('504'); + }, + ); + } +} diff --git a/sojorn_app/lib/widgets/offline_indicator.dart b/sojorn_app/lib/widgets/offline_indicator.dart new file mode 100644 index 0000000..f217726 --- /dev/null +++ b/sojorn_app/lib/widgets/offline_indicator.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import '../services/network_service.dart'; + +/// Banner that appears at top of screen when offline +class OfflineIndicator extends StatelessWidget { + const OfflineIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: NetworkService().connectionStream, + initialData: NetworkService().isConnected, + builder: (context, snapshot) { + final isConnected = snapshot.data ?? true; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: isConnected ? 0 : 30, + color: Colors.orange[700], + child: !isConnected + ? Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.wifi_off, size: 16, color: Colors.white), + const SizedBox(width: 8), + Text( + 'No internet connection', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ) + : null, + ); + }, + ); + } +}