feat(version-check): add in-app update prompt with Remote Config

Adds a production-grade in-app version check that prompts users to update when a new build is available. Soft updates are dismissable. Force updates block the app entirely. Configured via FirebaseRemote Config so rollouts can be triggered without redeploying.

- Sealed result types (NoUpdate / SoftUpdate / ForceUpdate) for type-safe pattern matching
- AppVersionCheckService
- AppUpdateGate widget encapsulates listener, route guard and dialog wireup, isolated from save_family_app
- Serialized notifier operations prevent race between dismiss and refresh, mounted checks blindar disposal edge cases                                                                                                  - Build-aware dismiss persistence via SharedPreferences
This commit is contained in:
2026-04-09 14:52:37 +02:00
parent 506dd5a80f
commit 693f55369c
24 changed files with 887 additions and 31 deletions

View File

@@ -857,5 +857,11 @@
"videoCall": "Videoanruf",
"watchesOnMap": "Smartwatches auf der Karte:",
"wifiSettings": "WLAN-Einstellungen",
"yesterday": "Gestern"
"yesterday": "Gestern",
"appUpdateAvailableTitle": "Update verfügbar",
"appUpdateAvailableMessage": "Eine neue Version von SaveFamily ist verfügbar.",
"appUpdateRequiredTitle": "Update erforderlich",
"appUpdateRequiredMessage": "Du musst SaveFamily aktualisieren, um die App weiterhin zu nutzen.",
"appUpdateLater": "Später",
"appUpdateNow": "Jetzt aktualisieren"
}

View File

@@ -857,5 +857,11 @@
"renewCardPinTitle": "Enter your security PIN to renew the card",
"renewCardSuccess": "Card renewed successfully",
"renewCardError": "Error renewing card",
"passwordLabel": "Password (6 to 12 characters)"
"passwordLabel": "Password (6 to 12 characters)",
"appUpdateAvailableTitle": "Update available",
"appUpdateAvailableMessage": "A new version of SaveFamily is available.",
"appUpdateRequiredTitle": "Update required",
"appUpdateRequiredMessage": "You need to update SaveFamily to keep using the app.",
"appUpdateLater": "Later",
"appUpdateNow": "Update now"
}

View File

@@ -857,5 +857,11 @@
"renewCardTokenHint": "Introduce el código de la nueva tarjeta",
"renewCardPinTitle": "Introduce tu PIN de seguridad para renovar la tarjeta",
"renewCardSuccess": "Tarjeta renovada correctamente",
"renewCardError": "Error al renovar la tarjeta"
"renewCardError": "Error al renovar la tarjeta",
"appUpdateAvailableTitle": "Actualización disponible",
"appUpdateAvailableMessage": "Hay una nueva versión de SaveFamily disponible.",
"appUpdateRequiredTitle": "Actualización requerida",
"appUpdateRequiredMessage": "Necesitas actualizar SaveFamily para seguir usando la app.",
"appUpdateLater": "Más tarde",
"appUpdateNow": "Actualizar ahora"
}

View File

@@ -857,5 +857,11 @@
"videoCall": "Appel vidéo",
"watchesOnMap": "Montres connectées sur la carte :",
"wifiSettings": "Paramètres WiFi",
"yesterday": "Hier"
"yesterday": "Hier",
"appUpdateAvailableTitle": "Mise à jour disponible",
"appUpdateAvailableMessage": "Une nouvelle version de SaveFamily est disponible.",
"appUpdateRequiredTitle": "Mise à jour requise",
"appUpdateRequiredMessage": "Vous devez mettre à jour SaveFamily pour continuer à utiliser l'application.",
"appUpdateLater": "Plus tard",
"appUpdateNow": "Mettre à jour maintenant"
}

View File

