Skip to content

Documentation

Cordova, React Native, Flutter and the web

Cordova and React Native use the same package by a different route; Flutter has no Dart package and uses the REST API instead; and web is what the SDK does in a browser today.

What we have actually run

Capacitor is the one to trust. The SDK was built against it, it is what our first customer uses, and it is the one described in the quickstart.

Cordova is proven on a real iPhone — a customer’s app receives universal links and their payloads through the method below. React Native and plain web are written and unit-tested but have not been run inside a real application of that kind. The platform layer for each is exercised through an injected fake, which proves the SDK does the right thing when a platform behaves as documented — and proves nothing about whether it behaves as documented.

We would rather tell you that than let you find out. If you are integrating on one of these and something is wrong, write to [email protected] and it will be fixed quickly — you will be the first, and that is worth something to us.

Cordova

Detected through the cordova global. Two different kinds of link arrive by two different routes, and they are not interchangeable:

  • A custom scheme — yourapp://…, which smart routing uses — arrives through the handleOpenURL global. The SDK chains onto yours rather than replacing it.
  • A universal link — https://…, which is what you hand to customers — does not. iOS delivers it as continueUserActivity, and core Cordova does not forward that to handleOpenURL. It needs a plugin.

Without such a plugin your app will open and no link will ever reach the SDK. Nothing errors. iOS launches the app, the activity is discarded, and onLink never fires — which looks exactly like the SDK being broken and is not.

AppDelegate.m — at the top, after the existing importsobjc
#import <WebKit/WebKit.h>

