AWS AmplifyとIonicを組み合わせてモバイルアプリまで爆速で開発する

はじめまして。Advent Calendar初参加になります！AWS Amplifyは一年前くらいから触り始めてその便利さから愛用しているので、今回はその知見をアウトプットできたらと思います。

# 目黒にあるAWS Loft TokyoでAWS Amplifyに出会った

1年ほど前、東京都の目黒駅徒歩1分の場所に「AWS Loft Tokyo」なる施設がオープンしました。
スタートアップとデベロッパーのための「挑戦をカタチにする場所」というコンセプトです。

https://aws.amazon.com/jp/start-ups/loft/tokyo/

今回のAWS Amplify Advent Calendarを作ったのもスタートアップソリューションアーキテクトの塚田さん（ [@akitsukada](https://twitter.com/akitsukada) ）ですね。

Loftにはオープンから足を運んでおり、コワーキング利用終了後に毎週のように開催される技術系イベントに参加していました。
参加したイベントの一つが「AWS Amplify Blackbelt公開収録 & AppSync入門」です。

# Ionic（アイオニック）とは

https://ionicframework.com/

Ionicとは、HTML / CSS / JSの技術でモバイルアプリを作るためのUIフレームワークと言ったところでしょうか。iOS / AndroidのUIに極限まで近づけたUIコンポーネントが提供されており、美しいUIのアプリを素早く構築することができます。

CordovaやCapacitorを使うことで、Ionicで作ったアプリをiOS / Android用アプリとしてビルドしてApp Store / Google Playに並べることができます。

僕は3年ほどスタートアップで一人エンジニアをやっていたので、iOS / Androidアプリを一気に構築できるIonicがとても気に入っていました。

そこにAWS Amplifyが加わることで、フロントエンドからバックエンドまで一人で作ることができるくらいスピードを上げることができます。

今回はそんなIonicで作ったアプリにAWS Amplifyを組み込んで使うための手順をまとめようと思います。

# 動作確認環境

- macOS Catalina 10.15
- VS Code
- node.js v12.7.0
- @angular/core 8.1.2
- @ionic/angular 4.7.1
- aws-amplify 1.2.4
- aws-amplify-angular 4.7.1
- @amplify/cli 3.17.0

# Ionicプロジェクトを新しく作成する

まずはIonic CLIを利用してプロジェクトを生成します。
`npx` を使うことでIonic CLIをグローバルインストールしていなくてもコマンドを利用できます。

```
# CLIでプロジェクトを作成開始
$ npx ionic start

# プロジェクト名を指定
? Project name: AmplifyIonicChat

# JSフレームワークを選択：今回はAngularを利用
? Framework: Angular

# テンプレートを選択
? Starter template: blank

# ディレクトリに移動
$ cd AmplifyIonicChat
```

この時点でのディレクトリ構造は次のようになります。
基本的にはAngular CLIで生成されるディレクトリと同じです。

```
$ tree -I node_modules
.
├── angular.json
├── browserslist
├── e2e
│   ├── protractor.conf.js
│   ├── src
│   │   ├── app.e2e-spec.ts
│   │   └── app.po.ts
│   └── tsconfig.json
├── ionic.config.json
├── karma.conf.js
├── package-lock.json
├── package.json
├── src
│   ├── app
│   │   ├── app-routing.module.ts
│   │   ├── app.component.html
│   │   ├── app.component.scss
│   │   ├── app.component.spec.ts
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   └── home
│   │       ├── home.module.ts
│   │       ├── home.page.html
│   │       ├── home.page.scss
│   │       ├── home.page.spec.ts
│   │       └── home.page.ts
│   ├── assets
│   │   ├── icon
│   │   │   └── favicon.png
│   │   └── shapes.svg
│   ├── environments
│   │   ├── environment.prod.ts
│   │   └── environment.ts
│   ├── global.scss
│   ├── index.html
│   ├── main.ts
│   ├── polyfills.ts
│   ├── test.ts
│   ├── theme
│   │   └── variables.scss
│   └── zone-flags.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json
```

これだけでもう動作確認ができるので、AWS Amplify JavaScript Frameworkを入れる前に一旦動かしてみます。

```
# 開発サーバーを起動
$ npx ionic serve
```

自動でデフォルトのブラウザが開き、アプリケーションが表示されます。
これだけでiOS端末ではiOSのスタイルが、Android端末ではマテリアルデザインのスタイルが適用されたアプリケーションの雛形が出来上がります。

<img width="1366" alt="amplify_01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142803/dac44c43-f045-ae80-c656-450481064796.png">

# AWS Amplify JavaScript Frameworkをインストール

Ionicアプリケーションの準備が整ったので、ここから本題のAWS Amplifyを導入します。
公式ドキュメントがAngular / Ionicでの導入方法を出してくれているので、それに従います。

https://aws-amplify.github.io/docs/js/angular

## AWS Amplify関連パッケージのインストール

まずAWS Amplify関連のパッケージをインストールします。 `aws-amplify-angular` というパッケージがあるのでそれを使います。

```
$ npm install aws-amplify aws-amplify-angular 
```

## Angular 6以上で必要となる設定を書く

`window.global` と `window.process` がセットされている必要があるとのことです。指示に従って以下の設定を `src/polyfills.ts` に追記します。

```typescript:src/polyfills.tsに追加する内容
(window as any).global = window;
(window as any).process = {
  env: { DEBUG: undefined },
};
```

`src/polyfills.ts` は最終的には以下のようになりました。Ionic CLI（Angular CLI）が説明のためのコメントを大量に書いてくれていますが、見やすくするために全て削除しています。

```typescript:編集後のsrc/polyfills.ts
import './zone-flags.ts';
import 'zone.js/dist/zone';

(window as any).global = window;
(window as any).process = {
  env: { DEBUG: undefined },
};
```

余談ですが、公式ドキュメントに、

> Please also note that the AWS Amplify Angular components do not yet support Ivy.

と書いてありました。Ivyのサポートはまだ無いようですね...

## TypeScriptのコンパイルオプションを調整する

`src/tsconfig.app.json` 内の `compilerOptions` セクションの `types` に `node` を追加します。ちなみに、 `src/tsconfig.json` というファイルもあるので混同しないよう注意が必要です。

```json:編集後のsrc/tsconfig.app.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": ["node"]
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "src/test.ts",
    "src/**/*.spec.ts"
  ]
}
```

これでIonicアプリへのAWS Amplify JavaScript Frameworkのインストールは完了です。

# AWS Amplifyの初期設定を行う

AWS Amplify CLIを使って初期設定を行います。初期設定はプロジェクトごとに行う必要がある操作になります。

```bash
# Amplify CLIの初期設定を開始
$ npx amplify init

# プロジェクト名を指定
? Enter a name for the project AmplifyIonicChat

# 環境名を指定
? Enter a name for the environment dev

# エディタを指定
? Choose your default editor: Visual Studio Code

# 開発に使う言語を指定
? Choose the type of app that you re building javascript

# 開発に使うフレームワークを指定
? What javascript framework are you using ionic

# ソースコードのディレクトリを指定
? Source Directory Path:  src

# ビルド後の成果物の格納先ディレクトリを指定
? Distribution Directory Path: www

# ビルドコマンドを指定
? Build Command:  npm run-script build

# 開発サーバー起動コマンドを指定
? Start Command: ionic serve

# Amplify CLIが使うAWSプロファイルを使う
? Do you want to use an AWS profile? Yes

# プロファイルを指定：今回は個人用のAWSに繋がるプロファイルとして用意してある「private」を指定
? Please choose the profile you want to use private
```

ここまで指定するとCloudFormationが起動して、バックエンドに必要なリソースを生成します。

ここでは以下のリソースが作成されます。「この時点で作るリソースあるの？」という感じですが、何か意図があるのでしょうか。。。

- S3バケット
- Cognitoフェデレーテッドアイデンティティでの認証済みクライアントが引き受けるロール（AuthRole）
- Cognitoフェデレーテッドアイデンティティでの未認証クライアントが引き受けるロール（UnauthRole）

# AWS AmplifyのAuthenticationカテゴリを使ってユーザー認証を実装する

ここからはAWS Amplify CLIを使ってAWSクラウド上にバックエンドを構築していきます。

## Authenticationカテゴリの設定をAmplify CLIで構築する

まずはアプリケーションにサインアップ・サインイン機能を作るために、AWS AmplifyのAuthenticationカテゴリをセットアップします。

こちらは公式ドキュメントの以下の箇所に沿って進めます。

https://aws-amplify.github.io/docs/js/authentication#automated-setup

```bash
# Amplify CLIを使ってAuthenticationカテゴリを追加
$ npx amplify add auth

# 認証の設定をどのように行うか指定：今回はデフォルトの設定を利用
Do you want to use the default authentication and security configuration? Default configuration

# サインインに使える属性を指定
How do you want users to be able to sign in? Username

# 追加の設定をするかどうかを指定
Do you want to configure advanced settings? No, I am done.
```

これで設定は完了です。この時点ではまだバックエンドに設定は適用されていません。

## 設定をバックエンドに適用する

では、実際に設定をバックエンドに適用します。

```bash
# 変更をバックエンドに適用
$ npx amplify push

# 変更内容が出るので、確認して先へ進む
Current Environment: dev

| Category | Resource name            | Operation | Provider plugin   |
| -------- | ------------------------ | --------- | ----------------- |
| Auth     | amplifyionicchatd1aa1b6e | Create    | awscloudformation |

? Are you sure you want to continue? (Y/n)
```

マネジメントコンソールで確認すると、Amazon Congitoのユーザープールが作成されていることがわかります。

<img width="678" alt="amplify_02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142803/9881ad95-8e19-ae0a-9ab7-f24df342a698.png">

# Ionicアプリにバックエンドへの接続情報を適用する

先ほどの `$ npx amplify push` コマンド実行のタイミングで、バックエンドの接続情報（CognitoのユーザープールIDやアイデンティティプールID等）が `src/aws-export.js` というファイルに出力されました。

`aws-export.js` は以下のようなシンプルなJSファイルです。

```js
// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

const awsmobile = {
    "aws_project_region": "ap-northeast-1",
    "aws_cognito_identity_pool_id": "＜アイデンティティプールID＞",
    "aws_cognito_region": "ap-northeast-1",
    "aws_user_pools_id": "＜ユーザープールID＞",
    "aws_user_pools_web_client_id": "＜ユーザープールクライアントID＞",
    "oauth": {}
};


export default awsmobile;
```

1行目にコメントで書いてある通り、このファイルは今後の `$ npx amplify push` コマンド実行時に上書きされるので、手作業で変更してはいけません。

# Amplify Frameworkを初期化する

Amplify Frameworkに `aws-exports.js` に書かれた設定情報を読み込ませるため、 `src/main.ts` に以下のようにAmplifyの初期化処理を書きます。

```typescript:src/main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

// 以下3行を追加
import Amplify from 'aws-amplify';
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.log(err));
```

ちなみに、公式ドキュメントでは、 `Amplify` のインポート元は `@aws-amplify/core` となっていますが、そのまま書いてもうまくいかず以下のようなエラーが出てしまいます。

<img width="1371" alt="amplify_04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142803/31fe0858-1139-6214-1c29-3f32c557be69.png">

これを解消するために、 `Amplify` は `aws-amplify` から読み込むようにしています。

これでフロントエンドとバックエンドの接続が完了です。

# Angularのモジュールの整理をしておく

ここで一旦今後のためにAngularプロジェクトとしての準備を行います。

- 未ログイン状態のときにアクセスできるページ群を管理する `PublicModule` を作成
- ログイン状態のときにアクセスできるページ群を管理する `ClosedModule` を作成
- アプリケーション全体から利用するモジュールをまとめた `SharedModule` を作成

## Angularの「モジュール」

Angularでは、アプリケーションを「モジュール」という単位に分割できます。ページコンポーネントを一つのモジュールとして作成しておくことで、そのページが必要となったタイミングでモジュールごと読み込む遅延読み込み（Lazy Loading）ができます。

モジュールの定義には `@NgModule` デコレータを使います。

あるモジュールAであるモジュールBを使いたい場合、モジュールBの定義箇所の `@NgModule` の宣言内の `imports` にモジュールAを指定します。

Angularには最上位のモジュールである `AppModule` が存在します。
そして、 `$ ionic start` コマンドでBlankテンプレートを選択して作成した雛形には、Homeページ用の `HomeModule` も生成されています。

## 未ログイン状態でアクセスできるページ群を管理するPublicModuleを作成する

Ionic CLIを使ってモジュールを作成します。

```bash
$ npx ionic g module pages/public
```

これで、 `src/app/pages/public/public.module.ts` が作成されます。中身は以下のようにしました。ルーティングは後ほど追加していくので、まだ何も作成していません。

```typescript:src/app/pages/public/public.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// ここにルーティングの定義を書いていく
const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forChild(routes)]
})
export class PublicModule { }
```

## ログイン状態でアクセスできるページ群を管理するClosedModuleを作成する

`PublicModule` と同様にモジュールを作成します。

```bash
$ npx ionic g module pages/closed
```

```typescript:src/app/pages/closed/closed.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

// ここにルーティングの定義を書いていく
const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forChild(routes)]
})
export class ClosedModule { }
```

## AppRoutingModuleにPublicModuleとClosedModuleを追加する

最後に、アプリケーションのルートのルーティング定義である `AppRoutingModule` に `PublicModule` と `ClosedModule` を登録します。
これで `PublicModule` や `ClosedModule` に書いたルーティングが有効になります。

```typescript:src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  // 直下へのアクセスは /auth へリダイレクト
  { path: '', redirectTo: 'auth', pathMatch: 'full' },
  // 未ログイン状態でアクセスできる画面群のルーティング
  {
    path: '',
    loadChildren: () => import('./pages/public/public.module').then(m => m.PublicModule)
  },
  // ログイン状態でアクセスできる画面群のルーティング
  {
    path: '',
    loadChildren: () => import('./pages/closed/closed.module').then(m => m.ClosedModule)
  },
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }
```

Angularでは、URLでの画面遷移を許可するかどうかを制御する「ガード」という仕組みがあります。このガードを `PublicModule` と `ClosedModule` にかけることで、ログイン状態のときはサインアップフォームへのアクセスはリダイレクト、未ログイン状態のときはサインアップフォームへリダイレクトといった制御をきれいに書くことができます。

## アプリケーション全体で使うモジュールをまとめたSharedModuleを作る

前述の通り各ページはそれぞれ独立したモジュールになっているので、それらのモジュールで共通したい処理はまとめておくと楽です。
全ページで使う `IonicModule` や `FormsModule` 等は全て `SharedModule` に含めてしまい、各ページのモジュールからはこの `SharedModule` のみインポートするようにします。

```bash
$ npx ionic g module shared
```

これで、 `src/app/shared/shared.module.ts` が生成されました。

内容は以下のようにします。

```typescript:src/app/shared/shared.module.ts
import { NgModule } from '@angular/core';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { AmplifyIonicModule, AmplifyService } from 'aws-amplify-angular';

const modules = [
  FormsModule,
  IonicModule,
  CommonModule,
  AmplifyIonicModule,
];

@NgModule({
  imports: [...modules],
  exports: [...modules],
  providers: [AmplifyService],
})
export class SharedModule { }
```

## importを絶対パスで書けるようにする

ソースコードをディレクトリで整理していくと、 `import` 文が以下のようになったりします。

```typescript
import { MyService } from '../../../../../services/myservice/myservice.service';
```

これだと辛いので、プロジェクトのルートディレクトリから絶対パスで指定できるようにしておきます。

`tsconfig.json` の `compilerOptions` に `paths` を指定します。

```json:tsconfig.json
{
  "compilerOptions": {
    （...略）
    "paths": {
      "@/*": ["src/*"]
    },
    （...略）
  }
}
```

この設定を書くことによって、先ほどの `import` 文は以下のように書くことができます。

```typescript
import { MyService } from '@/services/myservice/myservice.service`;
```

## 雛形で生成されていたAppModuleを調整する

後ほど `AppComponent` でAWS Amplifyが提供する `AmplifyService` を使うので、`AppModule` でも `SharedModule` をインポートするようにしておきます。

同時に雛形で生成されていた不要なモジュールのインポートも削除します。

```typescript:src/app/app.module.ts
import { NgModule } from '@angular/core';
import { RouteReuseStrategy } from '@angular/router';
import { BrowserModule } from '@angular/platform-browser';

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

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { SharedModule } from './shared/shared.module';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    SharedModule,
    IonicModule.forRoot(),
    AppRoutingModule,
  ],
  providers: [
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
```

## 雛形で生成されていたHomePageを調整する

最後に、整理したモジュールの構造の中に自動生成された `HomePage` を入れます。
生成されたときは `src/app/home/` にあった `HomePage` の一式を `src/app/pages/closed/` に移します。

そして、 `src/app/pages/closed/home/home.module.ts` を以下のように調整します。
（`FormsModule` / `IonicModule` / `CommonModule` のインポートを `SharedModule` のインポートに置き換えました）

```typescript:src/app/pages/closed/home/home.module.ts
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';
import { SharedModule } from '@/app/shared/shared.module';

@NgModule({
  imports: [
    SharedModule,
    RouterModule.forChild([{ path: '', component: HomePage }])
  ],
  declarations: [HomePage]
})
export class HomePageModule {}
```

最後に、 `ClosedModule` に `HomePage` のルーティングを定義します。

```typescript:src/pages/closed/closed.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: 'home',
    loadChildren: () => import('./home/home.module').then(m => m.HomePageModule),
  },
];

