【iOS】DynamicLinksのURLが正しいはずなのにhandleUniversalLinkでtrueが返らずにハマった

# 背景
仕事でDynamicLinksを用いてアプリを起動しようとしたら、パラメータは合っているはずなのになぜかパラメータをうまく受け取れなくてハマった...

今回はそのときどのような確認をして、どう解決したのかを備忘として残します。

# Firebase Dynamic Linksとは
そもそもの説明を簡単にします。

例えば、Webでサービスを利用しているユーザにアプリ利用を促進させたいケースがあります。
その場合に利用されるのが`Firebase Dynamic Links`です。

DynamicLinksはアプリのインストールの有無にかかわらず、複数のプラットフォームで機能するリンクです。DynamicLinksを開くと、ネイティブアプリのリンク先のコンテンツに直接移動します。

インストール済みであれば、アプリが起動してパラメータをもとに処理を定義することができます。
インストールしていなければ、iOSの場合はApple Storeのアプリページへの導線まで飛び、アプリのインストール・利用を促せます。

このDynamicLinksはFirebaseコンソールの方から簡単に設定できますし、バックエンドの方でリンクを動的に設定することもできます。
また、OS向けのパラメータも設定でき様々な設定ができます。

iOS向けパラメータの例
|パラメータ  |説明  |例 |
|---|---|---|
|ibi  |リンクを開くために利用するアプリのバンドルID|jp.co.hogehoge|
|imv  |リンクを開けるアプリの最小バージョンの番号|1.0.0|

今回のケースでは、後者の**バックエンド側でリンクを作成する方法**が該当します。

# SDK内部の処理を追ってみた
Firebaseの公式ドキュメントでもあるように、アプリがインストール済みの場合にユニバーサルリンクとして受信されるリンクの処理があります。
```swift: AppDelegate.swiftあたり
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
  let handled = DynamicLinks.dynamicLinks()
    .handleUniversalLink(userActivity.webpageURL!) { dynamiclink, error in
      // ...
    }

  return handled
}
```

しかし、DynamicLinksに設定するパラメータは正しいし、DynamicLinks内のディープリンクも間違えてはいないのに（**本当に**）、いくら試しても`handled`に`false`しか返ってこず、DynamicLinksを受け取った時の処理が再現されない。。😢

そこでSDK内部の処理まで追ってみることに。

まずは、呼び出している`handleUniversalLink`で何をしているのかを見てみました。
すると、`canHandleUniversalLink`という定数が`false`を返していることがわかり、深ぼってみると`canParseUniversalLinkURL`が`false`になっていることに気づきました。

```objc: FIRDynamicLinks.m
- (BOOL)handleUniversalLink:(NSURL *)universalLinkURL
                 completion:(FIRDynamicLinkUniversalLinkHandler)completion {
  if ([self matchesShortLinkFormat:universalLinkURL]) {
    __weak __typeof__(self) weakSelf = self;
    [self resolveShortLink:universalLinkURL
                completion:^(NSURL *url, NSError *error) {
    // 今回ここは関連しなかったので省略
    return YES;
  } else {
    [self dynamicLinkFromUniversalLinkURL:universalLinkURL completion:completion];
    BOOL canHandleUniversalLink =
    　    // canParseUniversalLinkURLで異変が起きているぽいな👀
        [self canParseUniversalLinkURL:universalLinkURL] && universalLinkURL.query.length > 0 &&
        FIRDLDictionaryFromQuery(universalLinkURL.query)[kFIRDLParameterLink];
    return canHandleUniversalLink;
  }
}
```

そして、`canHandleUniversalLink`の中には`FIRDLIsAValidDLWithFDLDomain`メソッドの記述しかなかったので、そこを辿ってみました。

```objc:FDLUtilities.m
BOOL FIRDLIsAValidDLWithFDLDomain(NSURL *_Nullable URL) {
  BOOL matchesRegularExpression = false;
  NSString *urlStr = URL.absoluteString;

  if ([URL.host containsString:@".page.link"] || [URL.host containsString:@".app.goo.gl"] ||
      [URL.host containsString:@".app.google"]) {

    // このケースに来ているな👀

    // Matches the *.page.link and *.app.goo.gl domains.
    matchesRegularExpression =
        ([urlStr rangeOfString:
                     @"^https?://"
                     @"[a-zA-Z0-9]+((\\.app\\.goo\\.gl)|(\\.page\\.link)|(\\.app\\.google))((\\/"
                     @"?\\?.*link=https?.*)|(\\/[a-zA-Z0-9-_]+)((\\/?\\?.*=.*)?$|$))"
                       options:NSRegularExpressionSearch]
             .location != NSNotFound);
    // ↑↑↑　こいつが怪しいな👀🤔😠　↑↑↑

    if (!matchesRegularExpression) {
        // こっちには落ちてきていない
    }
  }

  return matchesRegularExpression;
}
```

`matchesRegularExpression`の結果を返していることがわかったので、ここで定義されている正規表現が怪しいというところまでわかりました。
じゃあ実際、この正規表現はどうなっているのかをみていきましょう🕵️‍♂️

自分が検証していたリンクは`https://hogehoge.page.link/?ibi=xxxxx&isi=xxxxx&link=https://hugahuga`みたいな形式になっていました。
`page.link`までの正規表現に関してはひとまず問題はなさそうだったので、クエリパラメータが怪しそうです。

よく見てみると、なんかパラメータは`?link`から始まるようにしないといけない条件になっていないか...
```objc
((\\/?\\?.*link=https?.*)|(\\/[a-zA-Z0-9-_]+)((\\/?\\?.*=.*)?$|$))
```

まさかと思い、パラメータの先頭が`?link`から始まるように調整してもらったところ無事**正常に動いた...**

![スクリーンショット 2023-03-03 19.24.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550560/019acfa1-7478-a35c-29da-109531bc5738.png)

Firebaseのコンソール上から作成するDynamicLinksでは、`長いダイナミックリンク`を見るとわかるように、ドメインの後は`link`のパラメータがつくようになっていたのでこういう仕様なんだなと受け入れるしかないようです...
※画像雑ですみません🙇‍♂️

# まとめ
要するに、バックエンドで動的に作成するときにディープリンクを用いる場合はドメイン直後のパラメータの先頭はlinkから始まるようにしないといけないようです。

簡単に比較するとこんな感じ
```
🙆‍♂️ https://hogehoge.page.link/?link=https://hugahuga&ibi=xxxxx&isi=xxxxx
🙅‍♂️ https://hogehoge.page.link/?ibi=xxxxx&isi=xxxxx&link=https://hugahuga
```

パラメータは順不同であってほしい世界でした。
ちなみに、記事執筆時の`firebase-iOS-sdk`の最新バージョンは`10.7.0`でしたが、この正規表現に変更はありませんでしたので、引き続きこの仕様であることを注意しないといけません。

この投稿が同様にハマっている他の方の助けになることを願っています。

# 参照
- [ダイナミック リンク URL を手動で構築する - Firebase - Google](https://firebase.google.com/docs/dynamic-links/create-manually?hl=ja)
- [iOS でダイナミック リンクを受信する | Firebase Dynamic Links](https://firebase.google.com/docs/dynamic-links/ios/receive?hl=ja)
- [Firebase iOS SDK側の正規表現の実装](https://github.com/firebase/firebase-ios-sdk/blob/6dfadd235a638227290cb6c6a5df073c712c153a/FirebaseDynamicLinks/Sources/Utilities/FDLUtilities.m#L272-L276)