react-native-sdk icon indicating copy to clipboard operation
react-native-sdk copied to clipboard

Deep links are not working when app is closed

Open harrydema opened this issue 4 years ago • 5 comments

Hello!

I configured the deep links as explained on the docs and the deep links are working fine when the app is opened, in background, and you tap over a deep link.

But when the app is closed, the urlHandler is not fired, so the links are not opened.

I've only tested in IOS so far.

This is my AppDelegate.me:

#import "AppDelegate.h"
#import "BraintreeCore.h"

#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <GoogleMaps/GoogleMaps.h>
#import <FBSDKCoreKit/FBSDKCoreKit.h>
#import <CommonUISDK/CommonUISDK.h>
#import <UserNotifications/UserNotifications.h>
#import <React/RCTLinkingManager.h>
#import <Firebase.h>
@import IterableSDK;

#if DEBUG
#ifdef FB_SONARKIT_ENABLED
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>


static void InitializeFlipper(UIApplication *application) {
  FlipperClient *client = [FlipperClient sharedClient];
  SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
  [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
  [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
  [client addPlugin:[FlipperKitReactPlugin new]];
  [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
  [client start];
}
#endif
#endif

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  [GMSServices provideAPIKey:@"string/GOOGLE_MAPS_KEY"];
  if ([FIRApp defaultApp] == nil) {
    [FIRApp configure];
  }
  #if DEBUG
    #ifdef FB_SONARKIT_ENABLED
      InitializeFlipper(application);
    #endif
  #endif

  [self setupNotifications];
  [ZDKCommonTheme currentTheme].primaryColor = [UIColor colorWithRed: 0.25 green: 0.15 blue: 0.23 alpha: 1.00];
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                                                   moduleName:@"Test"
                                            initialProperties:nil];

  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  
  [BTAppSwitch setReturnURLScheme:self.paymentsURLScheme];
  
  return YES;
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

- (NSString *)paymentsURLScheme {
    NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
    return [NSString stringWithFormat:@"%@.%@", bundleIdentifier, @"payments"];
}

#pragma mark - UNUserNotificationCenterDelegate
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
    completionHandler(UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound);
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
    [IterableAppIntegration userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler];
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    [IterableAPI registerToken:deviceToken];
}

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    [IterableAppIntegration application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}

#pragma mark — Handling URLs

- (BOOL)application:(UIApplication *)application
        openURL:(NSURL *)url
        options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
  
  if ([url.scheme localizedCaseInsensitiveCompare:self.paymentsURLScheme] == NSOrderedSame) {
      return [BTAppSwitch handleOpenURL:url options:options];
  }

  if ([[url.scheme substringToIndex: 2] isEqualToString:@"fb"]) {
    return [[FBSDKApplicationDelegate sharedInstance]application:application
                                                         openURL:url
                                                         options:options];
  }
  BOOL handled = [RCTLinkingManager application:application openURL:url options:options] || [[FBSDKApplicationDelegate sharedInstance] application:application
    openURL:url
    sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]
    annotation:options[UIApplicationOpenURLOptionsAnnotationKey]
  ];
  
  return handled;
}


- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity
 restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
  return [IterableAPI handleUniversalLink:userActivity.webpageURL] || [RCTLinkingManager application:application
                   continueUserActivity:userActivity
                     restorationHandler:restorationHandler];
}

#pragma mark - private
- (void) setupNotifications {
    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
    center.delegate = self;
}


@end

This is my configuration on the JS side. It is executed on the App.js:

const config = new IterableConfig();
config.checkForDeferredDeepLink = true;
config.inAppHandler = () => {
  return IterableInAppShowResponse.show;
};
config.urlHandler = url => {
  if (isUrlHandledByApp(url)) {
    Linking.openURL(changeUrlToCustomScheme(url));
    return true;
  }
  return false;
};
Iterable.initialize(Config.ITERABLE_API_KEY, config);

