Ionic Firebase Authenticationを使った、登録ユーザーのメールアドレスの変更方法

#はじめに
・この記事はFirebase Authenticationを使った、登録ユーザーのメールアドレスの変更方法について書いています。

って訳で、Firebase Authenticationのメール認証を使っている人向けです。

・公式は[こちら](https://firebase.google.com/docs/auth/web/manage-users#set_a_users_email_address)

・ionic + Firebaseのユーザー認証については、[こちら](https://qiita.com/se_fy/items/386418f123fc30c306e6)を参考に。

**・ionicのプロジェクトにFirebaseが入っている事を前提とします。**

#概要
Firebase authenticationでは、メールアドレスの変更時に再認証が必要となる。

[Firebase のユーザーを管理する](https://firebase.google.com/docs/auth/web/manage-users?hl=ja#re-authenticate_a_user)より
>アカウントの削除、メインのメールアドレスの設定、パスワードの変更といったセキュリティ上>重要な操作を行うには、ユーザーが最近ログインしている必要があります。ユーザーが最近ログ>インしていない場合、このような操作を行うと失敗し、エラーになります。

なので、以下ざっくりとした順番。

①設定ページにメールアドレス変更ボタン
②メールアドレス変更ボタンを押すとモーダルが開く
③現在のメールアドレスとパスワードを入力**（ここが再認証）**
④新しいメールアドレスを入力

こんな感じでいく。

#auth.serviceにメールアドレス再設定の処理を書く
まずはserviceにメールアドレス再設定の処理を書く。

[パスワード再設定](https://qiita.com/kokogento/items/1462312a24ce593db8ed)の場合と同じで、serviceを使う。

```typescript:auth.service.ts
import { AngularFireAuth } from '@angular/fire/auth';
import { AlertController } from '@ionic/angular';

export interface User {
  uid: string;
  email: string;
  displayName?: string;
  myCustomData?: string;
  photoURL?: string;
}

@Injectable({
  providedIn: 'root'
})

export class AuthService {
constructor(
    private afAuth: AngularFireAuth,
    private alertCtrl: AlertController,
  ) { }

resetEmail(email: string) {
    const user = this.afAuth.auth.currentUser;
    return user
      .updateEmail(email)
      .then(() => {
        this.newEmailAlert(email);
      })
      .catch(error => {
        console.log(error);
        const code = error.code;
        const header = 'メールアドレの変更に失敗';
        let message = 'エラーが発生しました。もう一度お試しください。。。';
        if (code === 'auth/requires-recent-login') {
          message =
            'もう一度ログインする必要があります。ログアウトして再度ログインしてからお試しください。';
        } else if (code === 'auth/email-already-in-use') {
          message = 'このメールアドレスは既に登録されています。。。';
        }
        this.showAlert(header, message);
      });
  }

// メールアドレス変更完了のアラート
async newEmailAlert(email: string) {
    const alert = await this.alertCtrl.create({
      header: 'メールアドレスを変更しました',
      message: `次回からログインするときは${email}のメールアドレスを使ってください。`,
      buttons: ['OK']
    });
    await alert.present();
  }

}
```

#設定ページにモーダルを開くボタンを設置

```html:settings.page.html
<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/home"></ion-back-button>
    </ion-buttons>
    <ion-title>設定</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content *ngIf="currentUser" padding>
  <ion-grid>
    <ion-row>
      <ion-col size="12" size-sm="8" offset-sm="2">
        <ion-card>
            <ion-card-header text-center>
            <ion-card-title>アカウント情報</ion-card-title>
            <ion-card-subtitle>メールアドレスの変更ができます</ion-card-subtitle>
          </ion-card-header>
          <ion-card-content text-center>
            <div>
              <h2 class="ion-margin-bottom">現在のメールアドレス</h2>
              <p>
                <ion-icon name="mail"></ion-icon>
                {{ currentUser.email }}
              </p>
            </div>
            <div>
             <!-- このボタンでモーダルが開く -->
              <ion-button 
              class="ion-margin-bottom ion-margin-top" 
              expand="full" 
              fill="solid" 
              color="tertiary" 
              (click)="onChangeProfile()"
              >
              <ion-icon name="create" slot="start"></ion-icon>
              アカウント情報を変更する</ion-button>
            </div>
          </ion-card-content>
        </ion-card>
      </ion-col>
    </ion-row>
  </ion-grid>
</ion-content>
```

##モーダルのcomponentを作成

settingsページのフォルダ内に、`change-profile`componentを作成
**（componentとはページを構成する要素の部品のようなもの）**

`ionic g component settings/change-profile`

<img width="217" alt="スクリーンショット 2019-11-04 15.53.30.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/367a5d1e-35d4-cbe0-55ce-5e2f17c48af6.jpeg">

こんな感じになる。

**モーダルとして開くにはもう少しやる事がある。**

##親要素にモーダルのcomponentを登録する
`settingsページ`から見て子要素となる`ChangeProfileComponent`を、moduleページで登録。

```typescript:settings.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Routes, RouterModule } from '@angular/router';

import { IonicModule } from '@ionic/angular';

import { SettingsPage } from './settings.page';
import { ChangeProfileComponent } from './change-profile/change-profile.component';

const routes: Routes = [
  {
    path: '',
    component: SettingsPage
  }
];

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild(routes)
  ],
// declarationsとentryComponentsに追加
  declarations: [SettingsPage, ChangeProfileComponent],
  entryComponents: [ChangeProfileComponent]
})
export class SettingsPageModule {}
```

##ボタンを押してモーダルを開く


```typescript:settings.page.ts
import { ChangeProfileComponent } from './change-profile/change-profile.component';
import { ModalController } from '@ionic/angular';
import { AngularFireAuth } from '@angular/fire/auth';
import { User } from '../auth/auth.service';

@Component({
  selector: 'app-settings',
  templateUrl: './settings.page.html',
  styleUrls: ['./settings.page.scss']
})
export class SettingsPage implements OnInit {
  private currentUser: User;

constructor(
    private afAuth: AngularFireAuth,
    private modalCtrl: ModalController
  ) {}

  ngOnInit() {
    this.afAuth.auth.onAuthStateChanged((user) => {
      if (user != null) {
        this.currentUser = user;
      } else {
        this.router.navigateByUrl('/auth');
      }
    });
  }

onChangeProfile() {
    this.modalCtrl
      .create({
        component: ChangeProfileComponent,
     // 現在ログインしているユーザーの情報currentUserとしてを渡す
        componentProps: { currentUser: this.currentUser }
      })
      .then(modalEl => {
        modalEl.present();
      });
  }
}
```

１つ目の`settings.page.html`と全く同じです。。。

```html:settings.page.html
             <!-- このボタンでモーダルが開く -->
              <ion-button 
              class="ion-margin-bottom ion-margin-top" 
              expand="full" 
              fill="solid" 
              color="tertiary" 
              (click)="onChangeProfile()"
              >
              <ion-icon name="create" slot="start"></ion-icon>
              アカウント情報を変更する</ion-button>       
```

これでとりあえず空っぽのモーダルは開いた

#モーダルで開いたChangeProfileComponentを作る
このページでは
>③現在のメールアドレスとパスワードを入力**（ここが再認証）**
と
>④新しいメールアドレスを入力
を実装する。

**もし③の再認証に成功すれば、④の新しいメールアドレスを入力するタブが出現するようにしたい。**

どうすれば同じ`ChangeProfileComponent`ページ内で、上記の事を実現できるか？
これには**Angularの`ngIf`**を使う事でこれは行けそうだ。

##再認証のtrue or falseを確かめる
まずboolean型のプロパティ`isCredential`を**false**で用意。

ユーザーの再認証に成功すれば`isCredential`が**true**となり、`ngIf`でそれを感知して新しいパスワードの入力タブを出現させる。

これを実装すればOK!!!

まずはhtml

```html:change-profile.component.html
<ion-header>
  <ion-toolbar color="primary">
    <ion-title>アカウント情報の変更</ion-title>
    <ion-buttons slot="primary">
      <ion-button (click)="onCancel()">
        <ion-icon name="close"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content *ngIf="currentUser" padding>
  <ion-grid>

<!-- このion-rowは再認証されていない状態で存在する -->
    <ion-row *ngIf="!isCredential">
      <ion-col size="12" size-sm="8" offset-sm="2">
        <ion-item class="loginHeader" lines="none" text-center>
          <h2>再認証の為にログインが必要です。</h2>
        </ion-item>
        <form #f="ngForm">
          <ion-item class="formItem">
            <ion-label position="floating">
              <ion-icon name="mail"></ion-icon>
              現在のメールアドレス
            </ion-label>
            <ion-input disabled="true" email [(ngModel)]="currentUser.email" type="email" name="email" #emailCtrl="ngModel">
            </ion-input>
          </ion-item>
          <ion-item>
            <ion-label position="floating">
              <ion-icon name="key" padding-right></ion-icon>パスワード
            </ion-label>
            <ion-input required [(ngModel)]="password" type="password" name="password" #passwordCtrl="ngModel"
              minlength="6">
            </ion-input>
          </ion-item>
          <ion-item *ngIf="!passwordCtrl.valid && passwordCtrl.touched" lines="none">
            <ion-label color="danger">パスワードは6文字以上</ion-label>
          </ion-item>
          <ion-button [disabled]="!f.form.valid" (click)="makeCredential()" seize="large" expand="block">
            ログイン
          </ion-button>
        </form>
      </ion-col>
    </ion-row>

<!-- このion-rowが再認証された後に出現 -->
    <ion-row *ngIf="isCredential">
      <ion-col size="12" size-sm="8" offset-sm="2">
        <form #f="ngForm">
          <ion-item>
            <ion-label position="floating">
              <ion-icon name="mail"></ion-icon>
              新しいメールアドレス
            </ion-label>
            <ion-input required email type="email" [(ngModel)]="newEmail" name="email" #emailCtrl="ngModel">
            </ion-input>
          </ion-item>
          <ion-item *ngIf="!emailCtrl.valid && emailCtrl.touched" lines="none">
            <ion-label color="danger">メールアドレスを入力してね！</ion-label>
          </ion-item>
          <ion-button [disabled]="!f.form.valid" (click)="updateProfile()" seize="large" expand="block">
            メールアドレスを変更する
          </ion-button>
        </form>
      </ion-col>
    </ion-row>
  </ion-grid>
</ion-content>
```

続いてtsファイル

```typescript:change-profile.component.ts
import { AuthService } from './../../auth/auth.service';
import { Component, OnInit } from '@angular/core';
import { ModalController, AlertController } from '@ionic/angular';
import { User } from 'src/app/auth/auth.service';
import { AngularFireAuth } from '@angular/fire/auth';
import { Router } from '@angular/router';
import * as firebase from 'firebase/app';
import { HomeService } from 'src/app/home/home.service';

@Component({
  selector: 'app-change-profile',
  templateUrl: './change-profile.component.html',
  styleUrls: ['./change-profile.component.scss']
})
export class ChangeProfileComponent implements OnInit {
  currentUser: User;

// 初期設定でfalseとしておく
  isCredential = false;
  password: '';
  newEmail: '';

  constructor(
    private modalCtrl: ModalController,
    private authService: AuthService,
    private afAuth: AngularFireAuth,
    private router: Router,
    private alertCtrl: AlertController,
    private homeService: HomeService,
  ) {}

  ngOnInit() {
    this.afAuth.auth.onAuthStateChanged((user) => {
      if (user != null) {
        this.currentUser = user;
      } else {
        this.router.navigateByUrl('/auth');
      }
    });
  }

  onCancel() {
    this.modalCtrl.dismiss();
  }

  makeCredential() {
    const user = this.afAuth.auth.currentUser;
// EmailAuthProviderを使うので、ユーザーはパスワードだけ入力
    const credential = firebase.auth.EmailAuthProvider.credential(
      user.email,
      this.password
    );
    user.reauthenticateWithCredential(credential)
      .then(() => {
    // もし再認証が成功すればisCredentialがtrueとなる
        this.isCredential = true;
      }).catch(error => {
        console.log(error);
        const code = error.code;
        const header = 'ログイン失敗';
        let message = 'エラーが発生しました。もう一度お試しください。。。';
        if (code === 'auth/wrong-password') {
          message =
            'パスワードが違います。。。';
        }
        this.showAlert(header, message);
      });
  }

  private showAlert(header: string, message: string) {
    this.alertCtrl
      .create({
        header: header,
        message: message,
        buttons: ['OK']
      })
      .then(alertEl => alertEl.present());
  }

  updateProfile() {
    this.authService.resetEmail(this.newEmail);
  }
}
```

`updateProfile()`には、一番はじめに書いた`auth.service.ts`の`resetEmail()`メソッドを注入。

#最後に
メールアドレスの変更ごとき簡単にできるだろうと思っていたが、再認証が必要って所で結構つまづいた。。。

再認証無しだとほぼ確実に`auth/requires-recent-login`エラーを食う羽目になる。
そして[公式ドキュメント](https://firebase.google.com/docs/auth/web/manage-users?hl=ja#re-authenticate_a_user)がわかりにくい！！！
`reauthenticateWithCredential`に必要な**credential**はどうやって作るんだよ！！って思った。