import fetch from 'node-fetch'; const EXPO_PUSH_URL = 'https://exp.host/--/api/v2/push/send'; export interface PushNotificationPayload { to: string; title: string; body: string; data?: Record; sound?: string; badge?: number; priority?: 'default' | 'normal' | 'high'; channelId?: string; } export interface PushTicket { id: string; status: 'ok' | 'error'; message?: string; details?: { error?: string; }; } export interface PushReceipt { status: 'ok' | 'error'; message?: string; details?: { error?: string; }; } interface ExpoPushResponse { data?: PushTicket[] | Record; } export async function sendPushNotification(payload: PushNotificationPayload): Promise { const response = await fetch(EXPO_PUSH_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify(payload), }); const result = (await response.json()) as ExpoPushResponse; if (result.data && Array.isArray(result.data)) { return result.data[0] ?? { id: '', status: 'error', message: 'No ticket returned' }; } return { id: '', status: 'error', message: 'No ticket returned' }; } export async function sendPushNotifications(payloads: PushNotificationPayload[]): Promise { const chunks: PushNotificationPayload[][] = []; for (let i = 0; i < payloads.length; i += 100) { chunks.push(payloads.slice(i, i + 100)); } const tickets: PushTicket[] = []; for (const chunk of chunks) { const response = await fetch(EXPO_PUSH_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify(chunk), }); const result = (await response.json()) as ExpoPushResponse; if (result.data && Array.isArray(result.data)) { tickets.push(...result.data); } } return tickets; } export async function getPushReceipts(ticketIds: string[]): Promise { if (ticketIds.length === 0) return []; const response = await fetch(`${EXPO_PUSH_URL}/receipts`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify({ ids: ticketIds }), }); const result = (await response.json()) as ExpoPushResponse; const receipts: PushReceipt[] = []; if (result.data && !Array.isArray(result.data)) { for (const id of ticketIds) { receipts.push(result.data[id] ?? { status: 'error', message: 'No receipt found' }); } } return receipts; } export function isExpoPushToken(token: string): boolean { return token.startsWith('ExponentPushToken[') || token.startsWith('ExpoPushToken['); } export function createTaskReminderPayload( pushToken: string, taskTitle: string, dueTime: string, taskId: string, repeat?: string ): PushNotificationPayload { return { to: pushToken, title: taskTitle, body: `Due ${dueTime}${repeat && repeat !== 'none' ? ' ยท repeating' : ''}`, data: { type: 'task_reminder', taskId }, sound: 'default', priority: 'high', channelId: 'task-reminders', }; } export function createFriendRequestPayload( pushToken: string, fromUsername: string, friendshipId: string ): PushNotificationPayload { return { to: pushToken, title: 'New Friend Request', body: `${fromUsername} wants to be your friend`, data: { type: 'friend_request', friendshipId }, sound: 'default', priority: 'high', channelId: 'friend-requests', }; } export function createTaskAssignmentPayload( pushToken: string, taskTitle: string, fromUsername: string, taskId: string ): PushNotificationPayload { return { to: pushToken, title: 'Task Assigned', body: `${fromUsername} assigned you "${taskTitle}"`, data: { type: 'task_assignment', taskId }, sound: 'default', priority: 'high', channelId: 'task-assignments', }; }