invertase / react-native-firebase

πŸ”₯ A well-tested feature-rich modular Firebase implementation for React Native. Supports both iOS & Android platforms for all Firebase services.
https://rnfirebase.io
Other
11.64k stars 2.21k forks source link

πŸ”₯ DynamicLinks iOS: `await dynamicLinks().getInitialLink()` works on debug, but doesn't work on release mode [workaround possible! see comments] #2660

Closed noway closed 1 year ago

noway commented 4 years ago

EDIT: we think there is a workaround possible - https://github.com/invertase/react-native-firebase/issues/4548#issuecomment-1252028059

Issue

await dynamicLinks().getInitialLink() works in debug mode, but doesn't work in release mode on iOS, on certain iPhones. Please not that the app does not crash, but rather null is returned by await dynamicLinks().getInitialLink() when it should be returning the dynamic link url. onLink seems to be working fine, haven't noticed any issues with that. It is confirmed not to work on iPhone Xs with iOS 12.4.0 (device) and iPhone Xs with iOS 12.2.0 (Simulator). I use preview page in my links. Tested both from Safari via preview page and from Messages/Notes app, but NOT through App Store. See testing matrix:

iPhone Xs with iOS 12.4.0 (device), debug: not tested iPhone Xs with iOS 12.4.0 (device), release: ❌ broken iPhone Xs with iOS 12.2.0 (Simulator), debug: βœ… works iPhone Xs with iOS 12.2.0 (Simulator), release: ❌ broken iPhone Xs with iOS 13.0.0 (Simulator), debug: βœ… works iPhone Xs with iOS 13.0.0 (Simulator), release: βœ… works iPhone 7 Plus with iOS 12.4.1 (device), debug: βœ… works iPhone 7 Plus with iOS 12.4.1 (device), release: βœ… works

This issue is critical because it affects a significant amount of the iOS user base, I think it might be affecting any iPhone X and higher users who haven't updated to iOS 13 yet (i.e. everyone but the early adopters) which is a very substantial percentage of all iOS users.

The issue might be releated to https://github.com/invertase/react-native-firebase/issues/1901, and if so the issue has failed at least 2 Firebase customers, who since have chosen to switch to branch.io instead. See https://github.com/invertase/react-native-firebase/issues/1901#issuecomment-469242110 and https://github.com/invertase/react-native-firebase/issues/1901#issuecomment-477839585

Android is working flawlessly as far as I've tested, with getInitialLink, onLink, install through Play Store all working perfectly. Only issue is iOS.

Code I'm using:

  componentDidMount() {
    this.init()
  }

  init = async () => {
    const initialUrl = await dynamicLinks().getInitialLink()
    this.setState({ dynamicLinkUrl: initialUrl ? initialUrl.url : null })
    console.log('dynamicLinkUrl', initialUrl ? initialUrl.url : null)
    Alert.alert('dynamicLinkUrl', initialUrl ? initialUrl.url : 'null')
    await this.authenticate()
    console.log('this.props.user.id', this.props.user.id)
    this.unsubscribeOnLink = dynamicLinks().onLink(this.onLink)
  }

  onLink = ({ url }) => {
    console.log('onLink url', url, Date.now())
    this.onDynamicLink(url)
  }

Project Files

iOS

Click To Expand

