firebase with react-native

# はじめに

こちらは鈴鹿高専 Advent Calendar 2021の17日目の記事です。

https://qiita.com/advent-calendar/2021/snct

先日のpck2021モバイル部門に向けたアプリ開発でfirebaseを使ってみました！
とは言っても実際に使ったのはrealtime databaseとfirebase cloud messaging 、firebase authentication(のメールログイン)だけなのですが...

それぞれで使い方が調べてもなかなか分からないところがたくさんあったので備忘録としてまとめてみます
android限定なのでiosアプリの開発には役立たないかも

# 開発環境やモジュールのバージョン

バックエンドは10月中旬に開発をしたためnode.jsバージョンがフロントエンドよりも新しくなっています

言語：typescript

#### フロントエンド

react：17.0.1

react native：0.64.1

node.js：14.16.0

yarn：1.22.10

react-native-firebase : 12.7.3

#### バックエンド

Azure

OS：UbuntuServer 20.04.3 LTS

node.js：16.11.1

yarn：1.22.17

firebase-admin：10.0.0

# firebaseとreact-native appの接続について

まずは接続について、いろんな記事を読んでみて難しそうだなぁと思いましたが実際にやってみると手探りでも2時間ほどで終わったので簡単でした

では順番に...

yarn 又は npm でモジュールをインストールして

```shell
yarn add @react-native-firebase/app
npm install @react-native-firebase/app
```

androidパッケージネームを変更していきます、パッケージネームは自由に決めれますが既存のものと被っているとまずいので調べましょう

-  `market://details?id=<パッケージ名>`
-  `https://play.google.com/store/apps/details?id=<パッケージ名>`

このふたつで調べて被ってなかったら良いみたいです、このパッケージ名は後でfirebaseコンソールで使うので覚えておきましょう

プロジェクトのファイルで変更する場所は

- `android/app/BUCK`
- `android/app/src/main/AndroidManifest.xml`
- `android/app/src/main/java/com/[ProjectName]/MainActivity.java`
- `android/app/src/main/java/com/[ProjectName]/MainApplication.java`

の`package`のところと

- `android/app/build.gradle`

の`applicationId`のところです
パッケージ名を変更したら`android/app/src/main/java/com/[App Name]/MainActivity.java`を`android/app/src/main/java/[Package Name Folder]/MainActivity.java`に移動させます

次はfirebaseコンソールでのアプリの追加です
まずはコンソールにアクセスしてプロジェクトを作ってください
[firebaseコンソールはここ](https://console.firebase.google.com/)

次にアプリを作成します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1021455/2ea5741c-f5f2-2eda-3f31-93850ac9ffe9.png)

