Firebase Dynamic Links を使って ユーザー登録における 離脱率を減らそうって話

# 目的
FirebaseDynamicLinksを使って、開発したiOSアプリのメール認証→(ユーザー)登録`において離脱率を減らすこと`

※ 私が開発している Dieter というアプリではメール認証時の離脱率が深刻な問題となっていました。数値でいうと約30%のユーザーが離脱しています。iOSアプリ上でメールアドレス/パスワードを入力したユーザーのうち約30%がメール認証を行なっていない(or 行なっても,iOSアプリに戻りログインできていない)と言う状態です。

#### ユーザー登録フロー の変更点
`ユーザー情報入力→ユーザー登録→確認画面` に Firebase Dynamic Links を導入し、メール認証+アプリへの自動遷移 によるユーザー登録を可能とします。

**変更前**
<img width = 90% src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/764557/c21c67d2-6d1d-d508-28b7-9d68623ad332.png">

**変更後**
<img width = 90% src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/764557/5f35cfea-9a70-cad8-e2d8-a317cf04dd82.png">


# Firebase Dynamic Links って何？
**アプリを自動的に開いてくれるURLです。**(別途設定すれば)**アプリが未インストールの場合はAppStoreに遷移します。また、iOS/Android等 様々なプラットフォームで動作します。**

Firebaseが提供している技術です。

<img height = 100 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/764557/6b473992-e107-1241-d09b-a723556e6650.png">

<br>

