【Android】パスワードのリセットメールを送信して、ディープリンクを使ってアプリに戻る方法のまとめ

# はじめに

現在開発中のアプリでパスワードのリセット機能を作成する機会がありました。アプリ内で再認証を行いパスワードのリセットを行うこともできるのですが、今回はパスワードリセットメールの送信を行いパスワードのリセットを行うことに決めました。

その際にディープリンクの設定など初見では複雑にみえる処理などがあり、躓くポイントが何点かありました

今回は、NavigationコンポーネントとFirebaseを使用して、パスワードのリセットが完了した後にアプリの特定の画面を開く方法をわかりやすくまとめたいと思います。

# パスワードリセットメールを送信する

パスワードリセットメールを送信するには、`sendPasswordResetEmail()`を呼び出します。

公式ドキュメントでは、`addOnCompleteListener()`が使われていますが、`await()`を使うことでコールバック地獄を回避することができます。

※必ず`suspend`関数内で呼び出してください。

```kotlin
val auth = Firebase.auth
...

suspend fun sendPasswordResetEmail(email: String): Unit {
    try {
        auth.sendPasswordResetEmail(email).await()
    } catch (e: Throwable) {
        Log.d("sendPasswordResetEmail", "${e.message}")
    }
}
```

# ActionCodeSettingsを使ってパスワードリセットメールを送信する

`ActionCodeSettings`を使うことで、ディープリンクを設定することができるようになります。

これにより、アプリ内のブラウザでパスワードリセットのリンクを開き、パスワードをリセットした後に**Continue**ボタンをクリックすると、アプリがインストールされている場合はアプリにリダイレクトさせることができるようになります。

```kotlin
val actionCodeSettings = ActionCodeSettings.newBuilder()
    .setUrl(url)
    .setAndroidPackageName(
        packageName,
        true,
        "1"
    )
    .setHandleCodeInApp(false)
    .setDynamicLinkDomain("ダイナミックリンクのドメイン")
    .build()
```

https://firebase.google.com/docs/auth/android/passing-state-in-email-actions

`setUrl()`に指定するのは、アプリへのリダイレクトに使用するディープリンクです。ここで指定する値は、`continueUrl`に指定されるURLの`link`クエリパラーメーターの値に適用されます。

`setAndroidPackageName()`の第一引数には、アプリのパッケージ名を指定します。この値は、`continueUrl`に指定されるURLの`apn`クエリパラーメーターの値に適用されます。

`setAndroidPackageName()`の第二引数は`installIfNotAvailable`です。`true`を設定した場合はアプリがデバイスにインストールされていない場合はGoogle Play Storeに遷移させてアプリをインストールするようにします。

`setAndroidPackageName()`の第三引数にはアプリの最小バージョンを指定します。この値は`continueUrl`に指定されるURLの`amv`クエリパラーメーターの値に適用されます。この値も同じようにこの値に満たないバージョンのアプリがインストールされている場合は最新バージョンをインストールするようにストアに遷移させます。

`setDynamicLinkDomain()`には、使用したいダイナミックリンクのドメインを明示的に指定します。Firebaseプロジェクトには複数のダイナミックリンクドメインを設定することが可能なため、明示的に指定しない場合は一番最初に設定したダイナミックリンクドメインが使われてしまいます。それが嫌な場合にこのメソッドを使用します。

準備ができたら、作成したActionCodeSettingsを`sendPasswordResetEmail()`の第二引数に指定して呼び出します。

```kotlin
this.auth.sendPasswordResetEmail(email, actionCodeSettings).await()
```

# 送信されたリンクを開いてパスワードを変更する

受信したメールを開くとリンクが貼り付けられているので、端末で開きます。

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577119/14be09c1-da7a-bffe-2293-2d0f5901629d.png)

新しいパスワードを入力して**SAVE**をクリックするとパスワードの変更が完了します。