#### `ios/Podfile`: - [ ] I'm not using Pods - [x] I'm using Pods and my Podfile looks like: ```ruby # Uncomment the next line to define a global platform for your project platform :ios, '9.0' require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' target 'mycoolappapp' do # See http://facebook.github.io/react-native/docs/integration-with-existing-apps.html#configuring-cocoapods-dependencies pod 'React', :path => '../node_modules/react-native/' pod 'React-Core', :path => '../node_modules/react-native/React' pod 'React-DevSupport', :path => '../node_modules/react-native/React' pod 'React-ART', :path => '../node_modules/react-native/Libraries/ART' pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS' pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation' pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob' pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image' pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS' pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network' pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings' pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text' pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration' pod 'React-RCTWebSocket', :path => '../node_modules/react-native/Libraries/WebSocket' pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact' pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi' pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor' pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector' pod 'yoga', :path => '../node_modules/react-native/ReactCommon/yoga' pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec' pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec' pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec' target 'mycoolappappTests' do inherit! :search_paths # Pods for testing end use_native_modules! end target 'mycoolappapp-tvOS' do # Pods for mycoolappapp-tvOS target 'mycoolappapp-tvOSTests' do inherit! :search_paths # Pods for testing end end target 'OneSignalNotificationServiceExtension' do pod 'OneSignal', '>= 2.9.3', '< 3.0' end ``` #### `AppDelegate.m`: ```objc /** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ #import "AppDelegate.h" #import #import #import #import #import #import #import @import Firebase; @interface NSString(JB) -(NSString *) stringValue; @end @implementation NSString(JB) -(NSString *) stringValue { return self; } @end @implementation AppDelegate - (void)applicationDidBecomeActive:(UIApplication *)application { [FBSDKAppEvents activateApp]; } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [FIROptions defaultOptions].deepLinkURLScheme = @"app.mycoolapp"; if ([FIRApp defaultApp] == nil) { [FIRApp configure]; // It is recommended to add the line within the method BEFORE creating the RCTRootView } RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"mycoolappapp" initialProperties:nil]; rootView.backgroundColor = [[UIColor alloc] initWithRed:0.08f green:0.10f blue:0.15f alpha:1]; [[FBSDKApplicationDelegate sharedInstance] application:application didFinishLaunchingWithOptions:launchOptions]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; return YES; } // FYI: Deprecated in iOS 9 - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { BOOL isHandled = [[FBSDKApplicationDelegate sharedInstance] application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; if (!isHandled) { isHandled = [[RNFBDynamicLinksAppDelegateInterceptor sharedInstance] application:application openURL:url sourceApplication:sourceApplication annotation:annotation]; } return isHandled; } - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { BOOL isHandled = [[FBSDKApplicationDelegate sharedInstance] application:application openURL:url options:options]; if (!isHandled) { isHandled = [[RNFBDynamicLinksAppDelegateInterceptor sharedInstance] application:application openURL:url options:options]; } return isHandled; } // FYI: Seems to double up the onLink call. But i added a memoized debounce there so should be all good. - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray> *))restorationHandler { return [[RNFBDynamicLinksAppDelegateInterceptor sharedInstance] application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; } // Required to register for notifications - (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings { [RNCPushNotificationIOS didRegisterUserNotificationSettings:notificationSettings]; } // Required for the register event. - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { [RNCPushNotificationIOS didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; } // Required for the notification event. You must call the completion handler after handling the remote notification. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { [RNCPushNotificationIOS didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; } // Required for the registrationError event. - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [RNCPushNotificationIOS didFailToRegisterForRemoteNotificationsWithError:error]; } // Required for the localNotification event. - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { [RNCPushNotificationIOS didReceiveLocalNotification:notification]; } - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; #else return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif } @end ```


Android

Click To Expand

