migrant-web/app/Services/FCMService.php
2026-07-07 01:19:50 +05:30

170 lines
5.7 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class FCMService
{
/**
* Generate OAuth2 Access Token for Firebase Messaging.
* Caches the token for 50 minutes to avoid overhead.
*
* @return string|null
*/
public static function getAccessToken()
{
return Cache::remember('fcm_access_token', 3000, function () {
try {
$credentialsPath = base_path('migrant-fe5a7-firebase-adminsdk-fbsvc-f8b365ecd0.json');
if (!file_exists($credentialsPath)) {
Log::error("FCM Service Account JSON not found at: {$credentialsPath}");
return null;
}
$credentials = json_decode(file_get_contents($credentialsPath), true);
if (!$credentials) {
Log::error("FCM Service Account JSON is invalid or empty.");
return null;
}
$privateKey = $credentials['private_key'];
$clientEmail = $credentials['client_email'];
$header = json_encode(['alg' => 'RS256', 'typ' => 'JWT']);
$now = time();
$payload = json_encode([
'iss' => $clientEmail,
'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
'aud' => 'https://oauth2.googleapis.com/token',
'exp' => $now + 3600,
'iat' => $now,
]);
$base64UrlHeader = self::base64UrlEncode($header);
$base64UrlPayload = self::base64UrlEncode($payload);
$signatureInput = $base64UrlHeader . '.' . $base64UrlPayload;
$signature = '';
if (!openssl_sign($signatureInput, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
Log::error("Failed to sign JWT with private key using openssl_sign.");
return null;
}
$base64UrlSignature = self::base64UrlEncode($signature);
$jwtToken = $signatureInput . '.' . $base64UrlSignature;
$response = Http::withoutVerifying()->asForm()->post('https://oauth2.googleapis.com/token', [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwtToken,
]);
if ($response->failed()) {
Log::error("FCM OAuth2 request failed: " . $response->body());
return null;
}
$data = $response->json();
return $data['access_token'] ?? null;
} catch (\Exception $e) {
Log::error("FCM Access Token Generation Error: " . $e->getMessage());
return null;
}
});
}
/**
* Send a Push Notification to a specific FCM token.
*
* @param string $token
* @param string $title
* @param string $body
* @param array $data
* @return bool
*/
public static function sendPushNotification($token, $title, $body, array $data = [])
{
if (empty($token)) {
return false;
}
try {
$accessToken = self::getAccessToken();
if (!$accessToken) {
Log::error("Cannot send push notification: FCM Access Token is null.");
return false;
}
// Load credentials to fetch the correct project_id
$credentialsPath = base_path('migrant-fe5a7-firebase-adminsdk-fbsvc-f8b365ecd0.json');
$credentials = json_decode(file_get_contents($credentialsPath), true);
$projectId = $credentials['project_id'] ?? 'migrant-fe5a7';
// Convert all data array values to strings as required by FCM v1
$formattedData = [];
foreach ($data as $key => $value) {
$formattedData[(string)$key] = (string)$value;
}
$payload = [
'message' => [
'token' => $token,
'notification' => [
'title' => $title,
'body' => $body,
],
'android' => [
'notification' => [
'click_action' => 'FLUTTER_NOTIFICATION_CLICK',
'sound' => 'default',
],
],
'apns' => [
'payload' => [
'aps' => [
'sound' => 'default',
'badge' => 1,
],
],
],
]
];
if (!empty($formattedData)) {
$payload['message']['data'] = $formattedData;
}
$response = Http::withoutVerifying()->withToken($accessToken)
->post("https://fcm.googleapis.com/v1/projects/{$projectId}/messages:send", $payload);
if ($response->failed()) {
Log::error("FCM Push Notification failed: " . $response->body());
return false;
}
return true;
} catch (\Exception $e) {
Log::error("FCM Notification Dispatch Failure: " . $e->getMessage());
return false;
}
}
/**
* Base64 Url Encode helper.
*
* @param string $data
* @return string
*/
private static function base64UrlEncode($data)
{
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
}
}