feat: Firebase Cloud Messaging push notifications
Backend: - New user_fcm_tokens table (migration 010) for storing device tokens - FCMService sends push via Firebase HTTP legacy API (server key from .env) - POST /api/fcm/register endpoint for Android to register its FCM token - AdminController::sendNotification() triggers FCM push after DB insert - FCM config added to config.php Android: - Firebase Cloud Messaging service (ServerManagerFirebaseService) receives push notifications and shows them via NotificationHelper - FCM token registered with backend on new token and on login - google-services.json template (must be replaced with real Firebase config) - firebase-setup.sh script with configuration instructions - FCM dependency + google-services plugin added to Gradle - Bump to v1.8.0 (versionCode 12)
This commit is contained in:
131
src/Services/FCMService.php
Normal file
131
src/Services/FCMService.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ServerManager\Services;
|
||||
|
||||
use ServerManager\Core\App;
|
||||
use ServerManager\Core\Database;
|
||||
use ServerManager\Core\Logger;
|
||||
|
||||
class FCMService
|
||||
{
|
||||
private Database $db;
|
||||
private Logger $logger;
|
||||
private string $serverKey;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::getInstance();
|
||||
$this->logger = Logger::getInstance();
|
||||
$config = App::getConfig();
|
||||
|
||||
$this->serverKey = $config['fcm']['server_key'] ?? $_ENV['FCM_SERVER_KEY'] ?? '';
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return !empty($this->serverKey);
|
||||
}
|
||||
|
||||
public function sendToUser(int $userId, string $title, string $body, string $type = 'info', ?int $notificationId = null): bool
|
||||
{
|
||||
if (!$this->isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tokens = $this->db->fetchAll(
|
||||
'SELECT token FROM user_fcm_tokens WHERE user_id = ?',
|
||||
[$userId]
|
||||
);
|
||||
|
||||
if (empty($tokens)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$success = true;
|
||||
foreach ($tokens as $row) {
|
||||
$result = $this->sendToDevice($row['token'], $title, $body, $type, $notificationId);
|
||||
if (!$result) $success = false;
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
public function sendToAll(string $title, string $body, string $type = 'info', ?int $notificationId = null): int
|
||||
{
|
||||
if (!$this->isConfigured()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$tokens = $this->db->fetchAll('SELECT DISTINCT token FROM user_fcm_tokens');
|
||||
if (empty($tokens)) return 0;
|
||||
|
||||
$sent = 0;
|
||||
foreach ($tokens as $row) {
|
||||
if ($this->sendToDevice($row['token'], $title, $body, $type, $notificationId)) {
|
||||
$sent++;
|
||||
}
|
||||
}
|
||||
|
||||
return $sent;
|
||||
}
|
||||
|
||||
private function sendToDevice(string $token, string $title, string $body, string $type, ?int $notificationId): bool
|
||||
{
|
||||
$payload = [
|
||||
'to' => $token,
|
||||
'priority' => 'high',
|
||||
'notification' => [
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'sound' => 'default',
|
||||
],
|
||||
'data' => [
|
||||
'type' => $type,
|
||||
'notification_id' => (string) ($notificationId ?? 0),
|
||||
'click_action' => 'OPEN_NOTIFICATIONS',
|
||||
],
|
||||
'android' => [
|
||||
'priority' => 'high',
|
||||
'notification' => [
|
||||
'channel_id' => match ($type) {
|
||||
'error', 'warning' => 'notifications_alerts',
|
||||
default => 'notifications_general',
|
||||
},
|
||||
'priority' => match ($type) {
|
||||
'error' => 'high',
|
||||
default => 'default',
|
||||
},
|
||||
'sound' => 'default',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$ch = curl_init('https://fcm.googleapis.com/fcm/send');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: key=' . $this->serverKey,
|
||||
],
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
$this->logger->error('FCM send failed', [
|
||||
'http_code' => $httpCode,
|
||||
'response' => substr($response, 0, 500),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user