@NgModule({
  imports: [
    RouterModule.forChild(routes),
  ]
})
export class ClosedModule { }
```

結構調整作業が多くなってしまいました。本稿の主役はAWS Amplifyですが、せっかくなのである程度アプリケーションとしてスケールさせられる体制を整える方法まで書きたいと思っています。

# 認証を扱うページを生成する

先ほどバックエンドにAWSの認証基盤であるAmazon Cognitoを構築できたので、先ほど作ったIonicアプリでサインアップ・サインインできるようにします。

AWS Amplifyのすごいところは、**サインイン周りを一発で実装できるコンポーネントを各フレームワークで用意してくれている**ところです（！）

具体的には、 `<amplify-authenticator>` というコンポーネントが提供されていて、これをアプリケーション内に配置するとサインインフォームが出現します。

認証をWebアプリケーションに組み込む場合、

- サインアップ画面は `/sign-up`
- サインイン画面は `/sign-in`
- パスワードリマインダーは `/password`

のようにそれぞれURLを分けることが一般的かと思います。

しかし、今回は認証周りを `<amplify-authenticator>` コンポーネントを利用して行うため、認証を扱う画面は１つあれば十分です。

## 認証機能を配置するページを作成する

Ionic CLIを使って認証用の画面を用意します。CLIを使ってページを生成する場合、自動的にモジュールが作成され、遅延読み込みが有効になります。

認証を扱う画面は未ログイン状態でのみ閲覧可能としたいので、先ほど作成した `PublicModule` 以下にページのモジュールを作成します。

```bash
$ npx ionic g page pages/public/auth
```

これで以下のファイルが出来上がりました。

```text
src/app/pages/public/auth/auth.page.ts
src/app/pages/public/auth/auth.page.spec.ts
src/app/pages/public/auth/auth.page.html
src/app/pages/public/auth/auth.page.scss
src/app/pages/public/auth/auth.module.ts
src/app/pages/public/auth/auth-routing.module.ts
```

今回はルーターの設定も `auth.module.ts` に書いていくことにするので、 `auth-routing.module.ts` は削除します。
認証用のページのモジュールである `auth.module.ts` は以下のように記述します。

```typescript:src/app/pages/public/auth/auth.module.ts
import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { SharedModule } from '@/app/shared/shared.module';

