firebase FCM と ionic4 で Web Push を実装してみる 

ionic 弄り始めて約2ヶ月ですが firebase との組み合わせでうまく動いているので 
SPA(Single Page Application) での WebPush 実装方法をまとめてみました。

# 環境
各環境を整える

- [ionic4](https://ionicframework.com/docs)
Webテクノロジー（HTML、CSS、JavaScript）を使って、高性能かつ高品質なモバイルとデスクトップアプリケーションをつくるためのオープンソースのUIフレームワーク

- [firebase](https://firebase.google.com/?hl=ja)
モバイルおよびWebアプリケーション開発プラットフォーム
[Firebase Cloud Messaging(FCM)](https://firebase.google.com/docs/cloud-messaging?hl=ja)

- [Visual Studio Code](https://azure.microsoft.com/ja-jp/products/visual-studio-code/)
クラウド開発用の強力かつ軽量なコード エディター

# サンプルコード
- [ionic-webpush-sample](https://github.com/tfuru/ionic-webpush-sample)  
github リポジトリ
- [ionic-webpush-sample.web.app](https://ionic-webpush-sample.web.app/)  
動作確認用 サンプル


# firebase プロジェクト 初期設定
[firebase コンソール](https://firebase.google.com)でプロジェクトを作成し
適当な作業ディレクトリを作り初期化する。 

```
mkdir ionic-webpush-sample
cd ionic-webpush-sample
firebase init

#Functions と Hosting を有効にする
● Functions
● Hosting

#Functions は TypeScript を有効に。
? What language would you like to use to write Cloud Functions? 
  JavaScript 
❯ TypeScript 

#後は適当に…
```

# ionic プロジェクト 初期設定
firebase プロジェクト 初期設定したディレクトリで ionic 初期設定をして `firebase.json` を一部書き換える。

```
cd ionic-webpush-sample
ionic start ionicapp blank --type=angular
```

```firebase.json
  "hosting": {
    "public": "ionicapp/www", <- public ディレクトリではなく ionicapp/www に変更
  ...
```

# ionic 仮ビルド
`ionicapp` ディレクトリをビルドして `ionicapp/www` を生成してみる。

```
cd ionicapp
ionic build
```

# firebase ローカル実行で設定確認
`firebase.json` 設定が正しく設定されているか確認してみる。

```
firebase serve
# functions のエラーが表示されるがココでは無視していいです。
http://localhost:5000/
```

起動後 `http://localhost:5000/` にアクセスして `Ionic Blank` が表示されていれば
`firebase.json` の設定は成功しています。
<img width="282" alt="スクリーンショット 2019-07-24 0.00.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/a7e0878f-71a5-84c7-2f51-4b839da1d540.png">

ココまでの設定で `firebase` で `ionic` を利用する初期設定が終わりました。
続いて ionic 内で firebase を利用する設定を行います。

---

# ionic に firebase SDK を組み込む
ionic 初期化したディレクトリに移動しえ `firebase SDK` を組み込みます。

```
cd /ionic-webpush-sample/ionicapp
npm install --save firebase
```

# アプリにFirebaseの設定をする。 
`environments` を修正する。 
cofig の値は [firebaseコンソール](https://console.firebase.google.com) プロジェクトの設定 全般 → マイアプリ → 構成 で 取得することができます。

<img width="475" alt="スクリーンショット 2019-07-24 0.36.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/c8ba8d79-2805-d809-075d-9b2e4d0507c5.png">


```src/environments/environment.ts
export const environment = {
  production: false,
  config:{
    apiKey: "AIzaSy...",
    authDomain: "ionic-webpush-sample.firebaseapp.com",
    databaseURL: "https://ionic-webpush-sample.firebaseio.com",
    projectId: "ionic-webpush-sample",
    storageBucket: "ionic-webpush-sample.appspot.com",
    messagingSenderId: "2986...",
    appId: "1:298684318675:web:3..."
  }
};
```

```src/environments/environment.prod.ts
export const environment = {
  production: true,
  config:{
    apiKey: "AIzaSy...",
    authDomain: "ionic-webpush-sample.firebaseapp.com",
    databaseURL: "https://ionic-webpush-sample.firebaseio.com",
    projectId: "ionic-webpush-sample",
    storageBucket: "ionic-webpush-sample.appspot.com",
    messagingSenderId: "2986...",
    appId: "1:298684318675:web:3..."
  }
};
```

# アプリ内で firebase SDK 初期化処理を記載する
`app.component.ts` を修正して `firebase SDK` を初期化する

```src/app/app.component.ts
  import * as firebase from 'firebase';
  import { environment } from '../environments/environment';
...
  initializeApp() {
    ...
    firebase.initializeApp(environment.config);
  }
```

ココまでの設定で ionic アプリ内で firebase sdk を利用できるようになりました。
続いて最低限の ブラウザプッシュ受け取り 処理を書いていきます。

---

# プッシュ通知 受信の為 サービスワーカーの初期設定をする。
`app.component.ts` に 処理を追加する。
あわせて `assets` ディレクトリに `firebase-messaging-sw.js` を追加する。

```src/app/app.component.ts
  initializeApp() {
    this.platform.ready().then(() => {
      this.statusBar.styleDefault();
      this.splashScreen.hide();
    });

    //firebase 初期化
    firebase.initializeApp(environment.config);

    //プッシュ通知 受信の為のサービスワーカー利用設定
    navigator.serviceWorker.register('assets/script/firebase-messaging-sw.js')
    .then((registration) => {
      firebase.messaging().useServiceWorker(registration);
    });
  }
```

`YOUR-SENDER-ID` は [firebase コンソール](https://console.firebase.google.com/u/0/?hl=ja) で確認する事ができます。

```assets/script/firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/6.3.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/6.3.1/firebase-messaging.js');

firebase.initializeApp({
    'messagingSenderId': 'YOUR-SENDER-ID'
});

const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function(payload) {
  console.log('[firebase-messaging-sw.js] Received background message ', payload);
  // Customize notification here
  const notificationTitle = payload.notification.title;
  const notificationOptions = {
      body: payload.notification.body,
      icon: payload.notification.icon
    };
  return self.registration.showNotification(notificationTitle,notificationOptions);
});
```

# プッシュ通知パーミッション 確認処理を追加する
`home` に 処理を追加する。

```src/app/home/home.page.html 
# ボタンと トークン表示 欄を追加する
<ion-content>
  <ion-item>
    <ion-button (click)="clickBtnPushPermission()">パーミッション確認</ion-button>
  </ion-item>
  <ion-item>
      <ion-label>token</ion-label>
      <ion-text>{{token}}</ion-text>
  </ion-item>
</ion-content>
```

```src/app/home/home.page.ts
import * as firebase from 'firebase';
...
export class HomePage {
...
 token: string = "";

  clickBtnPushPermission() {
    //console.log("clickBtnPushPermission");
    const _self = this;
    firebase.messaging().requestPermission()
      .then(function () {
        //ユーザー毎のトークンを取得して画面に表示する
        firebase.messaging().getToken()
          .then(function (token) {
            _self.token = token;
          })
          .catch(function (err) {
            console.error('Unable to retrieve refreshed token ', err);
          });
      })
      .catch(function (err) {
        console.error('Unable to get permission to notify.', err);
      });
  }
}
```

# プッシュ通知 許可と トークン取得 確認
ココまでの設定で プッシュ通知 許可と トークン取得ができるようになっている筈なので
ionic ローカル環境を起動して確認してみる。

```
cd ionicapp
ionic serve
```
パーミッション確認 ボタンを押すと 確認ダイアログが表示され許可すると トークンが取得され画面に表示される。

<img width="431" alt="スクリーンショット 2019-07-24 1.58.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/185ffa30-36cc-a542-7b74-4be0b111cfea.png">

ココまでの設定で トークンが取得できた筈。
続いて `Cloud Functions` の 関数を定義して 関数経由で プッシュ通知を送る処理を書いていきます。

----
# プッシュ通知送信のための ボタンを 追加する
`home` に プッシュ通知送信の為のボタンと処理を追加

```src/app/home/home.page.html
<ion-content>
  <ion-item>
    <ion-button (click)="clickBtnPushPermission()">パーミッション確認</ion-button>
  </ion-item>
...
  <ion-item>
    <ion-button (click)="clickBtnPush()">プッシュ通知 送信</ion-button>
  </ion-item>
</ion-content>
```

```src/app/home/home.page.ts
 clickBtnPush(){
    //functions に 定義した関数にパラメータを送って実行する
    //TODO token は Realtime Database or Cloud Firestore に保存して
    //     他に漏れないように処理する必要がある / プッシュなりすまし等の対策
    const value = {
      title:"たいとる",
      body:"ほんぶん",
      icon:"star",
      token:this.token 
    };
    const sendNotification = firebase.functions().httpsCallable("send_notification");
    sendNotification(value).then(result => {
      console.log("sendNotification","result",result);
    });
  }
```

# プッシュ通知 受信時の処理
`app.component.ts` に 受信時に トーストを表示する処理を追加する。

```src/app/app.component.ts
import { ToastController } from '@ionic/angular';
 constructor(
    ...
    private toastController: ToastController
  ) {
...

  initializeApp() {
  ...
   //プッシュ通知 受信時の処理
    const _self = this;
    firebase.messaging().onMessage(function (payload) {
      //トーストを表示してみる
      _self.showNotificationToast(payload);
    });
  }

  //トーストを表示
  async showNotificationToast(payload:any) {
    let _self = this;
    //payload.notification.icon
    const toast = await this.toastController.create({
      header: payload.notification.title,
      message: payload.notification.body,
      position: 'top',
      buttons: [
        {
          side: 'start',
          icon: payload.notification.icon,
          handler: () => {
            console.log(payload);
          }
        }
      ]
    });
    toast.present();
  }
```

# WebPushを送信する処理を実装
`home` に書いた `firebase.functions().httpsCallable` で呼び出させれる関数を `functions/src/index.ts` に実装する。
`serviceAccountKey.json` は [firebase コンソール](https://console.firebase.google.com)の プロジェクト設定 で取得できる。

```functions/src/index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';

/*
admin.initializeApp({
    credential: admin.credential.applicationDefault(),
    databaseURL: "https://ionic-webpush-sample.firebaseio.com"
});
*/
admin.initializeApp();

//プッシュ通知するAPI
exports.send_notification = functions.https.onCall((data, context) => {
    console.log("data",data);
    //プッシュ送信
    const value = {
        notification: {
            title: data.title,
            body: data.body
        },
        webpush: {
            headers: {
                TTL: "60"
            },
            notification: {
                icon: data.icon,
                click_action: data.click_action
            }
        },
        token: data.token
    };

    return admin.messaging().send(value)
                        .then((response: any) => {
                            console.log('Successfully sent message:', response);
                            return Promise.resolve(response);
                        })
                        .catch((error: any) => {
                            console.log('Error sending message:', error);
                            return Promise.reject(error);
                        });
});
```

# functions デプロイ してみる
`functions` を デプロイして実行してみる。

```
cp functions
npm run deploy
cd ../ionicapp
ionic serve
```

# プッシュ通知が動作するか確認
`http://localhost:8100/home` にアクセスして トークン取得後に プッシュ通知送信 ボタンを押して見る。 画像のようなトーストが表示されれば成功です。

<img width="431" alt="スクリーンショット 2019-07-24 10.33.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100147/bf93f744-92dd-4fae-42c7-d8cee4616695.png">