Skip to content

Feature maintainance mode #71

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions lib/app/services/app_status_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';

/// {@template app_status_service}
/// A service dedicated to monitoring the application's lifecycle and
/// proactively triggering status checks.
///
/// This service ensures that the application can react to server-side
/// status changes (like maintenance mode or forced updates) in real-time,
/// both when the app is resumed from the background and during extended
/// foreground sessions.
///
/// It works by:
/// 1. Implementing [WidgetsBindingObserver] to listen for app lifecycle events.
/// 2. Triggering a remote configuration fetch via the [AppBloc] whenever the
/// app is resumed (`AppLifecycleState.resumed`).
/// 3. Using a periodic [Timer] to trigger fetches at a regular interval,
/// catching status changes even if the app remains in the foreground.
/// {@endtemplate}
class AppStatusService with WidgetsBindingObserver {
/// {@macro app_status_service}
///
/// Requires a [BuildContext] to access the [AppBloc] and a [Duration]
/// for the periodic check interval.
AppStatusService({
required BuildContext context,
required Duration checkInterval,
}) : _context = context,
_checkInterval = checkInterval {
// Immediately register this service as a lifecycle observer.
WidgetsBinding.instance.addObserver(this);
// Start the periodic checks.
_startPeriodicChecks();
}

/// The build context used to look up the AppBloc.
final BuildContext _context;

/// The interval at which to perform periodic status checks.
final Duration _checkInterval;

/// The timer responsible for periodic checks.
Timer? _timer;

/// Starts the periodic timer to trigger config fetches.
///
/// This ensures that even if the app stays in the foreground, it will
/// eventually learn about new server-side status changes.
void _startPeriodicChecks() {
// Cancel any existing timer to prevent duplicates.
_timer?.cancel();
// Create a new periodic timer.
_timer = Timer.periodic(_checkInterval, (_) {
print(
'[AppStatusService] Periodic check triggered. Requesting AppConfig fetch.',
);
// Add the event to the AppBloc to fetch the latest config.
_context.read<AppBloc>().add(const AppConfigFetchRequested());
});
}

/// Overridden from [WidgetsBindingObserver].
///
/// This method is called whenever the application's lifecycle state changes.
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// We are only interested in the 'resumed' state.
if (state == AppLifecycleState.resumed) {
print(
'[AppStatusService] App resumed. Requesting AppConfig fetch.',
);
// When the app comes to the foreground, immediately trigger a check.
// This is crucial for catching maintenance mode that was enabled
// while the app was in the background.
_context.read<AppBloc>().add(const AppConfigFetchRequested());
}
}