import { AuthPage } from './auth.page';

@NgModule({
  imports: [
    SharedModule,
    RouterModule.forChild([{ path: '', component: AuthPage }]),
  ],
  declarations: [AuthPage]
})
export class AuthPageModule {}
```

最後に、 `/auth` でこのページにアクセスできるよう、 `PublicModule` にルーティング定義を追加します。

```typescript:src/pages/public/public.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: 'auth',
    loadChildren: () => import('./auth/auth.module').then(m => m.AuthPageModule),
  },
];

@NgModule({
  imports: [
    RouterModule.forChild(routes),
  ]
})
export class PublicModule { }
```

これで `/auth` ページの準備は完了です。 `http://localhost:8100/auth` にブラウザでアクセスすると以下のような画面が表示されます。

<img width="1371" alt="amplify_03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142803/84e8a34c-fee8-584b-a30b-b8fc303f877e.png">

## AWS Amplifyが提供してくれている認証コンポーネントを配置する

では、作成した `AuthPage` にAWS Amplifyが提供してくれている `<amplify-authenticator-ionic>` コンポーネントを配置してみます。

```html:src/pages/public/auth/auth.page.html
<ion-header>
  <ion-toolbar>
    <ion-title>auth</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div class="ion-padding">
    <amplify-authenticator-ionic></amplify-authenticator-ionic>
  </div>
</ion-content>
```

Ionicを使わない素のAngularの場合、 `<amplify-authenticator>` コンポーネントを利用しますが、Ionicを使う場合は専用の `<amplify-authenticator-ionic>` コンポーネントを使うことができます。

なんとこれだけで認証周りの画面の実装が完了します。

<img width="1371" alt="amplify_05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142803/da8a462e-2767-9920-7a2c-d198a0779b7c.png">

## 試しにサインアップしてみる

表示されているサインイン画面の下の方の「Create account」をクリックします。

<img width="1371" alt="amplify_06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142803/0327abce-daab-2b6b-5374-535da13ead64.png">

すると、サインアップ画面に遷移します。
遷移する、といっても `<amplify-authenticator-ionic>` コンポーネントがサインアップフォームのUIに変わるだけでルーティングは変わりません。

サインアップフォームで情報を入力します。
デフォルトの設定では、以下の項目を入力する必要があります。

- ユーザー名
- メールアドレス
- パスワード（英数字 + 記号 / 8文字以上）
- 電話番号（ `090` の先頭のゼロは取る）

<img width="1371" alt="amplify_07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142803/be4026a7-798e-53d9-ff77-8f5f8c35ca52.png">

これで「Create Account」をクリックするとCognitoユーザープールにアカウントが作成されます。
メールアドレスの開通確認もデフォルトで用意されているので、入力したメールアドレスの受信トレイを確認します。

