expo icon indicating copy to clipboard operation
expo copied to clipboard

Branch deeplinks unreliable on iOS

Open mersiades opened this issue 8 months ago • 10 comments

Minimal reproducible example

https://github.com/mersiades/expo-router-branch-mre

Which package manager are you using? (Yarn is recommended)

yarn

If the issue is web-related, please select the bundler (web.bundler in the app.json)

None

Summary

The expected behaviour is that when a Branch deep link (eg https://17p1j.app.link/4nUHmYKnzKb) is tapped, it is handled corrected by expo-router. On the minimum reproducible example, the deep links should route to the "/reset-password" screen and show the correct value for the token query param. On Android, the expected behaviour is being met; on iOS, it is not.

Steps to reproduce:

  1. Clone minimum reproducible example (MRE)
  2. yarn install
  3. Set up new Branch.io project with some deep links, alter app.config.js to match new project (Or I could share the key for existing Branch project through secure means, to save you some time)
  4. Prebuild: APP_VARIANT=production BRANCH_LIVE_KEY=<key> yarn prebuild
  5. Run release build on physical device: yarn build:ios:local:release

The minimum reproducible example has two branches: main and using-native-intent.

On the main branch, Branch is added and configured and used as-is, 'straight out of the box'. On this branch, tapping a deep link leads to the 'not found' screen, because expo-router it treating the deep link's path (eg, /4nUHmYKnzKb) as the actual path. It's as though it's being passed through without being handled by react-native-branch first.

On the using-native-intent branch, I've added +native-intent.ts file to handle Branch deep links. This uses the branch.getLatestReferringParams() from the react-native-branch SDK. AFAIK, this works by:

  1. Deep link comes in, the Branch iOS module intercepts it.
  2. Branch iOS module makes an HTTP request to Branch.io, which in essence turns the deep link into useful data. In our case, it returns an object with a $deeplink_path field with a value of `/reset-password?token=test1". Obviously, this will be asynchronous and the request time can vary.
  3. branch.getLatestReferringParams() is run in the Javascript code, which asks the iOS module for the latest results returned from Branch.io.

Using branch.getLatestReferringParams() in the +native-intent.ts file has varying results.

  • on a development build, the link is handled and we're routed to the correct screen. However, usually it's routing based on the second-most recent deep link, not the most recent.
  • on a release build, we're consistently being routed to the 'not found' screen, with "/no-branch-params" as the pathname, indicating that branch.getLatestReferringParams() is returning an empty object.
  • on my actual project (not the MRE), in release build, where Sentry gives us slightly better insight into what is happening, the results are inconsistent. About 30% of the time branch.getLatestReferringParams() is returning the correct data and the link is being handled correctly, but for the other 70% branch.getLatestReferringParams() returns this object, which does not have the required $deeplink_path:
{
  +clicked_branch_link: false,
  +is_first_session: false,
  url: https://chatloop.app.link/9iJvE8YMeJb
}

So what is going on? I don't know, but my current thoughts are:

  • the inconsistency suggests a race condition, perhaps related to the time it takes the Branch iOS module to fetch the data for the most recent deep link.
  • According to the Branch SDK documentation, branch.getLatestReferringParams() should be used within a listener (usually branch.subscribe()). Would redirectSystemPath() within +native-intent.ts work as a "listener" in this case?

Best practice for this method is to receive the data from the listener (to prevent a race condition).

  • My actual project has been using Branch deep links for several years. They stopped working when I converted to expo-router from react-navigation. When using react-navigation, I was able to use Linking configuration to use branch.getLatestReferringParams() within the branch.subscribe() listener (like this).

I'll also note that I'm having problems with deferred deep link and branch.getFirstReferringParams(), which I also suspect is due to the lack of branch.subscribe(), but I'll leave that out of this issue because it's already plenty big enough.

Appendix: Here are the deep links I've been using:

https://17p1j.test-app.link/zZjzmpLzxFb // test reset test 1

https://17p1j.test-app.link/oiTZTyCsLKb // test reset test 2

https://17p1j.app.link/4nUHmYKnzKb //  live reset test 1

https://17p1j.app.link/GLDLy79tLKb //  live reset test 2

Environment

expo-env-info 1.2.0 environment info: System: OS: macOS 14.1.2 Shell: 5.9 - /bin/zsh Binaries: Node: 18.19.1 - ~/.nvm/versions/node/v18.19.1/bin/node Yarn: 1.22.17 - ~/.yarn/bin/yarn npm: 10.2.4 - ~/.nvm/versions/node/v18.19.1/bin/npm Watchman: 2024.06.10.00 - /opt/homebrew/bin/watchman Managers: CocoaPods: 1.15.2 - /Users/michael/.rvm/rubies/ruby-3.2.2/bin/pod SDKs: iOS SDK: Platforms: DriverKit 23.4, iOS 17.4, macOS 14.4, tvOS 17.4, visionOS 1.1, watchOS 10.4 Android SDK: API Levels: 28, 29, 30, 31, 32, 33, 34 Build Tools: 29.0.2, 30.0.3, 32.0.0, 32.1.0, 33.0.0, 33.0.1, 34.0.0 System Images: android-30 | Google APIs ARM 64 v8a, android-31 | Google APIs ARM 64 v8a, android-32 | Google APIs ARM 64 v8a, android-VanillaIceCream | Google Play ARM 64 v8a IDEs: Android Studio: 2023.2 AI-232.10300.40.2321.11567975 Xcode: 15.3/15E204a - /usr/bin/xcodebuild npmPackages: expo: ~51.0.14 => 51.0.14 expo-router: ~3.5.16 => 3.5.16 react: 18.2.0 => 18.2.0 react-dom: 18.2.0 => 18.2.0 react-native: 0.74.2 => 0.74.2 react-native-web: ~0.19.10 => 0.19.12 npmGlobalPackages: eas-cli: 10.0.2 Expo Workflow: bare // Not sure why this is saying 'bare'. Maybe because I've been doing local release builds?

mersiades avatar Jun 27 '24 09:06 mersiades