Skip to content

Commit 9cdcf76

Browse files
committed
Prepare for vercel deploy
1 parent 15d3232 commit 9cdcf76

6 files changed

Lines changed: 268 additions & 1 deletion

File tree

api/send-notification.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Vercel serverless function to send OneSignal push notifications
2+
// Environment variables ONESIGNAL_API_KEY and ONESIGNAL_APP_ID must be set in Vercel dashboard
3+
4+
export default async function handler(req, res) {
5+
// Set CORS headers
6+
res.setHeader('Access-Control-Allow-Origin', '*');
7+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
8+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
9+
10+
// Handle preflight
11+
if (req.method === 'OPTIONS') {
12+
return res.status(200).end();
13+
}
14+
15+
// Only allow POST requests
16+
if (req.method !== 'POST') {
17+
return res.status(405).json({ error: 'Method not allowed' });
18+
}
19+
20+
const { playerIds, title, message, data } = req.body;
21+
22+
// Validate required fields
23+
if (!playerIds || !Array.isArray(playerIds) || playerIds.length === 0) {
24+
return res.status(400).json({ error: 'playerIds array is required' });
25+
}
26+
27+
if (!message) {
28+
return res.status(400).json({ error: 'message is required' });
29+
}
30+
31+
// Get secrets from environment
32+
const apiKey = process.env.ONESIGNAL_API_KEY;
33+
const appId = process.env.ONESIGNAL_APP_ID;
34+
35+
if (!apiKey || !appId) {
36+
console.error('Missing ONESIGNAL_API_KEY or ONESIGNAL_APP_ID environment variables');
37+
return res.status(500).json({ error: 'Server configuration error' });
38+
}
39+
40+
// Build OneSignal notification payload
41+
const notificationPayload = {
42+
app_id: appId,
43+
include_player_ids: playerIds,
44+
headings: { en: title || 'Orbit' },
45+
contents: { en: message },
46+
data: data || {},
47+
web_url: '/', // Open app when notification is clicked
48+
};
49+
50+
try {
51+
// Call OneSignal REST API
52+
const response = await fetch('https://onesignal.com/api/v1/notifications', {
53+
method: 'POST',
54+
headers: {
55+
'Content-Type': 'application/json',
56+
'Authorization': `Basic ${apiKey}`,
57+
},
58+
body: JSON.stringify(notificationPayload),
59+
});
60+
61+
const result = await response.json();
62+
63+
if (!response.ok) {
64+
console.error('OneSignal API error:', result);
65+
return res.status(response.status).json({ error: 'OneSignal API error', details: result });
66+
}
67+
68+
console.log('Notification sent successfully:', result.id);
69+
return res.status(200).json({ success: true, notificationId: result.id });
70+
} catch (error) {
71+
console.error('Error sending notification:', error);
72+
return res.status(500).json({ error: 'Failed to send notification' });
73+
}
74+
}

lib/services/notification_service.dart

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import 'package:cloud_firestore/cloud_firestore.dart';
22
import 'package:firebase_messaging/firebase_messaging.dart';
33
import 'package:flutter/foundation.dart';
44
import '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;
59
import '../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,

pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,7 @@ packages:
915915
source: hosted
916916
version: "15.0.2"
917917
web:
918-
dependency: transitive
918+
dependency: "direct main"
919919
description:
920920
name: web
921921
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ dependencies:
5656
url_launcher: ^6.3.2
5757
font_awesome_flutter: ^10.12.0
5858
firebase_messaging: ^16.0.4
59+
web: ^1.1.0
5960

6061

6162

vercel.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"buildCommand": "flutter build web --release",
3+
"outputDirectory": "build/web",
4+
"framework": null,
5+
"rewrites": [
6+
{
7+
"source": "/(.*)",
8+
"destination": "/index.html"
9+
}
10+
]
11+
}

web/index.html

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,43 @@ <h3>Install Orbit</h3>
485485
}
486486
</script>
487487

488+
<!-- OneSignal Web Push SDK -->
489+
<script src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js" defer></script>
490+
<script>
491+
window.OneSignalDeferred = window.OneSignalDeferred || [];
492+
OneSignalDeferred.push(async function(OneSignal) {
493+
await OneSignal.init({
494+
appId: "YOUR_ONESIGNAL_APP_ID", // Replace with your OneSignal App ID
495+
notifyButton: {
496+
enable: false, // We use Flutter UI for notifications
497+
},
498+
allowLocalhostAsSecureOrigin: true, // For local development
499+
});
500+
501+
// Expose function to get player ID for Flutter
502+
window.getOneSignalPlayerId = async function() {
503+
try {
504+
const playerId = await OneSignal.User.PushSubscription.id;
505+
return playerId;
506+
} catch (e) {
507+
console.error('Error getting OneSignal player ID:', e);
508+
return null;
509+
}
510+
};
511+
512+
// Expose function to request permission
513+
window.requestPushPermission = async function() {
514+
try {
515+
await OneSignal.Notifications.requestPermission();
516+
return await window.getOneSignalPlayerId();
517+
} catch (e) {
518+
console.error('Error requesting push permission:', e);
519+
return null;
520+
}
521+
};
522+
});
523+
</script>
524+
488525
<script src="flutter_bootstrap.js" async></script>
489526
</body>
490527

0 commit comments

Comments
 (0)