#### `android/build.gradle`: ```groovy // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext { buildToolsVersion = "28.0.3" minSdkVersion = 16 compileSdkVersion = 28 targetSdkVersion = 28 supportLibVersion = "28.0.0" } repositories { google() jcenter() maven { url 'https://maven.google.com' } } dependencies { classpath("com.android.tools.build:gradle:3.5.0") classpath 'com.google.gms:google-services:4.3.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { mavenLocal() maven { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm url("$rootDir/../node_modules/react-native/android") } maven { // Android JSC is installed from npm url("$rootDir/../node_modules/jsc-android/dist") } google() jcenter() maven { url 'https://maven.google.com' } } } ``` #### `android/app/build.gradle`: ```groovy apply plugin: "com.android.application" import com.android.build.OutputFile /** * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets * and bundleReleaseJsAndAssets). * These basically call `react-native bundle` with the correct arguments during the Android build * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the * bundle directly from the development server. Below you can see all the possible configurations * and their defaults. If you decide to add a configuration block, make sure to add it before the * `apply from: "../../node_modules/react-native/react.gradle"` line. * * project.ext.react = [ * // the name of the generated asset file containing your JS bundle * bundleAssetName: "index.android.bundle", * * // the entry file for bundle generation * entryFile: "index.android.js", * * // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format * bundleCommand: "ram-bundle", * * // whether to bundle JS and assets in debug mode * bundleInDebug: false, * * // whether to bundle JS and assets in release mode * bundleInRelease: true, * * // whether to bundle JS and assets in another build variant (if configured). * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants * // The configuration property can be in the following formats * // 'bundleIn${productFlavor}${buildType}' * // 'bundleIn${buildType}' * // bundleInFreeDebug: true, * // bundleInPaidRelease: true, * // bundleInBeta: true, * * // whether to disable dev mode in custom build variants (by default only disabled in release) * // for example: to disable dev mode in the staging build type (if configured) * devDisabledInStaging: true, * // The configuration property can be in the following formats * // 'devDisabledIn${productFlavor}${buildType}' * // 'devDisabledIn${buildType}' * * // the root of your project, i.e. where "package.json" lives * root: "../../", * * // where to put the JS bundle asset in debug mode * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", * * // where to put the JS bundle asset in release mode * jsBundleDirRelease: "$buildDir/intermediates/assets/release", * * // where to put drawable resources / React Native assets, e.g. the ones you use via * // require('./image.png')), in debug mode * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", * * // where to put drawable resources / React Native assets, e.g. the ones you use via * // require('./image.png')), in release mode * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", * * // by default the gradle tasks are skipped if none of the JS files or assets change; this means * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to * // date; if you have any other folders that you want to ignore for performance reasons (gradle * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ * // for example, you might want to remove it from here. * inputExcludes: ["android/**", "ios/**"], * * // override which node gets called and with what additional arguments * nodeExecutableAndArgs: ["node"], * * // supply additional arguments to the packager * extraPackagerArgs: [] * ] */ project.ext.react = [ entryFile: "index.js", enableHermes: false, // clean and rebuild if changing ] apply from: "../../node_modules/react-native/react.gradle" /** * Set this to true to create two separate APKs instead of one: * - An APK that only works on ARM devices * - An APK that only works on x86 devices * The advantage is the size of the APK is reduced by about 4MB. * Upload all the APKs to the Play Store and people will download * the correct one based on the CPU architecture of their device. */ def enableSeparateBuildPerCPUArchitecture = false /** * Run Proguard to shrink the Java bytecode in release builds. */ def enableProguardInReleaseBuilds = false /** * The preferred build flavor of JavaScriptCore. * * For example, to use the international variant, you can use: * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` * * The international variant includes ICU i18n library and necessary data * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that * give correct results when using with locales other than en-US. Note that * this variant is about 6MiB larger per architecture than default. */ def jscFlavor = 'org.webkit:android-jsc:+' /** * Whether to enable the Hermes VM. * * This should be set on project.ext.react and mirrored here. If it is not set * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode * and the benefits of using Hermes will therefore be sharply reduced. */ def enableHermes = project.ext.react.get("enableHermes", false); android { compileSdkVersion rootProject.ext.compileSdkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } defaultConfig { applicationId "app.mycoolapp" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 236 versionName "2.3.6" multiDexEnabled true missingDimensionStrategy 'react-native-camera', 'general' } splits { abi { reset() enable enableSeparateBuildPerCPUArchitecture universalApk false // If true, also generate a universal APK include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" } } signingConfigs { debug { keyAlias 'mycoolapp-android-alias' keyPassword 'ugh' storeFile file('/Users/ilia/mycoolapp/mycoolapp-release-key-android.jks') storePassword 'ugh' } } buildTypes { debug { signingConfig signingConfigs.debug } release { // Caution! In production, you need to generate your own keystore file. // see https://facebook.github.io/react-native/docs/signed-apk-android. signingConfig signingConfigs.debug minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } // applicationVariants are e.g. debug, release applicationVariants.all { variant -> variant.outputs.each { output -> // For each separate APK per architecture, set a unique version code as described here: // https://developer.android.com/studio/build/configure-apk-splits.html def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] def abi = output.getFilter(OutputFile.ABI) if (abi != null) { // null for the universal-debug, universal-release variants output.versionCodeOverride = versionCodes.get(abi) * 1048576 + defaultConfig.versionCode } } } packagingOptions { pickFirst '**/armeabi-v7a/libc++_shared.so' pickFirst '**/x86/libc++_shared.so' pickFirst '**/arm64-v8a/libc++_shared.so' pickFirst '**/x86_64/libc++_shared.so' pickFirst '**/x86/libjsc.so' pickFirst '**/armeabi-v7a/libjsc.so' } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "com.facebook.react:react-native:+" // From node_modules implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'androidx.core:core:1.0.0' implementation 'androidx.multidex:multidex:2.0.1' implementation "com.google.android.gms:play-services-base:16.0.1" implementation "com.google.android.gms:play-services-location:16.0.0" implementation 'com.facebook.android:facebook-android-sdk:[5,6)' if (enableHermes) { def hermesPath = "../../node_modules/hermesvm/android/"; debugImplementation files(hermesPath + "hermes-debug.aar") releaseImplementation files(hermesPath + "hermes-release.aar") } else { implementation jscFlavor } } // Run this once to be able to run the application with BUCK // puts all compile dependencies into folder libs for BUCK to use task copyDownloadableDepsToLibs(type: Copy) { from configurations.compile into 'libs' } apply plugin: 'com.google.gms.google-services' apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) ``` #### `android/settings.gradle`: ```groovy rootProject.name = 'mycoolappapp' apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':@react-native-mapbox-gl_maps' project(':@react-native-mapbox-gl_maps').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-mapbox-gl/maps/android/rctmgl') include ':app' ``` #### `MainApplication.java`: ```java package app.mycoolapp; import com.facebook.FacebookSdk; import com.facebook.react.PackageList; import com.facebook.react.ReactApplication; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; import com.facebook.soloader.SoLoader; import androidx.multidex.MultiDexApplication; import java.util.List; public class MainApplication extends MultiDexApplication implements ReactApplication { private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { @Override public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } @Override protected List getPackages() { @SuppressWarnings("UnnecessaryLocalVariable") List packages = new PackageList(this).getPackages(); // Packages that cannot be autolinked yet can be added manually here, for example: // packages.add(new MyReactNativePackage()); return packages; } @Override protected String getJSMainModuleName() { return "index"; } }; @Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; } @Override public void onCreate() { super.onCreate(); SoLoader.init(this, /* native exopackage */ false); FacebookSdk.setAdvertiserIDCollectionEnabled(true); FacebookSdk.setAdvertiserIDCollectionEnabled(true); FacebookSdk.setAutoInitEnabled(true); FacebookSdk.fullyInitialize(); } } ``` #### `AndroidManifest.xml`: ```xml ```


Environment

Click To Expand

**`react-native info` output:** ``` System: OS: macOS 10.14.6 CPU: (8) x64 Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz Memory: 349.36 MB / 16.00 GB Shell: 5.3 - /bin/zsh Binaries: Node: 12.6.0 - ~/.nvm/versions/node/v12.6.0/bin/node npm: 6.11.3 - ~/.nvm/versions/node/v12.6.0/bin/npm Watchman: 4.9.0 - /usr/local/bin/watchman SDKs: iOS SDK: Platforms: iOS 13.0, DriverKit 19.0, macOS 10.15, tvOS 13.0, watchOS 6.0 IDEs: Android Studio: 3.5 AI-191.8026.42.35.5791312 Xcode: 11.0/11A420a - /usr/bin/xcodebuild npmPackages: react: 16.8.6 => 16.8.6 react-native: ^0.60.5 => 0.60.5 ``` - **Platform that you're experiencing the issue on**: - [x] iOS - [ ] Android - [ ] **iOS** but have not tested behavior on Android - [ ] **Android** but have not tested behavior on iOS - [ ] Both - **`react-native-firebase` version you're using that has this issue:** ``` "@react-native-firebase/app": "^6.0.0", "@react-native-firebase/auth": "^6.0.0", "@react-native-firebase/dynamic-links": "^6.0.0", ``` - **`Firebase` module(s) you're using that has the issue:** - `DynamicLinks` - **Are you using `TypeScript`?** - `N`


Think react-native-firebase is great? Please consider supporting all of the project maintainers and contributors by donating via our Open Collective where all contributors can submit expenses. [Learn More]

stale[bot] commented 3 years ago

Hello πŸ‘‹, to help manage issues we automatically close stale issues. This issue has been automatically marked as stale because it has not had activity for quite some time. Has this issue been fixed, or does it still require the community's attention?

This issue will be closed in 15 days if no further activity occurs. Thank you for your contributions.

hiteshSavkare commented 3 years ago

Facing same issue My application working fine with App-Center, TestFlight , development build but only the case is not working if user don't have app installed and user navigate through dynamic link to apple store and installing the released app, that time deepLink reference looks not stored, tried all the possible ways from different source. Any help would be appreciated

"@react-native-firebase/dynamic-links": "^12.1.0", "react-native": "0.64.2", platform: IOS

jonathanroze commented 3 years ago

Same issue for me, deep linking works great when app is installed but I cannot retrieve initial link just after app installed ! I tried many ways, not one works, in debug or release!

danlupascu commented 3 years ago

I'm having the same issue.

The app opens all the links when it's already installed but the link doesn't survive the installation.

Note: I'm creating the links manually and encoding them properly. I think the links I create are fine since the app opens correctly and navigates to the right screen when I press the links. The issue is that I can't get the link right after the installation.

Is it possible that it's not working because I'm not creating them with your package?

mikehardy commented 3 years ago

I am unsure - this appears like it might be a firebase-ios-sdk bug but I don't recall if anyone did the native SDK reproduction attempt using https://github.com/firebase/quickstart-ios/tree/master/dynamiclinks yet

I think this is still the authoritative state of the issue - I believe a native reproduction will be required for a fix, and I believe the fix will be in firebase-ios-sdk, react-native-firebase is just wrapping APIs in this case - I do not believe it has any ability to affect the link surviving app install or not

danlupascu commented 3 years ago

and what can we do now? We're using this package, we can't open an issue in firebase-ios-sdk (by we I mean all the people using the react-native package).

mikehardy commented 3 years ago

There is nothing stopping anyone from opening an issue on firebase-ios-sdk any time, it's an open source project. Like any project (including this one) they'll need you to put some effort in, in the form of a https://stackoverflow.com/help/minimal-reproducible-example - however, as linked above they offer these quickstarts that let you quickly show an issue in a format that is valid for their repo. Have you tried?

danlupascu commented 3 years ago

The problem is not opening an issue. The problem is that we're not using firebase-ios-sdk directly - we're using it through this package and it's not so easy to create a reproducible example just using firebase-ios-sdk. They'll say that they can't investigate the issue because we're not using it directly.

It should be someone that worked on the react-native package and knows what could possibly go wrong and would be able to provide more details and context...

mikehardy commented 3 years ago

I see what you're saying, however in this case I have no special knowledge, and I'm not sure there's anyone else. Someone interested is going to have to dig in here and do the repro work

faustoct1 commented 3 years ago

This is a workaround that worked for me.

  useEffect(() => {
    /FIXME => Using Linking, it's a working around that works in production. 
    //Because using FirebaseFactory.links().getInitialLink() doesn't work in production.
    //FirebaseFactory.links().getInitialLink().then(x=>alert(x))
    Linking.getInitialURL().then(url=>{if(url){handleDynamicLink(url,true)}}) //-->WORKAROUND
    const unsubscribe = FirebaseFactory.links().onLink((link)=>{if(link&&link.url)handleDynamicLink(link.url,false)})
    return () => {unmount();unsubscribe()}
  } ,[])

I figured out that it worked only in Debug mode. Since I submitted to Apple Store. I installed the app and it didn't work. I replace links().getInitialLink() for Linking.getInitialURL() . It worked. I had to resolve the dynamic url on my own, which I have to keep the links in my database.

danlupascu commented 3 years ago

@mikehardy I think it's because of this package since we're having this issue on both android and iOS, so it can't be because of firebase-ios-sdk.

However, we managed to find a solution based on this: I saw this issue: https://github.com/invertase/react-native-firebase/issues/4548 . THe issue is that getInitialLink returns null if called too soon after launch. Also, I noticed that there's a small notification when opening the app from a link that says something like "Link pasted from Chrome", and I realized that indeed, the link is not passed to the app as soon as the app is opened.

That's why we created a simple function that runs getInitialLink with a delay of 500ms until it reaches a maximum number of attempts or returns a url. It seems to be working well for both android and iOS.

import dynamicLinks, { FirebaseDynamicLinksTypes } from '@react-native-firebase/dynamic-links';

const getInitialLink = (
  maxAttempts: number,
): Promise<null | FirebaseDynamicLinksTypes.DynamicLink> => new Promise((resolve) => {
  let link: null | FirebaseDynamicLinksTypes.DynamicLink = null;

  const getLink = async (attempt: number = 1) => {
    try {
      link = await dynamicLinks().getInitialLink();
    } catch (e) {
      // Try again
    }

    if (link) {
      resolve(link);
    } else if (attempt <= maxAttempts) {
      setTimeout(() => {
        getLink(attempt + 1);
      }, 500);
    } else {
      resolve(null);
    }
  };

  getLink();
});

// And somewhere in a `useEffect` hook:
getInitialLink(20).then((link) => {
  if (link) {
    // handleDynamicLink(link);
  }
});
mikehardy commented 3 years ago

That's very interesting - I appreciate the detail, and report of success with workaround code. This seems similar to an issue we are currently working on with messaging where native needs to wait on Javascript side to come up before it sends events across the bridge. If ios startup time is the issue here maybe there is a way to handle all of these at the same time

mikehardy commented 3 years ago

Note that I tagged this "solution provided" but I consider it "workaround only" - if it's possible for it to work with a sleep, and the issue manifests across android/ios the most important label here is "needs review", though I will say that it may take quite some time if no one from the community takes it up to see exactly why there is such a delay, and/or if the method could be made to wait for successful resolution some reasonable amount of time or similar (#5547 is the PR I was alluding too earlier where there was a delay needed for correct native --> JS handoff in our react-native context) that would be great. If the native Linking is able to work, it should be possible for ours to work as well

vzhovnitsky commented 3 years ago

Facing the same problem, @danlupascu `s hack not helping, is there some other way of solving this issue?

danlupascu commented 3 years ago

Facing the same problem, @danlupascu `s hack not helping, is there some other way of solving this issue?

It's odd, it should work ... Are you sure you did everything correctly? Is it working fine after the first install when the app is not in the background?

vzhovnitsky commented 3 years ago

Finally @faustoct s workaround helped :D

useEffect(() => {
        const unsubscribe = dynamicLinks().onLink(handleDynamicLink);
        dynamicLinks().getInitialLink().then((result) => {
            if (result) {
                handleDynamicLink(result);
            } else {
                Linking.getInitialURL()
                    .then((initialUrl) => {
                        if (initialUrl) {
                            dynamicLinks().resolveLink(initialUrl)
                                .then((resolvedLink) => {
                                    handleDynamicLink(resolvedLink);
                                })
                                .catch(console.warn);
                        }
                    })
                    .catch(console.warn);
            }
        }).catch(console.warn);

        return () => {
            unsubscribe();
        };
    }, []);
faustoct1 commented 3 years ago

@vzhovnitsky I glad to see it helped you anyway. It's been simpler and effortless solution I found. It brings the minimal impact to me and probably to your code.

zaptrem commented 3 years ago

Neither of these fixes works for me. getInitialLink doesn't work at all in production and Linking.getInitialURL() can't check the clipboard for installation-surviving Firebase links. I can't check the clipboard manually either cuz the Firebase library wipes it after reading it on launch.

faustoct1 commented 3 years ago

Neither of these fixes works for me. getInitialLink doesn't work at all in production and Linking.getInitialURL() can't check the clipboard for installation-surviving Firebase links. I can't check the clipboard manually either cuz the Firebase library wipes it after reading it on launch.

you can check the versions running in your system. all of my env are up-to-date. It's the only help or try I can give you without a better context of your project.

mikehardy commented 3 years ago

As part of verification on the excellent addition of UTM params to the link data returned from link resolution in #5593 I discovered, much to my chagrin that our e2e test was all false-positive for the onLink and getInitialLink test cases for subtle reason related to the test infrastructure.

Working through it now, but I think I've actually got a reproduction for this one finally!

zaptrem commented 3 years ago

@mikehardy Does this mean it might be an issue fixable here after all? Looking forward to the results!

mikehardy commented 3 years ago

Maybe - I thought I had a reproduction then the more I probed it the less I was sure. This is a frustrating issue to pinpoint because there are types of dynamic link on iOS that only work when actually tapped, which is to say, testing them in an automated fashion is incredibly hard. There is quite a bit of reading out on the net about it, which I've read all of now :sweat_smile:

