From 1ac954a09ef6067fd4d8671b08f8d2b23f4e84b8 Mon Sep 17 00:00:00 2001 From: vjrj Date: Fri, 10 Jul 2026 15:18:16 +0200 Subject: [PATCH] feat(block2): global 'you're offline' banner (network state) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feedback: offline, the social layer stops working with no clear reason. A slim amber strip now shows above every screen when there's no network (connectivity_ plus), so it's obvious WHY sharing is paused — the inventory keeps working. Wrapped once via the MaterialApp builder; connectivity check is guarded so it no-ops where the plugin is absent (tests/unsupported platforms). i18n en/es/pt. Analyzer clean. --- apps/app_seeds/lib/app.dart | 5 ++ apps/app_seeds/lib/i18n/en.i18n.json | 3 +- apps/app_seeds/lib/i18n/es.i18n.json | 3 +- apps/app_seeds/lib/i18n/pt.i18n.json | 3 +- apps/app_seeds/lib/i18n/strings.g.dart | 4 +- apps/app_seeds/lib/i18n/strings_en.g.dart | 4 ++ apps/app_seeds/lib/i18n/strings_es.g.dart | 2 + apps/app_seeds/lib/i18n/strings_pt.g.dart | 2 + apps/app_seeds/lib/ui/offline_banner.dart | 87 +++++++++++++++++++++++ apps/app_seeds/pubspec.yaml | 3 + pubspec.lock | 24 +++++++ 11 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 apps/app_seeds/lib/ui/offline_banner.dart diff --git a/apps/app_seeds/lib/app.dart b/apps/app_seeds/lib/app.dart index db5133f..ffb87b1 100644 --- a/apps/app_seeds/lib/app.dart +++ b/apps/app_seeds/lib/app.dart @@ -26,6 +26,7 @@ import 'ui/home_screen.dart'; import 'ui/intro_screen.dart'; import 'ui/inventory_list_screen.dart'; import 'ui/market_screen.dart'; +import 'ui/offline_banner.dart'; import 'ui/profile_screen.dart'; import 'ui/settings_screen.dart'; import 'ui/theme.dart'; @@ -212,6 +213,10 @@ class TaneApp extends StatelessWidget { GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], + // A global "you're offline" strip above every screen — makes it clear + // why the social layer is paused (the inventory still works). + builder: (context, child) => + OfflineBanner(child: child ?? const SizedBox.shrink()), routerConfig: _router, ), ), diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index 7b56325..42be1cd 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -8,7 +8,8 @@ "delete": "Delete", "edit": "Edit", "type": "Type", - "comingSoon": "Coming soon" + "comingSoon": "Coming soon", + "offline": "You're offline — sharing is paused" }, "home": { "tagline": "Share and grow local seeds", diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index fa2ba35..990c711 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -8,7 +8,8 @@ "delete": "Eliminar", "edit": "Editar", "type": "Tipo", - "comingSoon": "Pronto" + "comingSoon": "Pronto", + "offline": "Sin conexión — el compartir está en pausa" }, "home": { "tagline": "Comparte y cultiva semillas locales", diff --git a/apps/app_seeds/lib/i18n/pt.i18n.json b/apps/app_seeds/lib/i18n/pt.i18n.json index 698880d..ac483c9 100644 --- a/apps/app_seeds/lib/i18n/pt.i18n.json +++ b/apps/app_seeds/lib/i18n/pt.i18n.json @@ -8,7 +8,8 @@ "delete": "Eliminar", "edit": "Editar", "type": "Tipo", - "comingSoon": "Em breve" + "comingSoon": "Em breve", + "offline": "Sem ligação — a partilha está em pausa" }, "home": { "tagline": "Partilha e cultiva sementes locais", diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index 342d3c9..4239971 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 3 -/// Strings: 1073 (357 per locale) +/// Strings: 1076 (358 per locale) /// -/// Built on 2026-07-10 at 11:35 UTC +/// Built on 2026-07-10 at 13:17 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index ee506e0..537e057 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -113,6 +113,9 @@ class Translations$common$en { /// en: 'Coming soon' String get comingSoon => 'Coming soon'; + + /// en: 'You're offline — sharing is paused' + String get offline => 'You\'re offline — sharing is paused'; } // Path: home @@ -1755,6 +1758,7 @@ extension on Translations { 'common.edit' => 'Edit', 'common.type' => 'Type', 'common.comingSoon' => 'Coming soon', + 'common.offline' => 'You\'re offline — sharing is paused', 'home.tagline' => 'Share and grow local seeds', 'home.openMarket' => 'Market', 'home.openMarketSubtitle' => 'Discover and share seeds nearby', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index bec2514..341cc31 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -98,6 +98,7 @@ class _Translations$common$es extends Translations$common$en { @override String get edit => 'Editar'; @override String get type => 'Tipo'; @override String get comingSoon => 'Pronto'; + @override String get offline => 'Sin conexión — el compartir está en pausa'; } // Path: home @@ -1057,6 +1058,7 @@ extension on TranslationsEs { 'common.edit' => 'Editar', 'common.type' => 'Tipo', 'common.comingSoon' => 'Pronto', + 'common.offline' => 'Sin conexión — el compartir está en pausa', 'home.tagline' => 'Comparte y cultiva semillas locales', 'home.openMarket' => 'Mercado', 'home.openMarketSubtitle' => 'Descubre y comparte semillas cerca', diff --git a/apps/app_seeds/lib/i18n/strings_pt.g.dart b/apps/app_seeds/lib/i18n/strings_pt.g.dart index fd060db..8dcdf17 100644 --- a/apps/app_seeds/lib/i18n/strings_pt.g.dart +++ b/apps/app_seeds/lib/i18n/strings_pt.g.dart @@ -98,6 +98,7 @@ class _Translations$common$pt extends Translations$common$en { @override String get edit => 'Editar'; @override String get type => 'Tipo'; @override String get comingSoon => 'Em breve'; + @override String get offline => 'Sem ligação — a partilha está em pausa'; } // Path: home @@ -1053,6 +1054,7 @@ extension on TranslationsPt { 'common.edit' => 'Editar', 'common.type' => 'Tipo', 'common.comingSoon' => 'Em breve', + 'common.offline' => 'Sem ligação — a partilha está em pausa', 'home.tagline' => 'Partilha e cultiva sementes locais', 'home.openMarket' => 'Mercado', 'home.openMarketSubtitle' => 'Descobre e partilha sementes por perto', diff --git a/apps/app_seeds/lib/ui/offline_banner.dart b/apps/app_seeds/lib/ui/offline_banner.dart new file mode 100644 index 0000000..f2a896c --- /dev/null +++ b/apps/app_seeds/lib/ui/offline_banner.dart @@ -0,0 +1,87 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; + +import '../i18n/strings.g.dart'; + +/// Wraps the app and shows a slim strip at the very top when there's no network, +/// so it's clear WHY the social layer (market, chat, profile) is paused — the +/// inventory keeps working offline. Wrap the whole app once (MaterialApp builder). +class OfflineBanner extends StatefulWidget { + const OfflineBanner({required this.child, super.key}); + + final Widget child; + + @override + State createState() => _OfflineBannerState(); +} + +class _OfflineBannerState extends State { + bool _offline = false; + StreamSubscription>? _sub; + + @override + void initState() { + super.initState(); + _init(); + } + + Future _init() async { + try { + final current = await Connectivity().checkConnectivity(); + if (mounted) setState(() => _offline = _isOffline(current)); + _sub = Connectivity().onConnectivityChanged.listen((results) { + if (mounted) setState(() => _offline = _isOffline(results)); + }); + } catch (_) { + // Platform without connectivity support → assume online, hide the banner. + } + } + + bool _isOffline(List results) => + results.isEmpty || + results.every((r) => r == ConnectivityResult.none); + + @override + void dispose() { + _sub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (_offline) + Material( + color: const Color(0xFFE8A200), // amber — not an error, just paused + child: SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.cloud_off, size: 16, color: Colors.white), + const SizedBox(width: 8), + Flexible( + child: Text( + context.t.common.offline, + style: const TextStyle( + color: Colors.white, + fontSize: 12.5, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ), + ), + Expanded(child: widget.child), + ], + ); + } +} diff --git a/apps/app_seeds/pubspec.yaml b/apps/app_seeds/pubspec.yaml index ad6dc3f..f6a3f49 100644 --- a/apps/app_seeds/pubspec.yaml +++ b/apps/app_seeds/pubspec.yaml @@ -71,6 +71,9 @@ dependencies: # typing a code. Low accuracy only; the value is immediately reduced to a # low-precision geohash. Behind an interface so it's fakeable and pluggable. geolocator: ^13.0.1 + # Network reachability (BSD-3) to show a global "you're offline" banner — the + # social layer needs a connection; the inventory works regardless. + connectivity_plus: ^6.1.0 dev_dependencies: flutter_test: diff --git a/pubspec.lock b/pubspec.lock index ed57afc..9a12b95 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -209,6 +209,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + connectivity_plus: + dependency: transitive + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://pub.dev" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed" + url: "https://pub.dev" + source: hosted + version: "2.1.0" convert: dependency: transitive description: @@ -860,6 +876,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" node_preamble: dependency: transitive description: