aws-amplify-react で sign up 時の電話番号を無効にする

## TL;DR

- aws-amplify-react の Auth モジュールが提供する `<SignUp>` は電話番号が必須になっている。
- これを無効にするには自分で専用の `<CustomSignUp>` を作る必要がある。
    - [awsmobile-cli](https://github.com/aws/awsmobile-cli) で生成した aws-exports.js から自動的に UI を変更してくれたりはしない。
    - [ドキュメント](https://aws-amplify.github.io/amplify-js/media/authentication_guide#customize-withauthenticator)だけだとどう実装すればいいのか分からないので、[ソースコード](https://github.com/aws-amplify/amplify-js/blob/e3c836514e8f55e3d5f7ed4c735dcb1dabc6c5c2/packages/aws-amplify-react/src/Auth/SignUp.jsx)を見る必要がある

## 背景: 電話番号を無効にしたい

[awsmobile-cli](https://github.com/aws/awsmobile-cli) を使って作った AWS Mobile Hub プロジェクトに AWS Cognito を使った認証機能を追加することにしました。 CLI から User Pool を作って、 aws-amplify-react の `withAuthenticator` HOC を使ってみたところ、アカウント登録画面で電話番号が必須になっていました:

![image.png](https://qiita-image-store.s3.amazonaws.com/0/153/d72b376d-fe23-c253-bf2d-dbf4889f66ab.png)

電話番号を省略しようとしてもバリデーションエラーが発生します:

![image.png](https://qiita-image-store.s3.amazonaws.com/0/153/0ba24f32-73e8-e506-d9da-4e38488ac8e8.png)

awsmobile-cli から作った User Pool の設定を見るとたしかに「必須の属性」に「電話番号」があるので、改めて今度は「必須の属性」を「なし」で User Pool を作り直し、設定ファイル（aws-exports.js）を更新しましたが、依然としてフォームに電話番号の欄があり、必須のままでした。どうすれば電話番号の入力を無効にできるのでしょう？

## aws-amplify-react はそこまで賢くない

aws-amplify は awsmobile-cli が生成する aws-exports.js という設定ファイルをそのまま読むことができます:

```js
import Amplify from "aws-amplify"

// User Pool ID とかそういう情報が入っている。awsmobile-cli で自動生成される
import awsmobile from "./aws-exports"

Amplify.configure(awsmobile)
```

しかし、この aws-exports.js の中身をよくよく読んでみると、 User Pool ID などは書かれていますが、どういうものが必須かとかそういう情報は入っていません。

さらに悪いことに aws-amplify-react は awsmobile-cli でデフォルトで作られる User Pool に対応するようになっていて、登録時の項目を変更したかった場合は、自分でコードをたくさん書く必要があります。

## カスタマイズする

[ドキュメント](https://aws-amplify.github.io/amplify-js/media/authentication_guide#customize-withauthenticator)によると `withAuthenticator` は内部的に `<Autheticator>` を使っており、こいつはさらに内部的に `<SignIn>` などを使っていて、特定のビューを変更するにはそのコンポーネントを拡張したコンポーネントを渡せば良いようです。今回の場合は `<SignUp>` を変更すれば良さそうです。残念ながら細かい部分はドキュメントが無いので[ソースコード](https://github.com/aws-amplify/amplify-js/blob/e3c836514e8f55e3d5f7ed4c735dcb1dabc6c5c2/packages/aws-amplify-react/src/Auth/SignUp.jsx)を見て空気を読んでやっていきます。

<details><summary>空気を読んで書いたコード</summary><div>
もとのファイルとの差分が分かりやすいように、削除したところをコメントアウトとして表現した。

```jsx:SignUpWithoutPhoneNumber.jsx
import { Auth, I18n } from "aws-amplify"
import {
  ButtonRow,
  FormSection,
  InputRow,
  Link,
  SectionBody,
  SectionFooter,
  SectionHeader,
  SignUp,
} from "aws-amplify-react"
import * as React from "react"

export default class SignUpWithoutPhoneNumber extends SignUp {
  showComponent(theme) {
    const { hide } = this.props
    if (hide && hide.includes(SignUp)) {
      return null
    }

    return (
      <FormSection theme={theme}>
        <SectionHeader theme={theme}>{I18n.get("Sign Up Account")}</SectionHeader>
        <SectionBody theme={theme}>
          <InputRow
            autoFocus={true}
            placeholder={I18n.get("Username")}
            theme={theme}
            key="username"
            name="username"
            onChange={this.handleInputChange}
          />
          <InputRow
            placeholder={I18n.get("Password")}
            theme={theme}
            type="password"
            key="password"
            name="password"
            onChange={this.handleInputChange}
          />
          <InputRow
            placeholder={I18n.get("Email")}
            theme={theme}
            key="email"
            name="email"
            onChange={this.handleInputChange}
          />
          {/*<InputRow
            placeholder={I18n.get('Phone Number')}
            theme={theme}
            key="phone_number"
            name="phone_number"
            onChange={this.handleInputChange}
          />*/}
          <ButtonRow onClick={this.signUp} theme={theme}>
            {I18n.get("Sign Up")}
          </ButtonRow>
        </SectionBody>
        <SectionFooter theme={theme}>
          <div style={theme.col6}>
            <Link theme={theme} onClick={() => this.changeState("confirmSignUp")}>
              {I18n.get("Confirm a Code")}
            </Link>
          </div>
          <div style={Object.assign({ textAlign: "right" }, theme.col6)}>
            <Link theme={theme} onClick={() => this.changeState("signIn")}>
              {I18n.get("Sign In")}
            </Link>
          </div>
        </SectionFooter>
      </FormSection>
    )
  }

  async signUp() {
    const { username, password, email /*, phone_number */ } = this.inputs
    try {
      await Auth.signUp({
        attributes: {
          email,
          // phone_number,
        },
        password,
        username,
      })
      this.changeState("confirmSignUp", username)
    } catch (err) {
      this.error(err)
    }
  }
}
```
</div>
</details>

こうしてできた `<SignUpWithoutPhoneNumber>` を `withAuthenticator` の第三引数に渡します:

```js:App.jsx
import {
  ConfirmSignIn,
  ConfirmSignUp,
  ForgotPassword,
  SignIn,
  VerifyContact,
  withAuthenticator,
} from "aws-amplify-react"
import * as React from "react"

import SignUpWithoutPhoneNumber from "./SignUpWithoutPhoneNumber"

class App extends React.Component {
  // ...
}

export default withAuthenticator(App, false, [
  <SignIn />,
  <ConfirmSignIn />,
  <VerifyContact />,
  <SignUpWithoutPhoneNumber />, // ここ
  <ConfirmSignUp />,
  <ForgotPassword />,
])
```

これで無事に電話番号無しでアカウント作成できるようになった。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/153/9e6570c2-8b64-15e3-9cc9-d7c00592e572.png)