Unfortunately, with inconclusive results, a very difficult reproduction prospect in our e2e harness, and no other reproduction I had to give up on it yesterday and move on. Not sure when I'll have another time box to throw at it, it could be a really long time unfortunately

Wish I had better news, I know this is a pain.

zaptrem commented 3 years ago

Maybe - I thought I had a reproduction then the more I probed it the less I was sure. This is a frustrating issue to pinpoint because there are types of dynamic link on iOS that only work when actually tapped, which is to say, testing them in an automated fashion is incredibly hard. There is quite a bit of reading out on the net about it, which I've read all of now πŸ˜…

Unfortunately, with inconclusive results, a very difficult reproduction prospect in our e2e harness, and no other reproduction I had to give up on it yesterday and move on. Not sure when I'll have another time box to throw at it, it could be a really long time unfortunately

Wish I had better news, I know this is a pain.

Sadly it kinda breaks our onboarding flow and leads to lots of lost/confused users. All that needs to be tested is the post-install production first fresh startup path, where Firebase reads the clipboard for the link put there by the Firebase website before sending users to the App Store.

If that’s impossible, is there a way to at least disable Firebase clearing the clipboard after reading the link so I can reimplement it myself?

mikehardy commented 3 years ago

Hey @zhigang1992 do you mean this?

https://github.com/invertase/react-native-firebase/blob/ddceb896bcf186bc4976a1b07e984c8a93c5cab6/packages/dynamic-links/ios/RNFBDynamicLinks/RNFBDynamicLinksAppDelegateInterceptor.m#L90

I am trying to determine what part of the system is the "it" in "it's" in this sentence:

Because it's using the method swizzling to do it at a later step.

...which might lead me to know which methods could be called manually (and where) from this statement:

Calling the methods manually would fix the issue

Would love any more insight you have πŸ™

zhigang1992 commented 3 years ago

@mikehardy sry forget to reply

In our testing, our clue was that the method swizzling done in

https://github.com/invertase/react-native-firebase/blob/ddceb896bcf186bc4976a1b07e984c8a93c5cab6/packages/dynamic-links/ios/RNFBDynamicLinks/RNFBDynamicLinksAppDelegateInterceptor.m#L32-L33

Was a bit too late, since the AppDelegate method was already called prior to the method getting swapped out.

So we just add these two methods manually and then disable the method swizzling code.

Which is working for us in the release build.

But I'm not 100% sure if that is the root cause / correct cause, all we know is calling these methods manually does work, where as the method swizzling does not.

Also, when it do fail, no amount of retry on getInitialUrl will work, since the data was never captured.

heletrix commented 3 years ago

Adding this code to my AppDelegate.m made it possible to receive link in dynamicLinks().getInitialLink() for me.

#import <RNFBDynamicLinksAppDelegateInterceptor.h>

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    RNFBDynamicLinksAppDelegateInterceptor *interceptor = [RNFBDynamicLinksAppDelegateInterceptor sharedInstance];
    if (url && !interceptor.initialLinkUrl)
    {
        FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url];
        if (dynamicLink.url) 
        {
            interceptor.initialLinkUrl = dynamicLink.url.absoluteString;
            interceptor.initialLinkMinimumAppVersion = dynamicLink.minimumAppVersion == nil ? [NSNull null] : dynamicLink.minimumAppVersion;
        }
    }
    return YES;
}

Source: https://vijayt.com/post/testing-dynamic-links-and-fixing-react-native-firebase-issues/

I'm not sure how it influences my app, but it works well in case when the app isn't installed yet and the app store is opened by link. This is a very important case for our referral program to work well.

Without this code I had null in getInitialLink method after installing the app with the error in xCode console:

[default] Failed to open URL com.example://google/link/?deep_link_id=https%3A%2F%example%2Ecom%2F%3Fref%3Dexample&match_type=weak&utm_medium=dynamic%5Flink&request_ip_version=IP%5FV4&utm_source=firebase&match_message=Best%20%28of%20many%29%20pre%2Dinstall%20links%20matched%20for%20this%20device%2E: Error Domain=NSOSStatusErrorDomain Code=-10814 "(null)" UserInfo={_LSLine=225, _LSFunction=-[_LSDOpenClient openURL:options:completionHandler:]}
mtnt commented 2 years ago

@heletrix I have added that code into my AppDelegate.m and I got an initialLink for closed but installed app. But I still getting null for using a link while the app is not installed (I tried to use: link -> app store -> install testFlight / by xCode -> null).

I also called [FIRDynamicLinks performDiagnosticsWithCompletion:nil]; from this page:

---- Firebase Dynamic Links diagnostic output start ----
Firebase Dynamic Links framework version 8.12.1
System information: OS iOS, OS version 15.3.1, model iPhone
Current date 2022-02-24 12:10:54 +0000
Device locale en-RU (raw en_RU), timezone Europe/Moscow
    Specified custom URL scheme is __mySheme__ and Info.plist contains such scheme in CFBundleURLTypes key.
    AppID Prefix: __prefix__, Team ID: __teamId__, AppId Prefix equal to Team ID: YES
