Flutter 1.9以降で認証プロバイダーとしてGoogleを使ったFirebaseログインを実装する

#はじめに
捨てコードをいっぱい書きました。
トリメンダスリーな数のgithub、firebase、そしてローカル上のプロジェクト達。
それぞれチャージが掛からなかったことに感謝したいと思います。

そもそもなぜ今の段階で、将来Dart実装が見込まれるFlutter WebアプリのGoogleサインイン実装を行うのかと言えば、いつDart実装が「出来ました」ステータスになるのか保証が一切ないからです。そこを甘えて案件に着手し、出来上がる頃に「いやー、グーグルがー」とか言うのは不誠実だしDudeとは言えないので調査しまくりました。

pub.dev上にある「Auth」で検索出来たGoogleとFirebaseのDartパッケージをいろいろと組み合わせながら片手間に2週間くらいスクラップアンドビルドしました。
ある時Google検索に「how to auth flutter_web」とたまたま打ち込んでヒットした[Using Firebase in Flutter Web](https://levelup.gitconnected.com/using-firebase-in-flutter-web-4b99952180aa)が、それまでの苦労を水の泡にした代わりに一足飛びの進捗をもたらしました。
そこから改めて実装をはじめ、ちょっとしたことなのに割と大変な問題を解決して一定の成果が得られたので、自身の行動をたどりながら、もっともタイトな形で実装しなおそう、ならばQiitaに載っけようと思った次第です。

今後Dartのみ実装が可能になったとき、ここでのコードがバランスを崩して動かないことになるかもしれませんが、その時は楽に実装しなおせると思っています。

#スマートフォン向け実装はWeb向けには動かない
もちろんいやいや動くでしょという範囲はありますが、たった今はdart:ioを利用できないことが拘束条件になっていて、ネットワークやストレージを使うコードは、Dartオンリーでは動きませんでした。振り返って見て、いやいやどうにか動くでしょと頑張るところではなかったと思います。しかし、やればやっただけ身に着くものはあるので反省はしてません。

#どうなら動くか？
検証していないので体感的な話にはなりますが、Firebase系のコードは[firebase package](https://pub.dev/packages/firebase)が窓口になります。pubspec.yamlにて取り込みますが、Dartパッケージはjavascriptラッパープラスアルファのような感じで、html側で取り込んだobjectを利用するようです。なので、index.html側で

```
<script src="https://www.gstatic.com/firebasejs/7.0.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.0.0/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.0.0/firebase-firestore.js"></script>
```
の上2行は最低限取り込む必要があります。
最下行はfirestoreのルールにログイン状態が正しく反映されているかを確認したい場合に追加したものです。
多分現況の解はこの辺の仕組みがベースになると思います。
スクリプトSDKの取り込みがない場合はobject missingだか何らかのエラーが発生するのでわかると思います。

#タイトな内容を目指して改めて着手
こんなのはもう趣味の範囲ですが、誠実に行きます。
UNIX系の人はパスなど読み替えてください。
##Flutterプロジェクト作成

```
D:\git>flutter create qiita_glfbsi_proj
```
##一旦ビルド
firebase initでbuild\webがないと言われそうなのでパスを作成します。

```
D:\git>cd qiita_glfbsi_proj
D:\git\qiita_glfbsi_proj>flutter build web
```
mkdirでもいいと思います。
##firebase側準備

##firebase環境の追加
ここで、アカウントがメンテ用のものなどになってないかいま一度確認してください。
適切なアカウントでfirebase loginして、

```
D:\git\qiita_glfbsi_proj>firebase init

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  D:\git\qiita_glfbsi_proj

? Are you ready to proceed? Yes
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, 
then Enter to confirm your choices.
 ( ) Database: Deploy Firebase Realtime Database Rules
 (*) Firestore: Deploy rules and create indexes for Firestore
 ( ) Functions: Configure and deploy Cloud Functions
>(*) Hosting: Configure and deploy Firebase Hosting sites
 ( ) Storage: Deploy Cloud Storage security rules
```
ここではFirestoreとHostingを選びました。
また、新しいプロジェクトとして「qiita-glfbsi-proj」を名称としました。
失敗しました。
firestoreのサーバーロケーションが決まってませんエラーです。

```
=== Firestore Setup

Error: Cloud resource location is not set for this project but the operation you are attempting to 
perform in Cloud Firestore requires it. Please see this documentation for more details: 
https://firebase.google.com/docs/projects/locations
```
エラーにはなりましたが、この時既にfirebase側にプロジェクトは出来ているのでWebブラウザーから場所を決めます。
##Webから調整
###1. プロジェクトを選択してDatabaseの「データベースの作成」から入る
![temp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/3af62999-9939-5fe6-a43e-1e35a4c47f61.png)
###2. セキュリティーはどちらでもよい
手元の設定ファイルで上書きされるのでどちらを選んでも構わないです。デフォルトで行きましょう。
![temp2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/6e4725e8-f29d-b298-f6da-e9655d38d73b.png)
###3. サーバーの場所を選択する
デフォルトにしました。案件の都合などに応じて選択するものだと思います。
![temp3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/ea7d1c51-dc8f-9a22-27bc-d397fdb265aa.png)
##コマンドラインに戻る
やり直しです。

```
? Please select an option: (Use arrow keys)
> Use an existing project
  Create a new project
  Add Firebase to an existing Google Cloud Platform project
  Don't set up a default project
```
もうあるので既存を選びます。

```
=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If you
have a build process for your assets, use your build's output directory.

? What do you want to use as your public directory? (public)
```
ここで「build\web」を設定します。以降、index.htmlを壊さないようにと思いましたが、flutterプロジェクトの構成上「web\index.html」が原本で、ビルドの度にbuild\webに複写されるのでどうでもいいです。
でもこれまで何度も繰り返した中で、上書きを断ってきたのでそうします。

```
=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If you
have a build process for your assets, use your build's output directory.

? What do you want to use as your public directory? build\web
? Configure as a single-page app (rewrite all urls to /index.html)? No
+  Wrote build\web/404.html
? File build\web/index.html already exists. Overwrite? No
i  Skipping write of build\web/index.html

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...

+  Firebase initialization complete!
```
今度は文句を言われずに済みました。
#git環境初期化
```
D:\git\qiita_glfbsi_proj>git init
Initialized empty Git repository in D:/git/qiita_glfbsi_proj/.git/

D:\git\qiita_glfbsi_proj>_
```
githubに上げる方法は割愛します。
#コードを開く
AndroidStudioを利用しました。
![temp4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/a611dda7-d171-0d03-32a0-e5eaca6dd7dc.png)
##pubspec.yaml調整

```yaml:pubspec.yaml
version: 1.0.0+1

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2

  googleapis_auth: 0.2.10
  firebase: 5.0.4
```
一部割愛しています。
最下2行が追加分です。
Packages getしといてください。ほっといてもいつかはやれと言われます。
#index.html調整
webフォルダー内のindex.htmlにjavascriptのSDKをロードするよう記述を追加します。

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script src="https://www.gstatic.com/firebasejs/7.0.0/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.0.0/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/7.0.0/firebase-firestore.js"></script>
  <title>qiita_glfbsi_proj</title>
</head>
<body>
  <script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
```
main.dart.jsのlazy load耐性がわからないので素直にhead部で読み込ませました。
こんなところで時間を食いたくはありません。
##Dartコード調整
まずfirebaseインスタンスの初期化をしましょうね（なぜか頭の中では沖縄アクセント）。
一旦main関数をばらして初期化コードを入れます。

```dart:main.dart
/// 抜粋：before
void main() => runApp(MyApp());
```
を、

```dart:main.dart
/// 抜粋：after
void main() {
  initializeApp(
    apiKey: "xxxx",
    authDomain: "xxxx",
    databaseURL: "xxxx",
    projectId: "xxxx",
    storageBucket: "",
    messagingSenderId: "xxxx",
  );
  runApp(MyApp());
}
```
ここでinitializeAppがエラーになったのでimport文を追加します。

```dart:main.dart
/// ↓importブロックに追加
import 'package:firebase/firebase.dart';
```
毎回apiKeyなどどれだっけとなるうちにコツをつかみました。
ちょうどいいタイミングなのでfirebaseのWeb画面に移ります。
#firebase Webの設定
Settingsを開きます。
![temp5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/8d425063-8d1e-dd43-c40a-c72527c90866.png)
開くとスクロールして下の方にアプリを作れと言ってる人がいるので作ります。
![temp6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/4069f4a1-a6ae-8560-ea76-564459aa8d45.png)
「</>」のボタンが入り口です。
ニックネームを入力、チェックを付けて「アプリを登録」。
![temp7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/983ab1da-ae49-245c-2677-de1734e2d695.png)
「②Firebase SDK の追加」、「③Firebase CLI のインストール」、「④Firebase Hosting へのデプロイ」をスキップして「コンソールに進む」します。
で「アプリがないよー」と書いてあった部分が更新されています。
ラジオボタンのCDNを選択すると現れる、スニペットの一部（下図の選択してある部分）を切り出してinitializeAppの引数として投入します。
![temp8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/612fb63e-d532-983d-2cd0-14baa51710f1.png)
そのときDartエラーになる行があればその行は削除します。それで完了です。
ここらで動かしてみましょう。
ビルドエラーになりました。
flutter cleanで再ビルド。
firebase serveで実行（自分で手順書代わりに実施して不足に気づいたので追記しました）。
動きました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/57b9b752-d47f-6804-8b08-b815792a0b4d.png)
文字が全部イタリックになってます。こんな現象初めてですｗ
とりあえず「そこじゃない」ので今回は以降無視します。
ここまででコード的に障害はないようです。

##サインインするところまで一気に
###個別のパーツインスタンス作成
MyHomePageに作成します。
デフォルトの以下コードを

```dart:main.dart
/// 抜粋：before
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}
```
邪魔なコメント文を除去しつつこうします。

```dart:main.dart
import 'package:firebase/firebase.dart'; /// <-この行がないと構文エラーになるので追加しました。 - 2020-10-07

/// 抜粋：after
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final _firebaseAuth = auth();
  final _googleSignIn = GoogleAuthProvider();

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}
```
###サインインシーケンス実装
この実装は、ほぼライブラリー側でいろいろやってくれるので楽です。
別ウィンドウが生成されるのは許容範囲だと思います。
_incrementCounter()関数を書き換えてサインインに使います。

```dart:main.dart
  /// 抜粋：after
  void _incrementCounter() async {
    /// credentialは実行の成否判断にしか利用しない
    final credential = await widget._firebaseAuth.signInWithPopup(widget._googleSignIn);
    if (credential != null) {
      /// 画面のリロード
      setState(() {});
    }
  }