シンプルなメールが届いていました。

<img width="527" alt="amplify_08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142803/5de9ad5d-1917-f800-c2a5-fcebd6f4c8b7.png">

サインアップすると `<amplify-authenticator-ionic>` コンポーネントは自動的に以下のようなメールアドレス開通確認コード入力画面に切り替わるので、メールで届いた6桁の確認コードを入力します。

<img width="1371" alt="amplify_09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142803/496fabbb-0dff-21da-73ac-134c7f212938.png">

「Confirm Code」をクリックするとサインアップが完了します。サインアップ後は改めてログインする必要があります。

<img width="1371" alt="amplify_10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142803/ea1ff80f-0154-22dd-750b-d51787417b74.png">

登録したアカウント情報でサインインすると、`<amplify-authenticator-ionic>` コンポーネントは以下のようなUIに切り替わりました。

<img width="1371" alt="amplify_11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142803/076fe587-646e-7e05-1f34-f5b5147eb6d8.png">

（ちなみにちょいちょいconsoleに出ているエラーはアカウント情報を何度か入力し間違えたためです💦）

これでIonicアプリにAWS Amplifyを使って認証システムを組み込むことができました。
認証周りは実は結構複雑で、以下のような画面をユーザーの状態に応じて出し分ける必要があります。

- サインアップ
- メールアドレスの開通確認
- サインイン
- パスワードリマインダー
- パスワード再設定