performDiagnostic completed successfully! No errors found.
---- Firebase Dynamic Links diagnostic output end ----
thespacemanatee commented 2 years ago

@mtnt Give #4548 a try, I had to add the workaround by @heletrix and retry getInitialLink() on cold start to finally make it work. Before #4548 the workaround only worked on Debug and not Release builds.

stale[bot] commented 2 years ago

Hello πŸ‘‹, to help manage issues we automatically close stale issues. This issue has been automatically marked as stale because it has not had activity for quite some time. Has this issue been fixed, or does it still require the community's attention?

This issue will be closed in 15 days if no further activity occurs. Thank you for your contributions.

EquesMedia commented 1 year ago

Adding this code to my AppDelegate.m made it possible to receive link in dynamicLinks().getInitialLink() for me.

#import <RNFBDynamicLinksAppDelegateInterceptor.h>

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    RNFBDynamicLinksAppDelegateInterceptor *interceptor = [RNFBDynamicLinksAppDelegateInterceptor sharedInstance];
    if (url && !interceptor.initialLinkUrl)
    {
        FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url];
        if (dynamicLink.url) 
        {
            interceptor.initialLinkUrl = dynamicLink.url.absoluteString;
            interceptor.initialLinkMinimumAppVersion = dynamicLink.minimumAppVersion == nil ? [NSNull null] : dynamicLink.minimumAppVersion;
        }
    }
    return YES;
}

Source: https://vijayt.com/post/testing-dynamic-links-and-fixing-react-native-firebase-issues/

I'm not sure how it influences my app, but it works well in case when the app isn't installed yet and the app store is opened by link. This is a very important case for our referral program to work well.

Without this code I had null in getInitialLink method after installing the app with the error in xCode console:

[default] Failed to open URL com.example://google/link/?deep_link_id=https%3A%2F%example%2Ecom%2F%3Fref%3Dexample&match_type=weak&utm_medium=dynamic%5Flink&request_ip_version=IP%5FV4&utm_source=firebase&match_message=Best%20%28of%20many%29%20pre%2Dinstall%20links%20matched%20for%20this%20device%2E: Error Domain=NSOSStatusErrorDomain Code=-10814 "(null)" UserInfo={_LSLine=225, _LSFunction=-[_LSDOpenClient openURL:options:completionHandler:]}

Hi @heletrix, how did you do this exactly? Did you just paste the code in the file and it was fixed? I am developing an app with Expo and I am currently trying to create a plugin to modify the native code, but I have to say it has not been a success yet.

I've been scouting the entire internet to find out why the dynamic link was not retrieved on a iOS release build and so far, this post describes exactly what I am experiencing and your code seems to be the only thing that's close to a solution.

To be clear, Dynamic Links work very well on Android and iOS, except for a first installation. In my case, it also works in debug and not in release mode:

