FirebaseCloudFunctions + FirebaseCloudMessaging でプッシュ通知を送ってみる

この記事でできるようになるもの

$ curl -X POST https://your-functions-url/push?device_token=your-device-token -d "message=I love fishing🎣" のようなリクエストをターミナルで叩けば下記のようなプッシュ通知を送信できるようになります。

f:id:yum_fishing:20200731201151p:plain

事前準備

  • Firebaseプロジェクト
  • Firebase/Messaging導入済のiosプロジェクト
  • APNsのFirebaseアップロード

FirebaseCLIインストール

まずは、FirebaseCLIをインストールすることでFunctionsのDeployやプロジェクトの切り替えなどをCLIで操作できるようにします。今回はnpmでインストールを行います。

npmインストール

とりあえず最新のものをnodebrewで取得してきてPathを通すとこまで終わらせます。

    $ brew install nodebrew
    $ nodebrew install-binary 13.7.0
    $ nodebrew use  v7.0.0
    $ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
    $ source ~/.bash_profile

firebase-toolsインストール

1.npmでfirebase-toolsをインストールします。

    $ npm install -g firebase-tools

2.firebase-toolsコマンドを使用して、操作を行うユーザの認証をします。下記のコマンドを実行するとWebブラウザが立ち上がるので、Firebaseプロジェクトで編集権限のあるアカウントでログインを行います。

    $ firebase login

3.firebaseのプロジェクトをuseコマンドを使って指定します。この操作によりfirebase/functionsなどのデプロイ先を変更できたりします。

    $ firebase use firebase_project_id

Functionsプロジェクト作成

今回はFunctionsのみ使用するので下記のコマンドでプロジェクトを立ち上げます。

    $ firebase init functions

すると下記のような構造のプロジェクトが立ち上がるので、主にindex.jsを編集して関数を作成して行きます。 スクリーンショット 2020-02-05 22.25.27.png

参照: https://firebase.google.com/docs/functions/get-started?hl=ja

FirebaseAdminSDKインストール

1.sdkの情報などを保存するpackage.jsonを作成します。

    $ npm init

2.firebase-admin npmパッケージをインストールします。

   $ npm install firebase-admin --save

3.次にfirebase-adminを初期化をするためにローカルの環境変数にFirebaseサービスアカウントの秘密鍵を生成したファイルへのパスを指定します。これを設定することでSDKの初期化時にキーが参照され、プロジェクトでの認証が完了します。CIなどでブランチごとにDeploy先を変更させたい時はどうやって秘密鍵を参照させるのがベストなんでしょうか?

   $ export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/service-account-file.json"

参照: https://firebase.google.com/docs/admin/setup?hl=ja

4.index.jsに移動してsdkの初期化コードを追加します。

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

node.jsの実装

index.js

const functions = require('firebase-functions');
const admin = require('firebase-admin');

admin.initializeApp();

//onRequestでhttpからの呼び出しを可能にします。
exports.push = functions.https.onRequest((request, response) => {
  if (request.query.device_token !== undefined && request.body.message !== undefined) {
    const device_token = request.query.device_token
    const message = request.body.message
    const payload = {
      notification: {
        body: message,
        badge: "1",
        sound:"default",
      }
    };
    switch (request.method) {
      case 'POST':
        push(device_token, payload, response);
        break
      default:
        response.status(400).send({ error: 'Invalid request method' })
        break
    }
  } else {
    response.status(400).send({ error: 'Invalid request parameters' })
  }
})

function push(token, payload, response) {

  const options = {
    priority: "high",
  };

  //FCMにAdminSDKを介してPush通知を送信します。
  admin.messaging().sendToDevice(token, payload, options)
  .then(pushResponse => {
    console.log("Successfully sent message:", pushResponse);
    response.status(200).send({message: 'Successfully sent message'})
  })
  .catch(error => {
    response.status(400).send({ error: 'Error sending message' })
  });
}

swiftの実装

AppDelegate.swift

import UIKit
import Firebase
import UserNotifications
import FirebaseMessaging

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    private var mainTabViewController: MainTabViewController?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        //環境ごとにプロジェクトを変えてるためplistを変更しています。
        let filePath = Bundle.main.path(forResource: Config.Server.instance.firebaseInfoPlistName, ofType:"plist")
        //Forced Unwrapping🚨
        FirebaseApp.configure(options: FirebaseOptions(contentsOfFile:filePath!)!)
        initFirebaseMessaging()
        initRemoteNotification(application)
        window = UIWindow(frame: UIScreen.main.bounds)
        window!.makeKeyAndVisible()
        navigate()
        return true
    }

    func navigate(_ isTrial: Bool = false) {
        guard let window = window else {
            assert(false)
            return
        }
        let previousVC = window.rootViewController
        for v in window.subviews {
            v.removeFromSuperview()
        }
        let vc = MainTabViewController()
        mainTabViewController = vc
        window.rootViewController = vc
        if let previousVC = previousVC {
            previousVC.dismiss(animated: false) {
                previousVC.view.removeFromSuperview()
            }
        }
    }

    private func initRemoteNotification(_ application: UIApplication) {
        UNUserNotificationCenter.current().delegate = self

        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        //TODO: Relocate requestAuthorization method.
        UNUserNotificationCenter.current().requestAuthorization(options: authOptions, completionHandler: {_, _ in })
        application.registerForRemoteNotifications()
    }

    private func initFirebaseMessaging() {
        //DelegateでdeviceTokenの変更を監視します。
        Messaging.messaging().delegate = self
        //明示的にdeviceTokenを取得します。
        InstanceID.instanceID().instanceID { (result, error) in
          if let error = error {
            //TODO: Error handling.
            print("Error fetching remote instance ID: \(error)")
          } else if let result = result {
            //TODO: Send token to parnovi api for update user fcm token. if authorized == true
            print("Remote instance ID token: \(result.token)")
          }
        }
    }
}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.badge, .sound, .alert])
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        completionHandler()
    }
}

extension AppDelegate: MessagingDelegate {
    //Observe firebase messaging token.
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) {
    }
}

FunctionsのDeploy

実際に関数をデプロイしてPush通知を送信してみます。

    $ firebase deploy --only functions

swiftのInstanceID.instanceID().instanceIDで取得したDeviceTokenを使ってcurlで下記のリクエストを叩くとプッシュ通知が送信されるようになります。

    $ curl -X POST https://yout-functions-url/push?device_token=your-device-token -d "message=I love fishing🎣"

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com