AWS Amplify JavaScript Frameworkを使うことでこの辺りをコンポーネント一発で構築できるのは非常に強力です。

# Amplifyの認証状態をIonicアプリに同期する

さて、準備の段階で頑張って `PublicModule` と `ClosedModule` を作って、未認証状態と認証状態の画面群を分けました。

サインインは `<amplify-authenticator-ionic>` コンポーネントで行えばよいですが、Amplifyが内部的に持っているログイン状態に応じてルーティングを変えたいです。

## AppComponentにAmplifyの認証状態を監視するコードを追加する

Ionicアプリのルートコンポーネントとして、 `AppComponent` というコンポーネントがあるので、そこでAmplifyの認証状態を監視してルーティングに反映させようと思います。

Ionic CLIでプロジェクトを作ると、 `AppComponent` にはネイティブアプリ向けのスプラッシュスクリーンやステータスバーの操作処理が書かれていますが、今回は必要ないのでガンガン削除します。

完成形のみ書きます。

```typescript:src/app/app.component.ts（一通り実装済み）
import { Component, OnInit } from '@angular/core';
import { NavController } from '@ionic/angular';
import { AmplifyService } from 'aws-amplify-angular';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss']
})
export class AppComponent implements OnInit {

  constructor(
    private navCtrl: NavController,
    private amplifyService: AmplifyService,
  ) {}

  ngOnInit() {
    this.amplifyService.authStateChange$.subscribe(authState => {
      switch (authState.state) {
        case 'signedIn':
          this.navCtrl.navigateForward(['/home']);
          break;
        case 'signedOut':
          this.navCtrl.navigateForward(['/auth']);
          break;
      }
    });
  }
}
```

