FlutterアプリをFirebase HostingでDeepLink対応する

## 概要
↓の構成で必要なファイルをFirebase Hostingにデプロイし、
Webページからアプリに飛ぶように対応したいと思います。

```
Android:  App Links
iOS: Universal Links
```

Hosting対象のURLは以下を想定して実装を行います。
(Firebase Hostingでデフォルトで設定されているドメインを使う想定です)

```
DEV環境: https://example-deeplink-dev.web.app
STG環境: https://example-deeplink-stg.web.app
```

また、DEV環境/STG環境用のFirebaseプロジェクトがある前提で
それぞれに登録したアプリに対応するようにしたいと思います。

```
DEV環境: com.example.deeplink.dev
STG環境: com.example.deeplink.stg
```
※ AndroidとiOSのAppIDは同じで登録している + Flavorの設定が終わっている前提です

### その他
実際に作業しながら時系列バラバラでメモした内容なので、どこか手順抜けてたりするかもなので
お気づきの点あればご指摘頂けると助かります :pray:

## Firebase Hosting準備 - 編
DeepLinkに対応する場合、AndroidもiOSもそれぞれ各OS用の関連付けファイルをWebページに設置する必要があります。
まずはその関連付けファイルをFirebase Hostingに設置できるように対応していきたいと思います。

### ディレクトリ構成

```
.
├── assetlinks
│   ├── assetlinks-dev.json
│   └── assetlinks-stg.json
├── association
│   ├── apple-app-site-association-dev
│   └── apple-app-site-association-stg
├── firebase.json
├── package.json
├── public
│   ├── .well-known
│   │   └──.gitkeep 
│   ├── 404.html
│   └── index.html
└── yarn.lock
```
firebase用のディレクトリを作成し、`firebase-tools` でHostingの初期化を行います。
次に Android用に **`assetlinks`** ディレクトリ、iOS用に **`association`** ディレクトリを作成しときます。
各ディレクトリとDEV/STG用の関連付けファイル一覧は以下になります。

|OS|環境|ファイルパス|
| --- | --- | --- |
| Android | DEV | assetlinks/assetlinks-dev.json |
| Android | STG | assetlinks/assetlinks-stg.json |
| iOS | DEV | association/apple-app-site-association-dev |
| iOS | STG | association/apple-app-site-association-stg |

`public` ディレクトリ配下に各関連付けファイルを配置する為の `.well-known` も作成しときます。

### デプロイ用のスクリプト
実際にFirebase Hostingにアップロードする際にはそれぞれの環境のAndorid/iOS用の関連付けファイルを
`.well-known` にリネームしてコピペし、アップロードする必要があります。
`npm run xxxx` 一発で完結出来るようにするために以下パッケージをインストールします。

```sh
yarn add -D cpx npm-run-all
```

次に `package.json` の `scripts` 箇所を以下に修正します。

```json
  "scripts": {
    "hosting:develop": "run-s hosting:develop:*",
    "hosting:develop:assetlinks": "cpx assetlinks/assetlinks-dev.json public/.well-known",
    "hosting:develop:assetlinks-rename": "mv public/.well-known/assetlinks-dev.json public/.well-known/assetlinks.json",
    "hosting:develop:association": "cpx association/apple-app-site-association-dev public/.well-known",
    "hosting:develop:association-rename": "mv public/.well-known/apple-app-site-association-dev public/.well-known/apple-app-site-association",
    "hosting:develop:deploy": "firebase deploy --only hosting --project develop",
    "hosting:staging": "run-s hosting:staging:*",
    "hosting:staging:assetlinks": "cpx assetlinks/assetlinks-stg.json public/.well-known",
    "hosting:staging:assetlinks-rename": "mv public/.well-known/assetlinks-stg.json public/.well-known/assetlinks.json",
    "hosting:staging:association": "cpx association/apple-app-site-association-stg public/.well-known",
    "hosting:staging:association-rename": "mv public/.well-known/apple-app-site-association-stg public/.well-known/apple-app-site-association",
    "hosting:staging:deploy": "firebase deploy --only hosting --project staging"
  },
```

↑の設定で `npm run hosting:develop` と実施すると `hosting:develop:xxxxx` となっている箇所が上から実行されていきます。
※ リポジトリに含ませないように以下を `.gitignore` に設定しときます。

```
public/.well-known/assetlinks.json
public/.well-known/apple-app-site-association
```