@@ -857,5 +857,11 @@
"videoCall": "Videochiamata",
"watchesOnMap": "Smartwatch sulla mappa:",
"wifiSettings": "Impostazioni WiFi",
"yesterday": "Ieri"
"yesterday": "Ieri",
"appUpdateAvailableTitle": "Aggiornamento disponibile",
"appUpdateAvailableMessage": "È disponibile una nuova versione di SaveFamily.",
"appUpdateRequiredTitle": "Aggiornamento richiesto",
"appUpdateRequiredMessage": "Devi aggiornare SaveFamily per continuare a usare l'app.",
"appUpdateLater": "Più tardi",
"appUpdateNow": "Aggiorna ora"
}

View File

@@ -857,5 +857,11 @@
"videoCall": "Videochamada",
"watchesOnMap": "Relógios inteligentes no mapa:",
"wifiSettings": "Configurações WiFi",
"yesterday": "Ontem"
"yesterday": "Ontem",
"appUpdateAvailableTitle": "Atualização disponível",
"appUpdateAvailableMessage": "Há uma nova versão do SaveFamily disponível.",
"appUpdateRequiredTitle": "Atualização necessária",
"appUpdateRequiredMessage": "Precisas atualizar o SaveFamily para continuar a usar a app.",
"appUpdateLater": "Mais tarde",
"appUpdateNow": "Atualizar agora"
}

View File

@@ -86,6 +86,12 @@ class I18n {
static const String appsSurveillance = 'appsSurveillance';
static const String appStore = 'appStore';
static const String appsUse = 'appsUse';
static const String appUpdateAvailableMessage = 'appUpdateAvailableMessage';
static const String appUpdateAvailableTitle = 'appUpdateAvailableTitle';
static const String appUpdateLater = 'appUpdateLater';
static const String appUpdateNow = 'appUpdateNow';
static const String appUpdateRequiredMessage = 'appUpdateRequiredMessage';
static const String appUpdateRequiredTitle = 'appUpdateRequiredTitle';
static const String appUsers = 'appUsers';
static const String average = 'average';
static const String back = 'back';

View File

@@ -4,6 +4,7 @@ library;
export 'src/clients/debug_tracking_client.dart';
export 'src/clients/firebase_tracking_client.dart';
export 'src/mixins/account_tracking.dart';
export 'src/mixins/app_update_tracking.dart';
export 'src/mixins/auth_tracking.dart';
export 'src/mixins/contacts_tracking.dart';
export 'src/mixins/control_panel_tracking.dart';

View File

@@ -0,0 +1,26 @@
import 'package:sf_tracking/src/tracking.dart';
const _prefix = 'app_update';
mixin AppUpdateTracking on Tracking {
Future<void> appUpdateDialogShown({
required String kind,
required int latestBuild,
required int currentBuild,
}) => trackEvent('${_prefix}_dialog_shown', {
'kind': kind,
'latest_build': latestBuild,
'current_build': currentBuild,
});
Future<void> appUpdateDialogDismissed({required int latestBuild}) =>
trackEvent('${_prefix}_dialog_dismissed', {'latest_build': latestBuild});
Future<void> appUpdateCtaTapped({
required String kind,
required int latestBuild,
}) => trackEvent('${_prefix}_cta_tapped', {
'kind': kind,
'latest_build': latestBuild,
});
}

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:sf_tracking/src/mixins/account_tracking.dart';
import 'package:sf_tracking/src/mixins/app_update_tracking.dart';
import 'package:sf_tracking/src/mixins/auth_tracking.dart';
import 'package:sf_tracking/src/mixins/contacts_tracking.dart';
import 'package:sf_tracking/src/mixins/control_panel_tracking.dart';
@@ -29,7 +30,8 @@ class SfTrackingRepository extends Tracking
SupportTracking,
OnboardingTracking,
LocationTracking,
ControlPanelTracking
ControlPanelTracking,
AppUpdateTracking
implements NavigationTracking {
SfTrackingRepository({required List<TrackingClient> clients})
: _clients = clients;