([Firebase Dynamic Links - Firebaseリファレンス](https://firebase.google.com/docs/dynamic-links?hl=ja) から詳細な説明を抜粋)

<details><summary>Firebase Dynamic Links について</summary>

> Dynamic Links により、ユーザーがリンクを開いたプラットフォームで得られる最高のエクスペリエンスを提供できるようになります。iOS または Android でダイナミックリンクを開くと、ネイティブアプリのリンク先のコンテンツに直接移動します。デスクトップ ブラウザでダイナミック リンクを開くと、ウェブサイト上の同じコンテンツに移動します。
さらに、ダイナミック リンクはアプリのインストールが必要な場面でも利用できます。たとえば、ダイナミック リンクを開いたユーザーの iOS デバイスまたは Android デバイスにアプリがインストールされていない場合にインストールを促し、インストール完了後にアプリを起動してリンクを開くといったこともできます。

</details>


:::note warn
**Firebase Dynamic Links の注意点**
- **2023年5月ごろから、Firebase Dynamic Links を用いた実装が非推奨となっている**
     - [公式によると](https://firebase.google.com/support/dynamic-links-faq?authuser=0&hl=ja)「詳細については、2023 年第 3 四半期に発表する予定」とのこと。私は、今後新しい遷移の技術が発表され、その技術に Firebase Dynamic Links が置き換わっていくと予想しています。
- **Firebase無料プラン(Spark)の場合、メール認証制限(5件/1日)がある**
    - 公式が公開している[無料枠の制限](https://firebase.google.com/docs/auth/limits?hl=ja#email_sending_limits)を見てみると 5件/1日 という制限が設けられていました。

        <img width = 80% src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/764557/e8c0cd29-93c1-cab5-3f5f-6eeb7c65e1a9.png">

    - 余談ですが、私はこれを機に 有料枠(Blaze) にアップグレードしました。制限ある中でサービスの運用するのはきついと判断したので ...
:::

# 他にURLからアプリへ遷移可能な技術はないのか？
**代表的なものが2つあります！**

### Custom URL Scheme
URLからアプリ画面へ**のみ**（つまりアプリインストールの有無に応じたWEBサイトへの遷移はできない）遷移可能な技術。遷移先アプリを未インストールの場合エラー発生。

遷移先として設定しているアプリ固有のスキームを設定しURLを設定する。
(例: `myapp://page1`, `myapp://user/123`)
※ この場合 `スキーム=myapp`

myapp:// に続く文字列(page1, user/123)に応じてアプリ側で処理を書きます。
この際、アプリ側の処理を効率的に行うため [Crossroad (CookPadのOSS)]((https://github.com/giginet/Crossroad)) が存在しています。CustomURLScheme,Crossroadについて詳細が知りたい方は以下のCookPadテックブログを参考にしてみてください。

https://techlife.cookpad.com/entry/2018/06/01/113000



### Universal Link

Safariを経由せずに(インストール済みの)アプリへ遷移可能。← Custom URL Scheme と同様
アプリ未インストール時に任意のWEBサイトへ遷移可能。
iOS端末専用の遷移方法である。

つまり、Custom URL Scheme で実現できていなかった `アプリインストールの有無に応じた遷移`が可能となったのです。
しかし、Apple専用のプロトコルであるため `Androidで共通して使えない`ことや、サーバー上に別途でファイル(※1)を作成しないといけない`=手間がかかる`、といったデメリットが存在していました。
※1 : apple-app-site-association


⭐️ これら Universal Link のデメリットを解決したものが、Firebase Dynamic Links です。


<details><summary>(再) Firebase Dynamic Links とは？</summary>

**アプリを自動的に開いてくれるURLです。**(別途設定すれば)**アプリが未インストールの場合はAppStoreに遷移します。また、iOS/Android等 様々なプラットフォームで動作します。**

</details>


# 【実装】Firebase Dynamic Links によるメール認証


## [1/5] Firebase Dynamic Links を設定

#### 1: エンゲージメント > Dynamic Links を押す

<img width = 80% src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/764557/a40e8046-f397-a356-4535-906430bcf309.png">

#### 2: `ドメインの設定`: 任意のドメインを設定 (例: example.page.link)

#### 3: 新しいダイナミックリンクを押す
  - `短縮URLの設定`: 任意の文字列を設定 (例: open)
  - `ダイナミックリンクの設定`(※2)
      - ディープリンクのURL設定: 基本は対象アプリがあるAppStoreのURL。
           - アプリ未インストール時の遷移先URL
      - ダイナミックリンク名: 任意 (例: AppStore)
  - `Apple 用のリンク動作の定義`: ディープリンクを Apple アプリで開く > 対象のFirebaseプロジェクトを選択(※3)
      - アプリがインストールされていない場合のユーザーの移動先: アプリのAppStoreページ
  - `Android 用のリンク動作の定義`: ブラウザでディープリンクの URL を開く
  - `キャンペーントラッキング、ソーシャルタグ、詳細オプション（省略可）`: 任意で設定

※2 : ブラウザを対象とした遷移先, モバイルの遷移先は次で設定可能
※3 : Firebaseプロジェクトに `Apple Store ID` と `チームID` を追加する必要あり

<details><summary>Firebaseプロジェクトの設定</summary>

<img width = 100% src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/764557/a481cf87-46bd-47a8-ff0c-791e60acaecd.png">

</details>

#### 4: 承認済みドメイン に apps.apple.com を追加

`Authentication` > `Setting` > `承認済みドメイン` に、`apps.apple.com` を追加します。
追加する理由は、([メール送信用の設定を実装](https://qiita.com/Soccerboy_Hamada/items/5cf42c2fcd9ec2477e12#1メール送信用の設定を実装)で)AppStoreのアプリを指定可能にするためです。

<img width = 90% src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/764557/b4a173d7-b855-88ae-931c-9f3405e8530f.png">


## [2/5] XCodeプロジェクトの設定

### URL Types の設定

**1: `TARGETS` > `Info` > `URL Types` > `+`**

<img width = 80% src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/764557/4769305a-b37f-a138-9bc7-ac77dd62a240.png">

※ Identifier: 任意
※ URL Schemes: アプリごと設定される **Bundle ID**。(確認 : `TARGETS` > `General` > `Identify` > `Bundle Identigy`)

<br>

### Associated Domains の設定

**1: `TARGET` > `Signing & Capabilities` > `+Capability` > `Associated Domains`**

**2: `Domains` に 「applink: `Firebase上 Dynamic Links を設定 > 2:ドメインの設定` で設定したドメイン」 を追加** (例: applink:example.page.link)

<img width = 90% src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/764557/3208cd20-bfd4-c56d-5521-7abeaf93082a.png">



## [3/5] メールを送りメール認証が可能であることを確認する

#### 1:メール送信用の設定を実装

アプリ起動時(`SceneDelegate.swift`)にメール送信のプログラムが動くようにしています。
今回はメールが届くかどうかを確認することが目的だったの...

```SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

//        〜〜〜
        
//        //MARK: practice Dynamic Links

        let actionUrl = "https://apps.apple.com/jp/app/timelab/id6446396011?l=en&platform=iphone"   // 対象アプリのURL
        let actionCodeSettings = ActionCodeSettings()
        actionCodeSettings.url = URL(string: actionUrl)
        // The sign-in operation has to always be completed in the app.
        actionCodeSettings.handleCodeInApp = true
        actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)

        let email = "hogehoge@example.com"   // テスト用のメール, @iCloud.com にメールが届かなかった... @gmail.comには正常に届く。

        // メール送信
        Auth.auth().sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings) { error in
            if let error = error {
                print("sendSignInLInk Error")
                print(error)
                return
            }
            UserDefaults.standard.set(email, forKey: "email")
            print("success sendSignInLink")
        }
        
//        〜〜〜
    }


}
```

メール受信を確認！

|  未インストール  |  インストール済み  |
| ---- | ---- |
|  <img width = 250 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/764557/c4dca421-d597-2b3c-1fb3-2b00eafa986c.gif">  |  <img width = 250 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/764557/afb11539-4bce-3450-88fb-a3095f31d417.gif">  |


## [4/5] URLから遷移したことを検知する

Firebase Dynamic Links のURLを押しiOSアプリへ遷移 → 遷移を検知 する必要があります。
遷移を検知するプログラムを SceneDelegate.swift にて実装をしていきます。

FirebaseDynamicLinksのチュートリアル や 様々な記事 で AppDelegate.swift による実装方法が挙げられています。
しかし、iOS13以降およびXcode11以降で生成されるプロジェクトにはSceneDelegate.swiftが追加され、AppDelegate.swiftではなくSceneDelegate.swiftに実装を行う必要があります。([DeNAさんテックブログ](https://engineering.dena.com/blog/2023/05/firebase_dynamic_links/#%E6%B3%A8%E6%84%8F%E7%82%B9%EF%BC%93%E3%81%A4%E7%9B%AE)より)

以下のプログラムは、`アプリのタスクが終了しているかどうか` に応じた2通りの実装を示しています。

```SeneDelegate.swift
import UIKit
import Firebase

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
//    〜

    // アプリのタスクが終了している状態から Firebase Dynamic Links を用いてアプリを起動した時
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        
        if let userActivity = connectionOptions.userActivities.first(where: { $0.webpageURL != nil }) {
            guard let url = userActivity.webpageURL else { return }
            if let user = Auth.auth().currentUser { return }   // ユーザーがログイン状態でないことを担保する
            
            DynamicLinks.dynamicLinks().handleUniversalLink(url) { [weak self] dynamicLink, err in
                if err != nil { return } else {
                    guard let url = dynamicLink?.url else { return }
                    guard (dynamicLink?.matchType == .unique || dynamicLink?.matchType == .default) else { return }
                    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) ,let queryItems = components.queryItems else { return }

                    // Firebase Dynamic Links を用いた処理を書く
                    print(url)
                }
            }
        }
    }

    // アプリのタスクが終了"していない"状態から Firebase Dynamic Links を用いてアプリを起動した時
    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
            guard let url = userActivity.webpageURL else { return }
            DynamicLinks.dynamicLinks().handleUniversalLink(url) { dynamicLink, err in
                if err != nil { return } else {
                    guard let url = dynamicLink?.url else { return }
                    guard (dynamicLink?.matchType == .unique || dynamicLink?.matchType == .default) else { return }
                    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) ,let queryItems = components.queryItems else { return }
                
                    // Firebase Dynamic Links を用いた処理を書く
                    print(url)
                }
            }
    }

//    〜
}
```

**`print(url)` の出力結果**

```
https://----------.firebaseapp.com/__/auth/action?apiKey=------------------&mode=signIn&oobCode=-------------------&continueUrl=https://〜〜〜.page.link/〜〜〜&lang=en
```
※ `continueUrl=https://〜〜〜.page.link/〜〜〜` が今後重要となります

## [5/5] 画面遷移後 ユーザー登録を行う
`Firebase Dynamic Links を押す` > `画面遷移` を行った後、(画面が開かれた瞬間(viewDidLoad))にユーザー登録を行います。

<img width = 90% src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/764557/e546f885-d0ff-a60c-f20b-16c9e8db6783.png">

ユーザー登録に使用する `メールアドレス や パスワード` は、アカウント作成画面で入力したデータを使用します。この時入力したデータは端末内に保存しておき、ユーザー登録時に端末内から呼び出す という流れでデータを使用していきます。

画面遷移後のユーザー登録に関するプログラムは、各プロダクトによって実装方針が変わるものだと思うため、今回割愛します。


#### 以上で「Firebase Dynamic Links を使って ユーザー登録における 離脱率を減らそうって話」は終わりです。より良い開発ライフをお過ごしください！


# 参考サイト

https://firebase.google.com/docs/dynamic-links/ios/create?hl=ja

https://firebase.google.com/docs/dynamic-links/ios/receive?hl=ja

https://engineering.dena.com/blog/2023/05/firebase_dynamic_links/#%E6%B3%A8%E6%84%8F%E7%82%B9%EF%BC%93%E3%81%A4%E7%9B%AE

https://hakobune.co.jp/category/engineer-blog/2584/#scene_willConnectTooptions_url