React Native + Expoで作成しているアプリで、外部URLから特定画面を直接開けるようにDeep LinkとAndroid App Linksを設定したときの作業メモ。
最終的には、開発時はカスタムURL Scheme、本番は通常のHTTPS URLによるAndroid App Linksを使い、Expo Routerの前段でapp/+native-intent.tsにURL変換を担当させる構成にした。
最終的な構成
| 用途 | 設定 |
|---|---|
| 本番Android package | com.anzaihome.mahjongapp |
| 開発Android package | com.anzaihome.mahjongapp.dev |
| 本番Scheme | mahjongapp |
| 開発Scheme | mahjongapp-dev |
| 本番App Links | https://www.anzai-home.com/mahjong/... |
| ルーティング | Expo Router |
| 外部URLの変換 | app/+native-intent.ts |
| ドメイン検証 | /.well-known/assetlinks.json |
https://www.anzai-home.com/mahjong/...
↓
Android App Links
↓
intentFilters
↓
app/+native-intent.ts
↓
Expo Router
↓
対象画面
カスタムURL Schemeを設定
まずDeep Link用のSchemeをExpo設定に追加した。本番はmahjongapp、開発版はmahjongapp-devとしている。
{
"expo": {
"scheme": "mahjongapp",
"android": {
"package": "com.anzaihome.mahjongapp"
}
}
}
これでmahjongapp://...のようなURLからアプリを起動できる。
開発版と本番版を同じ端末へ入れて確認できるように、packageとSchemeの両方を分けた。
本番
com.anzaihome.mahjongapp
mahjongapp://
開発
com.anzaihome.mahjongapp.dev
mahjongapp-dev://
SchemeやIntent Filterはネイティブ側の設定なので、変更後はDevelopment BuildやAPK/AABを作り直す。
Android App Linksを設定
共有用URLはカスタムSchemeではなく、通常のHTTPS URLをそのまま使いたかった。そのためandroid.intentFiltersを設定した。
{
"expo": {
"android": {
"package": "com.anzaihome.mahjongapp",
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{
"scheme": "https",
"host": "www.anzai-home.com",
"pathPrefix": "/mahjong"
}
],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
}
autoVerify: trueを指定して、AndroidにWebサイトとアプリの関連付けを検証させる。今回はpathPrefixを/mahjongにしている。
app/+native-intent.tsを作成
Android App Linksでアプリを起動できても、外部公開しているURLの構造とExpo Router内部のルート構造が常に同じとは限らない。そこでapp/+native-intent.tsを作成した。
app/
├── +native-intent.ts
├── _layout.tsx
├── index.tsx
└── ...
+native-intentは、ネイティブ側から入ってきたURLやpathをExpo Routerが処理する前に書き換えるための特殊ファイル。今回のプロジェクトではJSXを使わないため+native-intent.tsとしている。
基本形は次のようにした。
export function redirectSystemPath({
path,
initial,
}: {
path: string;
initial: boolean;
}) {
try {
const url = new URL(path, "mahjongapp://");
if (
url.hostname === "www.anzai-home.com" &&
url.pathname.startsWith("/mahjong/")
) {
// 必要に応じてExpo Router用のpathへ変換
return url.pathname;
}
return path;
} catch {
return "/";
}
}
redirectSystemPathの引数
pathには外部から渡されたURLまたはpathが入る。必ず完全なURLになるとは限らないので、その前提で処理する。
initialは、Deep Linkによってアプリが起動した場合はtrue、すでにアプリが起動している状態で新しいリンクを受け取った場合はfalseになる。
new URLに基準URLを指定
pathが/mahjong/...のような相対pathになる可能性もあるため、次のように基準URLを指定した。
const url = new URL(path, "mahjongapp://");
また、外部から想定外の文字列が渡される可能性があるため、redirectSystemPath全体をtry/catchで囲んでいる。ここで未処理例外を出してアプリを落とすより、安全なルートへ戻す方がよい。
+native-intent.tsで担当させる範囲
ここにはURLの正規化だけを担当させた。
- hostの確認
- pathの取り出し
- Web用URLからExpo Router用pathへの変換
- 古いDeep Linkから新しいrouteへの互換変換
- 想定外URLのフォールバック
一方で、ログイン状態、グループ登録済みかどうか、確認モーダル、戻る履歴などのアプリ状態に依存する処理は入れない。
Deep Link受信
↓
+native-intent.tsでURLを正規化
↓
Expo Routerで対象画面へ
↓
画面側で登録状態などを確認
↓
必要なら確認モーダルを表示
+native-intentはアプリのReactコンテキスト外で処理されるため、URL変換レイヤーとして割り切った方が整理しやすかった。
古いDeep Linkの互換処理にも使える
将来ルート構造を変えた場合でも、以前共有したURLをここで変換できる。
if (url.pathname.startsWith("/group/")) {
return url.pathname.replace("/group/", "/groups/");
}
一度外部へ共有したURLは後から回収できないので、変換処理を+native-intent.tsにまとめておくメリットは大きい。
Expo Router側
Expo Routerを使っているので、各画面用のDeep Link設定をReact Navigationで個別に大量設定する必要はなかった。
app/
└── groups/
└── [groupKey].tsx
このようなrouteがあれば、/groups/xxxxxxxxのようなpathをExpo Routerへ渡せる。
外部公開URLとアプリ内部のディレクトリ構造を完全に同じにせず、間に+native-intent.tsを挟んで変換できるようにした。
assetlinks.jsonを配置
intentFiltersを書くだけではAndroid App Linksとして検証されない。Web側にDigital Asset Linksファイルを配置した。
https://www.anzai-home.com/.well-known/assetlinks.json
基本形は次のとおり。
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.anzaihome.mahjongapp",
"sha256_cert_fingerprints": [
"SHA-256フィンガープリント"
]
}
}
]
package_nameはExpo側のandroid.packageと完全に一致させる。
wwwの有無に注意
今回はIntent Filterでwww.anzai-home.comを指定している。AndroidはIntent Filterに含まれる各ホストに対して/.well-known/assetlinks.jsonを確認するため、anzai-home.comとwww.anzai-home.comを同じものとして扱わないようにした。
SHA-256フィンガープリント
EAS Buildで管理している署名情報は次のコマンドから確認できる。
eas credentials -p android
Google Play App Signingを利用する場合は、Google Playから実際に配布されるアプリの署名証明書も確認する。必要ならsha256_cert_fingerprintsには複数のフィンガープリントを登録できる。
"sha256_cert_fingerprints": [
"EAS側のSHA-256",
"Google Play側のSHA-256"
]
直接インストールしたビルドではApp Linksが動くのにGoogle Play版では動かない場合、最初にここを確認する。
ADBでApp Linksを確認
App Linksの検証状態はADBで確認した。
adb shell pm verify-app-links --re-verify com.anzaihome.mahjongapp
少し待ってから状態を確認。
adb shell pm get-app-links com.anzaihome.mahjongapp
対象ドメインがverifiedになっているかを見る。
実際のURL起動もADBから確認できる。
adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://www.anzai-home.com/mahjong/"
うまく動かない場合の確認順
intentFiltersのhostが正しいかpathPrefixが正しいかautoVerifyがtrueかassetlinks.jsonをHTTPSで直接取得できるかpackage_nameが一致しているか- SHA-256が実際のアプリ署名と一致しているか
adb shell pm get-app-linksでverifiedになっているか+native-intent.tsが想定したpathを返しているか- 返したpathに対応するExpo Routerのrouteが存在するか
- ネイティブ設定変更後にアプリを再ビルドしたか
この順番で確認すると、Android App Links、+native-intent.ts、Expo Routerのどこに問題があるかを切り分けやすい。
Deep Link後の画面遷移は別に考える
今回のアプリでは、未登録グループや大会へのDeep Linkを受け取った場合に登録確認を行う。また、別のDeep Linkを続けて開いた場合や戻る操作をした場合のナビゲーション履歴も考慮する必要がある。
この部分はDeep Linkそのものの設定とは分離した。
URLを正規化する
→ +native-intent.ts
画面を開く
→ Expo Router
登録確認・モーダル・戻る履歴
→ アプリ側の画面遷移
最終的な使い方
ユーザーへ共有するリンクは通常のHTTPS URLにしている。
https://www.anzai-home.com/mahjong/...
Androidアプリがインストール済みでApp Linksの検証が成功していればアプリが直接起動し、+native-intent.tsでURLを変換した後、Expo Routerの対象画面へ遷移する。
アプリがインストールされていなければ、同じURLをWeb側で扱える。
一方、mahjongapp://とmahjongapp-dev://は主に開発・確認用途として残している。
まとめ
Deep Link対応はSchemeだけ設定すれば終わりではなく、実際には次の要素がつながっている。
Expo app設定
↓
intentFilters
↓
Android package
↓
署名証明書
↓
assetlinks.json
↓
Android App Links認証
↓
app/+native-intent.ts
↓
Expo Router
↓
アプリ独自の画面遷移
今回特に入れてよかったのがapp/+native-intent.ts。外部公開するURLとアプリ内部routeの間に変換レイヤーを置けるため、今後ルート構造を変更しても既存リンクとの互換性を保ちやすい。
