React Native(Expo)にUniversal Linksを実装して、Webからアプリに自動遷移させる!【前編】

2022/01/28に公開

CTOの名人です。

Webサイトにアクセスすると、勝手に対応するアプリに切り替わることってありますよね(Twitterとか、食べログとか)。あの技術は(OSによって微妙に呼び名は違うのですが)Universal Linksと呼ばれています。

本記事では、Universal Linksの設定を、React NativeかつExpo Managed Workflowなアプリに実装するまでの過程を示します。

Universal Links(Deep Links)実装までの道のり

おおむね、以下のような手順をたどります。単にアプリに飛ばしたいだけなら簡単なのですが、URL構造の要件や、ハンドリングの厳密さを要求するのであればそれなりに複雑な手順をたどる必要がありました。

簡単にいうと、そもそもWebサイトから紐付いたアプリに飛ばすための設定と、飛ばしたあとにアプリ内の適切な画面まで自動で移動する実装の2つが必要でした。

  • iOS AASAファイル設定
    • apple-app-site-associationファイルを作成
    • ルートドメインでアクセスできるように、S3およびCloudFrontを設定
    • appleのCDNにアクセスし、キャッシュされていることを確認
  • iOS ビルド設定変更
    • Associate Domainを有効化
    • app.jsonにassociatedDomainsの設定を書き足す
    • 新しいProvisioning Profileを使ってexpo buildする
  • Android ビルド設定変更
    • app.jsonにintentFiltersを書き足す
    • expo buildする
    • 一度ブラウザを介する方法か、直接アプリに行く方法か選択する
  • アプリ側のUniversal Links(Deep Links)ハンドリング
    • React Navigationのlinkingを実装
    • ログアウト時、ログイン時の考慮
    • キル状態から起動されたときの考慮
    • 遷移した画面から戻ろうとしたときにどこに戻るかの考慮
    • 遷移しようとした画面が見つからなかったときの考慮

iOS AASAファイル設定

まずはiOS側から解説します。iOSとAndroidで設定方法や挙動は異なるため順に説明します。

iOSでUniversal Linksが動作する原理

iOSでUniversal Linksが動作する原理ですが、ざっくり説明しますと

  • アプリビルド時に、アプリとどのWebサイトのドメインが紐づくかを指定する
  • 各スマートフォン端末が、ビルドされたアプリのインストール後に、指定されたドメインの規定のパスにアクセスしapple-app-site-associationファイルをフェッチする
  • apple-app-site-associationファイルに記述されたハンドリングルールを、端末内の任意のタイミングでURLを開こうとしたときにチェックすることで、アプリを起動したりしなかったりする

という感じです。なので、apple-app-site-associationファイルの作成および設置と、アプリのビルド手順の変更の2つが必要になります。

apple-app-site-association(AASA)ファイル

https://developer.apple.com/documentation/xcode/supporting-associated-domains

こちらの公式ドキュメントを読むと、以下のようなJSONを指定することでUniversal Linksによるハンドリングルールを記述できるとあります。

  "applinks": {
      "details": [
           {
             "appIDs": [ "ABCDE12345.com.example.app", "ABCDE12345.com.example.app2" ],
             "components": [
               {
                  "#": "no_universal_links",
                  "exclude": true,
                  "comment": "Matches any URL whose fragment equals no_universal_links and instructs the system not to open it as a universal link"
               },
               {
                  "/": "/buy/*",
                  "comment": "Matches any URL whose path starts with /buy/"
               },
               {
                  "/": "/help/website/*",
                  "exclude": true,
                  "comment": "Matches any URL whose path starts with /help/website/ and instructs the system not to open it as a universal link"
               },
               {
                  "/": "/help/*",
                  "?": { "articleNumber": "????" },
                  "comment": "Matches any URL whose path starts with /help/ and which has a query item with name 'articleNumber' and a value of exactly 4 characters"
               }
             ]
           }
       ]
   },

概ね、これを読むとどういったルールで記述するのがいいかわかると思います。

重要な点として、ASAAファイルの形式は以前リニューアルされたとのことで、iOS12以前もサポートする場合は古い書きかたで対応する必要があります。

https://qiita.com/satoru_pripara/items/cbc541eed3c85cf96b14

しかし、誰でも知っているようなアプリをリリースしている企業のapple-app-site-associationファイルをいくつか調べてみたのですが、だいたいiOS12以前の書きかたでした。おそらくはUniversal Linksのテストは自動化が難しいので、新形式で書き換えるのがなかなか難儀なのだろうと思います。

ルートドメインでアクセスできるように、S3およびCloudFrontを設定

ファイルを作成したら、紐付けたいドメインの/.well-known/apple-app-site-associationパスでアクセス可能にする必要があります。

マナリンクはCloudFront + Fargateで運用しているので、同ファイルをS3にアップロードしてオリジンとビヘイビアを設定することで対応しました。

