@@ -2,6 +2,10 @@ import 'package:cloud_firestore/cloud_firestore.dart';
22import 'package:firebase_messaging/firebase_messaging.dart' ;
33import 'package:flutter/foundation.dart' ;
44import 'package:shared_preferences/shared_preferences.dart' ;
5+ import 'package:http/http.dart' as http;
6+ import 'dart:convert' ;
7+ import 'dart:js_interop' ;
8+ import 'package:web/web.dart' as web;
59import '../models.dart' ;
610
711/// Service for managing push notifications (FCM) and in-app notifications
@@ -16,10 +20,14 @@ class NotificationService {
1620 static const String _pushEnabledKey = 'push_notifications_enabled' ;
1721 static const String _lastBirthdayCheckKey = 'last_birthday_check_date' ;
1822 static const String _lastMonthlyBirthdayKey = 'last_monthly_birthday_check' ;
23+
24+ // OneSignal Push API - Vercel serverless function path
25+ static const String _pushApiUrl = '/api/send-notification' ;
1926
2027 bool _initialized = false ;
2128 String ? _currentUserId;
2229 String ? _fcmToken;
30+ String ? _oneSignalPlayerId;
2331
2432 /// Initialize the notification service
2533 Future <void > initialize (String userId) async {
@@ -44,6 +52,11 @@ class NotificationService {
4452 // Handle background/terminated message taps
4553 FirebaseMessaging .onMessageOpenedApp.listen (_handleMessageOpenedApp);
4654
55+ // Get OneSignal player ID and save to Firestore (web only)
56+ if (kIsWeb) {
57+ await _getAndSaveOneSignalPlayerId ();
58+ }
59+
4760 _initialized = true ;
4861 debugPrint ('NotificationService initialized for user: $userId ' );
4962 }
@@ -119,11 +132,124 @@ class NotificationService {
119132 debugPrint ('Error removing FCM token: $e ' );
120133 }
121134
135+ // Also remove OneSignal player ID
136+ if (_oneSignalPlayerId != null ) {
137+ try {
138+ await _db.collection ('users' ).doc (_currentUserId).update ({
139+ 'oneSignalPlayerIds' : FieldValue .arrayRemove ([_oneSignalPlayerId]),
140+ });
141+ debugPrint ('OneSignal player ID removed for user: $_currentUserId ' );
142+ } catch (e) {
143+ debugPrint ('Error removing OneSignal player ID: $e ' );
144+ }
145+ }
146+
122147 _currentUserId = null ;
123148 _fcmToken = null ;
149+ _oneSignalPlayerId = null ;
124150 _initialized = false ;
125151 }
126152
153+ // ============= OneSignal Integration =============
154+
155+ /// Get OneSignal player ID from JavaScript and save to Firestore
156+ Future <void > _getAndSaveOneSignalPlayerId () async {
157+ if (! kIsWeb) return ;
158+
159+ try {
160+ // Call JavaScript function exposed in index.html
161+ final playerId = await _callJsGetPlayerId ();
162+
163+ if (playerId != null && playerId.isNotEmpty) {
164+ _oneSignalPlayerId = playerId;
165+ await _saveOneSignalPlayerIdToFirestore (playerId);
166+ }
167+ } catch (e) {
168+ debugPrint ('Error getting OneSignal player ID: $e ' );
169+ }
170+ }
171+
172+ /// Call JavaScript getOneSignalPlayerId function
173+ Future <String ?> _callJsGetPlayerId () async {
174+ if (! kIsWeb) return null ;
175+
176+ try {
177+ final result = (web.window as dynamic ).getOneSignalPlayerId? .call ();
178+ if (result != null ) {
179+ // Await the Promise
180+ final jsPromise = result as JSPromise ;
181+ final value = await jsPromise.toDart;
182+ return value? .toString ();
183+ }
184+ } catch (e) {
185+ debugPrint ('JS getOneSignalPlayerId error: $e ' );
186+ }
187+ return null ;
188+ }
189+
190+ /// Save OneSignal player ID to Firestore
191+ Future <void > _saveOneSignalPlayerIdToFirestore (String playerId) async {
192+ if (_currentUserId == null ) return ;
193+
194+ try {
195+ await _db.collection ('users' ).doc (_currentUserId).update ({
196+ 'oneSignalPlayerIds' : FieldValue .arrayUnion ([playerId]),
197+ 'lastOneSignalUpdate' : FieldValue .serverTimestamp (),
198+ });
199+ debugPrint ('OneSignal player ID saved for user: $_currentUserId ' );
200+ } catch (e) {
201+ debugPrint ('Error saving OneSignal player ID: $e ' );
202+ }
203+ }
204+
205+ /// Send push notification via Netlify serverless function
206+ Future <void > _sendPushNotification ({
207+ required List <String > playerIds,
208+ required String message,
209+ String ? title,
210+ Map <String , dynamic >? data,
211+ }) async {
212+ if (playerIds.isEmpty) return ;
213+
214+ try {
215+ final response = await http.post (
216+ Uri .parse (_pushApiUrl),
217+ headers: {'Content-Type' : 'application/json' },
218+ body: jsonEncode ({
219+ 'playerIds' : playerIds,
220+ 'title' : title ?? 'Orbit' ,
221+ 'message' : message,
222+ 'data' : data,
223+ }),
224+ );
225+
226+ if (response.statusCode == 200 ) {
227+ debugPrint ('Push notification sent successfully' );
228+ } else {
229+ debugPrint ('Push notification failed: ${response .body }' );
230+ }
231+ } catch (e) {
232+ debugPrint ('Error sending push notification: $e ' );
233+ }
234+ }
235+
236+ /// Get OneSignal player IDs for a user
237+ Future <List <String >> _getPlayerIdsForUser (String userId) async {
238+ try {
239+ final userDoc = await _db.collection ('users' ).doc (userId).get ();
240+ if (userDoc.exists) {
241+ final data = userDoc.data ();
242+ final playerIds = data? ['oneSignalPlayerIds' ];
243+ if (playerIds != null && playerIds is List ) {
244+ return List <String >.from (playerIds);
245+ }
246+ }
247+ } catch (e) {
248+ debugPrint ('Error getting player IDs for user $userId : $e ' );
249+ }
250+ return [];
251+ }
252+
127253 /// Handle foreground messages
128254 void _handleForegroundMessage (RemoteMessage message) {
129255 debugPrint ('Foreground message received: ${message .notification ?.title }' );
@@ -199,11 +325,29 @@ class NotificationService {
199325 // Create new notification
200326 await _db.collection ('notifications' ).add (notificationData);
201327 debugPrint ('Created notification for $userId : $message ' );
328+
329+ // Send push notification via OneSignal (don't await to avoid blocking)
330+ _sendPushToUser (userId, message, type.name);
202331 } catch (e) {
203332 debugPrint ('Error sending notification: $e ' );
204333 }
205334 }
206335
336+ /// Send push notification to a specific user
337+ Future <void > _sendPushToUser (String userId, String message, String type) async {
338+ // Don't send push to self
339+ if (userId == _currentUserId) return ;
340+
341+ final playerIds = await _getPlayerIdsForUser (userId);
342+ if (playerIds.isNotEmpty) {
343+ await _sendPushNotification (
344+ playerIds: playerIds,
345+ message: message,
346+ data: {'type' : type},
347+ );
348+ }
349+ }
350+
207351 /// Send notifications to multiple users
208352 Future <void > sendNotificationToMany ({
209353 required List <String > userIds,
0 commit comments