Notification system audit: fix 6 defects, close 5 gaps, add rich notifications (v1.1.0)

Defects fixed:
- D1: Fix notification ID range collision (report reminders 700k→2M+)
- D2: Streak risk now checks both dzikir pagi & petang
- D3: _cancelPrayerPending no longer kills non-prayer notifications
- D4: Push notifications carry deeplink in payload for proper routing
- D5: Add reconfigureTimeZoneIfNeeded() for TZ change detection
- D6: Defer launch notification routing until widget tree is ready

Gaps closed:
- G1: Add streak risk + weekly summary toggles to settings UI
- G2: Verified boot reschedule already in place (flutter_local_notifications v21)
- G3: Remove unused mirrorAdzanToInbox field and legacy cleanup calls
- G4: Add notif_push_opened analytics tracking
- G5: Add notif_settings_changed analytics tracking

Enhancements:
- O1: Rich notification with Sudah Sholat action button on report reminders
- O2: Permission check on app resume via WidgetsBindingObserver (30s throttle)
- O2b: Fix stretched notification icon (white crescent moon vector drawable)
- O3: Expired inbox cleanup in background sync
- O4: Haptic feedback on notification bell quick actions

Bump version 1.0.8+9 → 1.1.0+10
This commit is contained in:
Dwindi Ramadhana
2026-06-06 22:38:02 +07:00
parent 2bd8e3666a
commit 4badfb6521
13 changed files with 420 additions and 37 deletions

View File

@@ -4,8 +4,13 @@ import 'dart:ui' show ViewFocusEvent, ViewFocusState;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../core/providers/theme_provider.dart';
import '../data/local/hive_boxes.dart';
import '../data/local/models/app_settings.dart';
import '../data/services/notification_event_producer_service.dart';
import '../data/services/notification_service.dart';
import '../features/dashboard/data/prayer_times_provider.dart';
import 'router.dart';
import 'theme/app_theme.dart';
@@ -20,6 +25,7 @@ class App extends ConsumerStatefulWidget {
class _AppState extends ConsumerState<App> with WidgetsBindingObserver {
Timer? _midnightResyncTimer;
DateTime? _lastPermissionCheckAt;
@override
void initState() {
@@ -29,6 +35,7 @@ class _AppState extends ConsumerState<App> with WidgetsBindingObserver {
HardwareKeyboard.instance.syncKeyboardState();
});
_scheduleMidnightResync();
NotificationService.instance.consumePendingLaunchRoute();
}
@override
@@ -49,6 +56,7 @@ class _AppState extends ConsumerState<App> with WidgetsBindingObserver {
ref.invalidate(prayerTimesProvider);
unawaited(ref.read(prayerTimesProvider.future));
_scheduleMidnightResync();
_checkNotificationPermissionOnResume();
}
}
@@ -73,6 +81,32 @@ class _AppState extends ConsumerState<App> with WidgetsBindingObserver {
});
}
Future<void> _checkNotificationPermissionOnResume() async {
final now = DateTime.now();
if (_lastPermissionCheckAt != null &&
now.difference(_lastPermissionCheckAt!).inSeconds < 30) {
return; // Throttle: max once per 30 seconds.
}
_lastPermissionCheckAt = now;
try {
final settings = Hive.box<AppSettings>(HiveBoxes.settings)
.get('default') ??
AppSettings();
if (!settings.adhanEnabled.values.any((v) => v)) return;
final permissionStatus =
await NotificationService.instance.getPermissionStatus();
await NotificationEventProducerService.instance
.emitPermissionWarningsIfNeeded(
settings: settings,
permissionStatus: permissionStatus,
);
} catch (_) {
// Non-blocking: permission check on resume is best-effort.
}
}
@override
Widget build(BuildContext context) {
final themeMode = ref.watch(themeProvider);