ExpoでカスタムURLスキーマ / iOS Universal Links / Android Deep Links を実装する

Shunsuke Sawada

WebリンクからExpoで作ったアプリを開く方法をまとめました。
Expoの文脈ですが、iOS / Android ネイティブでも基本は同じです。

カスタムURLスキーマと Universal Links (Deep Links) という2つの方法があり、最近の主流は Universal Links となっています。
カスタムURLスキーマの実装は簡単ですが、スキーマが他のアプリと重複してしまうという問題があります。

Expo 公式ドキュメント:

前提条件:

カスタムURLスキーマ

httphttps でなく、独自のURLスキーマを定義することによって、ブラウザからアプリを起動できる仕組みです。
iOS と Android で以下のような挙動になります。

iOS

Android

Expo Managed workflow を前提として解説します。
iOS は app.json の scheme を追加するだけで良いですが、Android は intentFilters という設定が必要です。

app.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

{
  "expo": {
    "scheme": "myapp",
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "data": [{
            "scheme": "myapp"
          }],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

Expo Go でテスト

exp://HOST:PORT とブラウザにタイプすれば、Expo Go アプリが起動し、さらに開発中の自分のアプリが開く。
e.g. exp://192.168.11.3:19000/--/path/into/app?hello=world

パスやクエリストリングも指定できますが、指定した場合の処理は、もちろんアプリ側に実装しておく必要があります。
シュミレータ、実機共に Expo Go を使用するならば、同じルールでテスト可能です。
シュミレータでのテストであれば、ターミナルのコマンドからも実行可能。ただし、\? として ? をエスケープする必要がありました。

bash
1
2
npx uri-scheme open exp://192.168.11.3:19000/--/path/into/app\?hello=world --ios
npx uri-scheme open exp://192.168.11.3:19000/--/path/into/app\?hello=world --android

ビルドファイルを iOS Simulator でテスト

xcrun コマンドを使えば iOS シュミレータでテストが可能。
シュミレータにアプリをインストールして、下記を実行。

bash
1
xcrun simctl openurl booted myapp://path/into/app?hello=world

複数のシュミレータが立ち上がっている場合は、$ xcrun simctl list | grep Booted を実行してUUIDを確認。
booted を UUID に置き換えて実行する。

ビルドファイルを Android Emulator でテスト

bash
1
adb shell am start -a android.intent.action.VIEW -d "myapp://path/into/app?hello=world" com.example.myapp

参考:How to test custom URL scheme in android

iOS TestFlight や Android 内部テスト

expo build:ios / expo build:android を実行して、それぞれのストアに登録。
<a href="myapp://path/into/app?hello=world">ここをタップ</a> のようなリンクをタップしてテストする。

Universal Links / Deep Links

https:// リンクでアプリを起動させる方法。
Apple は Universal Links、Google は Deep Links と呼んでいるので紛らわしいし、実装方法も異なります。

カスタムURLスキーマでは、アプリがインストールされていなければ開けませんが、https:// であれば、フォールバック用のウェブサイトを表示させることができる利点があります。
ただし、リンク先のドメインが、設置されているサイトのドメインと一致している場合は、アプリが起動しません。すでにユーザーはウェブサイトを回遊している状態であり、アプリを変更する必要がないからです。

Universal Liks (iOS)

アプリのインストール時に、予めアプリで指定したサーバーから apple-app-site-association というファイルがダウンロードされる。このファイルで指定されたパスがアプリと関連付けられ、特定のURLをブラウザからアクセスした時に、アプリが起動する仕組みとなっています。

AASA設定

パブリックにアクセスできる状態で /.well-known/apple-app-site-association にファイルを作成し、以下のような指定をする。
この設定は、略して AASA設定 と呼ばれる。

.well-known/apple-app-site-association
1
2
3
4
5
6
7
8
9
10

{
  "applinks": {
    "apps": [], // 空でOKだが必須
    "details": [{
      "appID": "AAAAAA.com.example.myapp", // Team ID + Bundle Identifire
      "paths": ["/screens/*"] // 関連付けるパスを限定する
    }]
  }
}

どんなサーバーでも良いというわけではなく、未インストール時に表示したいウェブサイトのサーバーに設置。
パスを限定しておかないと、アプリに対応していないページでも「アプリで開きますか?」というバナーが Safari の上部に表示されるので注意してください。

https://example.com/.well-known/apple-app-site-association にアクセスして、ファイルが表示されるか確認しておきましょう。

app.json

Expo の app.json を編集します。
associatedDomains を追記して、先程AASA設定をしたサーバーのドメインを指定。
https:// は含まず、スペースなしで設定します。

app.json
1
2
3
4
5
6
7
8
9
10
11
{
  "expo": {
    "ios": {
      "bundleIdentifier": "com.example.myapp",
      "associatedDomains": [
        "applinks:example.com",
        "applinks:www.example.com"
      ]
    }
  }
}

App ID の設定

Certificates, Identifiers & Profiles にアクセスして、該当のバンドルIDをクリック。
Associated Domains をチェックして、保存します。

この作業の後、プロビジョニングプロファイルを再発行する必要があるので、以下のコマンドでビルドを再実行します。
この作業を行わないと、Transporterでの転送に失敗します。

bash
1
expo build:ios --clear-provisioning-profile

確認

これで Universal Links が有効になっています。
サーバー上に以下のようなHTMLファイルを設置して、iPhoneでリンクをタップしてみてください。アプリがインストールされている場合はアプリが開き、そうでなければウェブページが開くはずです。(対応するウェブページがあれば)

index.html
1
<a href="https://example.com/screens/">リンクのテスト</a>

直接ブラウザにアドレスをタイプすると起動しません。あくまでリンクをタップするなどの、ユーザーの明示的なアクションが必要で、Javascriptのリダイレクトも無効です。(カスタムURLスキーマの場合は有効)

参考:

Deep Links (Android)

Android は二段階の設定があります。
Deep Links は http:// からアプリを起動することができますが、そのURLを処理できるアプリが複数ある場合、ユーザーに選択させるダイアログを表示します。これが通常の Deep Links であり、iOS の Universal Links のような挙動させる場合は App Links が必要です。
といっても、設定することは iOS の場合とほぼ同じで、Deep Links + App Links = Universal Links と捉えることができます。

Deep Links

まず Deep Links に対応するために app.json を編集。
intentFilters の配列に追加します。カスタムURLスキーマの設定に追加してはいけません。

参考:受け取るリンクのインテント フィルタを追加する

app.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  "expo": {
    "scheme": "myapp",
    "android": {
      "intentFilters": [
        //カスタムURLスキーマ
        {
          "action": "VIEW",
          "data": [{
            "scheme": "testapp"
          }],
          "category": ["BROWSABLE", "DEFAULT"]
        },
        //DeepLinks
        {
          "action": "VIEW",
          "data": [{
            "scheme": "https",
            "host": "*.example.com",
            "pathPrefix": "/screens"
          }],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

これだけで完了。
ビルドして https://example.com/screens/ をタップすればアプリを選択するダイアログが表示されるはず。繰り返しになりますが、ブラウザにタイプしてもウェブサイトが表示されるだけです。

App Links

指定したアプリを直接起動するには、iOS の時と同じく、サーバーにファイルを設定します。assetlinks.json というファイルです。

.well-known/assetlinks.json
1
2
3
4
5
6
7
8
9

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.myapp",
    "sha256_cert_fingerprints": ["11:22:33:44:55:66:77:88:99 ... "]
  }
}]

sha256_cert_fingerprints ですが、アプリのインストール方法によって異なるようです。
Play Storeからインストールする場合
sha256_cert_fingerprints は Google Play Console の Setup > App integrity からコピーすることができます。

apkを直接インストールする場合
sha256_cert_fingerprints を以下のようにして取得します。

bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# keystore をダウンロード
expo fetch:android:keystore

Accessing credentials for xxx in project xxx
Saving Keystore to /Users/user_name/project_name/app_name.jks
Keystore credentials
  Keystore password: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  Key alias:         xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  Key password:      xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

  Path to Keystore:  /Users/user_name/project_name/app_name.jks

# SHA256を取得
keytool -list -v -keystore /Users/user_name/project_name/app_name.jks

Certificate fingerprints:
     MD5:  xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
     SHA1: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
     SHA256: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

適切に配置できたかどうか確認します。
example.com を assetlinks.json を設置したサーバーのドメインに置き換えて、ブラウザで確認してみてください。

bash
1
https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://example.com&relation=delegate_permission/common.handle_all_urls

以下のような表示になればOK。

bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "statements": [
    {
      "source": {
        "web": {
          "site": "https://example.com."
        }
      },
      "relation": "delegate_permission/common.handle_all_urls",
      "target": {
        "androidApp": {
          "packageName": "com.example.myapp",
          "certificate": {
            "sha256Fingerprint": "211:22:33:44:55:66:77:88:99 ... "
          }
        }
      }
    }
  ],
  "maxAge": "47.227340702s",
  "debugString": "********************* ERRORS *********************\nNone!\n********************* INFO MESSAGES *********************\n* Info: The following ..."
}

そして app.json を次のように修正。"autoVerify": true を追加します。
これにより、Android が自動でサーバーを確認してくれます。

app.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
  "expo": {
    "android": {
      "intentFilters": [
        //カスタムURLスキーマ
        //...        
        //DeepLinks
        {
          "action": "VIEW",
          "autoVerify": true, //追加
          "data": [{
            "scheme": "https",
            "host": "example.com",
            "pathPrefix": "/screens"
          }],
          "category": ["BROWSABLE", "DEFAULT"]
        },
        {
          "action": "VIEW",
          "autoVerify": true, //追加
          "data": [{
            "scheme": "https",
            "host": "*.example.com",
            "pathPrefix": "/screens"
          }],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

"host": "*.example.com" のようにワイルドカードを使用する場合は、example.com/.well-known/assetlinks.json のように、ルートドメインで assetlinks.json を参照できるよう設定しておく必要があります。

また、ルートドメイン自体を App Links に対応させるためには、intentFilters を1つ追加して、ルートドメインを明示的に指定する必要がありました。

これでサーバーとアプリの両者が関連付けられました。https:// へのアクセスで直接アプリが開くようになります。

参考:

Emulator で確認

ブラウザに入力してもテストできないため、以下のコマンドを利用します。

bash
1
adb shell am start -a android.intent.action.VIEW -d "https://example.com/path/into/app?hello=world" com.example.myapp

その他の参考:

まとめ

確認するには、アプリと関係のないウェブサイトにリンクを配置して、実機でタップしてみる必要があります。

デプロイ、ビルド、テスト... と確認に手間のかかる実装なので、時間には余裕を持って進めましょう。

6
Shunsuke Sawada

おすすめの記事

Udemy講師の収益と新しいコースを公開するまでの道のり
4
grpcが原因でexpoプロジェクトのnpm installが失敗する
Lottieで使えるアニメーションを作るワークフロー
11