FlutterでEmail確認付きのFirebase Authenticationを使った認証（Mac M1）


## はじめに
以前にFlutterから`Firesbase Authentication` を使ったログイン機能を実装したが、今回Macでそれを試したら、Flutterやパッケージのバージョンが上がっていたため動かなかった。
大きな変更は`null safety`に関わる部分、その他RaisedButtonの廃止などの修正をしたため、更新後のソースコードをメモとして残しておく。
以前の記事はこちら。[【FlutterでFirebase Authenticationを使った認証にEmail確認を組み込む。】](https://qiita.com/smiler5617/items/f39ea5768dd1a9e17f93)

## 実行環境
【PC】
　MacBook Air (M1, 2020)

【各SWバージョン】
　・macOS Big Sur 11.6.1
　・Flutter 2.5.3　（dart 2.14.4）
　・Xcode 13.1
　・Cocoapods 1.11.2
  ・VScode(AplleSilicon) 1.62.2

【パッケージ】
　・firebase_core: ^1.10.0
　・firebase_auth: ^3.2.0

## メモ内容
試しに作るものは以前と同じ。
Firabase Auth のそもそもの仕様としては事前のEmail確認がされないため、作成後に確認Emailを送り、その確認（Email内のリンクが開かれたかどうか）が済んでいるかのステータスより、Home画面へ遷移する仕組みを考えた。


## 各種dartコードの作成
今回作成してみたログイン機能の画面レイアウトや画面遷移は以下の様な感じ。
![Qiita-No038_img01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/654767/ba4e1b34-2160-6b87-2adf-ccc7472f342c.jpeg)




上記の構成を作るために以下のdartファイルを作成。
・`main.dart` ⇒ `flutter run` コマンドで最初に呼び出されるファイル。
・`login.dart` ⇒ 初期画面（左上の図）。
・`registration.dart` ⇒ アカウント登録画面（左下の図）。
・`authentication_error.dart` ⇒ エラーメッセージを日本語で表示するためのクラス。
・`email_check.dart` ⇒ Email確認（本人認証）が済んでいない場合に遷移する画面。（真ん中の図）。
・`home.dart` ⇒ ホーム画面（右上の図）。

　
以下、各`dart`ファイルのコードを貼り付ける。

```dart:main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'login_parts/login.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Firebase Auth',
      home: Login(),
      routes: <String, WidgetBuilder>{
        '/login': (_) => new Login(),
      },
    );
  }
}
```

```dart:login.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'authentication_error.dart';
import 'registration.dart';
import '../home.dart';
import 'email_check.dart';

class Login extends StatefulWidget {
  @override
  _Login createState() => _Login();
}

class _Login extends State<Login> {
  // Firebase 認証
  final _auth = FirebaseAuth.instance;

  String _login_Email = ""; // 入力されたメールアドレス
  String _login_Password = ""; // 入力されたパスワード
  String _infoText = ""; // ログインに関する情報を表示

  // エラーメッセージを日本語化するためのクラス
  final auth_error = Authentication_error_to_ja();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            // メールアドレスの入力フォーム
            Padding(
                padding: EdgeInsets.fromLTRB(25.0, 0, 25.0, 0),
                child: TextFormField(
                  decoration: InputDecoration(labelText: "メールアドレス"),
                  onChanged: (String value) {
                    _login_Email = value;
                  },
                )),

            // パスワードの入力フォーム
            Padding(
              padding: EdgeInsets.fromLTRB(25.0, 0, 25.0, 10.0),
              child: TextFormField(
                decoration: InputDecoration(labelText: "パスワード（8～20文字）"),
                obscureText: true, // パスワードが見えないようRにする
                maxLength: 20, // 入力可能な文字数
                maxLengthEnforced: false, // 入力可能な文字数の制限を超える場合の挙動の制御
                onChanged: (String value) {
                  _login_Password = value;
                },
              ),
            ),

            // ログイン失敗時のエラーメッセージ
            Padding(
              padding: EdgeInsets.fromLTRB(20.0, 0, 20.0, 5.0),
              child: Text(
                _infoText,
                style: TextStyle(color: Colors.red),
              ),
            ),

            // ログインボタンの配置
            SizedBox(
              width: 350.0,
              // height: 100.0,
              child: ElevatedButton(
                // ボタンの形状や背景色など
                style: ElevatedButton.styleFrom(
                  primary: Colors.blue, // background-color
                  onPrimary: Colors.white, //text-color
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),
                ),

                // ボタン内の文字と書式
                child: Text(
                  'ログイン',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),

                onPressed: () async {
                  try {
                    // メール/パスワードでログイン
                    UserCredential _result =
                        await _auth.signInWithEmailAndPassword(
                      email: _login_Email,
                      password: _login_Password,
                    );

                    // ログイン成功
                    User _user = _result.user!; // ログインユーザーのIDを取得

                    // Email確認が済んでいる場合のみHome画面へ
                    if (_user.emailVerified) {
                      Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (context) =>
                                Home(user_id: _user.uid, auth: _auth),
                          ));
                    } else {
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                            builder: (context) => Emailcheck(
                                email: _login_Email,
                                pswd: _login_Password,
                                from: 2)),
                      );
                    }
                  } catch (e) {
                    // ログインに失敗した場合
                    setState(() {
                      _infoText =
                          auth_error.login_error_msg(e.hashCode, e.toString());
                    });
                  }
                },
              ),
            ),

            // ログイン失敗時のエラーメッセージ
            TextButton(
              child: Text('上記メールアドレスにパスワード再設定メールを送信'),
              onPressed: () =>
                  _auth.sendPasswordResetEmail(email: _login_Email),
            ),
          ],
        ),
      ),

      // 画面下にアカウント作成画面への遷移ボタンを配置
      bottomNavigationBar:
          Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(20.0),
          child: SizedBox(
            width: 350.0,
            // height: 100.0,
            child: ElevatedButton(
                // ボタンの形状や背景色など
                style: ElevatedButton.styleFrom(
                  primary: Colors.blue[50], // background-color
                  onPrimary: Colors.blue, //text-color
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),
                ),
                child: Text(
                  'アカウントを作成する',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),

                // ボタンクリック後にアカウント作成用の画面の遷移する。
                onPressed: () {
                  Navigator.of(context).push(
                    MaterialPageRoute(
                      fullscreenDialog: true,
                      builder: (BuildContext context) => Registration(),
                    ),
                  );
                }),
          ),
        ),
      ]),
    );
  }
}
```
　

```dart:registration.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'authentication_error.dart';
import 'email_check.dart';

// アカウント登録ページ
class Registration extends StatefulWidget {
  @override
  _RegistrationState createState() => _RegistrationState();
}

class _RegistrationState extends State<Registration> {
  // Firebase Authenticationを利用するためのインスタンス
  final _auth = FirebaseAuth.instance;

  String _newEmail = ""; // 入力されたメールアドレス
  String _newPassword = ""; // 入力されたパスワード
  String _infoText = ""; // 登録に関する情報を表示
  bool _pswd_OK = false; // パスワードが有効な文字数を満たしているかどうか

  // エラーメッセージを日本語化するためのクラス
  final auth_error = Authentication_error_to_ja();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Padding(
                padding: EdgeInsets.fromLTRB(25.0, 0, 25.0, 30.0),
                child: Text('新規アカウントの作成',
                    style:
                        TextStyle(fontSize: 20, fontWeight: FontWeight.bold))),

            // メールアドレスの入力フォーム
            Padding(
                padding: EdgeInsets.fromLTRB(25.0, 0, 25.0, 0),
                child: TextFormField(
                  decoration: InputDecoration(labelText: "メールアドレス"),
                  onChanged: (String value) {
                    _newEmail = value;
                  },
                )),

            // パスワードの入力フォーム
            Padding(
              padding: EdgeInsets.fromLTRB(25.0, 0, 25.0, 10.0),
              child: TextFormField(
                  decoration: InputDecoration(labelText: "パスワード（8～20文字）"),
                  obscureText: true, // パスワードが見えないようRにする
                  maxLength: 20, // 入力可能な文字数
                  maxLengthEnforced: false, // 入力可能な文字数の制限を超える場合の挙動の制御
                  onChanged: (String value) {
                    if (value.length >= 8) {
                      _newPassword = value;
                      _pswd_OK = true;
                    } else {
                      _pswd_OK = false;
                    }
                  }),
            ),

            // 登録失敗時のエラーメッセージ
            Padding(
              padding: EdgeInsets.fromLTRB(20.0, 0, 20.0, 5.0),
              child: Text(
                _infoText,
                style: TextStyle(color: Colors.red),
              ),
            ),

            // アカウント作成のボタン配置
            SizedBox(
              width: 350.0,
              // height: 100.0,
              child: ElevatedButton(
                // ボタンの形状や背景色など
                style: ElevatedButton.styleFrom(
                  primary: Colors.blue, // background-color
                  onPrimary: Colors.white, //text-color
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),
                ),

                // ボタン内の文字と書式
                child: Text(
                  '登録',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
                onPressed: () async {
                  if (_pswd_OK) {
                    try {
                      // メール/パスワードでユーザー登録
                      UserCredential _result =
                          await _auth.createUserWithEmailAndPassword(
                        email: _newEmail,
                        password: _newPassword,
                      );

                      // 登録成功
                      User _user = _result.user!; // 登録したユーザー情報
                      _user.sendEmailVerification(); // Email確認のメールを送信
                      Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (context) => Emailcheck(
                                email: _newEmail, pswd: _newPassword, from: 1),
                          ));
                    } catch (e) {
                      // 登録に失敗した場合
                      setState(() {
                        _infoText = auth_error.register_error_msg(
                            e.hashCode, e.toString());
                      });
                    }
                  } else {
                    setState(() {
                      _infoText = 'パスワードは8文字以上です。';
                    });
                  }
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```


```dart:email_check.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../home.dart';

class Emailcheck extends StatefulWidget {
  // 呼び出し元Widgetから受け取った後、変更をしないためfinalを宣言。
  final String? email;
  final String? pswd;
  final int? from; //1 → アカウント作成画面から    2 → ログイン画面から

  Emailcheck({Key? key, @required this.email, this.pswd, this.from})
      : super(key: key);

  @override
  _Emailcheck createState() => _Emailcheck();
}

class _Emailcheck extends State<Emailcheck> {
  final _auth = FirebaseAuth.instance;
  String _nocheckText = '';
  String _sentEmailText = '';
  int _btn_click_num = 0;

  // 前画面から受け取った値はNull許容のため、入れ直し用の変数を用意
  late String _email;
  late String _pswd;

  @override
  Widget build(BuildContext context) {
    _email = widget.email ?? '';
    _pswd = widget.pswd ?? '';

    // 前画面から遷移後の初期表示内容
    if (_btn_click_num == 0) {
      if (widget.from == 1) {
        // アカウント作成画面から遷移した時
        _nocheckText = '';
        _sentEmailText = '${widget.email}\nに確認メールを送信しました。';
      } else {
        _nocheckText = 'まだメール確認が完了していません。\n確認メール内のリンクをクリックしてください。';
        _sentEmailText = '';
      }
    }

    return Scaffold(
      // メイン画面
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // 確認メール未完了時のメッセージ
            Padding(
              padding: EdgeInsets.fromLTRB(20.0, 0, 20.0, 20.0),
              child: Text(
                _nocheckText,
                style: TextStyle(color: Colors.red),
              ),
            ),

            // 確認メール送信時のメッセージ
            Text(_sentEmailText),

            // 確認メールの再送信ボタン
            Padding(
              padding: EdgeInsets.fromLTRB(0, 0, 0, 30.0),
              child: ButtonTheme(
                minWidth: 200.0,
                // height: 100.0,
                child: ElevatedButton(
                  // ボタンの形状や背景色など
                  style: ElevatedButton.styleFrom(
                    primary: Colors.grey, // background-color
                    onPrimary: Colors.white, //text-color
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(10),
                    ),
                  ),

                  // ボタン内の文字や書式
                  child: Text(
                    '確認メールを再送信',
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),

                  onPressed: () async {
                    UserCredential _result =
                        await _auth.signInWithEmailAndPassword(
                      email: _email,
                      password: _pswd,
                    );

                    _result.user!.sendEmailVerification();
                    setState(() {
                      _btn_click_num++;
                      _sentEmailText = '${widget.email}\nに確認メールを送信しました。';
                    });
                  },
                ),
              ),
            ),

            // メール確認完了のボタン配置（Home画面に遷移）
            SizedBox(
              width: 350.0,
              // height: 100.0,
              child: ElevatedButton(
                // ボタンの形状や背景色など
                style: ElevatedButton.styleFrom(
                  primary: Colors.blue, // background-color
                  onPrimary: Colors.white, //text-color
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),
                ),

                // ボタン内の文字や書式
                child: Text(
                  'メール確認完了',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),

                onPressed: () async {
                  UserCredential _result =
                      await _auth.signInWithEmailAndPassword(
                    email: _email,
                    password: _pswd,
                  );

                  // Email確認が済んでいる場合は、Home画面へ遷移
                  if (_result.user!.emailVerified) {
                    Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) =>
                              Home(user_id: _result.user!.uid, auth: _auth),
                        ));
                  } else {
                    // print('NG');
                    setState(() {
                      _btn_click_num++;
                      _nocheckText =
                          "まだメール確認が完了していません。\n確認メール内のリンクをクリックしてください。";
                    });
                  }
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```
　

```dart:authentication_error.dart
// Firebase Authentication利用時の日本語エラーメッセージ
class Authentication_error_to_ja {
  // ログイン時の日本語エラーメッセージ
  login_error_msg(int error_code, String org_error_msg) {
    String error_msg;

    if (error_code == 360587416) {
      error_msg = '有効なメールアドレスを入力してください。';
    } else if (error_code == 505284406) {
      // 入力されたメールアドレスが登録されていない場合
      error_msg = 'メールアドレスかパスワードが間違っています。';
    } else if (error_code == 185768934) {
      // 入力されたパスワードが間違っている場合
      error_msg = 'メールアドレスかパスワードが間違っています。';
    } else if (error_code == 447031946) {
      // メールアドレスかパスワードがEmpty or Nullの場合
      error_msg = 'メールアドレスとパスワードを入力してください。';
    } else {
      error_msg = org_error_msg + '[' + error_code.toString() + ']';
    }

    return error_msg;
  }

  // アカウント登録時の日本語エラーメッセージ
  register_error_msg(int error_code, String org_error_msg) {
    String error_msg;

    if (error_code == 360587416) {
      error_msg = '有効なメールアドレスを入力してください。';
    } else if (error_code == 34618382) {
      // メールアドレスかパスワードがEmpty or Nullの場合
      error_msg = '既に登録済みのメールアドレスです。';
    } else if (error_code == 447031946) {
      // メールアドレスかパスワードがEmpty or Nullの場合
      error_msg = 'メールアドレスとパスワードを入力してください。';
    } else {
      error_msg = org_error_msg + '[' + error_code.toString() + ']';
    }

    return error_msg;
  }
}
```
　　

```dart:home.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';

// [Themelist] インスタンスにおける処理。
class Home extends StatelessWidget {
  final String? user_id;
  final FirebaseAuth? auth;

  Home({Key? key, this.user_id, this.auth}) : super(key: key);

  // 前画面から受け取った値はNull許容のため、入れ直し用の変数を用意
  late String _user_id;

  @override
  Widget build(BuildContext context) {
    _user_id = user_id ?? 'ログインユーザー名取得失敗';

    const List<String> _popmenu_list = ["テスト", "ログアウト"];

    return Scaffold(
      // Header部分
      appBar: AppBar(
        leading: Icon(Icons.home),
        title: Text('ログイン後の画面'),
        backgroundColor: Colors.black87,
        centerTitle: true,
        elevation: 0.0,

        // 右上メニューボタン
        actions: <Widget>[
          // overflow menu
          PopupMenuButton<String>(
            icon: Icon(Icons.menu),
            onSelected: (String s) {
              if (s == 'ログアウト') {
                auth!.signOut();
                Navigator.of(context).pushNamed("/login");
              }
            },
            itemBuilder: (BuildContext context) {
              return _popmenu_list.map((String s) {
                return PopupMenuItem(
                  child: Text(s),
                  value: s,
                );
              }).toList();
            },
          ),
        ],
      ),

      // メイン画面
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('ようこそ',
                style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
            Text(_user_id),
          ],
        ),
      ),
    );
  }
}
```

　
#### 上記のコードのみで、一応は図で描いた様な動きにはなるので、もしよければ利用してみてください。
※コードをgitにアップロードしています。
https://github.com/Smiler5617/flutter_functions/tree/master/login_func_FirebaesAuthentication3B(Flutter2)-r2