ここのandroidのボタンを押しすと3つ入力欄が出ますが、一番上のパッケージネーム以外はオプションなので入力する必要はありません
ただしauthenticationで電話番号認証や、firebase Dynamic Linksを使いたい場合は一番下のSHA-1を入力する必要があるのでキーを入力しましょう
[android keystore sha-1](https://www.google.com/search?q=android+keystore+sha-1)でググると取得方法が分かります
一番上のパッケージネームに先ほど決めたものを入力してregistar appを押します
すると`google-services.json`がダウンロードできるようになるので、ダウンロードして`android/app`にコピーし、Nextを押します

次に、`Project-level build.gradle` の追加内容を`android/build.gradle`ファイルに、`App-level build.gradle`を`android/app/build.gradle`に追記します
これでfirebaseとの接続は完了です！

※バックエンドとの接続
firebaseはバックエンドからも使うことができます、今回はnode.jsを使ったのでその場合の接続方法を説明します
まずは必要なモジュールをインストールします

```shell
yarn add firebase-admin
npm install firebase-admin
```

次にfirebaseコンソールからプロジェクトの設定画面を開き、サービスアカウントのFirebase Admin SDKにある新しい秘密鍵の生成を押し、ファイルをダウンロード、またこのページに書いてあるコードをコピペしてスクリプトに貼り付けます、ダウンロードしたファイルはスクリプトが読める場所に置きましょう

# realtime databaseについて

realtime databaseを使う前に必要なモジュールをインストールします

```shell
yarn add @react-native-firebase/database
npm install @react-native-firebase/database
```

次にこれをスクリプトファイルにインポートします

```ts
import database from "@react-native-firebase/database"
```

## データベースへのアクセス
まずはrealtime databaseの構造について軽く説明します
簡単に言うと大きいjsonです

```json
"root":{
  "child":{
    "string":"hogehoge",
    "number":1234
  },
}
```

このような感じになります
では実際にアクセスしてみましょう

```ts
import database from "@react-native-firebase/database"

database().ref("child").child("grandchild")
```

`.ref("hogehoge")`に親のから見た第一階層を指定します、空の場合は親の階層が指定されます
`.child("hugahuga")`には`.ref()`で指定した階層から見た子階層を指定します、`.child()`メソッドは続けて書くことで深い層まで指定できます

### データ保存

データ保存は`.push()`もしくは`.set()`を使います
`.push()`を使うと一意のkeyが生成されますが使う機会があまり無かったので今回のpckでは`.set()`だけ使いました
指定したパスにデータがある場合は上書きされ、ない場合は追加されます
引数には`{"key":value}`のような感じで指定します、複数指定する場合は`"key":value`の組み合わせを`,`で区切って追加すればできます
`value`には文字列や数字のほか、bool型や配列も入れることができます

```ts
import database from "@react-native-firebase/database"

const Array : string[] = ["hogehoge", "hugahuga"]
const flag : boolean = false
const num : number = 0

database().ref().child("child").set({
  array : Array,
  bool : flag,
  number : num,
})

```

### データ取得

データ取得はその時点でのデータを取得するときは`.once("EventType", (snapshot)=>{})`を使います
データベースの変更を検知してコールバック関数を実行したい場合は`.on("EventType", (snapshot)=>{})`を使います
引数には`"value"`,`"child_changed"`,`"child_added"`,`"child_moved"`,`"child_removed"`を取ります、`"value"`は単に値の取得をするのみで`.once()`と組み合わせて使うことが多いです
その他4つのEventTypeは`.on()`と組み合わせて使うことが多いです、`"child_changed"`は子要素が変更されたとき、`"child_added"`は子要素に追加されたとき、などイベントが発生するタイミングは各EventTypeの文字が意味するものです

コールバック関数に渡される引数には取得したデータが入っています

```ts
import database from "@react-native-firebase/database"

database().ref("hoeghoge").child("child").once("value", (snapshot) => {
  snapshot.child("hugahuga").val() //valueを取得:any
  snapshot.key //keyを取得:string このコードの場合は"child"
})
```

このようにすることで子要素の値やkeyを取り出すことが出来ます
また中に入っている要素の数が不明で、全部を取り出したいときは

```ts
import database from "@react-native-firebase/database"

const values : any[] = [];

database().ref().once("value", (snapshot)=>{
  snapshot.forEach((element) => {
    if(element){
      values.push(element.val())
    }
    else{
      return true
    }
  })
})
```

このように書くことで値を取り出すことが可能です、`forEach`の中では取り出した子要素がnullやundefinedの場合は処理が実行できないので`if`で判定して`else`の場合に`return true`をすることにより処理を停止させる必要があります
`.on`はリスナーのようなものなので必要に応じて破棄する必要があります(そうでないとメモリ不足でアプリが落ちたり...)

```ts
import database from "@react-native-firebase/database"
import {useEffect} from "react"

useEffect(() => {
  const listner = database().ref().on("child_changed", (snapshot)=>{})
  return (() => {database().ref().off("child_changed", listner)})
},[])
```

このようにreact-nativeの`useEffect`内で使う場合は`.off()`を使い`return`のところでリスナーを破棄します
`useEffect`の第２引数には特別何かなければ`[]`を渡しましょう、指定しないのはダメ、絶対

### データの削除

データの削除には`.remove()`メソッドが用意されていますが動作しませんでした、そのため`.set()`を用いて削除しました

```ts
import database from "@react-native-firebase/database"

database().ref().child("child").set(null)
```

#### オフラインでもデータベースにアクセスしたい場合

データベースの永続化を有効にすることでオフラインでも使えるようになります

``` index.ts
import database from '@react-native-firebase/database'

database().setPersistenceEnabled(true)
```

オフライン時に変更したところはオンライン復帰時に自動的にmergeされます

### バックエンド側からのアクセス

バックエンドからデータベースにアクセスする際は

```ts
import admin from "firebase-admin"

const database = admin.database

database().ref()
```

のように書きます、あとはフロントエンドと同じような感じで保存や取得ができます

## 注意すること！

realtime database関連の処理は全て非同期処理です、そのためレンダリングや受け取ったデータをもとに更にデータを保存したりするときは`async await`や`.then()`を使って適切タイミングで処理を発火させる必要がります(私はここで何度も躓きました)
レンダリングでrealtime databaseから取得したデータを使う場合、reactのstateを使うとうまくできるかもしれません
これでrealtime databaseについては終わりです

# firebase cloud messagingについて

firebase cloud messagingはバックエンドからフロントエンドにデータを渡したり通知を送信するときに使います、使い方によっては処理を発火させることもできます

### バックエンドからデータや通知を送信する方法について

```ts
import admin from "firebase-admin"

admin.initializeApp()//適宜必要な引数を渡してください

admin.send({
  notification : {
    title : "タイトル",
    body : "内容",
  },//通知
  data : {
    key : valule,
  },//データ
  topic : "topic", token : "token",//宛先、どちらかを指定
})//送信
```

通知を送信したい場合は`notification`を、データを送信したい場合は`data`を使います、両方送信したい場合はこのコードのように両方指定しましょう
送信先の指定はtopicまたはtokenを指定します
topicはバックエンド側で自由に決めることができ、フロントエンド側で登録することで受信できます
tokenはフロントエンド側でtokenを取得し、それを使ってデータなどを送信します、tokenはrealtime databaseを通してフロントエンドからバックエンドに渡すことが一般的みたいです(今回のプロジェクトではtokenを使ってないので説明は割愛します)
### フロントエンドでデータなどを受け取ったときの処理について

まずはモジュールをインストールします

```shell
yarn add @react-native-firebase/messaging
npm install @react-native-firebase/messaging
```

`notification`にきたものはアプリの状態がフォアグラウンド以外であれば初期の状態で通知が表示されます、フォアグラウンドであれば通知は表示されません

通知をタップして起動したときの動作はバックグラウンドで起動してる場合と起動してない場合で定義の仕方が少し異なります
起動している場合は

```ts
import messaging from "@react-native-firebase/messaging"
import {useEffect} from "react"

useEffect(() => {
  const unsubscribe = messaging().onNotificationOpenedApp((message) => {/*ここに実行したい処理を書く*/})
  return unsubscribe
},[])
```

起動していない場合は

```ts
import messaging from "@react-native-firebase/messaging"
import {useEffect} from "react"

useEffect(() => {
  messaging().getInitialNotification().then((message) => {/*ここに実行したい処理を書く*/})
},[])
```

このように書きます

受け取ったデータは`.onNotificationOpenedApp`もしくは`.getInitialNotification().then`で取得した変数に入っています
データの型は`RemoteMessage`で詳しくは[リファレンス](https://rnfirebase.io/reference/messaging/remotemessage)に書いてあります

```ts
messaging().getInitialNotification().then((message) => {
  const data = message.data//{ [key: string]: string }
  const notification = message.notification//リファレンスを参照 https://rnfirebase.io/reference/messaging/notification
})
```

`data`の方はkeyを使い値を取り出します
`notification`の方はプロパティが多いためリファレンスを参照してください(~~説明するのがめんどくさい~~)

### tokenの取得について

```ts
import messaging from "@react-native-firebase/messaging"

messaging().getToken().then((token) => {})
```

このように取得します、返ってくる変数の型は`string`です
またtokenが更新された場合に処理を実行したい場合は

```ts
import messaging from "@react-native-firebase/messaging"

messaging().onTokenRefresh((token) => {/*実行したい処理*/})
```

このように書きます、このメソッドはリスナーなので`useEffect`などに書く場合は`return`をしてあげたほうがいいと思います(使ってないから分からないけど)
ここで取得したtokenをバックエンドに渡せればtokenを使ってデータなどを送信できます

### フロントエンドでtopicを追加したり削除したりする方法について

```ts
import messaging from "@react-native-firebase"

messaging().subscribeToTopic(topic : string)//topic追加
messaging().unsubscribeFromTopic(topic : string)//topic削除
```

これでfirebase cloud messagingについては終わりです

# firebase Authentication について

続いてはfirebase Authenticationについてです、これを使うことで電話番号認証やメール認証、更にgoogleログインや各種SNSアカウントでのログインを実装することが出来ます
今回はメールにdynamic linksを送信する方法での認証を実装したのでその説明をします
まずは認証周りのモジュールをインストールします

```shell
yarn add @react-native-firebase/auth
npm install @react-native-firebase/auth

yarn add @react-native-firebase/dynamic-links
npm install @react-native-firebase/dynamic-links
```

次にBundleIdをとる必要があるため次のモジュールもインストールしましょう

```shell
yarn add react-native-device-info
npm instal react-native-device-info
```

またメール送信で入力したメールアドレスは認証でも使うのでAsyncStorageなどに保存しておくと便利です

```shell
yarn add @react-native-community/async-storage
npm install @react-native-community/async-storage
```

認証にはfirebase Dynamic Linksを使うので予め設定をしておきましょう(この[サイト](https://note.com/navitime_tech/n/n4646be5368a4)が参考になりました)

### 認証の実装

では実際にメールアドレスを入力し認証をする機能を実装していきます

```ts
import auth from "@react-native-firebase/auth"
import DeviceInfo from "react-native-device-info"
import AsyncStorage from "@react-native-community/async-storage"

//認証メールの設定

const actionCodeSettings = {
  url : 'http://example.com'/*ディープリンク(対応端末以外でリンクが押されたときに開かれる)を指定*/,
  handleCodeInApp: true,
  iOS: {
    bundleId: DeviceInfo.getBundleId(),
  },
  android: {
    packageName: DeviceInfo.getBundleId(),
    installApp: true,
  },
  dynamicLinkDomain : 'example.page.link',
}

mailaddress : string = ""//メールアドレスを代入

auth().sendSignInLinkToEmail(mailaddress, actionCodeSettings)
  .then(() => {
    AsyncStorage.setItem('key', mailaddress)
  })//メール送信とメールアドレスの保存
```

これでメールが送信されます
次にメールリンクを開いた際の処理について
アプリがバックグラウンドで開かれているかそうでないかでメソッドが変わります

```ts
import auth from "@ract-native-firebase/auth"
import {useEffect} from "react-native"
import dynamiclinks, {
  FirebaseDynamicLinksTypes,
} from '@react-native-firebase/dynamic-links'

//バックグラウンドで起動していないとき
dynamiclinks().getInitialLink().then((link) => {
  if(link){
    _handleForgroundDynamicLinks(link)
  }
})

//バックグラウンドで起動しているとき
useEffect(() => {
  const unsubscribe = dynamiclinks().onLink(_handleForgroundDynamicLinks)
  return () => unsubscribe
},[])

//リンクのハンドリング
const _handleForgroundDynamicLinks = (llink : FirebaseDynamicLinksTypes) => {
  AsyncStorage.getItem('key')
    .then((mailaddress) => {
      if(mailaddress){
        auth().signInWithEmailLink(mailaddress, link.url)//認証
          .then(() => {/*ログイン後にしたい処理*/})
      }
    })
}
```

バックグラウンドで起動しているときは`.onLink()`メソッドでリスナーを作ります
本当は電話番号認証を実装する予定だったんですがreCAPTCHA認証のところがめんどくさそうだったので、PCKのアプリではメールリンク認証を採用しました(Expoを使っていたら簡単に実装できたのかも？)

# まとめとか感想
firebaseを使うとバックエンド・フロントエンド間の通信やデータベースの構築、認証が簡潔なコードで実装できました
しかし使っている人が多いのに「これだ！」という記事が少なく、実装に困ることがかなりありました
この記事ではそのあたりを重点的に説明したので今後firebaseを使って開発をする人の参考になればと思います

数日かけて書いたので書き方が統一されてないです。。。

次はfirebaseとreact、electronを使ってデスクトップアプリを開発したいなぁ