diff --git a/composer.json b/composer.json index bddf6dc..8727dbe 100644 --- a/composer.json +++ b/composer.json @@ -23,10 +23,11 @@ } ], "require": { - "php": "^7.1.3|^8.0", + "php": "^7.1.3 || ^8.0", "guzzlehttp/guzzle": "^6.3 || ^7.0.1", "illuminate/support": "~5.8 || ^6.0 || ^7.0 || ^8.0 || ^9.0|^10.0|^11.0|^12.0", - "illuminate/notifications": "~5.8 || ^6.0 || ^7.0 || ^8.0 || ^9.0|^10.0|^11.0|^12.0" + "illuminate/notifications": "~5.8 || ^6.0 || ^7.0 || ^8.0 || ^9.0|^10.0|^11.0|^12.0", + "google/apiclient": "^2.15" }, "require-dev": { "phpunit/phpunit": "^7.5 || ^8.0 || ^9.0|^10.5" @@ -46,4 +47,4 @@ } } } -} +} \ No newline at end of file diff --git a/readme.md b/readme.md index 49049e1..92cb7a9 100644 --- a/readme.md +++ b/readme.md @@ -11,6 +11,7 @@ This is an easy to use package to send push notification. * GCM * FCM +* FCMV1 * APN ## Installation @@ -59,6 +60,28 @@ $push->setConfig([ ]); ``` +The default configuration parameters for **FCMV1** are : + +* ```priority => 'normal'``` +* ```dry_run => false``` +* ```projectId => 'my-project-id'``` +* ```jsonFile => __DIR__ . '/fcmCertificates/file.json'``` + +You can dynamically update those values or adding new ones calling the method setConfig like so: +```php +$push->setConfig([ + 'priority' => 'high', + 'projectId' => 'my-real-project-id', + 'jsonFile' => 'path/to/credentials.json' +]); +``` + +To generate a credentials json file for your service account: + +* In the Firebase console, open **Settings** > [Service Accounts](https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk). +* Click **Generate New Private Key**, then confirm by clicking **Generate Key**. +* Securely store the JSON file containing the key. + The default configuration parameters for **APN** are: @@ -117,6 +140,11 @@ For FCM Service: $push = new PushNotification('fcm'); ``` +For FCMV1 Service: +```php +$push = new PushNotification('fcmv1'); +``` + Now you may use any method that you need. Please see the API List. @@ -139,6 +167,11 @@ Now you may use any method that you need. Please see the API List. - [sendByTopic](https://github.com/edujugon/PushNotification#sendbytopic) +### Only for Fcmv1 + +- [setProjectId](https://github.com/edujugon/PushNotification#setprojectid) +- [setJsonFile](https://github.com/edujugon/PushNotification#setjsonfile) + > Go to [Usage samples](https://github.com/edujugon/PushNotification#usage-samples) directly. #### setService @@ -173,6 +206,30 @@ object setMessage(array $data) object setApiKey($api_key) ``` +#### setProjectId + +> Only for fcmv1 + +`setProjectId` method sets the Project ID of your App as a string. + +**Syntax** + +```php +object setProjectId($project_id) +``` + +#### setJsonFile + +> Only for fcmv1 + +`setJsonFile` method sets the path of credentials json file of your App. + +**Syntax** + +```php +object setJsonFile($api_key) +``` + #### setDevicesToken `setDevicesToken` method sets the devices' tokens, which you pass the token through parameter as array or string if it was only one. diff --git a/src/Channels/FcmV1Channel.php b/src/Channels/FcmV1Channel.php new file mode 100644 index 0000000..33c806f --- /dev/null +++ b/src/Channels/FcmV1Channel.php @@ -0,0 +1,71 @@ +title != null || $message->body != null) { + $data = [ + 'notification' => [ + 'title' => $message->title, + 'body' => $message->body, + ], + ]; + } + + if ( + $message->icon || + $message->color || + $message->sound || + $message->click_action || + $message->badge + ) { + $data['android'] = [ + 'notification' => [] + ]; + + if (! empty($message->icon)) { + $data['android']['notification']['icon'] = $message->icon; + } + + if (! empty($message->color)) { + $data['android']['notification']['color'] = $message->color; + } + + if (! empty($message->sound)) { + $data['android']['notification']['sound'] = $message->sound; + } + + if (! empty($message->click_action)) { + $data['android']['notification']['click_action'] = $message->click_action; + } + + if (! empty($message->badge)) { + $data['android']['notification']['notification_count'] = $message->badge; + } + } + + if (! empty($message->extra)) { + $data['data'] = $message->extra; + } + + return $data; + } +} diff --git a/src/Config/config.php b/src/Config/config.php index fd3f91c..60a6d43 100644 --- a/src/Config/config.php +++ b/src/Config/config.php @@ -1,4 +1,5 @@ [], ], + 'fcmv1' => [ + 'priority' => 'normal', + 'dry_run' => false, + 'projectId' => 'my-project-id', + 'jsonFile' => __DIR__ . '/fcmCertificates/file.json', + // 'credentials_cache_seconds' => 55 * 60, // Optional. Credentials cache time in seconds. Maximum 60*60 seconds. + // 'cache_store' => null, // Optional. Cache store name. Null for default + // 'concurrentRequests' => 5, // Optional, default 10 + // Optional: Default Guzzle request options for each FCM request + // See https://docs.guzzlephp.org/en/stable/request-options.html + 'guzzle' => [], + ], 'apn' => [ 'certificate' => __DIR__ . '/iosCertificates/apns-dev-cert.pem', 'passPhrase' => 'secret', //Optional diff --git a/src/Config/fcmCertificates/.gitkeep b/src/Config/fcmCertificates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/FcmV1.php b/src/FcmV1.php new file mode 100644 index 0000000..0ad62f8 --- /dev/null +++ b/src/FcmV1.php @@ -0,0 +1,229 @@ +config = $this->initializeConfig('fcmv1'); + + $this->url = self::BASE_URL . $this->config['projectId'] . '/messages:send'; + + $this->client = new Client($this->config['guzzle'] ?? []); + + $this->concurrentRequests = $this->config['concurrentRequests'] ?? 10; + + $this->credentialsCacheSeconds = $this->config['credentials_cache_seconds'] ?? self::DEFAULT_CREDENTIALS_CACHE_SECONDS; + + $this->cacheStore = $this->config['cache_store'] ?? null; + } + + /** + * Set the apiKey for the notification + * @param string $apiKey + */ + public function setApiKey($apiKey) + { + throw new Exception('Not available on FCM V1'); + } + + /** + * Set the projectId for the notification + * @param string $projectId + */ + public function setProjectId($projectId) + { + $this->config['projectId'] = $projectId; + + $this->url = self::BASE_URL . $this->config['projectId'] . '/messages:send'; + } + + /** + * Set the jsonFile path for the notification + * @param string $jsonFile + */ + public function setJsonFile($jsonFile) + { + $this->config['jsonFile'] = $jsonFile; + } + + /** + * Update the values by key on config array from the passed array. If any key doesn't exist, it's added. + * @param array $config + */ + public function setConfig(array $config) + { + parent::setConfig($config); + + // Update url + $this->setProjectId($this->config['projectId']); + } + + /** + * Set the needed headers for the push notification. + * + * @return array + */ + protected function addRequestHeaders() + { + return [ + 'Authorization' => 'Bearer ' . $this->getOauthToken(), + 'Content-Type' => 'application/json', + ]; + } + + /** + * Send Push Notification + * + * @param array $deviceTokens + * @param array $message + * + * @return \stdClass GCM Response + */ + public function send(array $deviceTokens, array $message) + { + // FCM v1 does not allows multiple devices at once + + $headers = $this->addRequestHeaders(); + $jsonData = ['message' => $this->buildMessage($message)]; + + $this->feedbacks = []; + $this->unregisteredDeviceTokens = []; + + $requests = []; + foreach ($deviceTokens as $deviceToken) { + $jsonData['message']['token'] = $deviceToken; + + $body = json_encode($jsonData); + + $requests[$deviceToken] = new Request('POST', $this->url, $headers, $body); + } + + $pool = new Pool($this->client, $requests, [ + 'concurrency' => $this->concurrentRequests, + 'fulfilled' => function (GuzzleResponse $response, $deviceToken) { + // this is delivered each successful response + + $this->feedbacks[$deviceToken] = [ + 'success' => true, + 'response' => json_decode((string) $response->getBody(), true, 512, JSON_BIGINT_AS_STRING), + ]; + }, + 'rejected' => function (RequestException $reason, $deviceToken) { + // this is delivered each failed request + + $error = json_decode((string) $reason->getResponse()->getBody(), true); + + $this->feedbacks[$deviceToken] = [ + 'success' => false, + 'error' => $error, + ]; + + if (isset($error['error']['code']) && $error['error']['code'] === 404) { + $this->unregisteredDeviceTokens[] = $deviceToken; + } + }, + ]); + + // Initiate the transfers and create a promise + $promise = $pool->promise(); + + // Force the pool of requests to complete. + $promise->wait(); + + $this->setFeedback($this->feedbacks); + } + + /** + * Provide the unregistered tokens of the sent notification. + * + * @param array $devices_token + * @return array $tokenUnRegistered + */ + public function getUnregisteredDeviceTokens(array $devices_token) + { + return $this->unregisteredDeviceTokens; + } + + /** + * Prepare the data to be sent + * + * @param $topic + * @param $message + * @param $isCondition + * @return array + */ + protected function buildData($topic, $message, $isCondition) + { + $condition = $isCondition ? ['condition' => $topic] : ['to' => '/topics/' . $topic]; + + return [ + 'message' => array_merge($condition, $this->buildMessage($message)), + ]; + } + + protected function getOauthToken() + { + return Cache::store($this->cacheStore) + ->remember( + Str::slug('fcm-v1-oauth-token-' . $this->config['projectId']), + Carbon::now()->addSeconds($this->credentialsCacheSeconds), + function () { + $jsonFilePath = $this->config['jsonFile']; + + $googleClient = new GoogleClient(); + + $googleClient->setAuthConfig($jsonFilePath); + $googleClient->addScope(FirebaseCloudMessaging::FIREBASE_MESSAGING); + + $accessToken = $googleClient->fetchAccessTokenWithAssertion(); + + $oauthToken = $accessToken['access_token']; + + return $oauthToken; + } + ); + } +} diff --git a/src/PushNotification.php b/src/PushNotification.php index 6e7305f..6770f12 100644 --- a/src/PushNotification.php +++ b/src/PushNotification.php @@ -18,7 +18,8 @@ class PushNotification protected $servicesList = [ 'gcm' => Gcm::class, 'apn' => Apn::class, - 'fcm' => Fcm::class + 'fcm' => Fcm::class, + 'fcmv1' => FcmV1::class, ]; /**