ExpoでカスタムURLスキーマ / iOS Universal Links / Android Deep Links を実装する
WebリンクからExpoで作ったアプリを開く方法をまとめました。
Expoの文脈ですが、iOS / Android ネイティブでも基本は同じです。
カスタムURLスキーマと Universal Links (Deep Links) という2つの方法があり、最近の主流は Universal Links となっています。
カスタムURLスキーマの実装は簡単ですが、スキーマが他のアプリと重複してしまうという問題があります。
Expo 公式ドキュメント:
- https://docs.expo.dev/versions/latest/sdk/linking/
- https://docs.expo.dev/versions/v41.0.0/config/app/
- https://docs.expo.dev/distribution/building-standalone-apps/
前提条件:
- スキーマ:
myapp
- バンドルID/パッケージ名:
com.example.myapp
- ウェブサーバー:
example.com
カスタムURLスキーマ
http
や https
でなく、独自のURLスキーマを定義することによって、ブラウザからアプリを起動できる仕組みです。
iOS と Android で以下のような挙動になります。
iOS
- 指定したURLスキームをブラウザで開くと「アプリを開きますか?」とダイアログが表示される。
- アプリがインストールされている場合に限り有効。
- YES → アプリを開く
- NO → その場にとどまる
- アプリがインストールされていない場合「指定されたURLは開けません」と表示される。
Android
- 指定したURLスキームをブラウザで開くと、直接アプリが起動。
- アプリがインストールされていない場合、何もしない。
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 でテスト
- Expo Go(Expoのクライアントアプリ)でテストする場合、カスタムURLスキームは
exp
となる(expo
ではない)/--/
が入るので注意
expo start
の際に出力されるホストとポートを指定する
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スキーマの場合は有効)
参考:
- apple-app-site-associationのpaths(遷移対象パス)の作り方
- Universal Linksを試してみました。関連づけファイル(apple-app-site-association)はS3に置きました。
- iOS14 におけるUniversal Links の変更点
- Support Universal Links
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://
へのアクセスで直接アプリが開くようになります。
参考:
- Android アプリリンクを検証する
- App Linksについて
- AppLinks 実装方法
- How to extract Sha256 Cert Fingerprint for Branch.io
- 真面目にDeep Link対応したい話
Emulator で確認
ブラウザに入力してもテストできないため、以下のコマンドを利用します。
bash
1
adb shell am start -a android.intent.action.VIEW -d "https://example.com/path/into/app?hello=world" com.example.myapp
その他の参考:
- Deep linking
- Native Stack Navigator
- Easy Deep Linking with React Native and Expo
- Is there a way to integrate an Expo app with firebase dynamic links without detaching?
- FlutterでContextual(Deferred) Deep Linkする
- ディープリンクをめぐる歴史とReact NativeにFirebase Dynamic Linksを導入する手順
- ダイナミック リンク URL を手動で構築する
- 「アプリで開く」を実現する、Firebase Dynamic Linksの実装と運用Tips
まとめ
確認するには、アプリと関係のないウェブサイトにリンクを配置して、実機でタップしてみる必要があります。
デプロイ、ビルド、テスト... と確認に手間のかかる実装なので、時間には余裕を持って進めましょう。