- (Deep|Web) Linkがどういうものかわかる
- expo-routerでの(Deep|Web) Linkの対応方法がわかる
Deep Link
- アプリごとにURL Schemeを定め、そのSchemeの指定することでアプリを起動できるようにする仕組み
Web Link
- アプリをインストールした状態でWebサイトを開くと、アプリが開かれる仕組み
名称 | URL Scheme | 例 | 補足 |
---|---|---|---|
Deep Link | [任意文字列]:// |
examble-app:// |
Android/iOSどちらも共通名称 |
native deep-link | [任意文字列]:// |
examble-app:// |
ExpoのcontextでのDeep Link |
Web Link | https://... |
https://example-site.com |
App LinkとUniversal Linkをまとめた表現 Androidのドキュメントでちょっと出てくる |
Universal Link | https://... |
https://example-site.com |
iOSのcontextでのWeb Link |
App Link | https://... |
https://example-site.com |
AndroidのcontextでのWeb Link |
web deep-links | https://... |
https://example-site.com |
ExpoのcontextでのWeb Link |
- expo-routerを利用している場合は、app.config.tsでschemeを定めるだけ
- <scheme>://<dir-path>の指定でアプリが起動する
- 例
- scheme: my-app
- app/home/index.ts がある
- Deep Linkは my-app://home になる
React Navigationのドキュメントに書いてある
# Simulatorやら実機でアプリを立ち上げた状態で以下を実行する
$ npx uri-scheme open <scheme>://<expo-routerで設定しているpath> --<ios|android>
- Webサイトに設定ファイルをアップロードしておく
- app.config.tsに設定を追加する
- 「WebサイトのURLに対応するアプリの画面を開く」処理を実装する
それぞれのformatに従ったJSONを、Webサービスのドメインからアクセスできるようにする
- <team-id>.<bundle-id> の形式でアプリを指定する
- components で、「どのpathをアプリで起動する/除外する」を定義できる
- 制御はここでやらないほうがいい (最後にTipsで説明する)
{
"applinks": {
"details": [
{
"app-ids": ["<team-id>.<bundle-id>"],
"components": [
{
"/": "*"
},
{
"/": "/sign_up",
"exclude": true,
},
]
}
]
},
"webcredentials": {
"apps": ["<team-id>.<bundle-id>"]
}
}
iOSは設定確認が面倒なので注意
- 端末でDev modeを有効にする必要がある
- 設定ファイルがCDNにcacheされる必要があるし、24hかかる
- iOS14でUniversal Links対応することになった人が見る記事 #Swift - Qiita
- 待ちたくない場合は、app.config.tsでbypassする設定が必要
- iOSの設定ファイルと違い、「どのpathをアプリで起動する/除外する」を定義できないので注意
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "<package-name>",
"sha256_cert_fingerprints":
["14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"]
}
}]
- Play Storeに設定例が出ている
- コピペして、Develop/Staging環境などはpackage nameを変更すればいい
- app.config.tsを書き換える
- Appleのサイトで、Associate Domainを有効にする
- rebuildする
ios / android の項目それぞれに設定を追記する
{
"ios": {
// 開発中はQueryParameterを追加して "applinks:<bundle-id>?mode=developer" にしたほうがいい
// apple-app-site-associationをApple CDNではなく、Webサイトから直接参照してくれるので24hまたなくて良くなる
"associatedDomains": ["applinks:<bundle-id>"]
}
"android": {
// codelabで説明されている内容と同じ
// see: https://developer.android.com/codelabs/android-app-links-introduction?hl=ja#5
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{
"scheme": "https",
"host": "<web-domain>",
pathPattern: '.*', // どのURLをアプリで起動するかを指定する。NOTなpath指定ができないので注意
}
],
"category": ["BROWSABLE", "DEFAULT"]
}
]
}
}
以下の記事が丁寧なので読むといい
- app.config.tsの変更内容は、native layerの変更なので、rebuildする
- iOSは、Provisioning Profileの再生成も必要なため、 --non-intaractive は外して実行する
- Provisioning Profileがよくわからなかったら、 以前の登壇スライドを見てください
- 2.までやると、サイト開こうとするとアプリは起動する
- expo-routerのdefault: Webサイトのpathに合わせて画面遷移する
- 例: https://example.com/notification/12345 を開こうとすると example://notification/12345 が開かれる
- defaultのnavigationだと、アプリにとって不都合なことが多い
- webとモバイルアプリのURL設計がズレていることは多分にあるため
- defaultのnavigationをoverrideすることが必要になる
Customizing links - Expo Documentation
- redirectSystemPath():
- URLでアプリを起動したときに呼ばれる
- この関数から返されたpathを元にexpo-routerが画面遷移してくれる
import { URL } from 'react-native-url-polyfill';
export function redirectSystemPath({ path, initial }) {
// initial時に特定画面に遷移させると画面が表示されないことがあった
// app/index.tsに<Redirect />だけ書いて、とりあえずそこに飛ばすと動いたのでworkaroundとしてやってる
if (initial) return '/'
const url = new URL(path, 'myapp://app.home');
switch (url.pathname) {
// pathnameをアプリに合わせて返す
case '/notification':
return '/home/notification';
...
}
return path;
}
Flutterのドキュメントが丁寧
$ xcrun simctl openurl booted https://<web domain>/details
$ adb shell 'am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "http://<web-domain>/details"' \
<package name>
- initial時にpathを書き換えると、画面が描画されないという問題に遭遇した
- 症状として似たIssueがv2だがあった
+native-intent.tsx
import { URL } from 'react-native-url-polyfill';
export function redirectSystemPath({ path, initial }) {
// app/index.tsに<Redirect />だけ書いて、とりあえずそこに飛ばすと動いたのでworkaroundとしてやってる
if (initial) return '/'
...
}
app/index.tsx
// 初期ルートにredirectするだけの '/' を作ったら動いた
export default function Index() {
return <Redirect href="/home" />;
}
- 一般的にQuery Parameterはdecodeして渡すもの
- Query Parameterをdecodeしてから使うようにしたほうがいい
- WebView Screenに、外部からURLをQueryParameterで渡したい時などにやらかしがち
const params = useLocalSearchParams() as EncodedScreenParams;
const value = decodeURIComponent(params['value'])
「全部のURLを起動し、アプリに存在しない画面はWebViewで開く」のが現実的
- Android ビルド設定変更 | React Native(Expo)にUniversal Linksを実装して、Webからアプリに自動遷移させる!【前編】 Zenn
- iOSは、特定pathを除外する設定がapple-app-site-associationでできる
- Androidは、特定pathを除外する設定が面倒
- 「全部のURLを起動し、アプリに存在しない画面はWebViewで開く」ほうが実装しやすい
export const redirectSystemPath = ({ path, initial }: { path: string; initial: boolean }) => {
return isExpectedPath(path) ? path : `/webview?url=${encodeURIComponent(path)}`;
})
Routingの設計どうしたらいいか
- 認証フローがExpoのドキュメント通りに組んであるとラク
- 初手は認証済みのトップページに飛ばして、_layoutで(Web|Deep) Linkのhandlingをすると良さそう
- ログインしてなかったユーザーが、ログインできたときに、(Web|Deep) Linkで開こうとしてたページを開いてくれて親切
意図しないApp Linkを雑に /webview で開くと困ることがある
- 認証済みのRoutingに /webview を設置してあるという前提
- 意図しないWeb Linkを雑に /webview に投げつけると、未認証時にURLをアプリで開けないということが起こる
- 「新規会員登録」「問い合わせ」「パスワードリセット」などのURLを開こうとすると、未認証トップに飛んでしまう
- 一応、URLをブラウザにURLを直貼りするとブラウザで回遊できるが、リテラシーが試される
- Routing処理と別で「特定のpathが来たら、 WebBrowser で起動する」という処理を入れたほうが良さそう
- ただし、雑にWebBrowserを使うと、Androidでバグるので次のTipsを参照
export const redirectSystemPath = ({ path, initial }: { path: string; initial: boolean }) => {
const isAppUniversalLink = !isDeepLink(path);
if (isAppUniversalLink) {
const shouldOpenWebBrowser = await shouldOpenWebBrowserAsync(path);
if (shouldOpenWebBrowser) {
await openBrowserAsync(path); // WebBrowserのWraper
}
}
...
})
- AndroidのWeb Linkを設定し、WebBrowserで開こうとすると、無限ループする
- Web → +native-intent.tsx → WebBrowserがWeb開く → Webからアプリに飛ばされる → ...
- そのため「このURLはWebBrowserで開け」と明示的に指定してやる必要がある
export const openBrowserAsync = async (url) => {
// App Linkを設定していると、自分のアプリでURLを開こうとするため、明示的にブラウザのpackage nameを指定している
const browserPackageName =
Platform.OS === 'android'
? (await WebBrowser.getCustomTabsSupportingBrowsersAsync()).preferredBrowserPackage
: undefined;
await WebBrowser.openBrowserAsync(url.toString(), {
dismissButtonStyle: 'close',
showTitle: true,
toolbarColor: ColorPallets.white,
browserPackage: browserPackageName,
});
};