import 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; import 'package:convert/convert.dart'; import 'package:crypto/crypto.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:ndk/ndk.dart'; import 'package:ndk_objectbox/ndk_objectbox.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:zap_stream_flutter/const.dart'; import 'package:http/http.dart' as http; import 'package:zap_stream_flutter/firebase_options.dart'; import 'package:zap_stream_flutter/i18n/strings.g.dart'; import 'package:zap_stream_flutter/utils.dart'; import 'package:zap_stream_flutter/widgets/profile.dart'; class Notepush { final String base; final EventSigner signer; Notepush(this.base, {required this.signer}); Future register(String token) async { final pubkey = signer.getPublicKey(); final url = "$base/user-info/$pubkey/${Uri.encodeComponent(token)}?backend=fcm"; final rsp = await _sendPutRequest(url); return rsp.body; } Future> getWatchedKeys() async { final pubkey = signer.getPublicKey(); final url = "$base/user-info/$pubkey/notify"; final rsp = await _sendGetRequest(url); final List obj = JsonCodec().decode(rsp.body); return List.from(obj); } Future watchPubkey(String target, List kinds) async { final pubkey = signer.getPublicKey(); final url = "$base/user-info/$pubkey/notify/$target"; await _sendPutRequest(url, body: {"kinds": kinds}); } Future removeWatchPubkey(String target) async { final pubkey = signer.getPublicKey(); final url = "$base/user-info/$pubkey/notify/$target"; await _sendDeleteRequest(url); } Future setNotificationSettings(String token, List kinds) async { final pubkey = signer.getPublicKey(); final url = "$base/user-info/$pubkey/${Uri.encodeComponent(token)}/preference"; await _sendPutRequest(url, body: {"kinds": kinds}); } Future _sendPutRequest(String url, {Object? body}) async { final jsonBody = body != null ? JsonCodec().encode(body) : null; final auth = await _makeAuth("PUT", url, body: jsonBody); final rsp = await http .put( Uri.parse(url), body: jsonBody, headers: { "authorization": "Nostr $auth", "accept": "application/json", "content-type": "application/json", }, ) .timeout(Duration(seconds: 10)); developer.log(rsp.body); return rsp; } Future _sendGetRequest(String url, {Object? body}) async { final jsonBody = body != null ? JsonCodec().encode(body) : null; final auth = await _makeAuth("GET", url, body: jsonBody); final rsp = await http .get( Uri.parse(url), headers: { "authorization": "Nostr $auth", "accept": "application/json", "content-type": "application/json", }, ) .timeout(Duration(seconds: 10)); developer.log(rsp.body); return rsp; } Future _sendDeleteRequest(String url, {Object? body}) async { final jsonBody = body != null ? JsonCodec().encode(body) : null; final auth = await _makeAuth("DELETE", url, body: jsonBody); final rsp = await http .delete( Uri.parse(url), headers: { "authorization": "Nostr $auth", "accept": "application/json", "content-type": "application/json", }, ) .timeout(Duration(seconds: 10)); developer.log(rsp.body); return rsp; } Future _makeAuth(String method, String url, {String? body}) async { final pubkey = signer.getPublicKey(); var tags = [ ["u", url], ["method", method], ]; if (body != null) { final hash = hex.encode(sha256.convert(utf8.encode(body)).bytes); tags.add(["payload", hash]); } final authEvent = Nip01Event( pubKey: pubkey, kind: 27235, tags: tags, content: "", ); await signer.sign(authEvent); return authEvent.toBase64(); } } Notepush? getNotificationService() { final signer = ndk.accounts.getLoggedAccount()?.signer; return signer != null ? Notepush(dotenv.env["NOTEPUSH_URL"]!, signer: signer) : null; } class NotificationsState { final AuthorizationStatus status; final List notifyKeys; NotificationsState({required this.status, required this.notifyKeys}); NotificationsState copyWith({ AuthorizationStatus? newStatus, List? newNotifyKeys, }) { return NotificationsState( status: newStatus ?? status, notifyKeys: newNotifyKeys ?? notifyKeys, ); } static Future init(AuthorizationStatus status) async { if (status == AuthorizationStatus.authorized) { final svc = getNotificationService(); if (svc != null) { try { final keys = await svc.getWatchedKeys(); return NotificationsState(status: status, notifyKeys: keys); } catch (e) { developer.log("Failed to init NotificationsState: $e"); } } } return NotificationsState(status: status, notifyKeys: []); } } class NotificationsStore extends ValueNotifier { NotificationsStore(super.value); Future reload() async { if (value != null && value!.status == AuthorizationStatus.authorized) { final svc = getNotificationService(); if (svc != null) { final keys = await svc.getWatchedKeys(); value = value!.copyWith(newNotifyKeys: keys); } } } } Future _initLocalNotifications() async { await localNotifications.initialize( InitializationSettings( android: AndroidInitializationSettings("@drawable/ic_stat_name"), iOS: DarwinInitializationSettings(), ), ); } @pragma('vm:entry-point') Future _onBackgroundNotification(RemoteMessage msg) async { await LocaleSettings.useDeviceLocale(); final cache = DbObjectBox(attach: true); await _initLocalNotifications(); await _handleNotification(msg, cache); } Future _onNotification(RemoteMessage msg) async { await _handleNotification(msg, ndkCache); } Future _handleNotification(RemoteMessage msg, DbObjectBox cache) async { final notification = msg.notification; if (notification != null && notification.android != null) { final String? json = msg.data["nostr_event"]; final event = json != null ? Nip01Event.fromJson(JsonCodec().decode(json)) : null; await _showNotification(notification, ndkCache, event); } } Future _showNotification( RemoteNotification notification, DbObjectBox cache, Nip01Event? event, ) async { final stream = event != null ? StreamEvent(event) : null; final hostProfile = stream != null ? await cache.loadMetadata(stream.info.host) : null; final newTitle = hostProfile != null ? t.stream.notification( name: ProfileNameWidget.nameFromProfile(hostProfile), ) : null; localNotifications.show( notification.hashCode, newTitle ?? notification.title, stream?.info.title ?? notification.body, NotificationDetails( android: AndroidNotificationDetails( notification.android!.channelId ?? "fcm", "Push Notifications", category: AndroidNotificationCategory.social, ), ), ); } Future _onOpenMessage(RemoteMessage msg) async { try { final notification = msg.notification; final String? json = msg.data["nostr_event"]; if (notification != null && json != null) { // Just launch the URL because we support deep links final event = Nip01Event.fromJson(JsonCodec().decode(json)); final stream = StreamEvent(event); launchUrl(Uri.parse("https://zap.stream/${stream.link}")); } } catch (e) { developer.log("Failed to process push notification\n ${e.toString()}"); } } // global notifications store final notifications = NotificationsStore(null); Future setupNotifications() async { await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); final signer = ndk.accounts.getLoggedAccount()?.signer; if (signer != null) { await configureNotifications(signer); } } Future configureNotifications(EventSigner signer) async { FirebaseMessaging.onMessage.listen(_onNotification); //FirebaseMessaging.onBackgroundMessage(_onBackgroundNotification); FirebaseMessaging.onMessageOpenedApp.listen(_onOpenMessage); final settings = await FirebaseMessaging.instance.requestPermission( provisional: true, ); await FirebaseMessaging.instance.setAutoInitEnabled(true); await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions( alert: true, badge: true, sound: true, ); if (Platform.isIOS) { final apnsToken = await FirebaseMessaging.instance.getAPNSToken(); if (apnsToken == null) { throw "APNS token not availble"; } } await _initLocalNotifications(); final pusher = Notepush(dotenv.env["NOTEPUSH_URL"]!, signer: signer); FirebaseMessaging.instance.onTokenRefresh.listen((token) async { developer.log("NEW TOKEN: $token"); await pusher.register(token); await pusher.setNotificationSettings(token, [30_311]); }); final fcmToken = await FirebaseMessaging.instance.getToken(); if (fcmToken == null) { throw "Push token is null"; } await pusher.register(fcmToken); await pusher.setNotificationSettings(fcmToken, [30_311]); notifications.value = await NotificationsState.init( settings.authorizationStatus, ); }