/// Cleans up resources used by the service.
///
/// This must be called when the service is no longer needed (e.g., when
/// the main app widget is disposed) to prevent memory leaks from the
/// timer and the observer registration.
void dispose() {
print('[AppStatusService] Disposing service.');
// Stop the periodic timer.
_timer?.cancel();
// Remove this object from the list of lifecycle observers.
WidgetsBinding.instance.removeObserver(this);
}
}
34 changes: 34 additions & 0 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/app/config/app_environment.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/app/services/app_status_service.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/app/services/demo_data_migration_service.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/authentication/bloc/authentication_bloc.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/router/router.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/status/view/view.dart';
import 'package:go_router/go_router.dart';
import 'package:kv_storage_service/kv_storage_service.dart';
import 'package:ui_kit/ui_kit.dart';
Expand Down Expand Up @@ -132,6 +134,8 @@ class _AppViewState extends State<_AppView> {
late final GoRouter _router;
// Standard notifier that GoRouter listens to.
late final ValueNotifier<AppStatus> _statusNotifier;
// The service responsible for automated status checks.
AppStatusService? _appStatusService;
// Removed Dynamic Links subscription

@override
Expand All @@ -140,6 +144,15 @@ class _AppViewState extends State<_AppView> {
final appBloc = context.read<AppBloc>();
// Initialize the notifier with the BLoC's current state
_statusNotifier = ValueNotifier<AppStatus>(appBloc.state.status);

// Instantiate and initialize the AppStatusService.
// This service will automatically trigger checks when the app is resumed
// or at periodic intervals, ensuring the app status is always fresh.
_appStatusService = AppStatusService(
context: context,
checkInterval: const Duration(minutes: 15),
);

_router = createRouter(
authStatusNotifier: _statusNotifier,
authenticationRepository: widget.authenticationRepository,
Expand All @@ -159,6 +172,8 @@ class _AppViewState extends State<_AppView> {
@override
void dispose() {
_statusNotifier.dispose();
// Dispose the AppStatusService to cancel timers and remove observers.
_appStatusService?.dispose();
// Removed Dynamic Links subscription cancellation
super.dispose();
}
Expand All @@ -182,6 +197,25 @@ class _AppViewState extends State<_AppView> {
// Defer l10n access until inside a MaterialApp context

// Handle critical RemoteConfig loading states globally
// These checks have the highest priority and will lock the entire UI.
//
// Check for Maintenance Mode.
if (state.status == AppStatus.underMaintenance) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: MaintenancePage(),
);
}

// Check for a Required Update.
if (state.status == AppStatus.updateRequired) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: UpdateRequiredPage(),
);
}

// Check for Config Fetching state.
if (state.status == AppStatus.configFetching) {
return MaterialApp(
debugShowCheckedModeBanner: false,
Expand Down
30 changes: 30 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,36 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Bold'**
String get settingsAppearanceFontWeightBold;

/// Headline for the maintenance page
///
/// In en, this message translates to:
/// **'Under Maintenance'**
String get maintenanceHeadline;

/// Subheadline for the maintenance page
///
/// In en, this message translates to:
/// **'We are currently performing maintenance. Please check back later.'**
String get maintenanceSubheadline;

/// Headline for the force update page
///
/// In en, this message translates to:
/// **'Update Required'**
String get updateRequiredHeadline;

/// Subheadline for the force update page
///
/// In en, this message translates to:
/// **'A new version of the app is available. Please update to continue using the app.'**
String get updateRequiredSubheadline;

/// Button text for the force update page
///
/// In en, this message translates to:
/// **'Update Now'**
String get updateRequiredButton;
}

class _AppLocalizationsDelegate
Expand Down
17 changes: 17 additions & 0 deletions lib/l10n/app_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -804,4 +804,21 @@ class AppLocalizationsAr extends AppLocalizations {

@override
String get settingsAppearanceFontWeightBold => 'عريض';

@override
String get maintenanceHeadline => 'تحت الصيانة';

@override
String get maintenanceSubheadline =>
'نقوم حاليًا بإجراء صيانة. يرجى التحقق مرة أخرى لاحقًا.';

@override
String get updateRequiredHeadline => 'التحديث مطلوب';

@override
String get updateRequiredSubheadline =>
'يتوفر إصدار جديد من التطبيق. يرجى التحديث لمتابعة استخدام التطبيق.';

@override
String get updateRequiredButton => 'التحديث الآن';
}
17 changes: 17 additions & 0 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -804,4 +804,21 @@ class AppLocalizationsEn extends AppLocalizations {

@override
String get settingsAppearanceFontWeightBold => 'Bold';

@override
String get maintenanceHeadline => 'Under Maintenance';

@override
String get maintenanceSubheadline =>
'We are currently performing maintenance. Please check back later.';

@override
String get updateRequiredHeadline => 'Update Required';

@override
String get updateRequiredSubheadline =>
'A new version of the app is available. Please update to continue using the app.';

@override
String get updateRequiredButton => 'Update Now';
}
20 changes: 20 additions & 0 deletions lib/l10n/arb/app_ar.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1034,5 +1034,25 @@
"settingsAppearanceFontWeightBold": "عريض",
"@settingsAppearanceFontWeightBold": {
"description": "Label for the bold font weight option"
},
"maintenanceHeadline": "تحت الصيانة",
"@maintenanceHeadline": {
"description": "Headline for the maintenance page"
},
"maintenanceSubheadline": "نقوم حاليًا بإجراء صيانة. يرجى التحقق مرة أخرى لاحقًا.",
"@maintenanceSubheadline": {
"description": "Subheadline for the maintenance page"
},
"updateRequiredHeadline": "التحديث مطلوب",
"@updateRequiredHeadline": {
"description": "Headline for the force update page"
},
"updateRequiredSubheadline": "يتوفر إصدار جديد من التطبيق. يرجى التحديث لمتابعة استخدام التطبيق.",
"@updateRequiredSubheadline": {
"description": "Subheadline for the force update page"
},
"updateRequiredButton": "التحديث الآن",
"@updateRequiredButton": {
"description": "Button text for the force update page"
}
}
20 changes: 20 additions & 0 deletions lib/l10n/arb/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -1052,5 +1052,25 @@
"settingsAppearanceFontWeightBold": "Bold",
"@settingsAppearanceFontWeightBold": {
"description": "Label for the bold font weight option"
},
"maintenanceHeadline": "Under Maintenance",
"@maintenanceHeadline": {
"description": "Headline for the maintenance page"
},
"maintenanceSubheadline": "We are currently performing maintenance. Please check back later.",
"@maintenanceSubheadline": {
"description": "Subheadline for the maintenance page"
},
"updateRequiredHeadline": "Update Required",
"@updateRequiredHeadline": {
"description": "Headline for the force update page"
},
"updateRequiredSubheadline": "A new version of the app is available. Please update to continue using the app.",
"@updateRequiredSubheadline": {
"description": "Subheadline for the force update page"
},
"updateRequiredButton": "Update Now",
"@updateRequiredButton": {
"description": "Button text for the force update page"
}
}
33 changes: 33 additions & 0 deletions lib/status/view/maintenance_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart';
import 'package:ui_kit/ui_kit.dart';

/// A page displayed to the user when the application is in maintenance mode.
///
/// This is a simple, static page that informs the user that the app is
/// temporarily unavailable and asks them to check back later. It's designed
/// to be displayed globally, blocking access to all other app features.
class MaintenancePage extends StatelessWidget {
/// {@macro maintenance_page}
const MaintenancePage({super.key});

@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);

// The Scaffold provides the basic Material Design visual layout structure.
return Scaffold(
body: Padding(
// Use consistent padding from the UI kit.
padding: const EdgeInsets.all(AppSpacing.lg),
// The InitialStateWidget from the UI kit is reused here to provide a
// consistent look and feel for full-screen informational states.
child: InitialStateWidget(
icon: Icons.build_circle_outlined,
headline: l10n.maintenanceHeadline,
subheadline: l10n.maintenanceSubheadline,
),
),
);
}
}
Loading
Loading