import 'package:flutter/material.dart'; import 'package:awesome_snackbar_content/awesome_snackbar_content.dart'; import 'package:flutter/foundation.dart'; import 'package:supabase_flutter/supabase_flutter.dart' show PostgrestException; /// A global messenger key used to show snackbars from contexts without a /// Scaffold (e.g. dialogs, background callbacks, or tests). final GlobalKey scaffoldMessengerKey = GlobalKey(); /// Helper wrappers around `awesome_snackbar_content` so that callers /// can show snackbars with a consistent look and only specify a message and /// semantic type. /// Enumeration used by callers; the mapping to the package’s /// [ContentType] is internal. enum SnackType { success, info, warning, error } ContentType _mapSnackType(SnackType t) { switch (t) { case SnackType.success: return ContentType.success; case SnackType.info: return ContentType.help; case SnackType.warning: return ContentType.warning; case SnackType.error: return ContentType.failure; } } /// Core function used by all of the convenience helpers below. void showAwesomeSnackBar( BuildContext context, { required String title, required String message, required SnackType snackType, bool retry = true, }) { // Add margin and padding so even very short messages feel substantial. final snackBar = SnackBar( elevation: 0, behavior: SnackBarBehavior.floating, // give floating snackbar some breathing room margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: EdgeInsets.zero, backgroundColor: Colors.transparent, content: Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: AwesomeSnackbarContent( title: title, message: message, contentType: _mapSnackType(snackType), ), ), ); // Prefer a local ScaffoldMessenger when available, but prefer the global // `scaffoldMessengerKey` if it's already ready. This increases reliability // when callers are inside dialogs or other overlays. final globalMessenger = scaffoldMessengerKey.currentState; final localMessenger = ScaffoldMessenger.maybeOf(context); final messenger = localMessenger ?? globalMessenger; if (messenger == null) { if (kDebugMode) { debugPrint( 'showAwesomeSnackBar: no messenger available; scheduling fallback', ); } // If the scaffold messenger is not available yet, schedule a post-frame // callback that uses the global messenger (if it becomes available). if (retry) { WidgetsBinding.instance.addPostFrameCallback((_) { final gm = scaffoldMessengerKey.currentState; if (gm != null) { try { gm ..hideCurrentSnackBar() ..showSnackBar(snackBar); if (kDebugMode) { debugPrint( 'showAwesomeSnackBar: shown via global messenger (post-frame)', ); } } catch (e) { if (kDebugMode) { debugPrint('showAwesomeSnackBar: post-frame show failed: $e'); } } } else if (kDebugMode) { debugPrint( 'showAwesomeSnackBar: global messenger still null after frame', ); } }); } return; } try { messenger ..hideCurrentSnackBar() ..showSnackBar(snackBar); if (kDebugMode) { debugPrint( 'showAwesomeSnackBar: shown via ${identical(messenger, globalMessenger) ? 'global' : 'local'} messenger', ); } } catch (e) { if (kDebugMode) debugPrint('showAwesomeSnackBar: show failed: $e'); } } void showSuccessSnackBar(BuildContext context, String message) { showAwesomeSnackBar( context, title: 'Success', message: message, snackType: SnackType.success, ); } void showErrorSnackBar(BuildContext context, String message) { showAwesomeSnackBar( context, title: 'Error', message: message, snackType: SnackType.error, ); } void showInfoSnackBar(BuildContext context, String message) { showAwesomeSnackBar( context, title: 'Info', message: message, snackType: SnackType.info, ); } void showWarningSnackBar(BuildContext context, String message) { showAwesomeSnackBar( context, title: 'Warning', message: message, snackType: SnackType.warning, ); } /// Global helpers that use the app-level `scaffoldMessengerKey` directly. /// Use these from places where a valid `BuildContext` with a Scaffold may /// not be available (e.g. dialog builders or background callbacks). void showAwesomeSnackBarGlobal({ required String title, required String message, required SnackType snackType, }) { final gm = scaffoldMessengerKey.currentState; if (gm == null) { if (kDebugMode) { debugPrint('showAwesomeSnackBarGlobal: global messenger null'); } return; } final snackBar = SnackBar( elevation: 0, behavior: SnackBarBehavior.floating, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: EdgeInsets.zero, backgroundColor: Colors.transparent, content: Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), child: AwesomeSnackbarContent( title: title, message: message, contentType: _mapSnackType(snackType), ), ), ); try { gm ..hideCurrentSnackBar() ..showSnackBar(snackBar); if (kDebugMode) { debugPrint('showAwesomeSnackBarGlobal: shown'); } } catch (e) { if (kDebugMode) { debugPrint('showAwesomeSnackBarGlobal: show failed: $e'); } } } void showSuccessSnackBarGlobal(String message) => showAwesomeSnackBarGlobal( title: 'Success', message: message, snackType: SnackType.success, ); void showErrorSnackBarGlobal(String message) => showAwesomeSnackBarGlobal( title: 'Error', message: message, snackType: SnackType.error, ); void showInfoSnackBarGlobal(String message) => showAwesomeSnackBarGlobal( title: 'Info', message: message, snackType: SnackType.info, ); void showWarningSnackBarGlobal(String message) => showAwesomeSnackBarGlobal( title: 'Warning', message: message, snackType: SnackType.warning, ); /// Returns true when [error] is a network-connectivity failure (socket closed, /// no route to host, failed DNS lookup, etc.). /// /// When this is true in a Brick-enabled app, the write was already queued in /// the offline SQLite store and will sync automatically when the device /// reconnects — so the UI should treat it as a success rather than an error. bool isOfflineSaveError(Object error) { if (error is PostgrestException) { // Null code = no HTTP response received at all (pure network failure). if (error.code == null) return true; // Brick's RestOfflineQueueClient catches network errors and returns a // synthetic StreamedResponse(501, body: 'unknown internal error') rather // than rethrowing. PostgREST then parses this as // PostgrestException(code: '501', message: 'unknown internal error'). // This is the primary offline indicator when Supabase is used with Brick. if (error.code == '501' && error.message == 'unknown internal error') { return true; } // Fallback: Supabase may wrap a SocketException in a PostgrestException // message field before it reaches Brick's queue (rare, but defensive). final pmsg = error.message.toLowerCase(); if (pmsg.contains('socketexception') || pmsg.contains('failed host lookup') || pmsg.contains('connection refused') || pmsg.contains('network is unreachable') || pmsg.contains('no route to host') || pmsg.contains('connection timed out') || pmsg.contains('failed to connect')) { return true; } } // Non-PostgrestException network errors (SocketException, ClientException) // that propagate before Brick's HTTP interceptor can catch them. final msg = error.toString().toLowerCase(); return msg.contains('socketexception') || msg.contains('socket') && msg.contains('exception') || msg.contains('connection refused') || msg.contains('network is unreachable') || msg.contains('no route to host') || msg.contains('failed host lookup') || msg.contains('connection reset') || msg.contains('broken pipe') || msg.contains('connection timed out') || msg.contains('clientexception') && msg.contains('network') || msg.contains('failed to connect') || msg.contains('errno = 111') || // ECONNREFUSED msg.contains('errno = 101'); // ENETUNREACH }