```
サインイン成立後の挙動をbuild()関数に追加します（この実装は正常系開通のみを望む場合不要です）。

```dart:main.dart
  /// 抜粋：after
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            /// この部分
            widget._firebaseAuth?.currentUser?.displayName != null
                ? Text(widget._firebaseAuth.currentUser.displayName)
                : Container()
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
```
これでサインインされていれば、見慣れたデフォルト画面に名前が追加表示されます。
動かしてみましょう。
動きませんでした。
忘れてました。
firebaseでGoogleを認証プロバイダーとして利用するよう設定しなければなりません。
##認証プロバイダー設定
鉛筆マークはリスト行をポイントすると現れます。
![temp9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/d9170d2d-fbd3-4c29-1f2a-3889f0b3074a.png)
Google認証を有効にします。
プロジェクトの公開名は勝手に入りますが、プロジェクトのサポートメールは必須選択です。
stackoverflowなどでよく「動かねえぞ⇒サポートメール入れろし」交わされるところです。
![temp10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/164124db-64ee-ee40-5b86-523a107f0282.png)
##改めまして
とりあえず動かします。
先の認証プロバイダー設定を保存していなかったので失敗しました。
失敗の場合、サインイン用の小窓が開いたと思ったら消える挙動になります。
保存してもっかい。
![temp11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/06c2007d-58d8-2544-4b0f-919bc4c11506.png)
まあ出るんすよ。誰がやっても（何度もやってる割には手際悪し）。
これ、ローカルホストで動かしてますが、クラウド側初アクセスの場合は、それぞれのアカウントに設定されている認証方法が動くはずです（二段階認証など）。
では入ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214680/8c3f7b73-1202-0f8b-ccd7-9e23fa54a457.png)
名前。出ました。
firebase auth上のuidも取れます。
currentUser変数の型定義を参照してください。
##リロードでサインアウトする
無事動きましたがリロードするとサインアウトします。
正しくはサインインしているのに認識できずにサインアウト状態で表示されるということになります。
ソケット経由で直接APIにアクセスしたりしてみましたが、ホワイトリストがどうとか、クロスオリジンがどうとかで弾かれます。
検討の起点がおかしいと思い、セッション管理機構の方面からググり続けると見つかりました。
[認証状態の永続性 | Firebase](https://firebase.google.com/docs/auth/web/auth-state-persistence?hl=ja)
[認証状態の永続性の変更](https://firebase.google.com/docs/auth/web/auth-state-persistence?hl=ja#modifying_the_auth_state_persistence)を参考に不明瞭な点がありつつも、いつものように思いつく方法を片っ端からやろうとした矢先、初回の実装がヒットしました。

というようなことがあったのをここで実装追加します。
以下のコードを_MyHomePageStateクラスに追加します。

```dart:main.dart
  /// 追加内容
  @override
  void initState() {
    super.initState();
    /// サインイン状態が変わったことによるリロードはここで一括して行うことにする
    widget._firebaseAuth.onAuthStateChanged.listen((user) {
      /// 画面のリロード
      setState(() {});
    });
  }