// QubeRoute: state for the native-side delivery retry. File scope rather than
// instance state because the app delegate is a singleton and this outlives no
// launch.
static NSTimer *qrDeliveryTimer = nil;
static NSInteger qrDeliveryTicks = 0;
No Cordova import is needed: AppDelegate.h already imports <Cordova/CDVAppDelegate.h>, which is where viewController is declared.
AppDelegate.m — inside @implementation AppDelegateobjc
// QubeRoute: hand a universal link to the web view, retrying NATIVELY.
//
// Each tick is an independent evaluation into whatever document the web view
// currently holds, so it survives the navigation to index.html that destroys
// any JavaScript injected beforehand.
- (void)qrDeliverURL:(NSString *)urlString toWebView:(WKWebView *)webView
{
    [qrDeliveryTimer invalidate];
    qrDeliveryTimer = nil;
    qrDeliveryTicks = 0;

    NSString *escaped = [[urlString
        stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]
        stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"];

    __weak WKWebView *weakWebView = webView;

    qrDeliveryTimer = [NSTimer scheduledTimerWithTimeInterval:0.25
                                                      repeats:YES
                                                        block:^(NSTimer *timer) {
        WKWebView *wv = weakWebView;
        qrDeliveryTicks += 1;

        if (wv == nil || qrDeliveryTicks > 60) {
            NSLog(@"QUBEROUTE NATIVE 9 - gave up after %ld ticks, never delivered",
                  (long)qrDeliveryTicks);
            [timer invalidate];
            qrDeliveryTimer = nil;
            return;
        }

        NSString *probe = [NSString stringWithFormat:@"%@%@%@",
            @"(function(u){ if (typeof window.handleOpenURL === 'function') { ",
            @"window.handleOpenURL(u); return 'DELIVERED'; } return 'NOT_READY'; })('",
            [NSString stringWithFormat:@"%@');", escaped]];

        [wv evaluateJavaScript:probe completionHandler:^(id result, NSError *error) {
            NSLog(@"QUBEROUTE NATIVE 6 - tick %ld url=%@ isLoading=%d result=%@ error=%@",
                  (long)qrDeliveryTicks,
                  wv.URL.absoluteString,
                  (int)wv.isLoading,
                  result,
                  error.localizedDescription);

            if ([result isKindOfClass:[NSString class]] &&
                [(NSString *)result isEqualToString:@"DELIVERED"]) {
                NSLog(@"QUBEROUTE NATIVE 7 - DELIVERED after %ld ticks", (long)qrDeliveryTicks);
                [qrDeliveryTimer invalidate];
                qrDeliveryTimer = nil;
            }
        }];
    }];
}

- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
 restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> *restorableObjects))restorationHandler
{
    NSLog(@"QUBEROUTE NATIVE 1 - continueUserActivity fired, activityType=%@",
          userActivity.activityType);

    if (![userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
        NSLog(@"QUBEROUTE NATIVE 2 - not a web-browsing activity, ignoring");
        return NO;
    }

    NSURL *url = userActivity.webpageURL;
    NSLog(@"QUBEROUTE NATIVE 3 - url=%@", url.absoluteString);
    if (url == nil) {
        NSLog(@"QUBEROUTE NATIVE 3a - webpageURL was nil");
        return NO;
    }

    NSLog(@"QUBEROUTE NATIVE 4 - viewController=%@", self.viewController);

    UIView *view = self.viewController.webView;
    NSLog(@"QUBEROUTE NATIVE 5 - webView=%@ class=%@",
          view, NSStringFromClass([view class]));

    if (![view isKindOfClass:[WKWebView class]]) {
        NSLog(@"QUBEROUTE NATIVE 5a - no WKWebView available, cannot deliver");
        return NO;
    }

    WKWebView *webView = (WKWebView *)view;
    NSLog(@"QUBEROUTE NATIVE 5b - at fire time URL=%@ isLoading=%d",
          webView.URL.absoluteString, (int)webView.isLoading);

    [self qrDeliverURL:url.absoluteString toWebView:webView];
    return YES;
}
The retry is native. A JavaScript timer injected before index.html has loaded is destroyed when the new document replaces the JavaScript context; a native timer re-evaluating into whatever document the web view currently holds is not. Each NSLog line is explained in docs/cordova-appdelegate-universal-links.md. This exact code is compiled by clang on macOS inside a real Cordova project on every change.

You also need the Associated Domains entitlement naming your link host, which is the same requirement every iOS universal link has. platforms/ios is generated: if you run cordova platform rm ios && cordova platform add ios, this edit is erased and must be reapplied. Keep it in a config.xml hook, or commit platforms/ios.

Do not use cordova-universal-links-plugin

cordova-universal-links-plugin also delivers universal links, by exposing window.universalLinks, and the SDK still subscribes to it if it is there. Its most recent release is October 2016, older than cordova-ios 5, 6 and 7. We do not recommend it.

It implements continueUserActivity as an Objective-C category on your AppDelegate. A category and your own method of the same name are the same selector on the same class, and the category wins — so if the plugin is installed alongside the method above, your code never runs. There is no error, no warning, and the symptom is a link that opens the app while onLink stays silent. Remove the plugin before adding the method: cordova plugin rm cordova-universal-links-plugin.

The five lines are the same as Capacitor’s. What is different is underneath, and it is worth a paragraph because it explains the one extra step below.

Capacitor answers: the SDK asks it for the launch URL and gets one. Cordova tells — it calls handleOpenURL shortly after launch, at a moment of its own choosing. So on Cordova the SDK waits briefly at startup to see whether a link is coming, before deciding this was an ordinary launch. That wait costs your app nothing: init() does not block on it.

If you already define handleOpenURL, keep it. The SDK chains onto yours rather than replacing it — taking it over would break your app to serve ourselves.

Cordova — identical to Capacitortypescript
import { QubeRoute } from '@quberoute/sdk'

const qr = await QubeRoute.init({ key: 'qr_live_YOUR_KEY' })
qr.onLink(link => routeTo(link.data.path))

Cordova blocks outbound requests until you allow them

A Cordova WebView will not let the SDK call us unless its Content Security Policy says so, and the default index.html Cordova generates allows only 'self'. The symptom is TypeError: Load failed through onError, with nothing arriving on your dashboard at all — the request never leaves the phone.

If your index.html has a <meta http-equiv="Content-Security-Policy"> tag, add our host to connect-src. If you use cordova-plugin-whitelist, add the allow-navigation line too.

index.html — connect-src, and config.xml below itxml
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self' data: gap:;
               connect-src 'self' https://app.quberoute.com;
               style-src 'self' 'unsafe-inline';
               media-src *;
               img-src 'self' data: content:;">

<!-- config.xml, if you have cordova-plugin-whitelist -->
<allow-navigation href="https://app.quberoute.com/*" />
You do not need an App Transport Security exception: our endpoint negotiates TLS 1.3 with forward secrecy and a SHA-256 certificate, which satisfies Apple's defaults.

One extra step on Cordova, and what it buys you

Cordova can call handleOpenURL before your JavaScript bundle has loaded — so before the SDK exists to hear it. Nothing inside the SDK can recover a call that happened before it was there.

Add these two lines in index.html, above your bundle. The SDK drains the buffer when it starts.

index.html — before your app bundlexml
<script>
  window.QubeRoutePendingUrls = [];
  function handleOpenURL(url) { window.QubeRoutePendingUrls.push(url); }
</script>

Without it, warm resumes still work — a link tapped while your app is already running arrives normally. What can be missed is a cold start: the app not running, somebody taps a link, and the app opens without the SDK ever hearing which link it was. That is the case worth protecting, because it is the commonest one.

It catches custom schemes, not universal links. handleOpenURL is how Cordova delivers yourapp://…, so this buffer stays empty when an https:// link opens your app — on a working installation as much as a broken one. If you are debugging a universal link, window.QubeRoutePendingUrls and whether handleOpenURL fired tell you nothing. Check whether window.universalLinks exists and whether its subscription fires instead.

React Native

React Native has no localStorage and no module registry we can reach without adding a dependency — and adding react-native as a peer dependency would make this package refuse to install in a Capacitor project. So React Native needs two things passed in: storage, and Linking.

React Native — pass in what we cannot reachtypescript
import AsyncStorage from '@react-native-async-storage/async-storage'
import { Linking } from 'react-native'
import { QubeRoute } from '@quberoute/sdk'

// The SDK reads Linking from this global rather than importing it.
;(globalThis as Record<string, unknown>).QubeRouteLinking = Linking

const qr = await QubeRoute.init({
  key: 'qr_live_YOUR_KEY',
  storage: {
    get: key => AsyncStorage.getItem(key),
    set: (key, value) => AsyncStorage.setItem(key, value),
    remove: key => AsyncStorage.removeItem(key),
  },
})

qr.onLink(link => navigation.navigate(link.data.screen as string))

Without the storage adapter the SDK falls back to memory, which works for one session and forgets everything on the next launch — so every launch looks like a first launch and the deferred lookup runs every time. Nothing breaks, but it is wasteful and the attribution will be wrong.

Flutter

There is no Dart package, and there is no plan to pretend otherwise. The SDK is JavaScript. A Flutter app cannot import it, and leaving Flutter off this page — which is what we did until now — reads to a Flutter developer as “not supported” rather than “supported by a different route”.

The different route is the REST API, which is what the SDK calls anyway.

  • Use app_links or uni_links to receive the incoming URL — both are maintained Flutter packages and neither is ours.
  • POST that URL to /api/v1/sdk/resolve with your publishable key, using package:http. The response is the same link object the SDK hands to onLink: the alias, and your parameters.
  • For events, POST to the SDK events endpoint with the same key. The API reference has both shapes.

What you give up. The deferred-matching handshake on first launch is the one thing the package does that a single REST call does not — it is a sequence, not an endpoint. A link that opens an app somebody already has is one call and behaves identically. A link tapped before the app exists is the case that needs the SDK, and on Flutter you would be building that sequence yourself against the same endpoints.

If a Dart package would decide it for you, tell us. What people ask for is what gets built.

Plain web

On the web the current address is the link, so the SDK reads the page address at startup and there is no resume: a new link is a new page load. localStorage is used automatically.

Only the origin and path are sent — never the query string or the fragment. Our own links carry the alias in the path, and a page address on your site routinely carries an order id, an email or a session token in its query string, none of which is ours to receive.

A visit to a bare address with no path is treated as an ordinary launch and makes no request at all. There is no deferred case on the web, because there is no install: a first visit that is not a link produces nothing.

What is the same everywhere

  • the five lines, and the shape of link.data;
  • never crashing the host app, and never blocking startup;
  • the offline queue, and its hundred-event cap;
  • what is collected — the list on the privacy page is complete for every platform.

Ask the documentation

It answers from these pages only, and links what it used. If the answer is not here it says so rather than guessing — then email [email protected].

← All documentation