ちなみにCDKで実装しているので、ざっくり以下のような実装にしました。originAccessIdentity周りを思い出すのに時間がかかりました。

        {
          s3OriginSource: {
            s3BucketSource: Bucket.fromBucketAttributes(this, 'apple-app-site-association', {
              bucketName: config.appleAppSiteAssociationBucketName,
              bucketRegionalDomainName: config.appleAppSiteAssociationBucketRegionURL,
            }),
            originAccessIdentity: // ...
          },
          behaviors: [
            {
              pathPattern: '/.well-known/*',
              // ...
            },
          ],
        },

ここまで終了したら、手元でcurlを叩くなりして、アップロードが成功していることを確認しておきます。

appleのCDNにアクセスし、キャッシュされていることを確認

iOS14以降、各端末は前節で設定したルートドメインにアクセスするのではなく、AppleのCDNにアクセスすることになっています。

Starting with macOS 11 and iOS 14, apps no longer send requests for apple-app-site-association files directly to your web server. Instead, they send these requests to an Apple-managed content delivery network (CDN) dedicated to associated domains.
While you’re developing your app, if your web server is unreachable from the public internet, you can use the alternate mode feature to bypass the CDN and connect directly to your private domain.

そのため、以下のようにCDNに直接リクエストすることで、CDNまでデリバリーされたことを確認できます。

curl https://app-site-association.cdn-apple.com/a/v1/example.com

iOS ビルド設定変更

Associate Domainを有効化

続いて、アプリのビルド設定を変更します。

Identifiers ページにアクセスし、対象のアプリを選択します。

Associate Domains設定をONにします。

CleanShot 2022-01-28 at 16 42 45

app.jsonにassociatedDomainsの設定を書き足す

https://docs.expo.dev/guides/linking/#universaldeep-links-without-a-custom-scheme

上記ドキュメントを参考に、app.jsonないしapp.config.jsのassociatedDomainsセクションに以下のように書き足します。

      "associatedDomains": [
          "applinks:example.com"
      ],

新しいProvisioning Profileを使ってexpo buildする

そこまで終わったら、expo credentials:managerコマンドを実行するなどして、既存のProvisioning Profileを削除してしまいます。そのあとにexpo buildなどでビルドしなおせば、実行結果のコマンドをつぶさに見ているとわかりますが新しいProvisioning Profileを発行してビルドに組み込んでくれます。

さきほどAppleの管理画面側でAssociate DomainsをONにしているので、新しいProfileをビルドに使うことで、Universal Linksを有効化できます。


ここまで完成していれば、いったん、任意のアプリで対象のURLをクリックしたときに対応したアプリに遷移するようになっているはずです。できない場合は、ASAAファイルが正しくアップロードできているかを以下のようなツールでチェックしてください。

https://branch.io/resources/aasa-validator/

ちなみにビルドしなおさず単にPublishだけした場合はUniversal Linksが動かなかったので、ビルドが必要であることは間違いなさそうです。

続いてAndroidの設定の説明をします。

Android ビルド設定変更

Androidのほうはまったく複雑な実装などなく、configの修正だけで終わります。

app.jsonにintentFiltersを書き足す

以下のように設定します。

      "intentFilters": [
        {
          "action": "VIEW",
          "data": [
            {
              "scheme": "https",
              "host": "example.com",
              "pathPrefix": "/want-to-universal-links"
            }
          ],
          "category": [
            "BROWSABLE",
            "DEFAULT"
          ]
        }
      ]

ここでpathPrefixというものがありますが、前方一致しか使えない致命的な欠点があります。
また、他でpathPatternといういかにも正規表現が使えそうな設定もあるのですが、こちらの正規表現はアスタリスクのみしか使えない大変貧弱な設定です。
おそらくこういった分岐をアプリ内に持っているから、重たい分岐を書けないのだと邪推します。AppleのほうはあくまでCDNのほうが分岐の責務を持っているため、excludeのような細かい設定ができ、Universal LinksしたくないURLを弾くことができるのですが、前方一致および貧弱な正規表現だと、現実的には「アプリに飛ばされすぎてしまう」ことになります。

https://twitter.com/consomme72/status/994430972610297856

https://twitter.com/niwatly/status/1319125957068738561

https://twitter.com/Meijin_garden/status/1481516692500979715

expo buildする

expo buildします。それだけです。

一度ブラウザを介する方法か、直接アプリに行く方法か選択する

Androidの場合は、上記の設定のみの場合は、URLを押した瞬間に「どのアプリで開きますか?」でブラウザまたは指定のアプリの選択肢が表示されます。これを選択肢ナシで直接飛ばしたい場合は、Appleと同じ様にルートドメイン側に.well-knownパス配下に規定の書式のファイルを置く必要がありますが、今回は一度質問される要件でOKとなったため、こちらは実装しませんでした。個人的には、先の貧弱正規表現問題があるので、質問せずに遷移するところまで設定するのは逆にリスクなのではとすら思ったのですが、どうなのでしょうか(アプリ界隈に詳しくないので、客観的な事実から主観的に解釈するところには自信がありません)。


いったんまとめ

以上で、iOSやAndroidでWebからアプリに飛ばす方法の解説の前半を終了します。
一気に書くのがなかなか大変なので、次週以降に時間が取れるときに執筆しようと思います。
後編をお楽しみに!

マナリンク Tech Blog

Discussion