```
で、問題のsetPersistence()関数はsignInWithPopup前に実施することになりますので、_incrementCounter()に手を入れます。

```dart:main.dart
  void _incrementCounter() async {
    /// 戻り値は特になし
    await widget._firebaseAuth.setPersistence('session');
    /// 戻り値のcredential自体もう利用しない
    await widget._firebaseAuth.signInWithPopup(widget._googleSignIn);
  }
```

initState()関数に仕掛けたリスナーに応答が来た瞬間サインイン状態に移行しているのがわかると思います。

#firebaseにアップロード
ここまでで一度クリーンしているため、特にビルド環境を整えることなくビルドし、即アップロードします。

```
D:\git\qiita_glfbsi_proj>flutter build web --release
Compiling lib\main.dart for the Web...                             39.1s

D:\git\qiita_glfbsi_proj>firebase deploy --only hosting

=== Deploying to 'qiita-glfbsi-proj'...

i  deploying hosting
i  hosting[qiita-glfbsi-proj]: beginning deploy...
i  hosting[qiita-glfbsi-proj]: found 8 files in build\web
+  hosting[qiita-glfbsi-proj]: file upload complete
i  hosting[qiita-glfbsi-proj]: finalizing version...
+  hosting[qiita-glfbsi-proj]: version finalized
i  hosting[qiita-glfbsi-proj]: releasing new version...
+  hosting[qiita-glfbsi-proj]: release complete

+  Deploy complete!

Project Console: https://console.firebase.google.com/project/qiita-glfbsi-proj/overview
Hosting URL: https://qiita-glfbsi-proj.firebaseapp.com

D:\git\qiita_glfbsi_proj>_
```

本件のデモ：[Flutter Demo](https://qiita-glfbsi-proj.firebaseapp.com/#/)
本件のリポジトリー：[kendji/qiita_glfbsi_proj](https://github.com/kendji/qiita_glfbsi_proj)

#最後に
データベースにアクセスできることを書き切ろうと思いましたが、切り貼りして疲れましたのでまた別の機会にしたいと思います。また、こんなことを思い付きでいくらでもやらせてもらえる環境を提供してくれているGitHub(Microsoft)、Firebase(Google)に感謝します。

#そして
なぜアプリの文字が斜体になったのか不明です。
業務プロジェクトでは発生していないので追究する気はありません。
あざした。