FlutterでFirebase Authenticationを使った認証にEmail確認を組み込む。

## はじめに
Flutterを使ってネイティブアプリを作っているが、Widgetの使い方やFirebaseとのデータ連携の仕方など、コードの書き方が分からずそれなりに苦労している。
一旦分かってしまえばかなり高速でアプリ開発ができそうなので、使い方やコードの書き方のメモを残しておく。
先日 `Firesbase Authentication` を使ったログイン機能を実装してみたが、Email確認がなく登録ができてしまうため、これだと他人のアドレスとかでアカウントを作成し放題になってしまう。今回、その対策を考えてみたのでメモとして残しておく。

## Flutterの実行環境

- Ubuntu 18.04LTS（GCP上）
- Flutter 1.22.6
- Dart 2.10.5
- Android Studio 4.1.2
- VScode 1.53.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` ⇒ ホーム画面（右上の図）。

　※以前に標準的な`Email/Password`の機能を作っており、今回はそこに対する機能追加。
　　過去の記事は、[**【FlutterでFirebase Authenticationを使ったログインUIを作成してみた。】**](https://qiita.com/smiler5617/items/b004debc847ffed4e3af)を参考に。
　

以下、各`dart`ファイルのコードを貼り付ける。
※各`dart`ファイルの特記事項は、追加機能に対する部分を重点的に記載。

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

void main() {
  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;
  AuthResult _result;
  FirebaseUser _user;

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

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

 @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),),
                ),

                // ログインボタンの配置
                ButtonTheme(
                  minWidth: 350.0,
                  // height: 100.0,
                  child: RaisedButton(

                    // ボタンの形状
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(10),
                    ),

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

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

                        // Email確認が済んでいる場合のみHome画面へ
                        if (_user.isEmailVerified){
                          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.code);
                        });
                      }
                    },

                    // ボタン内の文字や書式
                    child: Text('ログイン',
                      style: TextStyle(fontWeight: FontWeight.bold),),
                    textColor: Colors.white,
                    color: Colors.blue,
                  ),
                ),

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

        // 画面下にアカウント作成画面への遷移ボタンを配置
        bottomNavigationBar: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[

            Padding(
              padding: const EdgeInsets.all(8.0),
              child:ButtonTheme(
                minWidth: 350.0,
                // height: 100.0,
                child: RaisedButton(
                  child: Text('アカウントを作成する',
                  style: TextStyle(fontWeight: FontWeight.bold),),
                  textColor: Colors.blue,
                  color: Colors.blue[50],
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),

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

                ),
              ),
            ),
          ]),
    );
 }
} 
```
・ログインボタンをクリックした際の挙動として、`isEmailVerified`でEmail確認が済んでいるかどうかで画面遷移先を制御。
・`TextButton`をクリックすることで、`sendPasswordResetEmail(email)`を使いパスワード再設定のメールを送信。
　

```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;
  AuthResult _result;
  FirebaseUser _user;

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

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

  @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),),
                ),

                // アカウント作成のボタン配置
                ButtonTheme(
                  minWidth: 350.0,
                  // height: 100.0,
                  child: RaisedButton(
                    child: Text('登録',
                      style: TextStyle(fontWeight: FontWeight.bold),),
                    textColor: Colors.white,
                    color: Colors.blue,
                    shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(10),
                    ),

                    onPressed: () async {
                      if (_pswd_OK){
                        try {
                          // メール/パスワードでユーザー登録
                          _result = await _auth.createUserWithEmailAndPassword(
                            email: _newEmail,
                            password: _newPassword,
                          );

                          // 登録成功
                          _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.code);
                          });
                        }

                      }else{
                        setState(() {
                          _infoText = 'パスワードは8文字以上です。';
                        });
                      }
                    },
                  ),
                ),
              ],
            ),
        ),
    );
  }
}
```
・ユーザー登録の成功後、`sendEmailVerification()`を使って確認Emailを送り、一旦は`email_check.dart`に遷移する様にする。
　

```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, this.email, this.pswd, this.from}) : super(key: key);

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


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

  @override
  Widget build(BuildContext context) {

    // 前画面から遷移後の初期表示内容   
    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: RaisedButton(

                  // ボタンの形状
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),

                  onPressed: () async {
                    _result =  await _auth.signInWithEmailAndPassword(
                      email: widget.email,
                      password: widget.pswd,
                    );

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

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

            // メール確認完了のボタン配置（Home画面に遷移）
            ButtonTheme(
              minWidth: 350.0,  
              // height: 100.0,
              child: RaisedButton(

                // ボタンの形状
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(10),
                ),

                onPressed: () async {
                  _result =  await _auth.signInWithEmailAndPassword(
                    email: widget.email,
                    password: widget.pswd,
                  );

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

                // ボタン内の文字や書式
                child: Text('メール確認完了',
                  style: TextStyle(fontWeight: FontWeight.bold),),
                textColor: Colors.white,
                color: Colors.blue,
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```
・Email確認が済んでいない場合に、ストップをかけるための機能（新しく作成した画面）。遷移元により初期表示内容が変更される仕組みにしている（`from`という変数を前画面からの引数で受け取り、その値により制御している）。
・Email確認の再送信ボタンを配置。
　

```dart:authentication_error.dart
// Firebase Authentication利用時の日本語エラーメッセージ
class Authentication_error {

  // ログイン時の日本語エラーメッセージ
  login_error_msg(String error_code){

    String error_msg;

    if(error_code == 'ERROR_INVALID_EMAIL'){
      error_msg = '有効なメールアドレスを入力してください。';

    }else if (error_code == 'ERROR_USER_NOT_FOUND'){
      // 入力されたメールアドレスが登録されていない場合
      error_msg = 'メールアドレスかパスワードが間違っています。';

    }else if (error_code == 'ERROR_WRONG_PASSWORD'){
      // 入力されたパスワードが間違っている場合
      error_msg = 'メールアドレスかパスワードが間違っています。';

    }else if (error_code == 'error'){
      // メールアドレスかパスワードがEmpty or Nullの場合
      error_msg = 'メールアドレスとパスワードを入力してください。';

    }else{
      error_msg = error_code;
    }

    return error_msg; 
  }


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

    String error_msg;

    if(error_code == 'ERROR_INVALID_EMAIL'){
      error_msg = '有効なメールアドレスを入力してください。';

    }else if (error_code == 'ERROR_EMAIL_ALREADY_IN_USE'){
      // メールアドレスかパスワードがEmpty or Nullの場合
      error_msg = '既に登録済みのメールアドレスです。';

    }else if (error_code == 'error'){
      // メールアドレスかパスワードがEmpty or Nullの場合
      error_msg = 'メールアドレスとパスワードを入力してください。';

    }else{
      error_msg = error_code;
    }

    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);

  @override
  Widget build(BuildContext context) {

    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_FirebaesAuthentication3/lib

**Flutterの最新版（NullSafetyなどが導入された以降のバージョンなど）では上記コードでは動かなかったため、修正が必要となる。修正後のコードはこちらの記事に記載。**
[**【FlutterでEmail確認付きのFirebase Authenticationを使った認証（Mac M1）】**](https://qiita.com/smiler5617/items/71d015081306dc140170)