Here are the steps I do to test:

  1. Open the dynamic link -> Safari opens up the "preview page". The checkbox to preserve the data is checked.

  2. It opens the App Store.

  3. If I install the app, there's a little notification showing that the app has pasted data from Safari.

  4. If I omit to install the app, and instead close the store, then install manually a development build from expo, it catches the deep link (bundle.id://google/link/etc). It is exactly the same code.

But it works on a development build. Same code.

I am not familiar with native development, so I wonder what I could be missing. Firebase configuration does not seem to be the issue.

Anyway, looking forward for some help creating a config plugin to modify the appDelegate file correctly to test out the solution @heletrix is suggesting.

Here is more info on what a config plugin is: https://docs.expo.dev/guides/config-plugins/#using-a-plugin-in-your-app

If I can provide any more information, feel free to poke me. I can also show you via video call or anything what the code of my app is if you're willing to help me.

Good day

mikehardy commented 1 year ago

Interesting - so in release mode, on iOS, on first install, the workaround above where some extra code is added to the openURL method in AppDelegate gives you a link, whereas the react-native-firebase dynamic link interceptor does not.

The most interesting part, to me, is that the code added here in the workaround is the same code that is actually in the react-native-firebase dynamic link interceptor, which should be called if swizzling was working. But clearly it is not.

So we have correct code (in the dynamic link intercepter, proven because when you basically copy it to AppDelegate directly as is suggested in this workaround), but the correct code is just not being executed on its own.

The workaround here https://github.com/invertase/react-native-firebase/issues/4548#issuecomment-1252028059 - which is basically forcing swizzling at app startup prior to when openURL should be called, seems like it comes closer to addressing what appears to be the root cause - an order of operations / swizzling happened too late issue.

I think the workaround in https://github.com/invertase/react-native-firebase/issues/4548#issuecomment-1252028059 is likely sufficient, and if you were trying to make an Expo plugin, I would just add the intercepter .h import, then add the call to sharedInstance on the intercepter object in your didFinishLaunchingWithOptions method, and be done with it.

Any Expo plugin that does that should look almost identical to the one for @react-native-firebase/app already, as that one does one import (of Firebase.h) and makes one call on an object. So it should be nearly copy/paste. https://github.com/invertase/react-native-firebase/blob/main/packages/app/plugin/src/ios/appDelegate.ts

If it works, then we will have moved the state of knowledge here forward and have a minimal + well-tested workaround at least, then in the future perhaps we can figure out why the dynamic link method is not being swizzled in early enough.

EquesMedia commented 1 year ago

Hi @mikehardy, sorry for the delay.

I created a plugin following your indications and it works. Here is the code:

const { mergeContents } = require("@expo/config-plugins/build/utils/generateCode");

const { withPlugins, withAppDelegate } = require("@expo/config-plugins");

function withRNFirebaseInitialDynamicLinkFirstInstallFix(config) {
  return withPlugins(config, [
    (config) =>
      withAppDelegate(config, (config) => {
        const { modResults } = config;
        let { contents } = modResults;

        const importIndex = /#import <Firebase\/Firebase.h>/g;

        contents = mergeContents({
          tag: "@react-native-firebase/app-dynamiclink-ios/import",
          src: contents,
          newSrc: "#import <RNFBDynamicLinksAppDelegateInterceptor.h>",
          anchor: importIndex,
          offset: 1,
          comment: "//"
        }).contents;

        const index = /-\s*\(BOOL\)\s*application:\s*\(UIApplication\s*\*\s*\)\s*\w+\s+didFinishLaunchingWithOptions:/g;

        const multilineMatcher = new RegExp(index.source + ".+\\n*{");
        const isHeaderMultiline = multilineMatcher.test(contents);

        contents = mergeContents({
          tag: "@react-native-firebase/app-dynamiclink-ios/fix",
          src: contents,
          newSrc: "[RNFBDynamicLinksAppDelegateInterceptor sharedInstance];",
          anchor: index,
          offset: isHeaderMultiline ? 2 : 1,
          comment: "//"
        }).contents;

        config.modResults.contents = contents;

        return config;
      })
  ]);
}

module.exports = withRNFirebaseInitialDynamicLinkFirstInstallFix;

Hope it can find people who had this same issue with their Expo app. It was driving me nuts. πŸ˜†

austin43 commented 1 year ago

@EquesMedia Excellent! πŸ˜„. I still need to test it but the code injects properly in my AppDelegate. I was actually writing a full app.plugin.js for the dynamic-links package as I checked this thread, and just finished getting the tests + snapshot done. It's basically a copy of the @react-native-firebase/app plugin per https://github.com/invertase/react-native-firebase/issues/2660#issuecomment-1271863864.

@mikehardy Do we want this as an official plugin? If so, I can submit my PR.

mikehardy commented 1 year ago

@austin43 apologies for the delay, it's Firebase Summit week and I've been working on major new release items.

But yes! A plugin PR would be great to have

if you made an official plugin that implemented that and posted a PR (maybe also with a change to the non-Expo dynamic-links install docs mentioning the issue and the workaround of import / sharedInstance call?) that should help everyone with this until the underlying swizzle issue is figured out

github-actions[bot] commented 1 year ago

Hello πŸ‘‹, to help manage issues we automatically close stale issues.

This issue has been automatically marked as stale because it has not had activity for quite some time.Has this issue been fixed, or does it still require attention?

This issue will be closed in 15 days if no further activity occurs.

Thank you for your contributions.

mikehardy commented 1 year ago

This is a hot item, and has a workaround at least, but needs investigation

ilyatsykunov commented 1 year ago

Hi @EquesMedia I am yet to try your Expo plugin, but I just wanted to ask if you had to use any workarounds in your navigation to resolve the dynamic link, or was just the plugin sufficient?

EquesMedia commented 1 year ago

Hi @EquesMedia I am yet to try your Expo plugin, but I just wanted to ask if you had to use any workarounds in your navigation to resolve the dynamic link, or was just the plugin sufficient?

The plugin only fixes the bug that causes the dynamic link to not be detected when installing an iOS app via the app store.

If we talk "implementation", to be honest, I've noticed disparities between the data received on Android and iOS depending on where the data is coming from and when, but using the functions of the + react native Linking package, you should be able to capture all the possibilities.

What's next is stuff that I've seen, but haven't had time to understand what or why is was happening due to... being rushed at my work.

Here are the things I had to handle:

Hopefully it can help you a bit!

github-actions[bot] commented 1 year ago

Hello πŸ‘‹, to help manage issues we automatically close stale issues.

This issue has been automatically marked as stale because it has not had activity for quite some time.Has this issue been fixed, or does it still require attention?

This issue will be closed in 15 days if no further activity occurs.

Thank you for your contributions.

github-actions[bot] commented 1 year ago

Hello πŸ‘‹, to help manage issues we automatically close stale issues.

This issue has been automatically marked as stale because it has not had activity for quite some time.Has this issue been fixed, or does it still require attention?

This issue will be closed in 15 days if no further activity occurs.

Thank you for your contributions.

github-actions[bot] commented 1 year ago

Hello πŸ‘‹, to help manage issues we automatically close stale issues.

This issue has been automatically marked as stale because it has not had activity for quite some time.Has this issue been fixed, or does it still require attention?

This issue will be closed in 15 days if no further activity occurs.

Thank you for your contributions.