progresspulse-pwa
Version:
A modern PWA for tracking progress and achieving goals with iPhone-style design
304 lines (268 loc) • 9.41 kB
text/typescript
import { initializeMessaging, getToken, onMessage, vapidKey } from './firebase';
import { db } from './database';
export class PushNotificationService {
private static instance: PushNotificationService;
private messaging: any = null;
private fcmToken: string | null = null;
private isNativeApp: boolean = false;
static getInstance(): PushNotificationService {
if (!PushNotificationService.instance) {
PushNotificationService.instance = new PushNotificationService();
}
return PushNotificationService.instance;
}
constructor() {
this.detectEnvironment();
this.initialize();
}
private detectEnvironment() {
// Detect if running in native app wrapper (Median.co)
this.isNativeApp = !!(
(window as any).median ||
(window as any).cordova ||
(window as any).PhoneGap ||
navigator.userAgent.includes('wv') || // WebView
navigator.userAgent.includes('Median')
);
console.log('Environment detected:', this.isNativeApp ? 'Native App' : 'Web Browser');
}
private async initialize() {
try {
if (this.isNativeApp) {
await this.initializeNativeNotifications();
} else {
await this.initializeWebNotifications();
}
} catch (error) {
console.error('Failed to initialize push notifications:', error);
}
}
private async initializeWebNotifications() {
try {
this.messaging = await initializeMessaging();
if (this.messaging) {
// Get FCM token for web
this.fcmToken = await getToken(this.messaging, {
vapidKey: vapidKey
});
console.log('FCM Token (Web):', this.fcmToken);
// Listen for foreground messages
onMessage(this.messaging, (payload) => {
console.log('Foreground message received:', payload);
this.handleForegroundMessage(payload);
});
}
} catch (error) {
console.error('Web notifications initialization failed:', error);
}
}
private async initializeNativeNotifications() {
try {
// For Median.co apps, use their native push notification API
if ((window as any).median && (window as any).median.push) {
console.log('Initializing Median.co push notifications');
// Register for push notifications
(window as any).median.push.register({
callback: (token: string) => {
this.fcmToken = token;
console.log('FCM Token (Native):', token);
this.sendTokenToServer(token);
},
error: (error: any) => {
console.error('Native push registration failed:', error);
}
});
// Listen for push messages
(window as any).median.push.onMessage({
callback: (payload: any) => {
console.log('Native push message received:', payload);
this.handleNativeMessage(payload);
}
});
} else {
// Fallback to web notifications for other native wrappers
await this.initializeWebNotifications();
}
} catch (error) {
console.error('Native notifications initialization failed:', error);
// Fallback to web notifications
await this.initializeWebNotifications();
}
}
private handleForegroundMessage(payload: any) {
const { notification, data } = payload;
// Show notification using browser API
if (Notification.permission === 'granted') {
new Notification(notification?.title || 'ProgressPulse', {
body: notification?.body,
icon: notification?.icon || '/pwa-192x192.png',
badge: '/pwa-64x64.png',
tag: data?.tag || 'default',
data: data
});
}
}
private handleNativeMessage(payload: any) {
// Handle native push message
console.log('Handling native message:', payload);
// Store notification in database
if (payload.userId) {
db.createNotification({
userId: payload.userId,
type: payload.type || 'general',
title: payload.title || 'ProgressPulse',
message: payload.body || payload.message || '',
read: false,
goalId: payload.goalId
});
}
}
async requestPermission(): Promise<boolean> {
try {
if (this.isNativeApp) {
// For native apps, permission is usually handled during app install
return true;
} else {
// For web, request notification permission
if (!('Notification' in window)) {
console.warn('This browser does not support notifications');
return false;
}
if (Notification.permission === 'granted') {
return true;
}
if (Notification.permission === 'denied') {
return false;
}
const permission = await Notification.requestPermission();
return permission === 'granted';
}
} catch (error) {
console.error('Permission request failed:', error);
return false;
}
}
async getToken(): Promise<string | null> {
return this.fcmToken;
}
private async sendTokenToServer(token: string) {
try {
// In a real app, send this token to your backend server
// The server will use this token to send push notifications
console.log('Token to send to server:', token);
// Store token locally for now
localStorage.setItem('fcm_token', token);
// You would typically send this to your backend:
// await fetch('/api/register-push-token', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ token, userId: currentUserId })
// });
} catch (error) {
console.error('Failed to send token to server:', error);
}
}
async sendNotificationToServer(data: {
title: string;
body: string;
userId?: string;
type?: string;
goalId?: string;
scheduledTime?: Date;
}) {
try {
// This is where you'd send the notification request to your backend
// Your backend would then use FCM Admin SDK to send the push notification
console.log('Notification data to send to server:', data);
// Example API call (implement this in your backend):
// await fetch('/api/send-push-notification', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({
// token: this.fcmToken,
// notification: {
// title: data.title,
// body: data.body
// },
// data: {
// type: data.type,
// userId: data.userId,
// goalId: data.goalId
// }
// })
// });
// For now, show local notification as fallback
if (this.isNativeApp && (window as any).median?.push) {
(window as any).median.push.showNotification({
title: data.title,
body: data.body,
data: {
type: data.type,
userId: data.userId,
goalId: data.goalId
}
});
} else {
// Web fallback
if (Notification.permission === 'granted') {
new Notification(data.title, {
body: data.body,
icon: '/pwa-192x192.png',
badge: '/pwa-64x64.png',
tag: data.type || 'default'
});
}
}
} catch (error) {
console.error('Failed to send notification:', error);
}
}
// Enhanced methods for different notification types
async sendGoalCompletedNotification(userId: string, goalTitle: string) {
await this.sendNotificationToServer({
title: '🏆 Goal Completed!',
body: `Congratulations! You've completed "${goalTitle}"!`,
userId,
type: 'goal_completed'
});
}
async sendDailyReminderNotification(userId: string, message: string) {
await this.sendNotificationToServer({
title: '⏰ Daily Reminder',
body: message,
userId,
type: 'daily_reminder'
});
}
async sendProgressUpdateNotification(userId: string, goalTitle: string, progress: number) {
await this.sendNotificationToServer({
title: '📈 Progress Update',
body: `Great progress on "${goalTitle}"! You're ${progress}% complete.`,
userId,
type: 'progress_update'
});
}
async sendStreakNotification(userId: string, days: number) {
await this.sendNotificationToServer({
title: '🔥 Streak Alert!',
body: `Amazing! You're on a ${days}-day streak. Keep it going!`,
userId,
type: 'streak_notification'
});
}
async scheduleNotification(data: {
title: string;
body: string;
scheduledTime: Date;
userId?: string;
type?: string;
}) {
const delay = data.scheduledTime.getTime() - Date.now();
if (delay > 0) {
setTimeout(() => {
this.sendNotificationToServer(data);
}, delay);
}
}
}
export const pushNotificationService = PushNotificationService.getInstance();