serverKey = config('services.fcm.server_key', ''); $this->mobileConfig = $mobileConfig; } /** * Check if notifications are enabled globally in settings. */ private function isEnabled(): bool { $config = $this->mobileConfig->all(); return filter_var($config['notifications']['enable_push_notifications'] ?? true, FILTER_VALIDATE_BOOLEAN); } /** * Send push notification to a single user (all their devices). */ public function sendToUser(int $userId, string $title, string $body, array $data = []): void { if (! $this->isEnabled()) { return; } $tokens = DeviceToken::where('user_id', $userId)->pluck('token')->toArray(); if (empty($tokens)) { return; } $this->sendToTokens($tokens, $title, $body, $data); } /** * Send to a list of device tokens. */ public function sendToTokens(array $tokens, string $title, string $body, array $data = []): void { if (! $this->isEnabled()) { return; } if (empty($this->serverKey)) { Log::warning('FCM server key not configured — push notification skipped'); return; } // FCM supports max 1000 tokens per request foreach (array_chunk($tokens, 1000) as $chunk) { $this->dispatch($chunk, $title, $body, $data); } } private function dispatch(array $tokens, string $title, string $body, array $data): void { $payload = [ 'registration_ids' => $tokens, 'notification' => ['title' => $title, 'body' => $body], 'data' => $data, 'priority' => 'high', ]; try { $response = Http::withHeaders([ 'Authorization' => 'key='.$this->serverKey, 'Content-Type' => 'application/json', ])->post($this->endpoint, $payload); if (! $response->successful()) { Log::error('FCM dispatch failed', ['status' => $response->status(), 'body' => $response->body()]); } else { $this->handleFcmResponse($response->json(), $tokens); } } catch (\Throwable $e) { Log::error('FCM HTTP error', ['error' => $e->getMessage()]); } } private function handleFcmResponse(array $result, array $tokens): void { if (empty($result['results'])) { return; } $invalidTokens = []; foreach ($result['results'] as $index => $res) { if (isset($res['error']) && in_array($res['error'], ['NotRegistered', 'InvalidRegistration'])) { $invalidTokens[] = $tokens[$index]; } } if (! empty($invalidTokens)) { DeviceToken::whereIn('token', $invalidTokens)->delete(); Log::info('FCM removed stale tokens', ['count' => count($invalidTokens)]); } } }