### Android側
[ウェブサイトの関連付けを宣言する](https://developer.android.com/training/app-links/verify-site-associations?hl=ja#web-assoc)

作成する関連付けファイルは以下になります。

```json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.deeplink.dev",
    "sha256_cert_fingerprints":
    ["XX:XX:XX:XX:XX:XX:..."]
  }
}]
```
DEV環境の場合は `package_name` に `com.example.deeplink.dev` を設定し `assetlinks-dev.json` として保存、
STG環境の場合は `package_name` に `com.example.deeplink.stg` を設定し `assetlinks-stg.json` として保存します。
またSHA認証も設定しておく必要があるのでDEV/STG環境それぞれ `keytool` で入手 & 設定しときます。
https://developers.google.com/android/guides/client-auth
設定は上記の関連付けファイルとFirebaseコンソールの設定も必要になります。

↓Firebaseコンソール「設定」>「全般タブ」>「マイアプリ」から設定
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/a0c7db07-5681-9b58-c89a-7e58f8ad15e7.png">


### iOS側

iOSアプリに関連づけを行うファイル(apple-app-site-association）をWeb上に配置する必要があります
[Supporting Associated Domains | Apple Developer Documentation](https://developer.apple.com/documentation/safariservices/supporting_associated_domains)

まずは以下の関連づけファイルをDEV/STG環境毎に作成し、`apple-app-site-association-dev`, `apple-app-site-association-stg` として保存しときます。
`XXXXXX` にはApple Developer PortalでのTeamIDが入ります。

```
{
  "applinks": {
      "details": [
           {
             "appIDs": [ "XXXXXX.com.example.deeplink.dev" ],
             "components": [
               {
                  "/": "*",
                  "comment": "Matches any URL"
               }
             ]
           }
       ]
   }
}
```
上記のサンプルは全てのパスで反応する用な設定になっています。
設置するパスはルート `/` でも `.well-known/` でもOKっぽいですが、今回は `.well-known/` 配下に設置しようかと思います。

##### Firebase Hostingを使用する場合の `content-type` 対応
iOSの関連づけファイルを設置する際 `content-type` は `application/json` で設置する必要があります。
`firebase.json` で `apple-app-site-association` のヘッダーを修正します。

```json
{
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "/",
        "destination": "/index.html"
      }
    ],
    "headers": [
      {
        "source": "/.well-known/apple-app-site-association",
        "headers": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ]
      }
    ]
  }
}
```

`headers` の部分を追加し、デプロイすると`content-type` が `application/json` になっています。
確認方法は `curl -v https://xxxxxx/.well-known/apple-app-site-association` で確認可能です。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/f9427c91-a754-df45-fd93-e31e8d2bb083.png">

## Android準備 - 編

[App Links](https://developer.android.com/training/app-links?hl=ja)

###  :computer:環境構築
****
AndroidStudioの「App Links Assistant」で作成も可能ですが、DEV環境/STG環境で設定を変える必要があるので
今回は直接設定していこうかと思います。

`buildTypes` と `productFlavors` の構成は以下のう想定で設定していきます。

```gradle:build.gradle
android {
    ...
    buildTypes {
        debug {
            ...
        }
        release {
            ...
        }
    }

    flavorDimensions "environment"
    productFlavors {
        development {
            dimension "environment"
            applicationIdSuffix ".dev"
            versionNameSuffix ".dev"
        }
        staging {
            dimension "environment"
            applicationIdSuffix ".stg"
            versionNameSuffix ".stg"
        }
    }
}
```

#### AndroidManifestの作成

DEV環境: `android/app/src/development/AndroidManifest.xml`
STG環境: `android/app/src/staging/AndroidManifest.xml`

↓ `android:host` はDEV/STG環境に応じてURLを設定する必要があります。

```
DEV環境: example-deeplink-dev.web.app
STG環境: example-deeplink-stg.web.app
```

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.deeplink">
    <uses-permission android:name="android.permission.INTERNET"/>
    <application>
        <activity android:name=".MainActivity">
            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="https" android:host="example-deeplink-dev.web.app" />
            </intent-filter>
        </activity>
    </application>
</manifest>

```
### :pencil: テスト
****

#### Digital Asset Links ファイルを確認

```
https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=[https://domain.name:optional_port]&relation=delegate_permission/common.handle_all_urls
```
`[https://domain.name:optional_port]` の部分に用意したサイトのURLを設定

正常だと以下の用なレスポンスが返ってきます :sparkles: 

```json
{
  "statements": [
    {
      "source": {
        "web": {
          "site": "https://xxxxxxxx"
        }
      },
      "relation": "delegate_permission/common.handle_all_urls",
      "target": {
        "androidApp": {
          "packageName": "xxxxxxxxxxxxxx",
          "certificate": {
            "sha256Fingerprint": "xxxxxxxxxxxxxxxxxxxxxxxx"
          }
        }
      }
    }
  ],
  "maxAge": "3465.849881714s",
  "debugString": "********************* ERRORS *********************\nNone!\n********************* INFO MESSAGES *********************\n* Info: The following statements were considered when processing the request:\n\n---\n...
}
```
### adb shellから起動させる

```
adb shell am start -W -a android.intent.action.VIEW -d "https://example-deeplink-dev.web.app"
```


###  :bomb: バッドノウハウ
****
- Android側とFirebase Hosting側の準備は終わったが、AndroidStudioのApp Links Assistantで 
    `we could not associate your app with the selected domain` が発生する!
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/585ae854-bd5f-302d-d9a3-873dc1eba4e8.png">

Associate Website でassetlinks.jsonを再度生成してアップして見たらうまくいきました
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/65d1c3f6-7d03-f382-51a2-ee6ce6fa0c85.png">

### :link: 参考になったURL
****
- [Android アプリリンクを検証する  |  Android デベロッパー  |  Android Developers](https://developer.android.com/training/app-links/verify-site-associations?hl=ja)
- [App Links実装手順 - Qiita](https://qiita.com/devWill/items/0fe5969ee841bb9f6aab)

## iOS準備 - 編

[Universal Links - Apple Developer](https://developer.apple.com/ios/universal-links/)

###  :computer:環境構築
****
- Apple Developer Portalでの設定
    - 「Identifiers」>「対象のAppID」>「Capabilities」の「Associated Domains」にチェックを入れる

    <img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/a2dba8f6-c044-6961-28c9-12453207f208.png">


- XcodeでAssociated Domainsの追加
    - Xcodeを開いて、「Signin & Capabilities」で左上の「+ Capability」から「Associated Domains」を追加し、Domainsを設定します

    <img width="554" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/2017f7e1-fde9-5d71-5044-15b652fa9ca0.png">


    追加する値は `applinks:[ドメイン]` になります。Firebase Hostingを使用していてドメインが `test-dev.web.app` であれば
    `applinks:test-dev.web.app` として設定します。
    今回の場合だと `applinks:example-deeplink-dev`になります。

### :pencil: テスト
****
- [App Search API Validation Tool](https://search.developer.apple.com/appsearch-validation-tool/)
    - Apple公式の関連付けファイルチェッカー

公式であるのはあるんですが、、
使い方は簡単で URL入力箇所に関連付けファイルを設置したURLを入力し、「Test URL」をクリックすれば
暫く経つと結果が表示されます。。。が
**`.well-known` 配下に設置した場合は `Error no apps associated with url` が発生します**
関連付けできてない事になってしまうので、今回の場合だとテストになりませんでした ><。
次回は素直に `.well-known`使わずにやってみたいと思います。。

## バッドノウハウ :bomb: 

- UniversalLinksが動作しない
  - [Objective-C - Universal Linkが実現できず困っています。｜teratail](https://teratail.com/questions/113521)
    - ↑の `「Target Membership」にチェックを入れる` を試してみる。
    - 「Build Phases」>「Copy Bundle Resources」に該当のentitlementを追加してみる

## 参考URL
- [URLスキーム・独自ディープリンク実装に代わる、Universal Links(iOS 9で導入)でより良いUXを実現 - Qiita](https://qiita.com/mono0926/items/2bf651246714f20df626)
- [Universal Linksを試してみました。関連づけファイル(apple-app-site-association）はS3に置きました。](https://dev.classmethod.jp/smartphone/universal-links/)
- [【iOS】Universal Links と Custom URL Scheme についてのメモ](http://snoozelag.hatenablog.com/entry/2017/07/07/095205)
- [HostingでUniversal Links対応する | 杏ｚ学習帳](https://blog.anzfactory.xyz/articles/20190207/firebase-hosting-unversal-links/)
- [iOSアプリのユニバーサルリンクの仕組み - lasciva blog](https://15dog.hatenablog.com/entry/2019/03/06/iOS%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E3%83%A6%E3%83%8B%E3%83%90%E3%83%BC%E3%82%B5%E3%83%AB%E3%83%AA%E3%83%B3%E3%82%AF%E3%81%AE%E4%BB%95%E7%B5%84%E3%81%BF)
- [UniversalLinks（ユニバーサルリンク）「apple-app-site-association」の書き方　パラメーター対応（iOS13以降） - Qiita](https://qiita.com/gurensouen/items/4fc282b91dff468cf9ed)

## Flutter準備 - 編

### Flutterの場合
今回は [uni_links](https://pub.dev/packages/uni_links) というパッケージを使います。
[Platform Channels](https://flutter.dev/docs/development/platform-integration/platform-channels)を使って自前で実装しても良さそうかと思います :sparkles: 

パッケージ導入し、↓Streamをlistenしときます

```dart
    // Webページからアプリへ戻ってきた際に呼ばれる
    getLinksStream().listen((String uri) async {
      if (uri.startsWith('https://example-deeplink') {
           // 何らかのハンドリング処理
      }
    });
```

#### 参考URL
- [Deep Links and Flutter applications. How to handle them properly. | by Aleksandr Denisov | Flutter Community | Medium](https://medium.com/flutter-community/deep-links-and-flutter-applications-how-to-handle-them-properly-8c9865af9283)