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:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
26
packages/sf_tracking/lib/src/mixins/app_update_tracking.dart
Normal file
26
packages/sf_tracking/lib/src/mixins/app_update_tracking.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user