projectId = env('FIREBASE_PROJECT_ID', ''); $this->serviceAccountPath = env('FIREBASE_SERVICE_ACCOUNT_PATH', APP_PATH . '/config/firebase-service-account.json'); } /** * Send a push notification to a specific user or device */ public function sendNotification(string $userId, string $title, string $body, array $data = [], ?string $deviceId = null): bool { $db = Database::getInstance(); // 1. Get push tokens for the user if ($deviceId) { $stmt = $db->prepare("SELECT push_token FROM user_devices WHERE user_id = ? AND device_fingerprint = ? AND push_token IS NOT NULL"); $stmt->execute([$userId, $deviceId]); } else { $stmt = $db->prepare("SELECT push_token FROM user_devices WHERE user_id = ? AND push_token IS NOT NULL AND is_active = 1"); $stmt->execute([$userId]); } $tokens = $stmt->fetchAll(\PDO::FETCH_COLUMN); if (empty($tokens)) { return false; } // 2. Save notification to database (Single direct insert) $stmt = $db->prepare("SELECT tenant_id FROM users WHERE id = ? LIMIT 1"); $stmt->execute([$userId]); $tenantId = $stmt->fetchColumn(); if ($tenantId) { $stmt = $db->prepare("INSERT INTO notifications (id, tenant_id, user_id, type, title, body, data, created_at) VALUES (UUID(), ?, ?, 'system', ?, ?, ?, NOW())"); $stmt->execute([$tenantId, $userId, $title, $body, json_encode($data)]); } // 3. Send to each token $successCount = 0; foreach ($tokens as $token) { if ($this->dispatchToFcm($token, $title, $body, $data)) { $successCount++; } } return $successCount > 0; } /** * Dispatch notification to Firebase via HTTP v1 API */ private function dispatchToFcm(string $token, string $title, string $body, array $data): bool { if (!file_exists($this->serviceAccountPath)) { error_log("[NotificationService] Firebase service account file missing: {$this->serviceAccountPath}"); return false; } $accessToken = $this->getAccessToken(); if (!$accessToken) return false; $url = "https://fcm.googleapis.com/v1/projects/{$this->projectId}/messages:send"; $payload = [ 'message' => [ 'token' => $token, 'notification' => [ 'title' => $title, 'body' => $body, ], 'data' => array_map('strval', $data), // FCM data values must be strings 'android' => [ 'priority' => 'high', 'notification' => [ 'sound' => 'default', 'channel_id' => 'high_importance_channel' ] ], 'apns' => [ 'payload' => [ 'aps' => [ 'sound' => 'default', ], ], ], ], ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json', ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { error_log("[NotificationService] FCM Send Error ($httpCode): " . $response); return false; } return true; } /** * Get OAuth2 Access Token for Firebase (Cache this in production!) * Note: This requires a JWT library or manual signing. * For simplicity, we assume the user might use a Google Auth library. * But since we avoid extra deps, I will provide a minimal implementation or suggestion. */ private function getAccessToken(): ?string { // This is a complex part that usually requires 'google/auth' library. // For now, I will return null and tell the user they need to install google/auth via composer // OR I can write a minimal JWT signer for Google Auth if they don't want composer. error_log("[NotificationService] OAuth2 Token generation needs google/auth library."); return null; } }