Polish navigation, Quran flows, and sharing UX
This commit is contained in:
104
lib/data/services/remote_notification_content_service.dart
Normal file
104
lib/data/services/remote_notification_content_service.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../local/models/app_settings.dart';
|
||||
import 'notification_inbox_service.dart';
|
||||
import 'notification_runtime_service.dart';
|
||||
import 'notification_service.dart';
|
||||
|
||||
/// Pulls server-defined notification content and maps it to local inbox items.
|
||||
class RemoteNotificationContentService {
|
||||
RemoteNotificationContentService._();
|
||||
static final RemoteNotificationContentService instance =
|
||||
RemoteNotificationContentService._();
|
||||
|
||||
final NotificationInboxService _inbox = NotificationInboxService.instance;
|
||||
final NotificationRuntimeService _runtime =
|
||||
NotificationRuntimeService.instance;
|
||||
|
||||
Future<void> sync({
|
||||
required AppSettings settings,
|
||||
}) async {
|
||||
if (!settings.inboxEnabled) return;
|
||||
|
||||
final endpoint = (dotenv.env['NOTIFICATION_FEED_URL'] ?? '').trim();
|
||||
if (endpoint.isEmpty) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
final lastSync = _runtime.lastRemoteSyncAt();
|
||||
if (lastSync != null &&
|
||||
now.difference(lastSync) < const Duration(hours: 6)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await http.get(Uri.parse(endpoint));
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) return;
|
||||
|
||||
final decoded = json.decode(response.body);
|
||||
final items = _extractItems(decoded);
|
||||
if (items.isEmpty) return;
|
||||
|
||||
for (final raw in items) {
|
||||
final id = (raw['id'] ?? '').toString().trim();
|
||||
final title = (raw['title'] ?? '').toString().trim();
|
||||
final body = (raw['body'] ?? '').toString().trim();
|
||||
if (id.isEmpty || title.isEmpty || body.isEmpty) continue;
|
||||
|
||||
final deeplink = (raw['deeplink'] ?? '').toString().trim();
|
||||
final type = (raw['type'] ?? 'content').toString().trim();
|
||||
final expiresAt =
|
||||
DateTime.tryParse((raw['expiresAt'] ?? '').toString().trim());
|
||||
final isPinned = raw['isPinned'] == true;
|
||||
final shouldPush = raw['push'] == true;
|
||||
|
||||
await _inbox.addItem(
|
||||
title: title,
|
||||
body: body,
|
||||
type: type.isEmpty ? 'content' : type,
|
||||
source: 'remote',
|
||||
deeplink: deeplink.isEmpty ? null : deeplink,
|
||||
dedupeKey: 'remote.$id',
|
||||
expiresAt: expiresAt,
|
||||
isPinned: isPinned,
|
||||
meta: <String, dynamic>{'remoteId': id},
|
||||
);
|
||||
|
||||
if (shouldPush && settings.alertsEnabled) {
|
||||
final notif = NotificationService.instance;
|
||||
await notif.showNonPrayerAlert(
|
||||
settings: settings,
|
||||
id: notif.nonPrayerNotificationId('remote.push.$id'),
|
||||
title: title,
|
||||
body: body,
|
||||
payloadType: 'content',
|
||||
silent: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await _runtime.setLastRemoteSyncAt(now);
|
||||
} catch (_) {
|
||||
// Non-fatal: remote feed is optional.
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _extractItems(dynamic decoded) {
|
||||
if (decoded is List) {
|
||||
return decoded.whereType<Map>().map(_toStringKeyedMap).toList();
|
||||
}
|
||||
if (decoded is Map) {
|
||||
final list = decoded['items'];
|
||||
if (list is List) {
|
||||
return list.whereType<Map>().map(_toStringKeyedMap).toList();
|
||||
}
|
||||
}
|
||||
return const <Map<String, dynamic>>[];
|
||||
}
|
||||
|
||||
Map<String, dynamic> _toStringKeyedMap(Map raw) {
|
||||
return raw.map((key, value) => MapEntry(key.toString(), value));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user