`Observable` を `subscribe` するときは、必要なくなったときに必ず `unsubscribe` しないとメモリリークにつながりますが、こと `AppComponent` に関してはアプリケーションが生きている間はずっと存在している間はずっと存在するので、特に `unsubscribe` する処理は書いていません。

AmplifyをAngularから使うためには、`aws-amplify` パッケージから個別に `Auth` や `API` をインポートしてもよいのですが、AngularにはDI（Dependency Injection）という強力な仕組みがあります。

`aws-amplify-angular` パッケージでは、そのDI経由でAmplifyを操作できるように `AmplifyService` というサービスクラスを提供してくれています。

また、AngularはRxJSと密結合です。`AmplifyService` では、Amplifyが内部的に持っているログイン状態の変化をRxJSの `Observable` として提供してくれています。（`this.amplifyService.authState()` の部分）

この状態でサインインすると、自動的に `HomePage` に遷移します。

## とりあえずHomePageにサインアウトボタンを設置する

サインアウトも試したいので、 `HomePage` にサインアウトボタンを設置します。Ionicを使っているので、ヘッダーにアイコンを使ったボタンを設置するのは一瞬でできてしまいます。

```html:src/app/pages/closed/home/home.page.html
<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic Blank
    </ion-title>

    <!-- ここから追加 -->
    <ion-buttons slot="end">
      <ion-button slot="icon-only" (click)="onSignOutButtonClicked()">
        <ion-icon name="log-out"></ion-icon>
      </ion-button>
    </ion-buttons>
    <!-- ここまで追加 -->

  </ion-toolbar>
</ion-header>

<ion-content>
  <div class="ion-padding">
    The world is your oyster.
    <p>If you get lost, the <a target="_blank" rel="noopener" href="https://ionicframework.com/docs/">docs</a> will be your guide.</p>
  </div>
</ion-content>
```

続いて `src/app/pages/closed/home/home.page.ts` にサインアウト処理を書いていきます。こちらも `AmplifyService` をDIして、用意されているメソッドを使えば簡単にできます。

```typescript:src/app/pages/closed/home/home.page.ts
import { Component } from '@angular/core';
import { AmplifyService } from 'aws-amplify-angular';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {

  constructor(
    private amplifyService: AmplifyService,
  ) {}

  onSignOutButtonClicked() {
    this.amplifyService.auth().signOut();
  }
}
```

こんな感じで動作します。いい感じですね！

![signin-sign-out.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142803/4963f7b0-fb56-8bf1-5b05-550345d3eda6.gif)

ここまででIonicアプリにAmplify Frameworkを組み込むことができました。

# 続きは続編にて：AWS AppSyncのセットアップとリクエスト

ここまでの作業をまとめると、

- Ionic CLIを使ってIonicプロジェクトを作成
- Amplify CLIを使ってバックエンドにAmazon Cognitoを構築
- Amplify Frameworkをセットアップ
- Amplify提供の `<amplify-authenticator-ionic>` コンポーネントを使って認証周りをサクッと構築
- Amplifyが持つ認証状態をAngular側で監視してルーティングを切り替える

になります。

一番書きたかったのはAWS AppSyncでGraphQL APIを作成してAWS Amplify JavaScript Frameworkから叩く部分でしたが、予想以上に準備の手順が多かったのでこのあたりで一旦切りたいと思います。

今回がQiitaのアドベントカレンダー初挑戦なのでこんな感じで良いのか若干不安ですが、勢いで2日間書くことにしているので、続編は12/17の記事でお送りできたらと思います。

ざっくりですが、この後はAWS AppSyncを使ってAPIを立てて、リアルタイムで同期されるチャットアプリを作ろうと思っています。
チャットアプリはAppSyncの練習としてよく題材にされているイメージです。
awslabsにもChatQLというAngular+AppSyncのサンプルがあったり。

この後の実装はチャットアプリに寄せたものになっていく予定ですが、本記事の内容は割と汎用性のあるものに仕上げたつもりです。

AWS Amplifyでバックエンドを爆速開発できるのに加えて、Ionicでフロントエンドも爆速開発できたらより夢が広がりませんか！？

AWS Amplify Advent Calendar、明日は @moaible4qiita さんです！