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 thehandleOpenURLglobal. 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 ascontinueUserActivity, and core Cordova does not forward that tohandleOpenURL. 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.
#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;// 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;
}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.
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.
<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/*" />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.
<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.
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_linksoruni_linksto receive the incoming URL — both are maintained Flutter packages and neither is ours. - POST that URL to
/api/v1/sdk/resolvewith your publishable key, usingpackage:http. The response is the same link object the SDK hands toonLink: 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].