Please help.

Thanks in advance!

harrydema avatar Apr 19 '21 15:04 harrydema

We are seeing this with

"@iterable/react-native-sdk": "1.0.26"
"react-native": "0.64.1",

I also tried version 1.0.28, on both iOS and Android. Never got a url to open after push from a terminated app. Worked for both backgrounded and foregrounded.

We also tested on 1.1.0-beta2. The IOS build failed, Android built but the same issue occured.

jehartzog avatar May 27 '21 17:05 jehartzog

I should mention our urlHandler was designed to just let the OS handle our custom schema links, like this:

  iterableUrlHandler = (url: string, context: IterableActionContext) => {
    return false;
  };

jehartzog avatar May 27 '21 17:05 jehartzog

TL/DR: We found and fixed the issue, it was particular to our project setup. Iterable push deep links are working correctly on @iterable/react-native-sdk: "1.0.28" with both OS's in all conditions (terminated, backgrounded, foregrounded).

I'm not completely sure if this is something that should be tweaked for this SDK, or given a note in the docs, or even belongs as an issue here at all. I just want to share what we found.

Long version.

  1. Background on our project

    1. We're using react-navigation v6, using their built-in linking to route based on incoming links
    2. We used ignite cli to scaffold our current app
    3. They structure it so you setup your services when you create the root store.
    4. Before the root nav is created (and all services have completed setup), none of the UI is rendered. There are trade offs to this, but it avoids race conditions of seeing flashes of temporary frames, or wasting CPU on renders that aren't needed on startup.
    5. It is also built with dependency injection, rather than singleton module imports.
    6. We had created an IterableService that relies on a DI injected configService to init the Iterable SDK as part of the overall await envrionment.setup()
  2. What the probem was

    1. When Iterable.initialize() is called, the SDK does a number of things, one of which appears to be handling any deep links which were sent via Iterable pushes and calling Linking.openUrl().
    2. Our problem was that Iterable.initialize() was called before our react-navigation RootNavigator was mounted, meaning we didn't have a listener yet set up to capture the Linking.openUrl() that Iterable.initialize() sent over.
    3. Normally this isn't an issue for a terminated app getting a deep link, since the linking libs always make a single call to getInitialURL() to see if a link request was sent before the listener was registered. But since Iterable.initialize() is called after the app is open, the URL it sends doesn't get into getInitialUrl(), and only sent to current listeners (which need to exist already to capture the events).
  3. Our fix

    1. I thought about quite a few different wants to fix this, but I settled on not delaying the mount of RootNavigator until all the services are .setup. There are a number of impacts this has to initial app launch which I won't get into here, but a side effect being that our linking url listener is sure to be active before we Iterable.initialize()

app.tsx

  React.useEffect(() => {
    if (dependencies) {
      dependencies.setup();
    }
  }, [dependencies]);

jehartzog avatar May 29 '21 15:05 jehartzog

@harrydema Short version of above post.

  1. For testing, throw a 5 second setTimeout around your Iterable.initialize(Config.ITERABLE_API_KEY, config);.
  2. If that fixes the deep link issue for you, then you have the same issue that we had. You need to make sure your RootNavigator is mounted before calling Iterable.initialize. Remove the setTimeout and refactor to make it work.

jehartzog avatar May 29 '21 15:05 jehartzog

We had a similar issue like @jehartzog so this was what solved our issue

Platform: IOS (Tested it only on IOS)

  1. Upgraded to the RN Iterable SDK newest version 1.1.3
  2. Ensure our React Native Root Navigator was mounted before the SDK initialization. As @jehartzog suggested above.
  3. Ensure to have ONLY one RN Iterable SDK instance initialized. (We had an extra call to the SDK, this was the root cause once we have ensured the Root Navigator was mounted before the SDK call) @jehartzog thanks Hopefully, this help someone here.

blarzHernandez avatar Feb 15 '22 17:02 blarzHernandez