ActionCodeSettingsを使用した場合は、**CONTINUE**をクリックするとアプリにリダイレクトさせることができます。

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577119/9a7f8ffe-f690-1288-a438-7b85190e626d.png)

このとき、`setUrul()`で指定したURLとマッチする`<intent-filter />`と`Composable()`が存在する場合は、その画面をディープリンク経由で開くことができます。

# intent-filterを設定する

`setUrl()`に指定したURLを通して特定の画面を開けるようにするため、AndroidManifest.xmlに`<intent-filter />`を設定します。

```xml
<activity
    ...
>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
    
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
    
        <data
            android:host="example.com"
            android:pathPrefix="/special_destination"
            android:scheme="https" />
    </intent-filter>
</activity>
```

例えば、`setUrl()`に指定したURLが次の場合

```md
https://example.com/special_destination
```

`<data />`の`android:scheme`には`https`を指定します。`android:host`には`example.com`を指定します。`android:pathPrefix`には`/special_destination`を指定します。

これにより、アプリで上記のリンクを開いた場合はこのアプリで`special_route`に一致する`composable()`を開くことができるようになります。

# URLにマッチするcomposable()を作成する

AndoridManifest.xmlで指定した`<intent-filter />`の内容とマッチする`composable()`は次のように作成します。

```kotlin

const val HTTP_SCHEME = "https"
const val DOMAIN = "example.com"

object SpecialDestination {
    override val route = "special_route"
    override val destination = "special_destination"

    val arguments = listOf<NamedNavArgument>()
    val deepLinks = listOf(
        navDeepLink {
            uriPattern =
                "$HTTP_SCHEME://$DOMAIN/$destination"
        }
    )
}

fun NavGraphBuilder.specialGraph(
    nestedGraphs: NavGraphBuilder.() -> Unit = {},
) {
    navigation(
        route = SpecialDestination.route,
        startDestination = SpecialDestination.destination
    ) {
        composable(
            route = SpecialDestination.destination,
            arguments = SpecialDestination.arguments,
            deepLinks = SpecialDestination.deepLinks,
        ) { navBackStackEntry ->
            SpecialRoute()
        }
        nestedGraphs()
    }
}
```

`navDeepLink()`の`uriPattern`には、`<intent-filter />`で設定した内容と同じ値を指定しています。ここでの`"$HTTP_SCHEME://$DOMAIN/$destination"`は`https://example.com/special_destination`と等価です。それをリストにまとめて、`composable`の`deepLinks`引数に指定しています。

# 2つのドメインを承認済みドメインに登録する

送信されたパスワードリセットメールの本文には、パスワードをリセットするためのリンクが貼り付けられています。

※実際に送信されるリンクには%26などのUTF-8でエンコードされた文字列がが含まれていますが、わかりやすいようにデコード処理を行なった上で表示していますのでご注意ください。

```
https://プロジェクト名.firebaseapp.com/__/auth/action?mode=resetPassword&oobCode=oobCodeの内容&apiKey=apiKeyの内容&continueUrl=https://ダイナミックリンクのドメイン?link=ディープリンクのスキーマ（http or https）://ディープリンクのドメイン/ディープリンクのパス&apn=アプリのパッケージ名&amv=1&lang=en
```

このリンクでは、`プロジェクト名.firebaseapp.com`以外に、**ダイナミックリンクのドメイン**と**ディープリンクのドメイン**の２つのドメインが登場します。

この２つのドメインはFirebaseコンソールにて、承認済みドメインのリストに登録する必要があります。

Firebaseコンソールで、承認済みドメインの設定を行わない状態でパスワードリセットメールの送信を試みると、エラーが発生することがあります。

```shell
An internal error has occurred. [ UNAUTHORIZED_DOMAIN:Domain not whitelisted by project ]
```

https://stackoverflow.com/questions/51020036/authenticate-with-firebase-using-email-link-in-android-unauthorized-domain-erro

これは、ダイナミックリンクのドメインを承認済みドメインに登録しないでリセットメールの送信を行おうとした場合のエラーです。

ディープリンクで使用するドメインを承認済みドメインに登録していない場合は、リンクを開いた際に次のようなページが開かれてパスワードのリセットを行うことができない状態になります。

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577119/70d4d568-c71b-6236-c873-ed81b8ad6df8.png)

## ダイナミックリンクを設定する

ダイナミックリンクを承認済みドメインに登録するために、まずはダイナミックリンクドメインの作成を行います。

FirebaseコンソールでDyanmic Linksを開いて、**始める**をクリックします。

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577119/28d5444d-5f22-ef62-b274-0e97614d0180.png)

セレクトボックスから任意のドメインを選択して**続行**をクリックします。

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577119/3104a790-3e9f-7abc-3764-1cc469f27f04.png)

追加が完了したら、**完了**をクリックします。

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577119/36904c05-2a38-9466-3f60-78c42494a353.png)

この画面で表示されているのが、ダイナミックリンクのドメインです。この値をコピーしておきます。

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577119/8dc13651-9026-be25-f6ca-19d29939a9a2.png)


## ダイナミックリンクのドメインとディープリンクで使用するドメインを承認済みドメインに登録する

承認済みドメインに任意のドメインを追加したい場合は、FirebaseコンソールでAuthenticationを開いて**Settings**タブをクリックします。

サイドナビゲーションから、**承認済みドメイン**をクリックします。

![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/577119/287fed3f-ee59-3bc5-956e-073f2ade8024.png)

**ドメインの追加**をクリックして、２つのドメインをそれぞれ分けて登録します。

# packageNameを取得する方法は？

`packageName`は`Context`を通して取得します。

JetpackComposeを使用している場合は、`LocalContext.current`を通して`Context`を取得し、`packageName`プロパティを使って`packageName`を取得できます。

```kotlin
val context = LocalContext.current
val packageName = context.packageName
```

# minimumVersionには何を指定すれば良いの？

`minimumVersion`に指定する値は、build.gradleで設定した`versionCode`の値です。

https://stackoverflow.com/questions/53402381/what-version-number-should-i-use-for-minimum-in-actioncodesettings-builder-setan

# アプリにリダイレクトはされるけど、指定した画面を開くことができない

アプリが終了した状態でリンクを開いて、期待した画面が開く場合には、AndriodManifest.xmlの`<Activity />`の`launchMode`が`singleTask`になっている場合があります。

```xml
<activity
    android:launchMode="singleTask"
>
</activity>
```

この場合、何も指定しないか、`standard`もしくは`singleTop`を指定すると改善する可能性がありますのでお試しください。

それ以外の場合には、設定したURLと`<intent-filter />`や`composable()`に設定した`deepLinks`の内容に相違がある場合がありますのでもう一度ご確認ください。

# まとめ

パスワードのリセットのためのメールの送信や、パスワードを更新する際のWeb上のページなど、Firebase側があらかじめ提供しれくれている機能を使用することで開発しないといけないアプリ上の機能などに時間を費やすことができるようになります。

みなさんも積極的にFirebaseを活用し、素敵なアプリの開発を行なってみてください。

# 参考にした記事

https://developer.android.com/guide/components/intents-filters?hl=ja

https://developer.android.com/guide/topics/manifest/activity-element?hl=ja

https://firebase.google.com/docs/auth/android/manage-users#send_a_password_reset_email

https://qiita.com/s_emoto/items/1eeac92342f224bdd372

https://qiita.com/ryuji_sato/items/a54a0530065637be1fa1

https://qiita.com/ryuji_sato/items/7e1a4289180922663782

https://qiita.com/dtakkiy/items/572d12d9d1e037c2272d

https://www.hulft.com/help/ja-jp/HULFT-V8/COM-CNV/Content/HULFT_CNV/code_table/From_UTF8.htm