Firebase Authのメール認証　確認メールをチェックしたかどうか判定

### 実行環境

Xcode 12.2
Swift 5.3.1

## Firebase Authのメール認証

Fiebaseを使用したアプリ開発において、ログイン周りやユーザーの管理にはAuthenticationの機能を使用されるかと思います。

私がつまづいた所は、アカウント作成、メアドの変更時に確認メールを送信するようにしているのですが、ユーザーがその確認メールに添付されたURLをクリックして中身を確認していないにも関わらず、変更したアドレスや新しいアカウントでログインできてしまうという問題です。

ユーザーが確認メールを受け取ったかどうかを判定して、確認できていないようならログインを防ぐ機能が必要です。

## 確認メールを確認したかどうか判定機能の実装

例として「アカウント作成時に、登録されたメアドに確認メールを送り、ユーザーがそのメールに添付されたURLをタップして確認していたら、認証完了ということでログインを許可する」一連を紹介します。

実装の流れとしては、
1.ログイン画面のViewControllerのviewWillAppearでユーザーの状態を取得するためにリスナーをアタッチ
2.アカウント作成時に同時に確認メールを送信
3.ログインボタンタップ時にユーザーの認証状態を判定
4.結果に応じてログインを認める
5.不要になったリスナーはデタッチ

以下はログイン画面のViewControllerです。

viewWillApperでリスナーをアタッチ、viewWillDisappearでリスナーをデタッチしています。

```Swift.LoginViewController.swift
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        AuthManeger.shared.attachListner()
        }
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        AuthManeger.shared.detachListner()
   }
```
```Swift.AuthManager.swift
  func attachListner() {
        authListner = Auth.auth().addStateDidChangeListener() { (auth, user) in
           user?.reload()
        }
    }
    
  func detachListner() {
        guard let authListner = authListner else { return }
        Auth.auth().removeStateDidChangeListener(authListner)
    }
```

attachListner()ではFirebase Authのメソッドを使ってリスナーをアタッチします。

コールバックから取得した引数userをreload()することでユーザーの認証状態を更新できます。ここで更新しないと、ユーザーが確認メールを確認しても認証状態が反映されません。

ここのuserはAuth.auth().currentUserと同じですので、現在のユーザーを表しています。

以下、アカウント作成処理です。

```Swift.AuthManager.swift
 func createUser(address: String, password: String, completion: ((UserModel?, Error?) -> Void)? = nil) {
    Auth.auth().createUser(withEmail: address, password: password) { authResult, error in
        if let error = error {
            completion?(nil, error)
            return
        }
        guard let authResult = authResult else {
            completion?(nil, FirebaseError.unknown)
            return
        }
        guard let currentUser = Auth.auth().currentUser else {
            completion?(nil, error)
            return}
        currentUser.sendEmailVerification() { error in
            if let error = error {
                completion?(nil, error)
                return
            }
            print("仮登録メールを送信しました。")
            let user = UserModel(user: authResult.user)
            completion?(user, nil)
        }
    }
}
```

AuthManagerクラスでアカウント作成と確認メール送信の処理を記述しています。

AuthenticationのメソッドであるcreateUser()でアカウント作成し、sendEmailVerification()で確認メールを送信します。

アカウント作成ボタンタップ時の処理の部分で、入力されたメアド、パスワードを引数として与えてこのメソッドを呼び出します。

以下、ユーザーがログインボタンをタップしてログインを試みた時に、送信された確認メールを確認したのかどうかを判定する処理です。

```Swift.AuthManager.swift
func needCheckEmail() -> Bool? {
        guard let user = Auth.auth().currentUser else {
            print("ユーザーがいない")
            return nil
        }
        
        if !user.isEmailVerified {
            print("確認メールを確認してませんね")
            return false
        }
        return true
    }
```

重要なのは if !user.isEmailVerified です。

この部分がユーザーが確認メール確認したかどうかを判定するAuthenticationのメソッドになります。ユーザーが確認メールを確認した場合はtrueを返します。

「確認メールを確認したかどうか」という言い方をしていますが、要は「認証されたユーザーかどうか」を判定しています。

確認メールのURLがタップされた場合は「認証済み」ということになります。

needCheckEmail()はユーザーが認証済みならtrue、認証されてなければfalse(確認メールを確認していたらtrue、確認してなければfalse)を返します。

そしてこのメソッドをLoginViewControllerのログインボタンタップ時の処理の部分で呼び出すことで、ユーザーの認証状態によって挙動を使い分けることができます。

```Swift
if !AuthManeger.shared.needCheckEmail() {
        SVProgressHUD.illegalLogin()
         return
 }
```

## 最後に
同じ問題に直面した方の参考になればと思います。