├── docs └── images │ ├── app-id.jpg │ ├── branch-io.jpg │ ├── app-id-team-prefix.jpg │ ├── developer-console.jpg │ └── app-associated-domains.jpg ├── src ├── ios │ ├── Utils │ │ ├── NSBundle+CULPlugin.m │ │ └── NSBundle+CULPlugin.h │ ├── JS │ │ ├── CDVInvokedUrlCommand+CULPlugin.m │ │ ├── CDVInvokedUrlCommand+CULPlugin.h │ │ ├── CDVPluginResult+CULPlugin.h │ │ └── CDVPluginResult+CULPlugin.m │ ├── Model │ │ ├── CULPath.m │ │ ├── CULPath.h │ │ ├── CULHost.m │ │ └── CULHost.h │ ├── Parser │ │ ├── CULConfigXmlParser.h │ │ ├── CULXmlTags.m │ │ ├── CULXmlTags.h │ │ └── CULConfigXmlParser.m │ ├── AppDelegate+CULPlugin.h │ ├── CULPlugin.h │ ├── AppDelegate+CULPlugin.m │ └── CULPlugin.m └── android │ └── com │ └── nordnetab │ └── cordova │ └── ul │ ├── js │ └── JSAction.java │ ├── model │ ├── ULPath.java │ ├── ULHost.java │ └── JSMessage.java │ ├── parser │ ├── XmlTags.java │ └── ULConfigXmlParser.java │ └── UniversalLinksPlugin.java ├── ul_web_hooks └── android_web_hook_tpl.html ├── LICENSE ├── package.json ├── hooks ├── lib │ ├── xmlHelper.js │ ├── configXmlHelper.js │ ├── configXmlParser.js │ ├── ios │ │ ├── projectEntitlements.js │ │ ├── appleAppSiteAssociationFile.js │ │ └── xcodePreferences.js │ └── android │ │ ├── webSiteHook.js │ │ └── manifestWriter.js ├── iosBeforePrepareHook.js ├── afterPrepareHook.js └── beforePluginInstallHook.js ├── www └── universal_links.js ├── CHANGELOG.md ├── plugin.xml └── README.md /docs/images/app-id.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poetic/cordova-universal-links-plugin/master/docs/images/app-id.jpg -------------------------------------------------------------------------------- /docs/images/branch-io.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poetic/cordova-universal-links-plugin/master/docs/images/branch-io.jpg -------------------------------------------------------------------------------- /docs/images/app-id-team-prefix.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poetic/cordova-universal-links-plugin/master/docs/images/app-id-team-prefix.jpg -------------------------------------------------------------------------------- /docs/images/developer-console.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poetic/cordova-universal-links-plugin/master/docs/images/developer-console.jpg -------------------------------------------------------------------------------- /docs/images/app-associated-domains.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poetic/cordova-universal-links-plugin/master/docs/images/app-associated-domains.jpg -------------------------------------------------------------------------------- /src/ios/Utils/NSBundle+CULPlugin.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSBundle+CULPlugin.m 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import "NSBundle+CULPlugin.h" 8 | 9 | @implementation NSBundle (CULPlugin) 10 | 11 | + (NSString *)pathToCordovaConfigXml { 12 | return [[NSBundle mainBundle] pathForResource:@"config" ofType:@"xml"]; 13 | } 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /src/ios/JS/CDVInvokedUrlCommand+CULPlugin.m: -------------------------------------------------------------------------------- 1 | // 2 | // CDVInvokedUrlCommand+CULPlugin.m 3 | // 4 | // Created by Nikolay Demyankov on 08.12.15. 5 | // 6 | 7 | #import "CDVInvokedUrlCommand+CULPlugin.h" 8 | 9 | @implementation CDVInvokedUrlCommand (CULPlugin) 10 | 11 | - (NSString *)eventName { 12 | if (self.arguments.count == 0) { 13 | return nil; 14 | } 15 | 16 | return self.arguments[0]; 17 | } 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /src/ios/Utils/NSBundle+CULPlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSBundle+CULPlugin.h 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import 8 | 9 | /** 10 | * Helper category to work with NSBundle. 11 | */ 12 | @interface NSBundle (CULPlugin) 13 | 14 | /** 15 | * Path to the config.xml file in the project. 16 | * 17 | * @return path to the config file 18 | */ 19 | + (NSString *)pathToCordovaConfigXml; 20 | 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /src/ios/JS/CDVInvokedUrlCommand+CULPlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // CDVInvokedUrlCommand+CULPlugin.h 3 | // 4 | // Created by Nikolay Demyankov on 08.12.15. 5 | // 6 | 7 | #import 8 | 9 | /** 10 | * Category to get the event name from the request, that is sent from JS side. 11 | */ 12 | @interface CDVInvokedUrlCommand (CULPlugin) 13 | 14 | /** 15 | * Get event name from JS request. 16 | * 17 | * @return event name 18 | */ 19 | - (NSString *)eventName; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /src/ios/Model/CULPath.m: -------------------------------------------------------------------------------- 1 | // 2 | // CULPath.m 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import "CULPath.h" 8 | 9 | @implementation CULPath 10 | 11 | - (instancetype)initWithUrlPath:(NSString *)urlPath andEvent:(NSString *)event { 12 | self = [super init]; 13 | if (self) { 14 | _url = [urlPath stringByReplacingOccurrencesOfString:@"*" withString:@".*"]; 15 | _event = event; 16 | } 17 | 18 | return self; 19 | } 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /src/ios/Parser/CULConfigXmlParser.h: -------------------------------------------------------------------------------- 1 | // 2 | // CULConfigXmlParser.h 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import 8 | #import "CULHost.h" 9 | 10 | /** 11 | * Parser for config.xml. Reads only plugin-specific preferences. 12 | */ 13 | @interface CULConfigXmlParser : NSObject 14 | 15 | /** 16 | * Parse config.xml 17 | * 18 | * @return list of hosts, defined in the config file 19 | */ 20 | + (NSArray *)parse; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /src/android/com/nordnetab/cordova/ul/js/JSAction.java: -------------------------------------------------------------------------------- 1 | package com.nordnetab.cordova.ul.js; 2 | 3 | /** 4 | * Created by Nikolay Demyankov on 09.09.15. 5 | *

6 | * Class holds list of method names that is called from JS side. 7 | */ 8 | public final class JSAction { 9 | 10 | /** 11 | * Subscribe to event. 12 | */ 13 | public static final String SUBSCRIBE = "jsSubscribeForEvent"; 14 | 15 | /** 16 | * Unsubscribe from event. 17 | */ 18 | public static final String UNSUBSCRIBE = "jsUnsubscribeFromEvent"; 19 | } 20 | -------------------------------------------------------------------------------- /src/ios/AppDelegate+CULPlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+CULPlugin.h 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import "AppDelegate.h" 8 | 9 | /** 10 | * Category for the AppDelegate that overrides application:continueUserActivity:restorationHandler method, 11 | * so we could handle application launch when user clicks on the link in the browser. 12 | */ 13 | @interface AppDelegate (CULPlugin) 14 | 15 | - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /src/ios/Parser/CULXmlTags.m: -------------------------------------------------------------------------------- 1 | // 2 | // CULXmlTags.m 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import "CULXmlTags.h" 8 | 9 | @implementation CULXmlTags 10 | 11 | NSString *const kCULMainXmlTag = @"universal-links"; 12 | 13 | NSString *const kCULHostXmlTag = @"host"; 14 | NSString *const kCULHostSchemeXmlAttribute = @"scheme"; 15 | NSString *const kCULHostNameXmlAttribute = @"name"; 16 | NSString *const kCULHostEventXmlAttribute = @"event"; 17 | 18 | NSString *const kCULPathXmlTag = @"path"; 19 | NSString *const kCULPathUrlXmlAttribute = @"url"; 20 | NSString *const kCULPathEventXmlAttribute = @"event"; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /src/ios/JS/CDVPluginResult+CULPlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // CDVPluginResult+CULPlugin.h 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import 8 | #import "CULHost.h" 9 | 10 | /** 11 | * Category to simplify plugin result generation. 12 | */ 13 | @interface CDVPluginResult (CULPlugin) 14 | 15 | /** 16 | * Get CDVPluginResult instance with information about the launch url that is send to JS. 17 | * 18 | * @param host host that corresponds to launch url 19 | * @param originalURL launching url 20 | * 21 | * @return instance of the CDVPluginResult 22 | */ 23 | + (instancetype)resultWithHost:(CULHost *)host originalURL:(NSURL *)originalURL; 24 | 25 | - (BOOL)isResultForEvent:(NSString *)eventName; 26 | 27 | - (NSString *)eventName; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /src/ios/Model/CULPath.h: -------------------------------------------------------------------------------- 1 | // 2 | // CULPath.h 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import 8 | 9 | /** 10 | * Model for entry for host in config.xml. 11 | */ 12 | @interface CULPath : NSObject 13 | 14 | /** 15 | * Event name that is dispatched when application is launched from the link with this path. 16 | * Defined as 'event' attribute. 17 | */ 18 | @property (nonatomic, readonly, strong) NSString *event; 19 | 20 | /** 21 | * Path url. 22 | * Defined as 'url' attribute. 23 | */ 24 | @property (nonatomic, readonly, strong) NSString *url; 25 | 26 | /** 27 | * Constructor 28 | * 29 | * @param urlPath url path 30 | * @param event event name 31 | * 32 | * @return instance of the CULPath 33 | */ 34 | - (instancetype)initWithUrlPath:(NSString *)urlPath andEvent:(NSString *)event; 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /ul_web_hooks/android_web_hook_tpl.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | [__LINKS__] 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/ios/Model/CULHost.m: -------------------------------------------------------------------------------- 1 | // 2 | // CULHost.m 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import "CULHost.h" 8 | 9 | // default event name 10 | static NSString *const DEFAULT_EVENT = @"didLaunchAppFromLink"; 11 | 12 | // default host scheme 13 | static NSString *const DEFAULT_SCHEME = @"http"; 14 | 15 | @interface CULHost() { 16 | NSMutableArray *_paths; 17 | } 18 | 19 | @end 20 | 21 | @implementation CULHost 22 | 23 | - (instancetype)initWithHostName:(NSString *)name scheme:(NSString *)scheme event:(NSString *)event { 24 | self = [super init]; 25 | if (self) { 26 | _event = event ? event : DEFAULT_EVENT; 27 | _scheme = scheme ? scheme : DEFAULT_SCHEME; 28 | _name = name; 29 | _paths = [[NSMutableArray alloc] init]; 30 | } 31 | return self; 32 | } 33 | 34 | - (void)addPath:(CULPath *)path { 35 | if (path) { 36 | [_paths addObject:path]; 37 | } 38 | } 39 | 40 | - (NSArray *)paths { 41 | return _paths; 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /src/ios/CULPlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // CULPlugin.h 3 | // 4 | // Created by Nikolay Demyankov on 14.09.15. 5 | // 6 | 7 | #import 8 | #import 9 | 10 | /** 11 | * Plugin main class. 12 | */ 13 | @interface CULPlugin : CDVPlugin 14 | 15 | /** 16 | * Subscribe to event. 17 | * 18 | * @param command command from js side with event name and callback id. 19 | */ 20 | - (void)jsSubscribeForEvent:(CDVInvokedUrlCommand *)command; 21 | 22 | /** 23 | * Unsubscribe from event. 24 | * 25 | * @param command command from js side with event name 26 | */ 27 | - (void)jsUnsubscribeFromEvent:(CDVInvokedUrlCommand *)command; 28 | 29 | /** 30 | * Try to hanlde application launch when user clicked on the link. 31 | * 32 | * @param userActivity object with information about the application launch 33 | * 34 | * @return true - if this is a universal link and it is defined in config.xml; otherwise - false 35 | */ 36 | - (BOOL)handleUserActivity:(NSUserActivity *)userActivity; 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /src/ios/AppDelegate+CULPlugin.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+CULPlugin.m 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import "AppDelegate+CULPlugin.h" 8 | #import "CULPlugin.h" 9 | 10 | /** 11 | * Plugin name in config.xml 12 | */ 13 | static NSString *const PLUGIN_NAME = @"UniversalLinks"; 14 | 15 | @implementation AppDelegate (CULPlugin) 16 | 17 | - (BOOL)application:(UIApplication *)application 18 | continueUserActivity:(NSUserActivity *)userActivity 19 | restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler { 20 | // ignore activities that are not for Universal Links 21 | if (![userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb] || userActivity.webpageURL == nil) { 22 | return NO; 23 | } 24 | 25 | // get instance of the plugin and let it handle the userActivity object 26 | CULPlugin *plugin = [self.viewController getCommandInstance:PLUGIN_NAME]; 27 | if (plugin == nil) { 28 | return NO; 29 | } 30 | 31 | return [plugin handleUserActivity:userActivity]; 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /src/android/com/nordnetab/cordova/ul/model/ULPath.java: -------------------------------------------------------------------------------- 1 | package com.nordnetab.cordova.ul.model; 2 | 3 | /** 4 | * Created by Nikolay Demyankov on 09.09.15. 5 | *

6 | * Model for entry for host in config.xml 7 | */ 8 | public class ULPath { 9 | 10 | private final String url; 11 | private final String event; 12 | 13 | /** 14 | * Constructor 15 | * 16 | * @param url path url 17 | * @param event event name 18 | */ 19 | public ULPath(final String url, final String event) { 20 | this.url = url.replace("*", "(.*)"); 21 | this.event = event; 22 | } 23 | 24 | /** 25 | * Getter for path url. 26 | * Defined as 'url' attribute. 27 | * 28 | * @return path url 29 | */ 30 | public String getUrl() { 31 | return url; 32 | } 33 | 34 | /** 35 | * Getter for the event name that is dispatched when application is launched from the link with this path. 36 | * Defined as 'event' attribute. 37 | * 38 | * @return event name 39 | */ 40 | public String getEvent() { 41 | return event; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 nordnet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/ios/Parser/CULXmlTags.h: -------------------------------------------------------------------------------- 1 | // 2 | // CULXmlTags.h 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import 8 | 9 | /** 10 | * XML tags that is used in config.xml to specify plugin preferences. 11 | */ 12 | @interface CULXmlTags : NSObject 13 | 14 | /** 15 | * Main tag in which we define plugin related stuff 16 | */ 17 | extern NSString *const kCULMainXmlTag; 18 | 19 | /** 20 | * Host main tag 21 | */ 22 | extern NSString *const kCULHostXmlTag; 23 | 24 | /** 25 | * Scheme attribute for the host entry 26 | */ 27 | extern NSString *const kCULHostSchemeXmlAttribute; 28 | 29 | /** 30 | * Name attribute for the host entry 31 | */ 32 | extern NSString *const kCULHostNameXmlAttribute; 33 | 34 | /** 35 | * Event attribute for the host entry 36 | */ 37 | extern NSString *const kCULHostEventXmlAttribute; 38 | 39 | /** 40 | * Path main tag 41 | */ 42 | extern NSString *const kCULPathXmlTag; 43 | 44 | /** 45 | * Url attribute for the path entry 46 | */ 47 | extern NSString *const kCULPathUrlXmlAttribute; 48 | 49 | /** 50 | * Event attribute for the path entry 51 | */ 52 | extern NSString *const kCULPathEventXmlAttribute; 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /src/android/com/nordnetab/cordova/ul/parser/XmlTags.java: -------------------------------------------------------------------------------- 1 | package com.nordnetab.cordova.ul.parser; 2 | 3 | /** 4 | * Created by Nikolay Demyankov on 10.09.15. 5 | *

6 | * XML tags that is used in config.xml to specify plugin preferences. 7 | */ 8 | final class XmlTags { 9 | 10 | /** 11 | * Main tag in which we define plugin related stuff 12 | */ 13 | public static final String MAIN_TAG = "universal-links"; 14 | 15 | /** 16 | * Host main tag 17 | */ 18 | public static final String HOST_TAG = "host"; 19 | 20 | /** 21 | * Scheme attribute for the host entry 22 | */ 23 | public static final String HOST_SCHEME_ATTRIBUTE = "scheme"; 24 | 25 | /** 26 | * Name attribute for the host entry 27 | */ 28 | public static final String HOST_NAME_ATTRIBUTE = "name"; 29 | 30 | /** 31 | * Event attribute for the host entry 32 | */ 33 | public static final String HOST_EVENT_ATTRIBUTE = "event"; 34 | 35 | /** 36 | * Path main tag 37 | */ 38 | public static final String PATH_TAG = "path"; 39 | 40 | /** 41 | * Url attribute for the path entry 42 | */ 43 | public static final String PATH_URL_TAG = "url"; 44 | 45 | /** 46 | * Event attribute for the path entry 47 | */ 48 | public static final String PATH_EVENT_TAG = "event"; 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-universal-links-plugin", 3 | "version": "1.1.0", 4 | "description": "Cordova plugin to add in your application support for Universal Links (iOS 9) and Deep Links (Android). Basically, open application through the link in the browser.", 5 | "cordova": { 6 | "id": "cordova-universal-links-plugin", 7 | "platforms": [ 8 | "ios", 9 | "android" 10 | ] 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/nordnet/cordova-universal-links-plugin.git" 15 | }, 16 | "keywords": [ 17 | "cordova", 18 | "links", 19 | "universal", 20 | "deep links", 21 | "universal links", 22 | "ecosystem:cordova", 23 | "cordova-ios", 24 | "cordova-android", 25 | "ios", 26 | "android" 27 | ], 28 | "engines": [ 29 | { 30 | "name": "cordova-ios", 31 | "version": ">=3.8" 32 | }, 33 | { 34 | "name": "cordova-android", 35 | "version": ">=4" 36 | } 37 | ], 38 | "dependencies": { 39 | "mkpath": ">=1.0.0", 40 | "xml2js": ">=0.4", 41 | "rimraf": ">=2.4", 42 | "node-version-compare": ">=1.0.1", 43 | "plist": ">=1.2.0" 44 | }, 45 | "author": "Nikolay Demyankov for Nordnet Bank AB", 46 | "license": "MIT", 47 | "bugs": { 48 | "url": "https://github.com/nordnet/cordova-universal-links-plugin/issues" 49 | }, 50 | "homepage": "https://github.com/nordnet/cordova-universal-links-plugin#readme" 51 | } 52 | -------------------------------------------------------------------------------- /src/ios/Model/CULHost.h: -------------------------------------------------------------------------------- 1 | // 2 | // CULHost.h 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import 8 | #import "CULPath.h" 9 | 10 | /** 11 | * Model for entry, specified in config.xml. 12 | */ 13 | @interface CULHost : NSObject 14 | 15 | /** 16 | * Host name. 17 | * Defined as 'name' attribute. 18 | */ 19 | @property (nonatomic, readonly, strong) NSString *name; 20 | 21 | /** 22 | * Host scheme. 23 | * Defined as 'scheme' attribute. 24 | */ 25 | @property (nonatomic, readonly, strong) NSString *scheme; 26 | 27 | /** 28 | * Event name that is sent to JS when user clicks on the link from this host. 29 | * Defined as 'event' attribute. 30 | */ 31 | @property (nonatomic, strong) NSString *event; 32 | 33 | /** 34 | * List of paths, that is set for that host in config.xml. 35 | */ 36 | @property (nonatomic, readonly, strong) NSArray *paths; 37 | 38 | /** 39 | * Constructor. 40 | * 41 | * @param name host name 42 | * @param scheme scheme; if nil - http will be used 43 | * @param event event name; if nil - didLaunchAppFromLink event name will be used 44 | * 45 | * @return instance of the CULHost 46 | */ 47 | - (instancetype)initWithHostName:(NSString *)name scheme:(NSString *)scheme event:(NSString *)event; 48 | 49 | /** 50 | * Add path entry to the host paths list. 51 | * 52 | * @param path path to add 53 | */ 54 | - (void)addPath:(CULPath *)path; 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /hooks/lib/xmlHelper.js: -------------------------------------------------------------------------------- 1 | /* 2 | Small helper class to read/write from/to xml file. 3 | */ 4 | (function() { 5 | 6 | var fs = require('fs'), 7 | xml2js = require('xml2js'); 8 | 9 | module.exports = { 10 | readXmlAsJson: readXmlAsJson, 11 | writeJsonAsXml: writeJsonAsXml 12 | }; 13 | 14 | /** 15 | * Read data from the xml file as JSON object. 16 | * 17 | * @param {String} filePath - absolute path to xml file 18 | * @return {Object} JSON object with the contents of the xml file 19 | */ 20 | function readXmlAsJson(filePath) { 21 | var xmlData, 22 | xmlParser, 23 | parsedData; 24 | 25 | try { 26 | xmlData = fs.readFileSync(filePath); 27 | xmlParser = new xml2js.Parser(); 28 | xmlParser.parseString(xmlData, function(err, data) { 29 | if (data) { 30 | parsedData = data; 31 | } 32 | }); 33 | } catch (err) {} 34 | 35 | return parsedData; 36 | } 37 | 38 | /** 39 | * Write JSON object as xml into the specified file. 40 | * 41 | * @param {Object} jsData - JSON object to write 42 | * @param {String} filePath - path to the xml file where data should be saved 43 | * @return {boolean} true - if data saved to file; false - otherwise 44 | */ 45 | function writeJsonAsXml(jsData, filePath, options) { 46 | var xmlBuilder = new xml2js.Builder(options), 47 | changedXmlData = xmlBuilder.buildObject(jsData), 48 | isSaved = true; 49 | 50 | try { 51 | fs.writeFileSync(filePath, changedXmlData); 52 | } catch (err) { 53 | console.log(err); 54 | isSaved = false; 55 | } 56 | 57 | return isSaved; 58 | } 59 | 60 | })(); 61 | -------------------------------------------------------------------------------- /www/universal_links.js: -------------------------------------------------------------------------------- 1 | var exec = require('cordova/exec'), 2 | channel = require('cordova/channel'), 3 | 4 | // Reference name for the plugin 5 | PLUGIN_NAME = 'UniversalLinks', 6 | 7 | // Default event name that is used by the plugin 8 | DEFAULT_EVENT_NAME = 'didLaunchAppFromLink'; 9 | 10 | // Plugin methods on the native side that can be called from JavaScript 11 | pluginNativeMethod = { 12 | SUBSCRIBE: 'jsSubscribeForEvent', 13 | UNSUBSCRIBE: 'jsUnsubscribeFromEvent' 14 | }; 15 | 16 | var universalLinks = { 17 | 18 | /** 19 | * Subscribe to event. 20 | * If plugin already captured that event - callback will be called immidietly. 21 | * 22 | * @param {String} eventName - name of the event you are subscribing on; if null - default plugin event is used 23 | * @param {Function} callback - callback that is called when event is captured 24 | */ 25 | subscribe: function(eventName, callback) { 26 | if (!callback) { 27 | console.warn('Universal Links: can\'t subscribe to event without a callback'); 28 | return; 29 | } 30 | 31 | if (!eventName) { 32 | eventName = DEFAULT_EVENT_NAME; 33 | } 34 | 35 | var innerCallback = function(msg) { 36 | callback(msg.data); 37 | }; 38 | 39 | exec(innerCallback, null, PLUGIN_NAME, pluginNativeMethod.SUBSCRIBE, [eventName]); 40 | }, 41 | 42 | /** 43 | * Unsubscribe from the event. 44 | * 45 | * @param {String} eventName - from what event we are unsubscribing 46 | */ 47 | unsubscribe: function(eventName) { 48 | if (!eventName) { 49 | eventName = DEFAULT_EVENT_NAME; 50 | } 51 | 52 | exec(null, null, PLUGIN_NAME, pluginNativeMethod.UNSUBSCRIBE, [eventName]); 53 | } 54 | }; 55 | 56 | module.exports = universalLinks; 57 | -------------------------------------------------------------------------------- /src/android/com/nordnetab/cordova/ul/model/ULHost.java: -------------------------------------------------------------------------------- 1 | package com.nordnetab.cordova.ul.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Created by Nikolay Demyankov on 09.09.15. 8 | *

9 | * Model for entry, specified in config.xml. 10 | */ 11 | public class ULHost { 12 | 13 | // default event name, that is dispatched to JS if none was set to the host or path 14 | private static final String DEFAULT_EVENT = "didLaunchAppFromLink"; 15 | 16 | // default scheme for the host 17 | private static final String DEFAULT_SCHEME = "http"; 18 | 19 | private final List paths; 20 | private final String name; 21 | private final String scheme; 22 | private String event; 23 | 24 | /** 25 | * Constructor 26 | * 27 | * @param name host name 28 | * @param scheme host scheme 29 | * @param event event that corresponds to this host 30 | */ 31 | public ULHost(final String name, final String scheme, final String event) { 32 | this.name = name; 33 | this.scheme = (scheme == null) ? DEFAULT_SCHEME : scheme; 34 | this.event = (event == null) ? DEFAULT_EVENT : event; 35 | this.paths = new ArrayList(); 36 | } 37 | 38 | /** 39 | * Getter for the event name that is sent to JS when user clicks on the link from this host. 40 | * Defined as 'event' attribute. 41 | * 42 | * @return event name 43 | */ 44 | public String getEvent() { 45 | return event; 46 | } 47 | 48 | /** 49 | * Setter for event name. 50 | * 51 | * @param event event name 52 | */ 53 | public void setEvent(final String event) { 54 | this.event = event; 55 | } 56 | 57 | /** 58 | * Getter for the list of paths, that is set for that host in config.xml. 59 | * 60 | * @return list of hosts 61 | */ 62 | public List getPaths() { 63 | return paths; 64 | } 65 | 66 | /** 67 | * Getter for the host name. 68 | * Defined as 'name' attribute. 69 | * 70 | * @return host name 71 | */ 72 | public String getName() { 73 | return name; 74 | } 75 | 76 | /** 77 | * Getter for host scheme. 78 | * Defined as 'scheme' attribute. 79 | * 80 | * @return scheme 81 | */ 82 | public String getScheme() { 83 | return scheme; 84 | } 85 | } -------------------------------------------------------------------------------- /hooks/iosBeforePrepareHook.js: -------------------------------------------------------------------------------- 1 | /* 2 | Hook executed before the 'prepare' stage. Only for iOS project. 3 | It will check if project name has changed. If so - it will change the name of the .entitlements file to remove that file duplicates. 4 | If file name has no changed - hook would not do anything. 5 | */ 6 | 7 | var path = require('path'), 8 | fs = require('fs'), 9 | ConfigXmlHelper = require('./lib/configXmlHelper.js'); 10 | 11 | module.exports = function(ctx) { 12 | run(ctx); 13 | }; 14 | 15 | /** 16 | * Run the hook logic. 17 | * 18 | * @param {Object} ctx - cordova context object 19 | */ 20 | function run(ctx) { 21 | var projectRoot = ctx.opts.projectRoot, 22 | iosProjectFilePath = path.join(projectRoot, 'platforms', 'ios'), 23 | configXmlHelper = new ConfigXmlHelper(ctx), 24 | oldProjectName = getOldProjectName(iosProjectFilePath), 25 | newProjectName = configXmlHelper.getProjectName(); 26 | 27 | // if name has not changed - do nothing 28 | if (oldProjectName.length > 0 && oldProjectName === newProjectName) { 29 | return; 30 | } 31 | 32 | console.log('Project name has changed. Renaming .entitlements file.'); 33 | 34 | // if it does - rename it 35 | var oldEntitlementsFilePath = path.join(iosProjectFilePath, oldProjectName, 'Resources', oldProjectName + '.entitlements'), 36 | newEntitlementsFilePath = path.join(iosProjectFilePath, oldProjectName, 'Resources', newProjectName + '.entitlements'); 37 | 38 | try { 39 | fs.renameSync(oldEntitlementsFilePath, newEntitlementsFilePath); 40 | } catch (err) { 41 | console.warn('Failed to rename .entitlements file.'); 42 | console.warn(err); 43 | } 44 | } 45 | 46 | // region Private API 47 | 48 | /** 49 | * Get old name of the project. 50 | * Name is detected by the name of the .xcodeproj file. 51 | * 52 | * @param {String} projectDir absolute path to ios project directory 53 | * @return {String} old project name 54 | */ 55 | function getOldProjectName(projectDir) { 56 | var files = [], 57 | projectName = ''; 58 | 59 | try { 60 | files = fs.readdirSync(projectDir); 61 | } catch (err) { 62 | return ''; 63 | } 64 | 65 | // find file with .xcodeproj extension, use it as an old project name 66 | files.some(function(fileName) { 67 | if (path.extname(fileName) === '.xcodeproj') { 68 | projectName = path.basename(fileName, '.xcodeproj'); 69 | return true; 70 | } 71 | 72 | return false; 73 | }); 74 | 75 | return projectName; 76 | } 77 | 78 | // endregion 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.1.0 (2015-12-18) 4 | 5 | **Bug fixes:** 6 | 7 | - [Issue #26](https://github.com/nordnet/cordova-universal-links-plugin/issues/26). Fixed support for multiple wildcards in path. Thanks to [@tdelmas](https://github.com/tdelmas) for helping with solution. 8 | - Other minor bug fixes. 9 | 10 | **Enchancements:** 11 | 12 | - [Issue #18](https://github.com/nordnet/cordova-universal-links-plugin/issues/18). Added JS module through which you can subscribe for launch events. Solves timing issue with the previous `document.addEventListener()` approach. 13 | - [Issue #20](https://github.com/nordnet/cordova-universal-links-plugin/issues/20). Lowered min iOS version to 8.0. Plugin want work on devices prior to iOS 9, but if your application includes this plugin - it now will run on iOS 8 devices. Before you had to drop iOS 8 support. 14 | - [Issue #22](https://github.com/nordnet/cordova-universal-links-plugin/issues/22). Plugin now compatible with Cordova v5.4. 15 | - [Issue #24](https://github.com/nordnet/cordova-universal-links-plugin/issues/24). Now you can define iOS Team ID as plugin preference. It will be used for generation of `apple-app-site-association` files. 16 | - [Issue #25](https://github.com/nordnet/cordova-universal-links-plugin/issues/25). Plugin now compatible with Cordova iOS platform v4.0.0. 17 | 18 | **Docs:** 19 | 20 | - Added `Migrating from previous versions` section. 21 | - Updated `Cordova config preferences` section. 22 | - Updated `Application launch handling` section. 23 | - Other minor changes because of new release. 24 | 25 | ## 1.0.1 (2015-10-23) 26 | 27 | **Bug fixes:** 28 | 29 | - Android. Fixed [issue #9](https://github.com/nordnet/cordova-universal-links-plugin/issues/9). Now when application is resumed from the link click - appropriate event is dispatched to the JavaScript side. 30 | 31 | **Enchancements:** 32 | 33 | - iOS. [Issue #6](https://github.com/nordnet/cordova-universal-links-plugin/issues/6). Scheme is now removed from the url matching process, since it is not needed: only hostname and path are used. 34 | - Merged [pull request #1](https://github.com/nordnet/cordova-universal-links-plugin/pull/1). Now dependency npm packages are taken from the package.json file. Thanks to [@dpa99c](https://github.com/dpa99c). 35 | 36 | **Docs:** 37 | 38 | - Added `Useful notes on Universal Links for iOS` section. 39 | - Updated `Android web integration` section. Added more information about web integration process. 40 | - Added some additional links on the Android documentation. 41 | - Fixed some broken links inside the docs. 42 | - Added CHANGELOG.md file. 43 | -------------------------------------------------------------------------------- /hooks/afterPrepareHook.js: -------------------------------------------------------------------------------- 1 | /** 2 | Hook is executed at the end of the 'prepare' stage. Usually, when you call 'cordova build'. 3 | 4 | It will inject required preferences in the platform-specific projects, based on 5 | data you have specified in the projects config.xml file. 6 | */ 7 | 8 | var configParser = require('./lib/configXmlParser.js'), 9 | androidManifestWriter = require('./lib/android/manifestWriter.js'), 10 | androidWebHook = require('./lib/android/webSiteHook.js'), 11 | iosProjectEntitlements = require('./lib/ios/projectEntitlements.js'), 12 | iosAppSiteAssociationFile = require('./lib/ios/appleAppSiteAssociationFile.js'), 13 | iosProjectPreferences = require('./lib/ios/xcodePreferences.js'), 14 | ANDROID = 'android', 15 | IOS = 'ios'; 16 | 17 | module.exports = function(ctx) { 18 | run(ctx); 19 | }; 20 | 21 | /** 22 | * Execute hook. 23 | * 24 | * @param {Object} cordovaContext - cordova context object 25 | */ 26 | function run(cordovaContext) { 27 | var pluginPreferences = configParser.readPreferences(cordovaContext), 28 | platformsList = cordovaContext.opts.platforms; 29 | 30 | // if no preferences are found - exit 31 | if (pluginPreferences == null) { 32 | return; 33 | } 34 | 35 | // if no host is defined - exit 36 | if (pluginPreferences.hosts == null || pluginPreferences.hosts.length == 0) { 37 | console.warn('No host is specified in the config.xml. Universal Links plugin is not going to work.'); 38 | return; 39 | } 40 | 41 | platformsList.forEach(function(platform) { 42 | switch (platform) { 43 | case ANDROID: 44 | { 45 | activateUniversalLinksInAndroid(cordovaContext, pluginPreferences); 46 | break; 47 | } 48 | case IOS: 49 | { 50 | activateUniversalLinksInIos(cordovaContext, pluginPreferences); 51 | break; 52 | } 53 | } 54 | }); 55 | } 56 | 57 | /** 58 | * Activate Deep Links for Android application. 59 | * 60 | * @param {Object} cordovaContext - cordova context object 61 | * @param {Object} pluginPreferences - plugin preferences from the config.xml file. Basically, content from tag. 62 | */ 63 | function activateUniversalLinksInAndroid(cordovaContext, pluginPreferences) { 64 | // inject preferenes into AndroidManifest.xml 65 | androidManifestWriter.writePreferences(cordovaContext, pluginPreferences); 66 | 67 | // generate html file with the tags that you should inject on the website. 68 | androidWebHook.generate(cordovaContext, pluginPreferences); 69 | } 70 | 71 | /** 72 | * Activate Universal Links for iOS application. 73 | * 74 | * @param {Object} cordovaContext - cordova context object 75 | * @param {Object} pluginPreferences - plugin preferences from the config.xml file. Basically, content from tag. 76 | */ 77 | function activateUniversalLinksInIos(cordovaContext, pluginPreferences) { 78 | // modify xcode project preferences 79 | iosProjectPreferences.enableAssociativeDomainsCapability(cordovaContext); 80 | 81 | // generate entitlements file 82 | iosProjectEntitlements.generateAssociatedDomainsEntitlements(cordovaContext, pluginPreferences); 83 | 84 | // generate apple-site-association-file 85 | iosAppSiteAssociationFile.generate(cordovaContext, pluginPreferences); 86 | } 87 | -------------------------------------------------------------------------------- /hooks/lib/configXmlHelper.js: -------------------------------------------------------------------------------- 1 | /* 2 | Helper class to read data from config.xml file. 3 | */ 4 | (function() { 5 | var path = require('path'), 6 | xmlHelper = require('./xmlHelper.js'), 7 | ANDROID = 'android', 8 | IOS = 'ios', 9 | CONFIG_FILE_NAME = 'config.xml', 10 | context, 11 | projectRoot; 12 | 13 | module.exports = ConfigXmlHelper; 14 | 15 | // region public API 16 | 17 | /** 18 | * Constructor. 19 | * 20 | * @param {Object} cordovaContext - cordova context object 21 | */ 22 | function ConfigXmlHelper(cordovaContext) { 23 | context = cordovaContext; 24 | projectRoot = context.opts.projectRoot; 25 | } 26 | 27 | /** 28 | * Read config.xml data as JSON object. 29 | * 30 | * @return {Object} JSON object with data from config.xml 31 | */ 32 | ConfigXmlHelper.prototype.read = function() { 33 | var filePath = getConfigXmlFilePath(); 34 | 35 | return xmlHelper.readXmlAsJson(filePath); 36 | } 37 | 38 | /** 39 | * Get package name for the application. Depends on the platform. 40 | * 41 | * @param {String} platform - 'ios' or 'android'; for what platform we need a package name 42 | * @return {String} package/bundle name 43 | */ 44 | ConfigXmlHelper.prototype.getPackageName = function(platform) { 45 | var configFilePath = getConfigXmlFilePath(), 46 | config = getCordovaConfigParser(configFilePath), 47 | packageName; 48 | 49 | switch (platform) { 50 | case ANDROID: 51 | { 52 | packageName = config.android_packageName(); 53 | break; 54 | } 55 | case IOS: 56 | { 57 | packageName = config.ios_CFBundleIdentifier(); 58 | break; 59 | } 60 | } 61 | if (packageName === undefined || packageName.length == 0) { 62 | packageName = config.packageName(); 63 | } 64 | 65 | return packageName; 66 | } 67 | 68 | /** 69 | * Get name of the current project. 70 | * 71 | * @return {String} name of the project 72 | */ 73 | ConfigXmlHelper.prototype.getProjectName = function() { 74 | return getProjectName(); 75 | } 76 | 77 | // endregion 78 | 79 | // region Private API 80 | 81 | /** 82 | * Get config parser from cordova library. 83 | * 84 | * @param {String} configFilePath absolute path to the config.xml file 85 | * @return {Object} 86 | */ 87 | function getCordovaConfigParser(configFilePath) { 88 | var ConfigParser; 89 | 90 | // If we are running Cordova 5.4 or abova - use parser from cordova-common. 91 | // Otherwise - from cordova-lib. 92 | try { 93 | ConfigParser = context.requireCordovaModule('cordova-common/src/ConfigParser/ConfigParser'); 94 | } catch (e) { 95 | ConfigParser = context.requireCordovaModule('cordova-lib/src/configparser/ConfigParser') 96 | } 97 | 98 | return new ConfigParser(configFilePath); 99 | } 100 | 101 | /** 102 | * Get absolute path to the config.xml. 103 | */ 104 | function getConfigXmlFilePath() { 105 | return path.join(projectRoot, CONFIG_FILE_NAME); 106 | } 107 | 108 | /** 109 | * Get project name from config.xml 110 | */ 111 | function getProjectName() { 112 | var configFilePath = getConfigXmlFilePath(), 113 | config = getCordovaConfigParser(configFilePath); 114 | 115 | return config.name(); 116 | } 117 | 118 | // endregion 119 | 120 | })(); 121 | -------------------------------------------------------------------------------- /src/ios/Parser/CULConfigXmlParser.m: -------------------------------------------------------------------------------- 1 | // 2 | // CULConfigXmlParser.m 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import "CULConfigXmlParser.h" 8 | #import "NSBundle+CULPlugin.h" 9 | #import "CULPath.h" 10 | #import "CULXmlTags.h" 11 | 12 | @interface CULConfigXmlParser() { 13 | NSMutableArray *_hostsList; 14 | BOOL _isInsideMainTag; 15 | BOOL _didParseMainBlock; 16 | BOOL _isInsideHostBlock; 17 | CULHost *_processedHost; 18 | } 19 | 20 | @end 21 | 22 | @implementation CULConfigXmlParser 23 | 24 | #pragma mark Public API 25 | 26 | + (NSArray *)parse { 27 | CULConfigXmlParser *parser = [[CULConfigXmlParser alloc] init]; 28 | 29 | return [parser parseConfig]; 30 | } 31 | 32 | - (NSArray *)parseConfig { 33 | NSURL *cordovaConfigURL = [NSURL fileURLWithPath:[NSBundle pathToCordovaConfigXml]]; 34 | NSXMLParser *configParser = [[NSXMLParser alloc] initWithContentsOfURL:cordovaConfigURL]; 35 | if (configParser == nil) { 36 | NSLog(@"Failed to initialize XML parser."); 37 | return nil; 38 | } 39 | 40 | _hostsList = [[NSMutableArray alloc] init]; 41 | [configParser setDelegate:self]; 42 | [configParser parse]; 43 | 44 | return _hostsList; 45 | } 46 | 47 | #pragma mark NSXMLParserDelegate implementation 48 | 49 | - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { 50 | if (_didParseMainBlock) { 51 | return; 52 | } 53 | 54 | if ([elementName isEqualToString:kCULMainXmlTag]) { 55 | _isInsideMainTag = YES; 56 | return; 57 | } 58 | if (!_isInsideMainTag) { 59 | return; 60 | } 61 | 62 | if ([elementName isEqualToString:kCULHostXmlTag]) { 63 | [self processHostTag:attributeDict]; 64 | } else if ([elementName isEqualToString:kCULPathXmlTag]) { 65 | [self processPathTag:attributeDict]; 66 | } 67 | } 68 | 69 | - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { 70 | if (_didParseMainBlock || !_isInsideMainTag) { 71 | return; 72 | } 73 | 74 | if ([elementName isEqualToString:kCULHostXmlTag]) { 75 | _isInsideHostBlock = NO; 76 | [_hostsList addObject:_processedHost]; 77 | } 78 | } 79 | 80 | #pragma mark XML Processing 81 | 82 | /** 83 | * Parse host tag. 84 | * 85 | * @param attributes host tag attributes 86 | */ 87 | - (void)processHostTag:(NSDictionary *)attributes { 88 | _processedHost = [[CULHost alloc] initWithHostName:attributes[kCULHostNameXmlAttribute] 89 | scheme:attributes[kCULHostSchemeXmlAttribute] 90 | event:attributes[kCULHostEventXmlAttribute]]; 91 | _isInsideHostBlock = YES; 92 | } 93 | 94 | /** 95 | * Parse path tag. 96 | * 97 | * @param attributes path tag attributes 98 | */ 99 | - (void)processPathTag:(NSDictionary *)attributes { 100 | NSString *urlPath = attributes[kCULPathUrlXmlAttribute]; 101 | NSString *event = attributes[kCULPathEventXmlAttribute]; 102 | 103 | // ignore '*' paths; we don't need them here 104 | if ([urlPath isEqualToString:@"*"] || [urlPath isEqualToString:@".*"]) { 105 | // but if path has event name - set it to host 106 | if (event) { 107 | _processedHost.event = event; 108 | } 109 | 110 | return; 111 | } 112 | 113 | // if event name is empty - use one from the host 114 | if (event == nil) { 115 | event = _processedHost.event; 116 | } 117 | 118 | // create path entry 119 | CULPath *path = [[CULPath alloc] initWithUrlPath:urlPath andEvent:event]; 120 | [_processedHost addPath:path]; 121 | } 122 | 123 | @end 124 | -------------------------------------------------------------------------------- /hooks/beforePluginInstallHook.js: -------------------------------------------------------------------------------- 1 | /** 2 | Hook is executed when plugin is added to the project. 3 | It will check all necessary module dependencies and install the missing ones locally. 4 | */ 5 | 6 | var exec = require('child_process').exec, 7 | path = require('path'), 8 | fs = require('fs'), 9 | INSTALLATION_FLAG_FILE_NAME = '.installed'; 10 | 11 | // region NPM specific 12 | 13 | /** 14 | * Check if node package is installed. 15 | * 16 | * @param {String} moduleName 17 | * @return {Boolean} true if package already installed 18 | */ 19 | function isNodeModuleInstalled(moduleName) { 20 | var installed = true; 21 | try { 22 | var module = require(moduleName); 23 | } catch (err) { 24 | installed = false; 25 | } 26 | 27 | return installed; 28 | } 29 | 30 | /** 31 | * Install node module locally. 32 | * Basically, it runs 'npm install module_name'. 33 | * 34 | * @param {String} moduleName 35 | * @param {Callback(error)} callback 36 | */ 37 | function installNodeModule(moduleName, callback) { 38 | if (isNodeModuleInstalled(moduleName)) { 39 | printLog('Node module ' + moduleName + ' is found'); 40 | callback(null); 41 | return; 42 | } 43 | printLog('Can\'t find module ' + moduleName + ', running npm install'); 44 | 45 | var cmd = 'npm install -D ' + moduleName; 46 | exec(cmd, function(err, stdout, stderr) { 47 | callback(err); 48 | }); 49 | } 50 | 51 | /** 52 | * Install all required node packages. 53 | */ 54 | function installRequiredNodeModules(modulesToInstall) { 55 | if (!modulesToInstall.length) { 56 | return; 57 | } 58 | 59 | var moduleName = modulesToInstall.shift(); 60 | installNodeModule(moduleName, function(err) { 61 | if (err) { 62 | printLog('Failed to install module ' + moduleName + ':' + err); 63 | return; 64 | } 65 | 66 | printLog('Module ' + moduleName + ' is installed'); 67 | installRequiredNodeModules(modulesToInstall); 68 | }); 69 | } 70 | 71 | // endregion 72 | 73 | // region Logging 74 | 75 | function logStart() { 76 | console.log('Checking dependencies:'); 77 | } 78 | 79 | function printLog(msg) { 80 | var formattedMsg = ' ' + msg; 81 | console.log(formattedMsg); 82 | } 83 | 84 | // endregion 85 | 86 | // region Private API 87 | 88 | /** 89 | * Check if we already executed this hook. 90 | * 91 | * @param {Object} ctx - cordova context 92 | * @return {Boolean} true if already executed; otherwise - false 93 | */ 94 | function isInstallationAlreadyPerformed(ctx) { 95 | var pathToInstallFlag = path.join(ctx.opts.projectRoot, 'plugins', ctx.opts.plugin.id, INSTALLATION_FLAG_FILE_NAME), 96 | isInstalled = false; 97 | try { 98 | var content = fs.readFileSync(pathToInstallFlag); 99 | isInstalled = true; 100 | } catch (err) { 101 | } 102 | 103 | return isInstalled; 104 | } 105 | 106 | /** 107 | * Create empty file - indicator, that we tried to install dependency modules after installation. 108 | * We have to do that, or this hook is gonna be called on any plugin installation. 109 | */ 110 | function createPluginInstalledFlag(ctx) { 111 | var pathToInstallFlag = path.join(ctx.opts.projectRoot, 'plugins', ctx.opts.plugin.id, INSTALLATION_FLAG_FILE_NAME); 112 | 113 | fs.closeSync(fs.openSync(pathToInstallFlag, 'w')); 114 | } 115 | 116 | // endregion 117 | 118 | /** 119 | * Read dependencies from the package.json. 120 | * We will install them on the next step. 121 | * 122 | * @param {Object} ctx - cordova context 123 | * @return {Array} list of modules to install 124 | */ 125 | function readDependenciesFromPackageJson(ctx) { 126 | var data = require(path.join(ctx.opts.projectRoot, 'plugins', ctx.opts.plugin.id, 'package.json')), 127 | dependencies = data['dependencies'], 128 | modules = []; 129 | 130 | if (!dependencies) { 131 | return modules; 132 | } 133 | 134 | for (var module in dependencies) { 135 | modules.push(module); 136 | } 137 | 138 | return modules; 139 | } 140 | 141 | // hook's entry point 142 | module.exports = function(ctx) { 143 | // exit if we already executed this hook once 144 | if (isInstallationAlreadyPerformed(ctx)) { 145 | return; 146 | } 147 | 148 | logStart(); 149 | 150 | var modules = readDependenciesFromPackageJson(ctx); 151 | installRequiredNodeModules(modules); 152 | 153 | createPluginInstalledFlag(ctx); 154 | }; 155 | -------------------------------------------------------------------------------- /hooks/lib/configXmlParser.js: -------------------------------------------------------------------------------- 1 | /* 2 | Parser for config.xml file. Read plugin-specific preferences (from tag) as JSON object. 3 | */ 4 | (function() { 5 | 6 | var path = require('path'), 7 | fs = require('fs'), 8 | xml2js = require('xml2js'), 9 | ConfigXmlHelper = require('./configXmlHelper.js'), 10 | DEFAULT_SCHEME = 'http'; 11 | 12 | module.exports = { 13 | readPreferences: readPreferences 14 | }; 15 | 16 | // region Public API 17 | 18 | /** 19 | * Read plugin preferences from the config.xml file. 20 | * 21 | * @param {Object} cordovaContext - cordova context object 22 | * @return {Array} list of host objects 23 | */ 24 | function readPreferences(cordovaContext) { 25 | // read data from projects root config.xml file 26 | var configXml = new ConfigXmlHelper(cordovaContext).read(); 27 | if (configXml == null) { 28 | console.warn('config.xml not found! Please, check that it exist\'s in your project\'s root directory.'); 29 | return null; 30 | } 31 | 32 | // look for data from the tag 33 | var ulXmlPreferences = configXml.widget['universal-links']; 34 | if (ulXmlPreferences == null || ulXmlPreferences.length == 0) { 35 | console.warn(' tag is not set in the config.xml. Universal Links plugin is not going to work.'); 36 | return null; 37 | } 38 | 39 | var xmlPreferences = ulXmlPreferences[0]; 40 | 41 | // read hosts 42 | var hosts = constructHostsList(xmlPreferences); 43 | 44 | // read ios team ID 45 | var iosTeamId = getTeamIdPreference(xmlPreferences); 46 | 47 | return { 48 | 'hosts': hosts, 49 | 'iosTeamId': iosTeamId 50 | }; 51 | } 52 | 53 | // endregion 54 | 55 | // region Private API 56 | 57 | function getTeamIdPreference(xmlPreferences) { 58 | if (xmlPreferences.hasOwnProperty('ios-team-id')) { 59 | return xmlPreferences['ios-team-id'][0]['$']['value']; 60 | } 61 | 62 | return null; 63 | } 64 | 65 | /** 66 | * Construct list of host objects, defined in xml file. 67 | * 68 | * @param {Object} xmlPreferences - plugin preferences from config.xml as JSON object 69 | * @return {Array} array of JSON objects, where each entry defines host data from config.xml. 70 | */ 71 | function constructHostsList(xmlPreferences) { 72 | var hostsList = []; 73 | 74 | // look for defined hosts 75 | var xmlHostList = xmlPreferences['host']; 76 | if (xmlHostList == null || xmlHostList.length == 0) { 77 | return []; 78 | } 79 | 80 | xmlHostList.forEach(function(xmlElement) { 81 | var host = constructHostEntry(xmlElement); 82 | if (host) { 83 | hostsList.push(host); 84 | } 85 | }); 86 | 87 | return hostsList; 88 | } 89 | 90 | /** 91 | * Construct host object from xml data. 92 | * 93 | * @param {Object} xmlElement - xml data to process. 94 | * @return {Object} host entry as JSON object 95 | */ 96 | function constructHostEntry(xmlElement) { 97 | var host = { 98 | scheme: DEFAULT_SCHEME, 99 | name: '', 100 | paths: [] 101 | }, 102 | hostProperties = xmlElement['$']; 103 | 104 | if (hostProperties == null || hostProperties.length == 0) { 105 | return null; 106 | } 107 | 108 | // read host name 109 | host.name = hostProperties.name; 110 | 111 | // read scheme if defined 112 | if (hostProperties['scheme'] != null) { 113 | host.scheme = hostProperties.scheme; 114 | } 115 | 116 | // construct paths list, defined for the given host 117 | host.paths = constructPaths(xmlElement); 118 | 119 | return host; 120 | } 121 | 122 | /** 123 | * Construct list of path objects from the xml data. 124 | * 125 | * @param {Object} xmlElement - xml data to process 126 | * @return {Array} list of path entries, each on is a JSON object 127 | */ 128 | function constructPaths(xmlElement) { 129 | if (xmlElement['path'] == null) { 130 | return ['*']; 131 | } 132 | 133 | var paths = []; 134 | xmlElement.path.some(function(pathElement) { 135 | var url = pathElement['$']['url']; 136 | 137 | // Ignore explicit paths if '*' is defined 138 | if (url === '*') { 139 | paths = ['*']; 140 | return true; 141 | } 142 | 143 | paths.push(url); 144 | }); 145 | 146 | return paths; 147 | } 148 | 149 | // endregion 150 | 151 | })(); 152 | -------------------------------------------------------------------------------- /src/android/com/nordnetab/cordova/ul/parser/ULConfigXmlParser.java: -------------------------------------------------------------------------------- 1 | package com.nordnetab.cordova.ul.parser; 2 | 3 | import android.content.Context; 4 | import android.text.TextUtils; 5 | 6 | import com.nordnetab.cordova.ul.model.ULHost; 7 | import com.nordnetab.cordova.ul.model.ULPath; 8 | 9 | import org.apache.cordova.ConfigXmlParser; 10 | import org.xmlpull.v1.XmlPullParser; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | /** 16 | * Created by Nikolay Demyankov on 09.09.15. 17 | *

18 | * Parser for config.xml. Reads only plugin-specific preferences. 19 | */ 20 | public class ULConfigXmlParser extends ConfigXmlParser { 21 | 22 | private final Context context; 23 | private List hostsList; 24 | 25 | private boolean isInsideMainTag; 26 | private boolean didParseMainBlock; 27 | private boolean isInsideHostBlock; 28 | private ULHost processedHost; 29 | 30 | // region Public API 31 | 32 | /** 33 | * Constructor 34 | * 35 | * @param context application context 36 | */ 37 | public ULConfigXmlParser(Context context) { 38 | this.context = context; 39 | } 40 | 41 | /** 42 | * Parse config.xml 43 | * 44 | * @return list of hosts, defined in the config file 45 | */ 46 | public List parse() { 47 | resetValuesToDefaultState(); 48 | super.parse(context); 49 | 50 | return hostsList; 51 | } 52 | 53 | // endregion 54 | 55 | // region XML processing 56 | 57 | @Override 58 | public void handleStartTag(XmlPullParser xml) { 59 | if (didParseMainBlock) { 60 | return; 61 | } 62 | 63 | final String name = xml.getName(); 64 | if (!isInsideMainTag && XmlTags.MAIN_TAG.equals(name)) { 65 | isInsideMainTag = true; 66 | return; 67 | } 68 | 69 | if (!isInsideMainTag) { 70 | return; 71 | } 72 | 73 | if (!isInsideHostBlock && XmlTags.HOST_TAG.equals(name)) { 74 | isInsideHostBlock = true; 75 | processHostBlock(xml); 76 | return; 77 | } 78 | 79 | if (isInsideHostBlock && XmlTags.PATH_TAG.equals(name)) { 80 | processPathBlock(xml); 81 | } 82 | } 83 | 84 | @Override 85 | public void handleEndTag(XmlPullParser xml) { 86 | if (didParseMainBlock) { 87 | return; 88 | } 89 | 90 | final String name = xml.getName(); 91 | 92 | if (isInsideHostBlock && XmlTags.HOST_TAG.equals(name)) { 93 | isInsideHostBlock = false; 94 | hostsList.add(processedHost); 95 | processedHost = null; 96 | return; 97 | } 98 | 99 | if (XmlTags.MAIN_TAG.equals(name)) { 100 | isInsideMainTag = false; 101 | didParseMainBlock = true; 102 | } 103 | } 104 | 105 | /** 106 | * Parse 107 | */ 108 | private void processHostBlock(XmlPullParser xml) { 109 | final String hostName = xml.getAttributeValue(null, XmlTags.HOST_NAME_ATTRIBUTE); 110 | final String eventName = xml.getAttributeValue(null, XmlTags.HOST_EVENT_ATTRIBUTE); 111 | final String scheme = xml.getAttributeValue(null, XmlTags.HOST_SCHEME_ATTRIBUTE); 112 | 113 | processedHost = new ULHost(hostName, scheme, eventName); 114 | } 115 | 116 | /** 117 | * Parse 118 | */ 119 | private void processPathBlock(XmlPullParser xml) { 120 | final String url = xml.getAttributeValue(null, XmlTags.PATH_URL_TAG); 121 | String event = xml.getAttributeValue(null, XmlTags.PATH_EVENT_TAG); 122 | 123 | // skip wildcard urls 124 | if ("*".equals(url) || ".*".equals(url)) { 125 | // but if path has event name - set it to host 126 | if (!TextUtils.isEmpty(event)) { 127 | processedHost.setEvent(event); 128 | } 129 | 130 | return; 131 | } 132 | 133 | // if event name is empty - use one from the host 134 | if (TextUtils.isEmpty(event)) { 135 | event = processedHost.getEvent(); 136 | } 137 | 138 | // create path entry 139 | ULPath path = new ULPath(url, event); 140 | processedHost.getPaths().add(path); 141 | } 142 | 143 | // endregion 144 | 145 | // region Private API 146 | 147 | private void resetValuesToDefaultState() { 148 | hostsList = new ArrayList(); 149 | isInsideMainTag = false; 150 | didParseMainBlock = false; 151 | isInsideHostBlock = false; 152 | processedHost = null; 153 | } 154 | 155 | // endregion 156 | } -------------------------------------------------------------------------------- /src/ios/CULPlugin.m: -------------------------------------------------------------------------------- 1 | // 2 | // CULPlugin.m 3 | // 4 | // Created by Nikolay Demyankov on 14.09.15. 5 | // 6 | 7 | #import "CULPlugin.h" 8 | #import "CULConfigXmlParser.h" 9 | #import "CULPath.h" 10 | #import "CULHost.h" 11 | #import "CDVPluginResult+CULPlugin.h" 12 | #import "CDVInvokedUrlCommand+CULPlugin.h" 13 | 14 | @interface CULPlugin() { 15 | NSArray *_supportedHosts; 16 | CDVPluginResult *_storedEvent; 17 | NSMutableDictionary *_subscribers; 18 | } 19 | 20 | @end 21 | 22 | @implementation CULPlugin 23 | 24 | #pragma mark Public API 25 | 26 | - (void)pluginInitialize { 27 | [self localInit]; 28 | // Can be used for testing. 29 | // Just uncomment, close the app and reopen it. That will simulate application launch from the link. 30 | // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume:) name:UIApplicationWillEnterForegroundNotification object:nil]; 31 | } 32 | 33 | //- (void)onResume:(NSNotification *)notification { 34 | // NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb]; 35 | // [activity setWebpageURL:[NSURL URLWithString:@"http://site2.com/news/page?q=1&v=2#myhash"]]; 36 | // 37 | // [self handleUserActivity:activity]; 38 | //} 39 | 40 | - (BOOL)handleUserActivity:(NSUserActivity *)userActivity { 41 | [self localInit]; 42 | 43 | NSURL *launchURL = userActivity.webpageURL; 44 | CULHost *host = [self findHostByURL:launchURL]; 45 | if (host == nil) { 46 | return NO; 47 | } 48 | 49 | [self storeEventWithHost:host originalURL:launchURL]; 50 | 51 | return YES; 52 | } 53 | 54 | - (void)onAppTerminate { 55 | _supportedHosts = nil; 56 | _subscribers = nil; 57 | _storedEvent = nil; 58 | 59 | [super onAppTerminate]; 60 | } 61 | 62 | #pragma mark Private API 63 | 64 | - (void)localInit { 65 | if (_supportedHosts) { 66 | return; 67 | } 68 | 69 | _subscribers = [[NSMutableDictionary alloc] init]; 70 | 71 | // get supported hosts from the config.xml 72 | _supportedHosts = [CULConfigXmlParser parse]; 73 | } 74 | 75 | /** 76 | * Store event data for future use. 77 | * If we are resuming the app - try to consume it. 78 | * 79 | * @param host host that matches the launch url 80 | * @param originalUrl launch url 81 | */ 82 | - (void)storeEventWithHost:(CULHost *)host originalURL:(NSURL *)originalUrl { 83 | _storedEvent = [CDVPluginResult resultWithHost:host originalURL:originalUrl]; 84 | [self tryToConsumeEvent]; 85 | } 86 | 87 | /** 88 | * Find host entry that corresponds to launch url. 89 | * 90 | * @param launchURL url that launched the app 91 | * @return host entry; nil if none is found 92 | */ 93 | - (CULHost *)findHostByURL:(NSURL *)launchURL { 94 | NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:launchURL resolvingAgainstBaseURL:YES]; 95 | CULHost *host = nil; 96 | for (CULHost *supportedHost in _supportedHosts) { 97 | if ([supportedHost.name isEqualToString:urlComponents.host]) { 98 | host = supportedHost; 99 | break; 100 | } 101 | } 102 | 103 | return host; 104 | } 105 | 106 | #pragma mark Methods to send data to JavaScript 107 | 108 | /** 109 | * Try to send event to the web page. 110 | * If there is a subscriber for the event - it will be consumed. 111 | * If not - it will stay until someone subscribes to it. 112 | */ 113 | - (void)tryToConsumeEvent { 114 | if (_subscribers.count == 0 || _storedEvent == nil) { 115 | return; 116 | } 117 | 118 | NSString *storedEventName = [_storedEvent eventName]; 119 | for (NSString *eventName in _subscribers) { 120 | if ([storedEventName isEqualToString:eventName]) { 121 | NSString *callbackID = _subscribers[eventName]; 122 | [self.commandDelegate sendPluginResult:_storedEvent callbackId:callbackID]; 123 | _storedEvent = nil; 124 | break; 125 | } 126 | } 127 | } 128 | 129 | #pragma mark Methods, available from JavaScript side 130 | 131 | - (void)jsSubscribeForEvent:(CDVInvokedUrlCommand *)command { 132 | NSString *eventName = [command eventName]; 133 | if (eventName.length == 0) { 134 | return; 135 | } 136 | 137 | _subscribers[eventName] = command.callbackId; 138 | [self tryToConsumeEvent]; 139 | } 140 | 141 | - (void)jsUnsubscribeFromEvent:(CDVInvokedUrlCommand *)command { 142 | NSString *eventName = [command eventName]; 143 | if (eventName.length == 0) { 144 | return; 145 | } 146 | 147 | [_subscribers removeObjectForKey:eventName]; 148 | } 149 | 150 | 151 | 152 | @end 153 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Universal Links Plugin 6 | 7 | Cordova plugin to add in your application support for Universal Links (iOS 9) and Deep Links (Android). 8 | Basically, open application through the link in the browser. 9 | 10 | MIT 11 | cordova,links,universal 12 | 13 | https://github.com/nordnet/cordova-universal-links-plugin 14 | https://github.com/nordnet/cordova-universal-links-plugin/issues 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /hooks/lib/ios/projectEntitlements.js: -------------------------------------------------------------------------------- 1 | /* 2 | Script creates entitlements file with the list of hosts, specified in config.xml. 3 | File name is: ProjectName.entitlements 4 | Location: ProjectName/ 5 | 6 | Script only generates content. File it self is included in the xcode project in another hook: xcodePreferences.js. 7 | */ 8 | (function() { 9 | 10 | var path = require('path'), 11 | fs = require('fs'), 12 | plist = require('plist'), 13 | mkpath = require('mkpath'), 14 | ConfigXmlHelper = require('../configXmlHelper.js'), 15 | ASSOCIATED_DOMAINS = 'com.apple.developer.associated-domains', 16 | context, 17 | projectRoot, 18 | projectName, 19 | entitlementsFilePath; 20 | 21 | module.exports = { 22 | generateAssociatedDomainsEntitlements: generateEntitlements 23 | }; 24 | 25 | // region Public API 26 | 27 | /** 28 | * Generate entitlements file content. 29 | * 30 | * @param {Object} cordovaContext - cordova context object 31 | * @param {Object} pluginPreferences - plugin preferences from config.xml; already parsed 32 | */ 33 | function generateEntitlements(cordovaContext, pluginPreferences) { 34 | context = cordovaContext; 35 | 36 | var currentEntitlements = getEntitlementsFileContent(), 37 | newEntitlements = injectPreferences(currentEntitlements, pluginPreferences); 38 | 39 | saveContentToEntitlementsFile(newEntitlements); 40 | } 41 | 42 | // endregion 43 | 44 | // region Work with entitlements file 45 | 46 | /** 47 | * Save data to entitlements file. 48 | * 49 | * @param {Object} content - data to save; JSON object that will be transformed into xml 50 | */ 51 | function saveContentToEntitlementsFile(content) { 52 | var plistContent = plist.build(content), 53 | filePath = pathToEntitlementsFile(); 54 | 55 | // ensure that file exists 56 | mkpath.sync(path.dirname(filePath)); 57 | 58 | // save it's content 59 | fs.writeFileSync(filePath, plistContent, 'utf8'); 60 | } 61 | 62 | /** 63 | * Read data from existing entitlements file. If none exist - default value is returned 64 | * 65 | * @return {String} entitlements file content 66 | */ 67 | function getEntitlementsFileContent() { 68 | var pathToFile = pathToEntitlementsFile(), 69 | content; 70 | 71 | try { 72 | content = fs.readFileSync(pathToFile, 'utf8'); 73 | } catch (err) { 74 | return defaultEntitlementsFile(); 75 | } 76 | 77 | return plist.parse(content); 78 | } 79 | 80 | /** 81 | * Get content for an empty entitlements file. 82 | * 83 | * @return {String} default entitlements file content 84 | */ 85 | function defaultEntitlementsFile() { 86 | return {}; 87 | } 88 | 89 | /** 90 | * Inject list of hosts into entitlements file. 91 | * 92 | * @param {Object} currentEntitlements - entitlements where to inject preferences 93 | * @param {Object} pluginPreferences - list of hosts from config.xml 94 | * @return {Object} new entitlements content 95 | */ 96 | function injectPreferences(currentEntitlements, pluginPreferences) { 97 | var newEntitlements = currentEntitlements, 98 | content = generateAssociatedDomainsContent(pluginPreferences); 99 | 100 | newEntitlements[ASSOCIATED_DOMAINS] = content; 101 | 102 | return newEntitlements; 103 | } 104 | 105 | /** 106 | * Generate content for associated-domains dictionary in the entitlements file. 107 | * 108 | * @param {Object} pluginPreferences - list of hosts from conig.xml 109 | * @return {Object} associated-domains dictionary content 110 | */ 111 | function generateAssociatedDomainsContent(pluginPreferences) { 112 | var domainsList = [], 113 | link; 114 | 115 | // generate list of host links 116 | pluginPreferences.hosts.forEach(function(host) { 117 | link = domainsListEntryForHost(host); 118 | domainsList.push(link); 119 | }); 120 | 121 | return domainsList; 122 | } 123 | 124 | /** 125 | * Generate domain record for the given host. 126 | * 127 | * @param {Object} host - host entry 128 | * @return {String} record 129 | */ 130 | function domainsListEntryForHost(host) { 131 | return 'applinks:' + host.name; 132 | } 133 | 134 | // endregion 135 | 136 | // region Path helper methods 137 | 138 | /** 139 | * Path to entitlements file. 140 | * 141 | * @return {String} absolute path to entitlements file 142 | */ 143 | function pathToEntitlementsFile() { 144 | if (entitlementsFilePath === undefined) { 145 | entitlementsFilePath = path.join(getProjectRoot(), 'platforms', 'ios', getProjectName(), 'Resources', getProjectName() + '.entitlements'); 146 | } 147 | 148 | return entitlementsFilePath; 149 | } 150 | 151 | /** 152 | * Projects root folder path. 153 | * 154 | * @return {String} absolute path to the projects root 155 | */ 156 | function getProjectRoot() { 157 | return context.opts.projectRoot; 158 | } 159 | 160 | /** 161 | * Name of the project from config.xml 162 | * 163 | * @return {String} project name 164 | */ 165 | function getProjectName() { 166 | if (projectName === undefined) { 167 | var configXmlHelper = new ConfigXmlHelper(context); 168 | projectName = configXmlHelper.getProjectName(); 169 | } 170 | 171 | return projectName; 172 | } 173 | 174 | // endregion 175 | 176 | })(); 177 | -------------------------------------------------------------------------------- /src/ios/JS/CDVPluginResult+CULPlugin.m: -------------------------------------------------------------------------------- 1 | // 2 | // CDVPluginResult+CULPlugin.m 3 | // 4 | // Created by Nikolay Demyankov on 15.09.15. 5 | // 6 | 7 | #import "CDVPluginResult+CULPlugin.h" 8 | 9 | #pragma mark keys for the message structure 10 | 11 | // event name 12 | static NSString *const EVENT = @"event"; 13 | 14 | // message data block 15 | static NSString *const DATA = @"data"; 16 | 17 | #pragma mark keys for the message data block 18 | 19 | // path part from the url 20 | static NSString *const PATH_ATTRIBUTE = @"path"; 21 | 22 | // scheme from the url 23 | static NSString *const SCHEME_ATTRIBUTE = @"scheme"; 24 | 25 | // host name from the url 26 | static NSString *const HOST_ATTRIBUTE = @"host"; 27 | 28 | // hash (fragment) from the url; data after '#' 29 | static NSString *const HASH_ATTRIBUTE = @"hash"; 30 | 31 | // launch url without any changes 32 | static NSString *const ORIGIN_ATTRIBUTE = @"url"; 33 | 34 | // query parameters from the url; data after '?' 35 | static NSString *const URL_PARAMS_ATTRIBUTE = @"params"; 36 | 37 | @implementation CDVPluginResult (CULPlugin) 38 | 39 | #pragma mark Public API 40 | 41 | + (instancetype)resultWithHost:(CULHost *)host originalURL:(NSURL *)originalURL { 42 | NSDictionary *message = [self prepareMessageForHost:host originalURL:originalURL]; 43 | 44 | CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:message]; 45 | [result setKeepCallbackAsBool:YES]; 46 | 47 | return result; 48 | } 49 | 50 | - (BOOL)isResultForEvent:(NSString *)eventName { 51 | NSString *eventInMessage = [self eventName]; 52 | if (eventInMessage.length == 0 || eventName.length == 0) { 53 | return NO; 54 | } 55 | 56 | return [eventInMessage isEqualToString:eventName]; 57 | } 58 | 59 | - (NSString *)eventName { 60 | if (self.message == nil || ![self.message isKindOfClass:[NSDictionary class]]) { 61 | return nil; 62 | } 63 | 64 | NSDictionary *data = self.message; 65 | 66 | return data[EVENT]; 67 | } 68 | 69 | #pragma mark Private API 70 | 71 | /** 72 | * Create dictionary for message, that should be send to JS. 73 | * Holds event name and event details. 74 | * 75 | * @param host host entry that corresponds to the url 76 | * @param originalURL launch url 77 | * 78 | * @return messasge dictionary 79 | */ 80 | + (NSDictionary *)prepareMessageForHost:(CULHost *)host originalURL:(NSURL *)originalURL { 81 | NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:originalURL resolvingAgainstBaseURL:YES]; 82 | NSMutableDictionary *messageDict = [[NSMutableDictionary alloc] init]; 83 | 84 | // set event name 85 | NSString *eventName = [self getEventNameBasedOnHost:host originalURLComponents:urlComponents]; 86 | [messageDict setObject:eventName forKey:EVENT]; 87 | 88 | // set event details 89 | NSDictionary *data = [self getDataDictionaryForURLComponents:urlComponents]; 90 | [messageDict setObject:data forKey:DATA]; 91 | 92 | return messageDict; 93 | } 94 | 95 | /** 96 | * Find event name based on the launch url and corresponding host entry. 97 | * 98 | * @param host host entry 99 | * @param urlComponents launch url components 100 | * 101 | * @return event name 102 | */ 103 | + (NSString *)getEventNameBasedOnHost:(CULHost *)host originalURLComponents:(NSURLComponents *)urlComponents { 104 | NSString *eventName = host.event; 105 | NSArray *hostPaths = host.paths; 106 | NSString *originalPath = urlComponents.path; 107 | 108 | if (originalPath.length == 0) { 109 | return eventName; 110 | } 111 | 112 | for (CULPath *hostPath in hostPaths) { 113 | NSRange range = [originalPath rangeOfString:hostPath.url options:NSRegularExpressionSearch]; 114 | if (range.location != NSNotFound && range.location == 0) { 115 | eventName = hostPath.event; 116 | break; 117 | } 118 | } 119 | 120 | return eventName; 121 | } 122 | 123 | /** 124 | * Create dictionary with event details. 125 | * 126 | * @param originalURLComponents launch url components 127 | * 128 | * @return dictionary with url information 129 | */ 130 | + (NSDictionary *)getDataDictionaryForURLComponents:(NSURLComponents *)originalURLComponents { 131 | NSMutableDictionary *dataDict = [[NSMutableDictionary alloc] init]; 132 | 133 | NSString *originUrl = originalURLComponents.URL.absoluteString; 134 | NSString *host = originalURLComponents.host ? originalURLComponents.host : @""; 135 | NSString *path = originalURLComponents.path ? originalURLComponents.path : @""; 136 | NSString *scheme = originalURLComponents.scheme ? originalURLComponents.scheme : @""; 137 | NSString *hash = originalURLComponents.fragment ? originalURLComponents.fragment : @""; 138 | 139 | [dataDict setObject:originUrl forKey:ORIGIN_ATTRIBUTE]; 140 | [dataDict setObject:host forKey:HOST_ATTRIBUTE]; 141 | [dataDict setObject:path forKey:PATH_ATTRIBUTE]; 142 | [dataDict setObject:scheme forKey:SCHEME_ATTRIBUTE]; 143 | [dataDict setObject:hash forKey:HASH_ATTRIBUTE]; 144 | 145 | // set query params 146 | NSArray *queryItems = originalURLComponents.queryItems; 147 | NSMutableDictionary *qParams = [[NSMutableDictionary alloc] init]; 148 | for (NSURLQueryItem *qItem in queryItems) { 149 | NSString *value = qItem.value ? qItem.value : @""; 150 | [qParams setValue:value forKey:qItem.name]; 151 | } 152 | [dataDict setObject:qParams forKey:URL_PARAMS_ATTRIBUTE]; 153 | 154 | return dataDict; 155 | } 156 | 157 | @end 158 | -------------------------------------------------------------------------------- /hooks/lib/android/webSiteHook.js: -------------------------------------------------------------------------------- 1 | /* 2 | Class creates android_web_hook.html file in your Cordova project root folder. 3 | File holds tags, which are generated based on data, specified in config.xml. 4 | You need to include those tags on your website to link web pages to the content inside your application. 5 | 6 | More documentation on that can be found here: 7 | https://developer.android.com/training/app-indexing/enabling-app-indexing.html 8 | */ 9 | (function() { 10 | 11 | var fs = require('fs'), 12 | path = require('path'), 13 | mkpath = require('mkpath'), 14 | ConfigXmlHelper = require('../configXmlHelper.js'), 15 | WEB_HOOK_FILE_PATH = path.join('ul_web_hooks', 'android', 'android_web_hook.html'), 16 | WEB_HOOK_TPL_FILE_PATH = path.join('plugins', 'cordova-universal-links-plugin', 'ul_web_hooks', 'android_web_hook_tpl.html'), 17 | LINK_PLACEHOLDER = '[__LINKS__]', 18 | LINK_TEMPLATE = ''; 19 | 20 | module.exports = { 21 | generate: generateWebHook 22 | }; 23 | 24 | // region Public API 25 | 26 | /** 27 | * Generate website hook for android application. 28 | * 29 | * @param {Object} cordovaContext - cordova context object 30 | * @param {Object} pluginPreferences - plugin preferences from config.xml file; already parsed 31 | */ 32 | function generateWebHook(cordovaContext, pluginPreferences) { 33 | var projectRoot = cordovaContext.opts.projectRoot, 34 | configXmlHelper = new ConfigXmlHelper(cordovaContext), 35 | packageName = configXmlHelper.getPackageName('android'), 36 | template = readTemplate(projectRoot); 37 | 38 | // if template was not found - exit 39 | if (template == null || template.length == 0) { 40 | return; 41 | } 42 | 43 | // generate hook content 44 | var linksToInsert = generateLinksSet(projectRoot, packageName, pluginPreferences), 45 | hookContent = template.replace(LINK_PLACEHOLDER, linksToInsert); 46 | 47 | // save hook 48 | saveWebHook(projectRoot, hookContent); 49 | } 50 | 51 | // endregion 52 | 53 | // region Public API 54 | 55 | /** 56 | * Read hook teplate from plugin directory. 57 | * 58 | * @param {String} projectRoot - absolute path to cordova's project root 59 | * @return {String} data from the template file 60 | */ 61 | function readTemplate(projectRoot) { 62 | var filePath = path.join(projectRoot, WEB_HOOK_TPL_FILE_PATH), 63 | tplData = null; 64 | 65 | try { 66 | tplData = fs.readFileSync(filePath, 'utf8'); 67 | } catch (err) { 68 | console.warn('Template file for android web hook is not found!'); 69 | console.warn(err); 70 | } 71 | 72 | return tplData; 73 | } 74 | 75 | /** 76 | * Generate list of tags based on plugin preferences. 77 | * 78 | * @param {String} projectRoot - absolute path to cordova's project root 79 | * @param {String} packageName - android application package name 80 | * @param {Object} pluginPreferences - plugin preferences, defined in config.xml; already parsed 81 | * @return {String} list of tags 82 | */ 83 | function generateLinksSet(projectRoot, packageName, pluginPreferences) { 84 | var linkTpl = LINK_TEMPLATE.replace('', packageName), 85 | content = ''; 86 | 87 | pluginPreferences.hosts.forEach(function(host) { 88 | host.paths.forEach(function(hostPath) { 89 | content += generateLinkTag(linkTpl, host.scheme, host.name, hostPath) + '\n'; 90 | }); 91 | }); 92 | 93 | return content; 94 | } 95 | 96 | /** 97 | * Generate tag. 98 | * 99 | * @param {String} linkTpl - template to use for tag generation 100 | * @param {String} scheme - host scheme 101 | * @param {String} host - host name 102 | * @param {String} path - host path 103 | * @return {String} tag 104 | */ 105 | function generateLinkTag(linkTpl, scheme, host, path) { 106 | linkTpl = linkTpl.replace('', scheme).replace('', host); 107 | if (path == null || path === '*') { 108 | return linkTpl.replace('', ''); 109 | } 110 | 111 | // for android we need to replace * with .* for pattern matching 112 | if (path.indexOf('*') >= 0) { 113 | path = path.replace(/\*/g, '.*'); 114 | } 115 | 116 | // path should start with / 117 | if (path.indexOf('/') != 0) { 118 | path = '/' + path; 119 | } 120 | 121 | return linkTpl.replace('', path); 122 | } 123 | 124 | /** 125 | * Save data to website hook file. 126 | * 127 | * @param {String} projectRoot - absolute path to project root 128 | * @param {String} hookContent - data to save 129 | * @return {boolean} true - if data was saved; otherwise - false; 130 | */ 131 | function saveWebHook(projectRoot, hookContent) { 132 | var filePath = path.join(projectRoot, WEB_HOOK_FILE_PATH), 133 | isSaved = true; 134 | 135 | // ensure directory exists 136 | createDirectoryIfNeeded(path.dirname(filePath)); 137 | 138 | // write data to file 139 | try { 140 | fs.writeFileSync(filePath, hookContent, 'utf8'); 141 | } catch (err) { 142 | console.warn('Failed to create android web hook!'); 143 | console.warn(err); 144 | isSaved = false; 145 | } 146 | 147 | return isSaved; 148 | } 149 | 150 | /** 151 | * Create directory if it doesn't exist yet. 152 | * 153 | * @param {String} dir - absolute path to directory 154 | */ 155 | function createDirectoryIfNeeded(dir) { 156 | try { 157 | mkpath.sync(dir); 158 | } catch (err) { 159 | console.log(err); 160 | } 161 | } 162 | 163 | // endregion 164 | 165 | })(); 166 | -------------------------------------------------------------------------------- /hooks/lib/ios/appleAppSiteAssociationFile.js: -------------------------------------------------------------------------------- 1 | /* 2 | Script generates apple-app-site-association files: one for each domain, defined in config.xml. 3 | It is executed on 'after prepare' stage, usually when you execute 'cordova build'. Files are placed in 'ul_web_hooks/ios/' folder 4 | of your projects root. 5 | 6 | Files are created with the following name: 7 | hostname#apple-app-site-association 8 | 9 | Prefix 'hostname#' describes on which host this file should be placed. Don't forget to remove it before uploading file on your host. 10 | Also, in the file you need to replace with the real team id from the member center, if preference was not set in projects config.xml. 11 | 12 | In order to activate support for Universal Links on iOS you need to sign them with the valid SSL certificate and place in the root of your domain. 13 | 14 | Additional documentation regarding apple-app-site-association file can be found here: 15 | - https://developer.apple.com/library/ios/documentation/General/Conceptual/AppSearch/UniversalLinks.html 16 | - https://developer.apple.com/library/ios/documentation/Security/Reference/SharedWebCredentialsRef/index.html#//apple_ref/doc/uid/TP40014989 17 | */ 18 | 19 | (function() { 20 | 21 | var path = require('path'), 22 | mkpath = require('mkpath'), 23 | fs = require('fs'), 24 | rimraf = require('rimraf'), 25 | ConfigXmlHelper = require('../configXmlHelper.js'), 26 | IOS_TEAM_ID = '', 27 | ASSOCIATION_FILE_NAME = 'apple-app-site-association', 28 | bundleId, 29 | context; 30 | 31 | module.exports = { 32 | generate: generate 33 | }; 34 | 35 | // region Public API 36 | 37 | /** 38 | * Generate apple-app-site-association files. 39 | * 40 | * @param {Object} cordovaContext - cordova context object 41 | * @param {Object} pluginPreferences - list of hosts from the config.xml; already parsed 42 | */ 43 | function generate(cordovaContext, pluginPreferences) { 44 | context = cordovaContext; 45 | removeOldFiles(); 46 | createNewAssociationFiles(pluginPreferences); 47 | } 48 | 49 | // endregion 50 | 51 | // region Content generation 52 | 53 | /** 54 | * Remove old files from ul_web_hooks/ios folder. 55 | */ 56 | function removeOldFiles() { 57 | rimraf.sync(getWebHookDirectory()); 58 | } 59 | 60 | /** 61 | * Generate new set of apple-app-site-association files. 62 | * 63 | * @param {Object} pluginPreferences - list of hosts from config.xml 64 | */ 65 | function createNewAssociationFiles(pluginPreferences) { 66 | var teamId = pluginPreferences.iosTeamId; 67 | if (!teamId) { 68 | teamId = IOS_TEAM_ID; 69 | } 70 | 71 | pluginPreferences.hosts.forEach(function(host) { 72 | var content = generateFileContentForHost(host, teamId); 73 | saveContentToFile(host.name, content); 74 | }); 75 | } 76 | 77 | /** 78 | * Generate content of the apple-app-site-association file for the specific host. 79 | * 80 | * @param {Object} host - host information 81 | * @return {Object} content of the file as JSON object 82 | */ 83 | function generateFileContentForHost(host, teamId) { 84 | var appID = teamId + '.' + getBundleId(); 85 | 86 | return { 87 | "applinks": { 88 | "apps": [], 89 | "details": [{ 90 | "appID": appID, 91 | "paths": host.paths 92 | }] 93 | } 94 | }; 95 | } 96 | 97 | /** 98 | * Save data to the the apple-app-site-association file. 99 | * 100 | * @param {String} filePrefix - prefix for the generated file; usually - hostname 101 | * @param {Object} content - file content as JSON object 102 | * @return {Boolean} true - if data was saved; otherwise - false 103 | */ 104 | function saveContentToFile(filePrefix, content) { 105 | var dirPath = getWebHookDirectory(), 106 | filePath = path.join(dirPath, filePrefix + '#' + ASSOCIATION_FILE_NAME), 107 | isSaved = true; 108 | 109 | // create all directories from file path 110 | createDirectoriesIfNeeded(dirPath); 111 | 112 | // write content to the file 113 | try { 114 | fs.writeFileSync(filePath, JSON.stringify(content, null, 2), 'utf8'); 115 | } catch (err) { 116 | console.log(err); 117 | isSaved = false; 118 | } 119 | 120 | return isSaved; 121 | } 122 | 123 | /** 124 | * Create all directories from the given path. 125 | * 126 | * @param {String} dirPath - full path to directory 127 | * @return {Boolean} true - if directories were created; false - some error happened during the execution 128 | */ 129 | function createDirectoriesIfNeeded(dirPath) { 130 | var isCreated = true; 131 | try { 132 | mkpath.sync(dirPath); 133 | } catch (err) { 134 | isCreated = false; 135 | console.log(err); 136 | } 137 | 138 | return isCreated; 139 | } 140 | 141 | // endregion 142 | 143 | // region Support methods 144 | 145 | /** 146 | * Path to the ios web hook directory. 147 | * 148 | * @return {String} path to web hook directory 149 | */ 150 | function getWebHookDirectory() { 151 | return path.join(getProjectRoot(), 'ul_web_hooks', 'ios'); 152 | } 153 | 154 | /** 155 | * Project root directory 156 | * 157 | * @return {String} absolute path to project root 158 | */ 159 | function getProjectRoot() { 160 | return context.opts.projectRoot; 161 | } 162 | 163 | /** 164 | * Get bundle id from the config.xml file. 165 | * 166 | * @return {String} bundle id 167 | */ 168 | function getBundleId() { 169 | if (bundleId === undefined) { 170 | var configXmlHelper = new ConfigXmlHelper(context); 171 | bundleId = configXmlHelper.getPackageName('ios'); 172 | } 173 | 174 | return bundleId; 175 | } 176 | 177 | // endregion 178 | 179 | })(); 180 | -------------------------------------------------------------------------------- /src/android/com/nordnetab/cordova/ul/model/JSMessage.java: -------------------------------------------------------------------------------- 1 | package com.nordnetab.cordova.ul.model; 2 | 3 | import android.net.Uri; 4 | import android.util.Log; 5 | 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | /** 13 | * Created by Nikolay Demyankov on 10.09.15. 14 | *

15 | * Model for the message entry, that is send to JS. 16 | */ 17 | public class JSMessage extends JSONObject { 18 | 19 | // keys for the message base structure 20 | private static final class JSGeneralKeys { 21 | /** 22 | * Event name 23 | */ 24 | public static final String EVENT = "event"; 25 | 26 | /** 27 | * Message data block 28 | */ 29 | public static final String DATA = "data"; 30 | } 31 | 32 | // keys for the message data block 33 | private static final class JSDataKeys { 34 | 35 | /** 36 | * Path part of the url 37 | */ 38 | public static final String PATH = "path"; 39 | 40 | /** 41 | * Scheme of the url 42 | */ 43 | public static final String SCHEME = "scheme"; 44 | 45 | /** 46 | * Host of the url 47 | */ 48 | public static final String HOST = "host"; 49 | 50 | /** 51 | * Hash (fragment) from the url - data after '#' 52 | */ 53 | public static final String HASH = "hash"; 54 | 55 | /** 56 | * Query parameters - data after '?' 57 | */ 58 | public static final String PARAMS = "params"; 59 | 60 | /** 61 | * Launch url as it is 62 | */ 63 | public static final String ORIGIN = "url"; 64 | } 65 | 66 | private String eventName; 67 | 68 | /** 69 | * Constructor 70 | * 71 | * @param host host entry that corresponds to the launching url 72 | * @param originalUri launch url 73 | */ 74 | public JSMessage(ULHost host, Uri originalUri) { 75 | setEventName(host, originalUri); 76 | setMessageData(host, originalUri); 77 | } 78 | 79 | /** 80 | * Getter for event name of this message. 81 | * 82 | * @return event name 83 | */ 84 | public String getEventName() { 85 | return eventName; 86 | } 87 | 88 | // region Event name setters 89 | 90 | /** 91 | * Set event name for this message entry. 92 | */ 93 | private void setEventName(ULHost host, Uri originalUri) { 94 | eventName = getEventName(host, originalUri); 95 | 96 | try { 97 | put(JSGeneralKeys.EVENT, eventName); 98 | } catch (JSONException e) { 99 | Log.d("UniversalLinks", "Failed to set event name", e); 100 | } 101 | } 102 | 103 | /** 104 | * Find event name based on the launching url. 105 | * By default, event name from the host object will be used. 106 | * But if we have some path entry in the host and it matches the one from the launch url - his event name will be used. 107 | */ 108 | private String getEventName(ULHost host, Uri originalUri) { 109 | String event = host.getEvent(); 110 | final String originPath = originalUri.getPath(); 111 | final List hostPathsList = host.getPaths(); 112 | for (ULPath hostPath : hostPathsList) { 113 | final String hostPathUrl = hostPath.getUrl(); 114 | if (hostPathUrl == null) { 115 | continue; 116 | } 117 | 118 | if (originPath.matches(hostPathUrl)) { 119 | event = hostPath.getEvent(); 120 | break; 121 | } 122 | } 123 | 124 | return event; 125 | } 126 | 127 | // endregion 128 | 129 | // region Data block setters 130 | 131 | /** 132 | * Fill data block with corresponding information. 133 | */ 134 | private void setMessageData(ULHost host, Uri originalUri) { 135 | final JSONObject dataObject = new JSONObject(); 136 | 137 | try { 138 | setOriginalUrl(dataObject, originalUri); 139 | setHostData(dataObject, host); 140 | setPathData(dataObject, originalUri); 141 | 142 | put(JSGeneralKeys.DATA, dataObject); 143 | } catch (JSONException e) { 144 | Log.d("UniversalLinks", "Failed to set event data", e); 145 | } 146 | } 147 | 148 | /** 149 | * Put launch url to the data block 150 | */ 151 | private void setOriginalUrl(JSONObject dataObject, Uri originalUri) throws JSONException { 152 | dataObject.put(JSDataKeys.ORIGIN, originalUri.toString()); 153 | } 154 | 155 | /** 156 | * Put host name and scheme into data block 157 | */ 158 | private void setHostData(JSONObject dataObject, ULHost host) throws JSONException { 159 | dataObject.put(JSDataKeys.HOST, host.getName()); 160 | dataObject.put(JSDataKeys.SCHEME, host.getScheme()); 161 | } 162 | 163 | /** 164 | * Put path information into data block 165 | */ 166 | private void setPathData(JSONObject dataObject, Uri originalUri) throws JSONException { 167 | dataObject.put(JSDataKeys.HASH, originalUri.getFragment()); 168 | dataObject.put(JSDataKeys.PATH, originalUri.getPath()); 169 | 170 | final JSONObject queryParams = getQueryParamsFromUri(originalUri); 171 | dataObject.put(JSDataKeys.PARAMS, queryParams); 172 | } 173 | 174 | /** 175 | * Parse query params. 176 | * For example, if we have link like so: http://somedomain.com/some/path?foo=fooVal&bar=barVal , then 177 | * resulting object will be {foo: fooVal, bar: barVal}. 178 | * 179 | * @return json object 180 | */ 181 | private JSONObject getQueryParamsFromUri(Uri originalUri) throws JSONException, UnsupportedOperationException { 182 | JSONObject queryParams = new JSONObject(); 183 | Set keysList = originalUri.getQueryParameterNames(); 184 | for (String key : keysList) { 185 | final String value = originalUri.getQueryParameter(key); 186 | queryParams.put(key, value); 187 | } 188 | 189 | return queryParams; 190 | } 191 | 192 | // endregion 193 | } -------------------------------------------------------------------------------- /src/android/com/nordnetab/cordova/ul/UniversalLinksPlugin.java: -------------------------------------------------------------------------------- 1 | package com.nordnetab.cordova.ul; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.text.TextUtils; 6 | import android.util.Log; 7 | 8 | import com.nordnetab.cordova.ul.js.JSAction; 9 | import com.nordnetab.cordova.ul.model.JSMessage; 10 | import com.nordnetab.cordova.ul.model.ULHost; 11 | import com.nordnetab.cordova.ul.parser.ULConfigXmlParser; 12 | 13 | import org.apache.cordova.CallbackContext; 14 | import org.apache.cordova.CordovaArgs; 15 | import org.apache.cordova.CordovaInterface; 16 | import org.apache.cordova.CordovaPlugin; 17 | import org.apache.cordova.CordovaWebView; 18 | import org.apache.cordova.PluginResult; 19 | import org.json.JSONException; 20 | 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Set; 25 | 26 | /** 27 | * Created by Nikolay Demyankov on 09.09.15. 28 | *

29 | * Plugin main class. 30 | * Communicates with the JS side, handles launch intents and so on. 31 | */ 32 | public class UniversalLinksPlugin extends CordovaPlugin { 33 | 34 | // list of hosts, defined in config.xml 35 | private List supportedHosts; 36 | 37 | // list of subscribers 38 | private Map subscribers; 39 | 40 | // stored message, that is captured on application launch 41 | private JSMessage storedMessage; 42 | 43 | // region Public API 44 | 45 | @Override 46 | public void initialize(CordovaInterface cordova, CordovaWebView webView) { 47 | super.initialize(cordova, webView); 48 | 49 | supportedHosts = new ULConfigXmlParser(cordova.getActivity()).parse(); 50 | 51 | if (subscribers == null) { 52 | subscribers = new HashMap(); 53 | } 54 | 55 | handleIntent(cordova.getActivity().getIntent()); 56 | } 57 | 58 | @Override 59 | public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) throws JSONException { 60 | boolean isHandled = true; 61 | if (JSAction.SUBSCRIBE.equals(action)) { 62 | subscribeForEvent(args, callbackContext); 63 | } else if (JSAction.UNSUBSCRIBE.equals(action)) { 64 | unsubscribeFromEvent(args); 65 | } else { 66 | isHandled = false; 67 | } 68 | 69 | return isHandled; 70 | } 71 | 72 | @Override 73 | public void onNewIntent(Intent intent) { 74 | handleIntent(intent); 75 | } 76 | 77 | // endregion 78 | 79 | // region JavaScript methods 80 | 81 | /** 82 | * Add subscriber for the event. 83 | * 84 | * @param arguments arguments, passed from JS side 85 | * @param callbackContext callback to use when event is captured 86 | */ 87 | private void subscribeForEvent(final CordovaArgs arguments, final CallbackContext callbackContext) { 88 | final String eventName = getEventNameFromArguments(arguments); 89 | if (TextUtils.isEmpty(eventName)) { 90 | return; 91 | } 92 | 93 | subscribers.put(eventName, callbackContext); 94 | tryToConsumeEvent(); 95 | } 96 | 97 | /** 98 | * Remove subscriber from the event. 99 | * 100 | * @param arguments arguments, passed from JS side 101 | */ 102 | private void unsubscribeFromEvent(final CordovaArgs arguments) { 103 | if (subscribers.size() == 0) { 104 | return; 105 | } 106 | 107 | final String eventName = getEventNameFromArguments(arguments); 108 | if (TextUtils.isEmpty(eventName)) { 109 | return; 110 | } 111 | 112 | subscribers.remove(eventName); 113 | } 114 | 115 | /** 116 | * Get event name from the cordova arguments. 117 | * 118 | * @param arguments received arguments 119 | * @return event name; null if non is found 120 | */ 121 | private String getEventNameFromArguments(final CordovaArgs arguments) { 122 | String eventName = null; 123 | try { 124 | eventName = arguments.getString(0); 125 | } catch (JSONException e) { 126 | Log.d("UniversalLinks", "Failed to get event name from the JS arguments", e); 127 | } 128 | 129 | return eventName; 130 | } 131 | 132 | /** 133 | * Try to send event to the subscribers. 134 | */ 135 | private void tryToConsumeEvent() { 136 | if (subscribers.size() == 0 || storedMessage == null) { 137 | return; 138 | } 139 | 140 | final String storedEventName = storedMessage.getEventName(); 141 | final Set> subscribersSet = subscribers.entrySet(); 142 | for (Map.Entry subscriber : subscribersSet) { 143 | final String eventName = subscriber.getKey(); 144 | if (eventName.equals(storedEventName)) { 145 | sendMessageToJs(storedMessage, subscriber.getValue()); 146 | storedMessage = null; 147 | break; 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * Send message to JS side. 154 | * 155 | * @param message message to send 156 | * @param callback to what callback we are sending the message 157 | */ 158 | private void sendMessageToJs(JSMessage message, CallbackContext callback) { 159 | final PluginResult result = new PluginResult(PluginResult.Status.OK, message); 160 | result.setKeepCallback(true); 161 | callback.sendPluginResult(result); 162 | } 163 | 164 | // endregion 165 | 166 | // region Intent handling 167 | 168 | /** 169 | * Handle launch intent. 170 | * If it is an UL intent - then event will be dispatched to the JS side. 171 | * 172 | * @param intent launch intent 173 | */ 174 | private void handleIntent(Intent intent) { 175 | if (intent == null || supportedHosts == null || supportedHosts.size() == 0) { 176 | return; 177 | } 178 | 179 | // read intent 180 | String action = intent.getAction(); 181 | Uri launchUri = intent.getData(); 182 | 183 | // if app was not launched by the url - ignore 184 | if (!Intent.ACTION_VIEW.equals(action) || launchUri == null) { 185 | return; 186 | } 187 | 188 | // try to find host in the hosts list from the config.xml 189 | ULHost host = findHostByUrl(launchUri); 190 | if (host == null) { 191 | Log.d("UniversalLinks", "Host " + launchUri.getHost() + " is not supported"); 192 | return; 193 | } 194 | 195 | // store message and try to consume it 196 | storedMessage = new JSMessage(host, launchUri); 197 | tryToConsumeEvent(); 198 | } 199 | 200 | /** 201 | * Find host entry that matches the launch url. 202 | * 203 | * @param url launch url 204 | * @return host entry; null - if none were found 205 | */ 206 | private ULHost findHostByUrl(Uri url) { 207 | ULHost host = null; 208 | for (ULHost supportedHost : supportedHosts) { 209 | if (supportedHost.getName().equals(url.getHost())) { 210 | host = supportedHost; 211 | break; 212 | } 213 | } 214 | 215 | return host; 216 | } 217 | 218 | // endregion 219 | } -------------------------------------------------------------------------------- /hooks/lib/ios/xcodePreferences.js: -------------------------------------------------------------------------------- 1 | /* 2 | Script activates support for Universal Links in the application by setting proper preferences in the xcode project file. 3 | Which is: 4 | - deployment target set to iOS 9.0 5 | - .entitlements file added to project PBXGroup and PBXFileReferences section 6 | - path to .entitlements file added to Code Sign Entitlements preference 7 | */ 8 | 9 | (function() { 10 | 11 | var path = require('path'), 12 | compare = require('node-version-compare'), 13 | ConfigXmlHelper = require('../configXmlHelper.js'), 14 | // pbxFile = require('xcode/lib/pbxFile'), 15 | IOS_DEPLOYMENT_TARGET = '8.0', 16 | COMMENT_KEY = /_comment$/, 17 | context; 18 | 19 | module.exports = { 20 | enableAssociativeDomainsCapability: enableAssociativeDomainsCapability 21 | } 22 | 23 | // region Public API 24 | 25 | /** 26 | * Activate associated domains capability for the application. 27 | * 28 | * @param {Object} cordovaContext - cordova context object 29 | */ 30 | function enableAssociativeDomainsCapability(cordovaContext) { 31 | context = cordovaContext; 32 | 33 | var projectFile = loadProjectFile(); 34 | 35 | // adjust preferences 36 | activateAssociativeDomains(projectFile.xcode); 37 | 38 | // add entitlements file to pbxfilereference 39 | addPbxReference(projectFile.xcode); 40 | 41 | // save changes 42 | projectFile.write(); 43 | } 44 | 45 | // endregion 46 | 47 | // region Alter project file preferences 48 | 49 | /** 50 | * Activate associated domains support in the xcode project file: 51 | * - set deployment target to ios 9; 52 | * - add .entitlements file to Code Sign Entitlements preference. 53 | * 54 | * @param {Object} xcodeProject - xcode project preferences; all changes are made in that instance 55 | */ 56 | function activateAssociativeDomains(xcodeProject) { 57 | var configurations = nonComments(xcodeProject.pbxXCBuildConfigurationSection()), 58 | entitlementsFilePath = pathToEntitlementsFile(), 59 | config, 60 | buildSettings, 61 | deploymentTargetIsUpdated; 62 | 63 | for (config in configurations) { 64 | buildSettings = configurations[config].buildSettings; 65 | buildSettings['CODE_SIGN_ENTITLEMENTS'] = '"' + entitlementsFilePath + '"'; 66 | 67 | // if deployment target is less then the required one - increase it 68 | if (buildSettings['IPHONEOS_DEPLOYMENT_TARGET']) { 69 | if (compare(buildSettings['IPHONEOS_DEPLOYMENT_TARGET'], IOS_DEPLOYMENT_TARGET) == -1) { 70 | buildSettings['IPHONEOS_DEPLOYMENT_TARGET'] = IOS_DEPLOYMENT_TARGET; 71 | deploymentTargetIsUpdated = true; 72 | } 73 | } else { 74 | buildSettings['IPHONEOS_DEPLOYMENT_TARGET'] = IOS_DEPLOYMENT_TARGET; 75 | deploymentTargetIsUpdated = true; 76 | } 77 | } 78 | 79 | if (deploymentTargetIsUpdated) { 80 | console.log('IOS project now has deployment target set as: ' + IOS_DEPLOYMENT_TARGET); 81 | } 82 | 83 | console.log('IOS project Code Sign Entitlements now set to: ' + entitlementsFilePath); 84 | } 85 | 86 | // endregion 87 | 88 | // region PBXReference methods 89 | 90 | /** 91 | * Add .entitlemets file into the project. 92 | * 93 | * @param {Object} xcodeProject - xcode project preferences; all changes are made in that instance 94 | */ 95 | function addPbxReference(xcodeProject) { 96 | var fileReferenceSection = nonComments(xcodeProject.pbxFileReferenceSection()), 97 | entitlementsRelativeFilePath = pathToEntitlementsFile(); 98 | 99 | if (isPbxReferenceAlreadySet(fileReferenceSection, entitlementsRelativeFilePath)) { 100 | console.log('Entitlements file is in reference section.'); 101 | return; 102 | } 103 | 104 | console.log('Entitlements file is not in references section, adding it'); 105 | createPbxFileReference(xcodeProject, entitlementsRelativeFilePath); 106 | } 107 | 108 | /** 109 | * Check if .entitlemets file reference already set. 110 | * 111 | * @param {Object} fileReferenceSection - PBXFileReference section 112 | * @param {String} entitlementsRelativeFilePath - relative path to entitlements file 113 | * @return true - if reference is set; otherwise - false 114 | */ 115 | function isPbxReferenceAlreadySet(fileReferenceSection, entitlementsRelativeFilePath) { 116 | var isAlreadyInReferencesSection = false, 117 | uuid, 118 | fileRefEntry; 119 | 120 | for (uuid in fileReferenceSection) { 121 | fileRefEntry = fileReferenceSection[uuid]; 122 | if (fileRefEntry.path && fileRefEntry.path.indexOf(entitlementsRelativeFilePath) > -1) { 123 | isAlreadyInReferencesSection = true; 124 | break; 125 | } 126 | } 127 | 128 | return isAlreadyInReferencesSection; 129 | } 130 | 131 | /** 132 | * Create reference to the entitlements file in the xcode project. 133 | * 134 | * @param {Object} xcodeProject - xcode project preferences; all changes are made in that instance 135 | * @param {String} entitlementsRelativeFilePath - relative path to entitlemets file 136 | */ 137 | function createPbxFileReference(xcodeProject, entitlementsRelativeFilePath) { 138 | // commented for now 139 | // var rootGroup = nonComments(xcodeProject.pbxGroupByName('CustomTemplate')), 140 | // entitlementsPbxFile = new pbxFile(entitlementsRelativeFilePath); 141 | // 142 | // entitlementsPbxFile.fileRef = xcodeProject.generateUuid(), 143 | // entitlementsPbxFile.uuid = xcodeProject.generateUuid(); 144 | // 145 | // xcodeProject.addToPbxFileReferenceSection(entitlementsPbxFile); 146 | // 147 | // rootGroup.children.push({ 148 | // 'value': entitlementsPbxFile.fileRef, 149 | // 'comment': path.basename(entitlementsRelativeFilePath) 150 | // }); 151 | xcodeProject.addResourceFile(path.basename(entitlementsRelativeFilePath)); 152 | } 153 | 154 | // region Xcode project file helpers 155 | 156 | /** 157 | * Load iOS project file from platform specific folder. 158 | * 159 | * @return {Object} projectFile - project file information 160 | */ 161 | function loadProjectFile() { 162 | var platform_ios, 163 | projectFile; 164 | 165 | try { 166 | // try pre-5.0 cordova structure 167 | platform_ios = context.requireCordovaModule('cordova-lib/src/plugman/platforms')['ios']; 168 | projectFile = platform_ios.parseProjectFile(iosPlatformPath()); 169 | } catch (e) { 170 | // let's try cordova 5.0 structure 171 | platform_ios = context.requireCordovaModule('cordova-lib/src/plugman/platforms/ios'); 172 | projectFile = platform_ios.parseProjectFile(iosPlatformPath()); 173 | } 174 | 175 | return projectFile; 176 | } 177 | 178 | /** 179 | * Remove comments from the file. 180 | * 181 | * @param {Object} obj - file object 182 | * @return {Object} file object without comments 183 | */ 184 | function nonComments(obj) { 185 | var keys = Object.keys(obj), 186 | newObj = {}; 187 | 188 | for (var i = 0, len = keys.length; i < len; i++) { 189 | if (!COMMENT_KEY.test(keys[i])) { 190 | newObj[keys[i]] = obj[keys[i]]; 191 | } 192 | } 193 | 194 | return newObj; 195 | } 196 | 197 | // endregion 198 | 199 | // region Path helpers 200 | 201 | function iosPlatformPath() { 202 | return path.join(projectRoot(), 'platforms', 'ios'); 203 | } 204 | 205 | function projectRoot() { 206 | return context.opts.projectRoot; 207 | } 208 | 209 | function pathToEntitlementsFile() { 210 | var configXmlHelper = new ConfigXmlHelper(context), 211 | projectName = configXmlHelper.getProjectName(), 212 | fileName = projectName + '.entitlements'; 213 | 214 | return path.join(projectName, 'Resources', fileName); 215 | } 216 | 217 | // endregion 218 | 219 | })(); 220 | -------------------------------------------------------------------------------- /hooks/lib/android/manifestWriter.js: -------------------------------------------------------------------------------- 1 | /** 2 | Class injects plugin preferences into AndroidManifest.xml file. 3 | */ 4 | (function() { 5 | 6 | var path = require('path'), 7 | xmlHelper = require('../xmlHelper.js'); 8 | 9 | module.exports = { 10 | writePreferences: writePreferences 11 | }; 12 | 13 | // region Public API 14 | 15 | /** 16 | * Inject preferences into AndroidManifest.xml file. 17 | * 18 | * @param {Object} cordovaContext - cordova context object 19 | * @param {Object} pluginPreferences - plugin preferences as JSON object; already parsed 20 | */ 21 | function writePreferences(cordovaContext, pluginPreferences) { 22 | var pathToManifest = path.join(cordovaContext.opts.projectRoot, 'platforms', 'android', 'AndroidManifest.xml'), 23 | manifestSource = xmlHelper.readXmlAsJson(pathToManifest), 24 | cleanManifest, 25 | updatedManifest; 26 | 27 | // remove old intent-filters 28 | cleanManifest = removeOldOptions(manifestSource); 29 | 30 | // inject intent-filters based on plugin preferences 31 | updatedManifest = injectOptions(cleanManifest, pluginPreferences); 32 | 33 | // save new version of the AndroidManifest 34 | xmlHelper.writeJsonAsXml(updatedManifest, pathToManifest); 35 | } 36 | 37 | // endregion 38 | 39 | // region Manifest cleanup methods 40 | 41 | /** 42 | * Remove old intent-filters from the manifest file. 43 | * 44 | * @param {Object} manifestData - manifest content as JSON object 45 | * @return {Object} manifest data without old intent-filters 46 | */ 47 | function removeOldOptions(manifestData) { 48 | var cleanManifest = manifestData, 49 | activities = manifestData['manifest']['application'][0]['activity']; 50 | 51 | activities.forEach(removeIntentFiltersFromActivity); 52 | cleanManifest['manifest']['application'][0]['activity'] = activities; 53 | 54 | return cleanManifest; 55 | } 56 | 57 | /** 58 | * Remove old intent filters from the given activity. 59 | * 60 | * @param {Object} activity - activity, from which we need to remove intent-filters. 61 | * Changes applied to the passed object. 62 | */ 63 | function removeIntentFiltersFromActivity(activity) { 64 | var oldIntentFilters = activity['intent-filter'], 65 | newIntentFilters = []; 66 | if (oldIntentFilters == null || oldIntentFilters.length == 0) { 67 | return; 68 | } 69 | 70 | oldIntentFilters.forEach(function(intentFilter) { 71 | if (!isIntentFilterForUniversalLinks(intentFilter)) { 72 | newIntentFilters.push(intentFilter); 73 | } 74 | }); 75 | 76 | activity['intent-filter'] = newIntentFilters; 77 | } 78 | 79 | /** 80 | * Check if given intent-filter is for Universal Links. 81 | * 82 | * @param {Object} intentFilter - intent-filter to check 83 | * @return {Boolean} true - if intent-filter for Universal Links; otherwise - false; 84 | */ 85 | function isIntentFilterForUniversalLinks(intentFilter) { 86 | var actions = intentFilter['action'], 87 | categories = intentFilter['category'], 88 | data = intentFilter['data']; 89 | 90 | return isActionForUniversalLinks(actions) && isCategoriesForUniversalLinks(categories) && isDataTagForUniversalLinks(data); 91 | } 92 | 93 | /** 94 | * Check if actions from the intent-filter corresponds to actions for Universal Links. 95 | * 96 | * @param {Array} actions - list of actions in the intent-filter 97 | * @return {Boolean} true - if action for Universal Links; otherwise - false 98 | */ 99 | function isActionForUniversalLinks(actions) { 100 | // there can be only 1 action 101 | if (actions == null || actions.length != 1) { 102 | return false; 103 | } 104 | 105 | var action = actions[0]['$']['android:name']; 106 | 107 | return ('android.intent.action.VIEW' === action); 108 | } 109 | 110 | /** 111 | * Check if categories in the intent-filter corresponds to categories for Universal Links. 112 | * 113 | * @param {Array} categories - list of categories in the intent-filter 114 | * @return {Boolean} true - if action for Universal Links; otherwise - false 115 | */ 116 | function isCategoriesForUniversalLinks(categories) { 117 | // there can be only 2 categories 118 | if (categories == null || categories.length != 2) { 119 | return false; 120 | } 121 | 122 | var isBrowsable = false, 123 | isDefault = false; 124 | 125 | // check intent categories 126 | categories.forEach(function(category) { 127 | var categoryName = category['$']['android:name']; 128 | if (!isBrowsable) { 129 | isBrowsable = 'android.intent.category.BROWSABLE' === categoryName; 130 | } 131 | 132 | if (!isDefault) { 133 | isDefault = 'android.intent.category.DEFAULT' === categoryName; 134 | } 135 | }); 136 | 137 | return isDefault && isBrowsable; 138 | } 139 | 140 | /** 141 | * Check if data tag from intent-filter corresponds to data for Universal Links. 142 | * 143 | * @param {Array} data - list of data tags in the intent-filter 144 | * @return {Boolean} true - if data tag for Universal Links; otherwise - false 145 | */ 146 | function isDataTagForUniversalLinks(data) { 147 | // can have only 1 data tag in the intent-filter 148 | if (data == null || data.length != 1) { 149 | return false; 150 | } 151 | 152 | var dataHost = data[0]['$']['android:host'], 153 | dataScheme = data[0]['$']['android:scheme'], 154 | hostIsSet = dataHost != null && dataHost.length > 0, 155 | schemeIsSet = dataScheme != null && dataScheme.length > 0; 156 | 157 | return hostIsSet && schemeIsSet; 158 | } 159 | 160 | // endregion 161 | 162 | // region Methods to inject preferences into AndroidManifest.xml file 163 | 164 | /** 165 | * Inject options into manifest file. 166 | * 167 | * @param {Object} manifestData - manifest content where preferences should be injected 168 | * @param {Object} pluginPreferences - plugin preferences from config.xml; already parsed 169 | * @return {Object} updated manifest data with corresponding intent-filters 170 | */ 171 | function injectOptions(manifestData, pluginPreferences) { 172 | var changedManifest = manifestData, 173 | activitiesList = changedManifest['manifest']['application'][0]['activity'], 174 | launchActivityIndex = getMainLaunchActivityIndex(activitiesList), 175 | ulIntentFilters = [], 176 | launchActivity; 177 | 178 | if (launchActivityIndex < 0) { 179 | console.warn('Could not find launch activity in the AndroidManifest file. Can\'t inject Universal Links preferences.'); 180 | return; 181 | } 182 | 183 | // get launch activity 184 | launchActivity = activitiesList[launchActivityIndex]; 185 | 186 | // generate intent-filters 187 | pluginPreferences.hosts.forEach(function(host) { 188 | host.paths.forEach(function(hostPath) { 189 | ulIntentFilters.push(createIntentFilter(host.name, host.scheme, hostPath)); 190 | }); 191 | }); 192 | 193 | // add Universal Links intent-filters to the launch activity 194 | launchActivity['intent-filter'] = launchActivity['intent-filter'].concat(ulIntentFilters); 195 | 196 | return changedManifest; 197 | } 198 | 199 | /** 200 | * Find index of the applications launcher activity. 201 | * 202 | * @param {Array} activities - list of all activities in the app 203 | * @return {Integer} index of the launch activity; -1 - if none was found 204 | */ 205 | function getMainLaunchActivityIndex(activities) { 206 | var launchActivityIndex = -1; 207 | activities.some(function(activity, index) { 208 | if (isLaunchActivity(activity)) { 209 | launchActivityIndex = index; 210 | return true; 211 | } 212 | 213 | return false; 214 | }); 215 | 216 | return launchActivityIndex; 217 | } 218 | 219 | /** 220 | * Check if the given actvity is a launch activity. 221 | * 222 | * @param {Object} activity - activity to check 223 | * @return {Boolean} true - if this is a launch activity; otherwise - false 224 | */ 225 | function isLaunchActivity(activity) { 226 | var intentFilters = activity['intent-filter'], 227 | isLauncher = false; 228 | 229 | if (intentFilters == null || intentFilters.length == 0) { 230 | return false; 231 | } 232 | 233 | isLauncher = intentFilters.some(function(intentFilter) { 234 | var action = intentFilter['action'], 235 | category = intentFilter['category']; 236 | 237 | if (action == null || action.length != 1 || category == null || category.length != 1) { 238 | return false; 239 | } 240 | 241 | var isMainAction = ('android.intent.action.MAIN' === action[0]['$']['android:name']), 242 | isLauncherCategory = ('android.intent.category.LAUNCHER' === category[0]['$']['android:name']); 243 | 244 | return isMainAction && isLauncherCategory; 245 | }); 246 | 247 | return isLauncher; 248 | } 249 | 250 | /** 251 | * Create JSON object that represent intent-filter for universal link. 252 | * 253 | * @param {String} host - host name 254 | * @param {String} scheme - host scheme 255 | * @param {String} pathName - host path 256 | * @return {Object} intent-filter as a JSON object 257 | */ 258 | function createIntentFilter(host, scheme, pathName) { 259 | var intentFilter = { 260 | '$' : { 261 | 'android:autoVerify': 'true' 262 | }, 263 | 'action': [{ 264 | '$': { 265 | 'android:name': 'android.intent.action.VIEW' 266 | } 267 | }], 268 | 'category': [{ 269 | '$': { 270 | 'android:name': 'android.intent.category.DEFAULT' 271 | } 272 | }, { 273 | '$': { 274 | 'android:name': 'android.intent.category.BROWSABLE' 275 | } 276 | }], 277 | 'data': [{ 278 | '$': { 279 | 'android:host': host, 280 | 'android:scheme': scheme 281 | } 282 | }] 283 | }; 284 | 285 | injectPathComponentIntoIntentFilter(intentFilter, pathName); 286 | 287 | return intentFilter; 288 | } 289 | 290 | /** 291 | * Inject host path into provided intent-filter. 292 | * 293 | * @param {Object} intentFilter - intent-filter object where path component should be injected 294 | * @param {String} pathName - host path to inject 295 | */ 296 | function injectPathComponentIntoIntentFilter(intentFilter, pathName) { 297 | if (pathName == null || pathName === '*') { 298 | return; 299 | } 300 | 301 | var attrKey = 'android:path'; 302 | if (pathName.indexOf('*') >= 0) { 303 | attrKey = 'android:pathPattern'; 304 | pathName = pathName.replace(/\*/g, '.*'); 305 | } 306 | 307 | if (pathName.indexOf('/') != 0) { 308 | pathName = '/' + pathName; 309 | } 310 | 311 | intentFilter['data'][0]['$'][attrKey] = pathName; 312 | } 313 | 314 | // endregion 315 | 316 | })(); 317 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cordova Universal Links Plugin 2 | This Cordova plugin adds support for opening an application from the browser when user clicks on the link. Better known as: 3 | - [Universal Links on iOS](https://developer.apple.com/library/ios/documentation/General/Conceptual/AppSearch/UniversalLinks.html) 4 | - [Deep Linking on Android](https://developer.android.com/training/app-indexing/deep-linking.html) 5 | 6 | Basically, you can have a single link that will either open your app or your website, if the app isn't installed. 7 | 8 | Integration process is simple: 9 | 10 | 1. Add the plugin to your project (see [Installation](#installation)). 11 | 2. Define supported hosts and paths in Cordova's `config.xml` (see [Cordova config preferences](#cordova-config-preferences)). 12 | 3. Write some JavaScript code to listen for application launch by the links (see [Application launch handling](#application-launch-handling)). 13 | 4. Build project from the CLI. 14 | 5. Activate support for UL on your website (see [Android web integration](#android-web-integration) and [iOS web integration](#ios-web-integration)). 15 | 6. Test it (see [Test UL for Android locally](#testing-ul-for-android-locally) and [Testing iOS application](#testing-ios-application)). 16 | 17 | It is important not only to redirect users to your app from the web, but also provide them with the information they were looking for. For example, if someone clicks on `http://mysite.com/news` and get redirected in the app - they are probably hoping to see the `news` page in it. The plugin will help developers with that. In `config.xml` you can specify an event name that is dispatched when user opens the app from the certain link. This way, the appropriate method of your web project will be called, and you can show to user the requested content. 18 | 19 | **Note:** At the moment the plugin doesn't support custom url schemes, but they can be added later. 20 | 21 | ## Supported Platforms 22 | - Android 4.0.0 or above. 23 | - iOS 8.0 or above. Xcode 7 is required. To build plugin with Xcode 6 - [read the instructions](#how-to-build-plugin-in-xcode-6) below. 24 | 25 | ## Documentation 26 | - [Installation](#installation) 27 | - [Migrating from previous versions](#migrating-from-previous-versions) 28 | - [How to build plugin in Xcode 6](#how-to-build-plugin-in-xcode-6) 29 | - [Cordova config preferences](#cordova-config-preferences) 30 | - [Application launch handling](#application-launch-handling) 31 | - [Android web integration](#android-web-integration) 32 | - [Modify web pages](#modify-web-pages) 33 | - [Verify your website on Webmaster Tools](#verify-your-website-on-webmaster-tools) 34 | - [Connect your app in the Google Play console](#connect-your-app-in-the-google-play-console) 35 | - [Testing UL for Android locally](#testing-ul-for-android-locally) 36 | - [iOS web integration](#ios-web-integration) 37 | - [Activate UL support in member center](#activate-ul-support-in-member-center) 38 | - [Configure apple-app-site-association file for website](#configure-apple-app-site-association-file-for-website) 39 | - [Testing iOS application](#testing-ios-application) 40 | - [Useful notes on Universal Links for iOS](#useful-notes-on-universal-links-for-ios) 41 | - [Additional documentation links](#additional-documentation-links) 42 | 43 | ### Installation 44 | This requires cordova 5.0+ (current stable 1.1.0) 45 | 46 | ```sh 47 | cordova plugin add cordova-universal-links-plugin 48 | ``` 49 | 50 | It is also possible to install via repo url directly (**unstable**) 51 | 52 | ```sh 53 | cordova plugin add https://github.com/nordnet/cordova-universal-links-plugin.git 54 | ``` 55 | 56 | ### Migrating from previous versions 57 | 58 | ##### From v1.0.x to v1.1.x 59 | 60 | In v1.0.x to capture universal links events you had to subscribe on them like so: 61 | ```js 62 | document.addEventListener('eventName', didLaunchAppFromLink, false); 63 | 64 | function didLaunchAppFromLink(event) { 65 | var urlData = event.detail; 66 | console.log('Did launch application from the link: ' + urlData.url); 67 | // do some work 68 | } 69 | ``` 70 | And there were some problems with the timing: event could be fired long before you were subscribing to it. 71 | 72 | From v1.1.0 it changes to the familiar Cordova style: 73 | ```js 74 | var app = { 75 | // Application Constructor 76 | initialize: function() { 77 | this.bindEvents(); 78 | }, 79 | 80 | // Bind Event Listeners 81 | bindEvents: function() { 82 | document.addEventListener('deviceready', this.onDeviceReady, false); 83 | }, 84 | 85 | // deviceready Event Handler 86 | onDeviceReady: function() { 87 | universalLinks.subscribe('eventName', app.didLaunchAppFromLink); 88 | }, 89 | 90 | didLaunchAppFromLink: function(eventData) { 91 | alert('Did launch application from the link: ' + eventData.url); 92 | } 93 | }; 94 | 95 | app.initialize(); 96 | ``` 97 | 98 | As you can see, now you subscribe to event via `universalLinks` module when `deviceready` is fired. Actually, you can subscribe to it in any place of your application: plugin stores the event internally and dispatches it when there is a subscriber for it. 99 | 100 | Also, in v1.0.x `ul_didLaunchAppFromLink` was used as a default event name. From v1.1.0 you can just do like that: 101 | ```js 102 | universalLinks.subscribe(null, callbackFunction); 103 | ``` 104 | If you didn't specify event name for the `path` or `host` - in the JS code just pass `null` as event name. But just for readability you might want to specify it `config.xml`. 105 | 106 | ### How to build plugin in Xcode 6 107 | 108 | If you are still using Xcode 6 and there is no way for you to upgrade right now to Xcode 7 - follow the instructions below in order to use this plugin. 109 | 110 | 1. Clone the `xcode6-support` branch of the plugin from the GitHub: 111 | 112 | ```sh 113 | mkdir /Workspace/Mobile/CordovaPlugins 114 | cd /Workspace/Mobile/CordovaPlugins 115 | git clone -b xcode6-support https://github.com/nordnet/cordova-universal-links-plugin.git 116 | ``` 117 | 118 | 2. Go to your applications project and add plugin from the cloned source: 119 | 120 | ```sh 121 | cd /Workspace/Mobile/CoolApp 122 | cordova plugin add /Workspace/Mobile/CordovaPlugins/cordova-universal-links-plugin/ 123 | ``` 124 | 125 | Now you can build your project in Xcode 6. 126 | 127 | ### Cordova config preferences 128 | Cordova uses `config.xml` file to set different project preferences: name, description, starting page and so on. Using this config file you can also set options for the plugin. 129 | 130 | Those preferences are specified inside the `` block. For example: 131 | 132 | ```xml 133 | 134 | 135 | 136 | 137 | 138 | ``` 139 | 140 | In it you define hosts and paths that application should handle. You can have as many hosts and paths as you like. 141 | 142 | #### host 143 | `` tag lets you describe hosts, that your application supports. It can have three attributes: 144 | - `name` - hostname. **This is a required attribute.** 145 | - `scheme` - supported url scheme. Should be either `http` or `https`. If not set - `http` is used. 146 | - `event` - name of the event, that is used to match application launch from this host to a callback on the JS side. If not set - pass `null` as event name when you are subscribing in JS code. 147 | 148 | For example, 149 | 150 | ```xml 151 | 152 | 153 | 154 | ``` 155 | 156 | defines, that when user clicks on any `https://example.com` link - callback, that was set for `ul_myExampleEvent` gets called. More details regarding event handling can be found [below](#application-launch-handling). 157 | 158 | #### path 159 | In `` tag you define which paths for the given host you want to support. If no `` is set - then we want to handle all of them. If paths are defined - then application will process only those links. 160 | 161 | Supported attributes are: 162 | - `url` - path component of the url; should be relative to the host name. **This is a required attribute.** 163 | - `event` - name of the event, that is used to match application launch from the given hostname and path to a callback on the JS side. If not set - pass `null` as event name when you are subscribing in JS code. 164 | 165 | For example, 166 | 167 | ```xml 168 | 169 | 170 | 171 | 172 | 173 | ``` 174 | 175 | defines, that when user clicks on `http://example.com/some/path` - application will be launched, and default callback gets called. All other links from that host will be ignored. 176 | 177 | Query parameters are not used for link matching. For example, `http://example.com/some/path?foo=bar#some_tag` will work the same way as `http://example.com/some/path` does. 178 | 179 | In order to support all links inside `/some/path/` you can use `*` like so: 180 | 181 | ```xml 182 | 183 | 184 | 185 | 186 | 187 | ``` 188 | 189 | `*` can be used only for paths, but you can place it anywhere. For example, 190 | 191 | ```xml 192 | 193 | 194 | 195 | 196 | 197 | ``` 198 | 199 | states, that application can handle any link from `http://example.com` which has `mypath` string in his path component: `http://example.com/some/long/mypath/test.html`, `http://example.com/testmypath.html` and so on. 200 | 201 | **Note:** Following configuration 202 | 203 | ```xml 204 | 205 | 206 | 207 | ``` 208 | 209 | is the same as: 210 | 211 | ```xml 212 | 213 | 214 | 215 | 216 | 217 | ``` 218 | 219 | #### ios-team-id 220 | 221 | As described in `Step 2` of [Configure apple-app-site-association file for website](#configure-apple-app-site-association-file-for-website) section: when application is build from the CLI - plugin generates `apple-app-site-association` files for each host, defined in `config.xml`. In them there's an `appID` property that holds your iOS Team ID and Bundle ID: 222 | 223 | ```json 224 | { 225 | "applinks": { 226 | "apps": [], 227 | "details": [ 228 | { 229 | "appID": ".", 230 | "paths": [ 231 | "/some/path/*" 232 | ] 233 | } 234 | ] 235 | } 236 | } 237 | ``` 238 | 239 | - `` is replaced with the id, that is defined in the `widget` of your `config.xml`. For example: 240 | 241 | ```xml 242 | 243 | ``` 244 | 245 | - `` - that property is defined in the member center of your iOS account. So, you can either put it in the generated `apple-app-site-association` file manually, or use `` preference in `config.xml` like so: 246 | 247 | ```xml 248 | 249 | 250 | 251 | ``` 252 | 253 | For example, following `config.xml` 254 | ```xml 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | ``` 267 | 268 | will result into 269 | ```json 270 | { 271 | "applinks": { 272 | "apps": [], 273 | "details": [ 274 | { 275 | "appID": "1Q2WER3TY.com.example.ul", 276 | "paths": [ 277 | "/some/path/*" 278 | ] 279 | } 280 | ] 281 | } 282 | } 283 | ``` 284 | 285 | This is iOS-only preference, Android doesn't need it. 286 | 287 | ### Application launch handling 288 | As mentioned - it is not enough just to redirect a user into your app, you will also need to display the correct content. In order to solve that - plugin provides JavaScript module: `universalLinks`. To get notified on application launch do the following: 289 | ```js 290 | universalLinks.subscribe('eventName', function (eventData) { 291 | // do some work 292 | console.log('Did launch application from the link: ' + eventData.url); 293 | }); 294 | ``` 295 | 296 | If you didn't specify event name for path and host in `config.xml` - just pass `null` as a first parameter: 297 | ```js 298 | universalLinks.subscribe(null, function (eventData) { 299 | // do some work 300 | console.log('Did launch application from the link: ' + eventData.url); 301 | }); 302 | ``` 303 | 304 | `eventData` holds information about the launching url. For example, for `http://myhost.com/news/ul-plugin-released.html?foo=bar#cordova-news` it will be: 305 | 306 | ```json 307 | { 308 | "url": "http://myhost.com/news/ul-plugin-released.html?foo=bar#cordova-news", 309 | "scheme": "http", 310 | "host": "myhost.com", 311 | "path": "/news/ul-plugin-released.html", 312 | "params": { 313 | "foo": "bar" 314 | }, 315 | "hash": "cordova-news" 316 | } 317 | ``` 318 | 319 | - `url` - original launch url; 320 | - `scheme` - url scheme; 321 | - `host` - hostname from the url; 322 | - `path` - path component of the url; 323 | - `params` - dictionary with query parameters; the ones that after `?` character; 324 | - `hash` - content after `#` character. 325 | 326 | If you want - you can also unsubscribe from the events later on: 327 | ```js 328 | universalLinks.unsubscribe('eventName'); 329 | ``` 330 | 331 | Now it's time for some examples. In here we are gonna use Android, because it is easier to test (see [testing for Android](#testing-ul-for-android-locally) section). JavaScript side is platform independent, so all the example code below will also work for iOS. 332 | 333 | 1. Create new Cordova application and add Android platform. 334 | 335 | ```sh 336 | cordova create TestAndroidApp com.example.ul TestAndroidApp 337 | cd ./TestAndroidApp 338 | cordova platform add android 339 | ``` 340 | 341 | 2. Add UL plugin: 342 | 343 | ```sh 344 | cordova plugin add cordova-universal-links-plugin 345 | ``` 346 | 347 | 3. Add `` preference into `config.xml`: 348 | 349 | ```xml 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | ``` 358 | 359 | As you can see - we want our application to be launched, when user goes to the `news` section of our website. And for that - we are gonna dispatch different events to understand, what has happened. 360 | 361 | 4. Subscribe to `openNewsListPage` and `openNewsDetailedPage` events. For that - open `www/js/index.js` and make it look like that: 362 | 363 | ```js 364 | var app = { 365 | // Application Constructor 366 | initialize: function() { 367 | this.bindEvents(); 368 | }, 369 | 370 | // Bind Event Listeners 371 | bindEvents: function() { 372 | document.addEventListener('deviceready', this.onDeviceReady, false); 373 | }, 374 | 375 | // deviceready Event Handler 376 | onDeviceReady: function() { 377 | console.log('Device is ready for work'); 378 | universalLinks.subscribe('openNewsListPage', app.onNewsListPageRequested); 379 | universalLinks.subscribe('openNewsDetailedPage', app.onNewsDetailedPageRequested); 380 | }, 381 | 382 | // openNewsListPage Event Handler 383 | onNewsListPageRequested: function(eventData) { 384 | console.log('Showing list of awesome news.'); 385 | 386 | // do some work to show list of news 387 | }, 388 | 389 | // openNewsDetailedPage Event Handler 390 | onNewsDetailedPageRequested: function(eventData) { 391 | console.log('Showing to user details page: ' + eventData.path); 392 | 393 | // do some work to show detailed page 394 | } 395 | }; 396 | 397 | app.initialize(); 398 | ``` 399 | 400 | Now, if the user clicks on `http://myhost.com/news/` link - method `onNewsListPageRequested` will be called, and for every link like `http://myhost.com/news/*` - `onNewsDetailedPageRequested`. Basically, we created a mapping between the links and JavaScript methods. 401 | 402 | 5. Build and run your application: 403 | 404 | ```sh 405 | cordova run android 406 | ``` 407 | 408 | 6. Close your app. 409 | 410 | 7. Execute in the terminal: 411 | 412 | ```sh 413 | adb shell am start -W -a android.intent.action.VIEW -d "http://myhost.com/news/" com.example.ul 414 | ``` 415 | 416 | As a result, your application will be launched, and in JavaScript console you will see message: 417 | 418 | ``` 419 | Showing to user list of awesome news. 420 | ``` 421 | 422 | Repeat operation, but this time with the command: 423 | 424 | ```sh 425 | adb shell am start -W -a android.intent.action.VIEW -d "http://myhost.com/news/ul-plugin-released.html" com.example.ul 426 | ``` 427 | 428 | Application will be launched and you will see in JS console: 429 | 430 | ``` 431 | Showing to user details page: /news/ul-plugin-released.html 432 | ``` 433 | 434 | Now, let's say, you want your app to handle all links from `myhost.com`, but you need to keep the mapping for the paths. For that you just need to modify your `config.xml` and add default event handler on JavaScript side: 435 | 436 | 1. Open `config.xml` and change `` block like so: 437 | 438 | ```xml 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | ``` 447 | 448 | As you can see - we added `*` as `path`. This way we declared, that application should be launched from any `http://myhost.com` link. 449 | 450 | 2. Add handling for default UL event in the `www/js/index.js`: 451 | 452 | ```js 453 | var app = { 454 | // Application Constructor 455 | initialize: function() { 456 | this.bindEvents(); 457 | }, 458 | 459 | // Bind Event Listeners 460 | bindEvents: function() { 461 | document.addEventListener('deviceready', this.onDeviceReady, false); 462 | }, 463 | 464 | // deviceready Event Handler 465 | onDeviceReady: function() { 466 | console.log('Handle deviceready event if you need'); 467 | universalLinks.subscribe('openNewsListPage', app.onNewsListPageRequested); 468 | universalLinks.subscribe('openNewsDetailedPage', app.onNewsDetailedPageRequested); 469 | universalLinks.subscribe('launchedAppFromLink', app.onApplicationDidLaunchFromLink); 470 | }, 471 | 472 | // openNewsListPage Event Handler 473 | onNewsListPageRequested: function(eventData) { 474 | console.log('Showing to user list of awesome news'); 475 | 476 | // do some work to show list of news 477 | }, 478 | 479 | // openNewsDetailedPage Event Handler 480 | onNewsDetailedPageRequested: function(eventData) { 481 | console.log('Showing to user details page for some news'); 482 | 483 | // do some work to show detailed page 484 | }, 485 | 486 | // launchedAppFromLink Event Handler 487 | onApplicationDidLaunchFromLink: function(eventData) { 488 | console.log('Did launch app from the link: ' + eventData.url); 489 | } 490 | }; 491 | 492 | app.initialize(); 493 | ``` 494 | 495 | That's it! Now, by default for `myhost.com` links `onApplicationDidLaunchFromLink` method will be called, but for `news` section - `onNewsListPageRequested` and `onNewsDetailedPageRequested`. 496 | 497 | ### Android web integration 498 | 499 | If you have already tried to use `adb` to simulate application launch from the link - you probably saw chooser dialog with at least two applications in it: browser and your app. This happens because web content can be handled by multiple things. To prevent this from happening you need to activate app indexing. App indexing is the second part of deep linking, where you link that URI/URL between Google and your app. 500 | 501 | Integration process consists of three steps: 502 | 503 | 1. Modify your web pages by adding special `` tags in the `` section. 504 | 2. Verify your website on Webmaster Tools. 505 | 3. Connect your app in the Google Play console. 506 | 507 | #### Modify web pages 508 | 509 | To create a link between your mobile content and the page on the website you need to include proper `` tags in the `` section of your website. 510 | 511 | Link tag is constructed like so: 512 | 513 | ```html 514 | 516 | ``` 517 | 518 | where: 519 | - `` - your application's package name; 520 | - `` - url scheme; 521 | - `` - hostname; 522 | - `` - path component. 523 | 524 | For example, if your `config.xml` file looks like this: 525 | 526 | ```xml 527 | 528 | 529 | 530 | 531 | 532 | 533 | ``` 534 | 535 | and a package name is `com.example.ul`, then `` section on your website will be: 536 | 537 | ```html 538 | 539 | 540 | 541 | 542 | 543 | 544 | ``` 545 | 546 | Good news is that **plugin generates those tags for you**. When you run `cordova build` (or `cordova run`) - they are placed in `ul_web_hooks/android/android_web_hook.html` file inside your Cordova project root directory. 547 | 548 | So, instead of manually writing them down - you can take them from that file and put on the website. 549 | 550 | #### Verify your website on Webmaster Tools 551 | 552 | If your website is brand new, you’ll want to verify it through [Webmaster Tools](https://www.google.com/webmasters/tools/). That’s how the Google crawler knows that it’s there and can index it to do everything it needs to do. In order to do that - just add your website in the console and follow the instructions to versify, that you own the site. Most likely, they will ask you to add something on your page. 553 | 554 | #### Connect your app in the Google Play console 555 | 556 | Next, you’ll want to connect your app using the Google Play Console so the app indexing starts working. If you go to your app, there’s a menu that says `Services and API` in which you can click `Verify Website`, and provide the URL to check that it has the appropriate tags in the HTML. Once that’s all set up, it will start showing in search results. 557 | 558 | ### Testing UL for Android locally 559 | 560 | To test Android application for Deep Linking support you just need to execute the following command in the console: 561 | 562 | ```sh 563 | adb shell am start 564 | -W -a android.intent.action.VIEW 565 | -d 566 | ``` 567 | 568 | where 569 | - `` - url that you want to test; 570 | - `` - your application's package name. 571 | 572 | **Note:** if you didn't configure your website for UL support - then most likely after executing the `adb` command you will see a chooser dialog with multiple applications (at least browser and your test app). This happens because you are trying to view web content, and this can be handled by several applications. Just choose your app and proceed. If you configured your website as [described above](#android-web-integration) - then no dialog is shown and your application will be launched directly. 573 | 574 | Let's create new application to play with: 575 | 1. Create new Cordova project and add Android platform to it: 576 | 577 | ```sh 578 | cordova create TestAndroidApp com.example.ul TestAndroidApp 579 | cd ./TestAndroidApp 580 | cordova platform add android 581 | ``` 582 | 583 | 2. Add UL plugin: 584 | 585 | ```sh 586 | cordova plugin add cordova-universal-links-plugin 587 | ``` 588 | 589 | 3. Add `` preference into `config.xml` (`TestAndroidApp/config.xml`): 590 | 591 | ```xml 592 | 593 | 594 | 595 | 596 | ``` 597 | 598 | 4. Build and run the app: 599 | 600 | ```sh 601 | cordova run android 602 | ``` 603 | 604 | 5. Close your application and return to console. 605 | 6. Enter in console: 606 | 607 | ```sh 608 | adb shell am start -W -a android.intent.action.VIEW -d "http://myhost.com/any/path" com.example.ul 609 | ``` 610 | 611 | As a result, your application will be launched and you will see in console: 612 | 613 | ``` 614 | Starting: Intent { act=android.intent.action.VIEW dat=http://myhost.com/any/path pkg=com.example.ul } 615 | Status: ok 616 | Activity: com.example.ul/.MainActivity 617 | ThisTime: 52 618 | TotalTime: 52 619 | Complete 620 | ``` 621 | 622 | If you'll try to use host (or path), that is not defined in `config.xml` - you'll get a following error: 623 | 624 | ``` 625 | Starting: Intent { act=android.intent.action.VIEW dat=http://anotherhost.com/path pkg=com.example.ul } 626 | Error: Activity not started, unable to resolve Intent { act=android.intent.action.VIEW dat=http://anotherhost.com/path flg=0x10000000 pkg=com.example.ul } 627 | ``` 628 | 629 | This way you can experiment with your Android application and check how it corresponds to different links. 630 | 631 | ### iOS web integration 632 | 633 | In the case of iOS integration of the Universal Links is a little harder. It consist of two steps: 634 | 635 | 1. Register your application on [developer console](https://developer.apple.com) and enable `Associated Domains` feature. Make sure your website is SSL ready. 636 | 2. Generate, and upload `apple-app-site-association` file on your website (if you don't have it yet). 637 | 638 | First one you will have to do manually, but plugin will help you with the second step. 639 | 640 | #### Activate UL support in member center 641 | 642 | 1. Go to your [developer console](https://developer.apple.com). Click on `Certificate, Identifiers & Profiles` and then on `Identifiers`. 643 | 644 | ![Developer console](docs/images/developer-console.jpg?raw=true) 645 | 646 | 2. If you already have a registered App Identifier - just skip this and go to `3`. If not - create it by clicking on `+` sign, fill out `name` and `bundle ID`. `name` can be whatever you want, but `bundle ID` should be the one you defined in your Cordova's `config.xml`. 647 | 648 | ![App ID](docs/images/app-id.jpg?raw=true) 649 | 650 | 3. In the `Application Services` section of your App Identifier activate `Associated Domains` and save the changes. 651 | 652 | ![App ID](docs/images/app-associated-domains.jpg?raw=true) 653 | 654 | Now your App ID is registered and has `Associated Domains` feature. 655 | 656 | #### Configure apple-app-site-association file for website 657 | 658 | In order for Universal Links to work - you need to associate your application with the certain domain. For that you need to: 659 | 660 | 1. Get SSL certification for your domain name. 661 | 2. Create `apple-app-site-association` file, containing your App ID and paths you want to handle. 662 | 3. Upload `apple-app-site-association` file in the root of your website. 663 | 664 | ##### Step 1 665 | 666 | We are not gonna describe stuff regarding certificate acquiring. You can find lots of information about that on the Internet. For example, you can do as described [here](https://blog.branch.io/how-to-setup-universal-links-to-deep-link-on-apple-ios-9). 667 | 668 | ##### Step 2 669 | 670 | When you run `cordova build` (or `cordova run`) - plugin takes data from `config.xml` and generates `apple-app-site-association` files for each host you defined. Files are placed in the `ul_web_hooks/ios/` folder of your Cordova project. File names are: 671 | ``` 672 | #apple-app-site-association 673 | ``` 674 | 675 | For example, let's say your application's bundle ID is `com.example.ul`, and `config.xml` has several hosts: 676 | 677 | ```xml 678 | 679 | 680 | 681 | 682 | 683 | 684 | ``` 685 | 686 | Run `cordova build`, and then go to `ul_web_hooks/ios/` folder in your Cordova project. You will see there two files: 687 | 688 | ``` 689 | firsthost.com#apple-app-site-association 690 | secondhost.com#apple-app-site-association 691 | ``` 692 | 693 | Content of the first one is: 694 | ```json 695 | { 696 | "applinks": { 697 | "apps": [], 698 | "details": [ 699 | { 700 | "appID": ".com.example.ul", 701 | "paths": [ 702 | "/some/path/*" 703 | ] 704 | } 705 | ] 706 | } 707 | } 708 | ``` 709 | 710 | And the second one: 711 | ```json 712 | { 713 | "applinks": { 714 | "apps": [], 715 | "details": [ 716 | { 717 | "appID": ".com.example.ul", 718 | "paths": [ 719 | "*" 720 | ] 721 | } 722 | ] 723 | } 724 | } 725 | ``` 726 | 727 | Before uploading them on your servers - you need to replace `` with your actual team ID from the member center. You can find it in `Developer Account Summary` section on the [developer.apple.com](https://developer.apple.com/membercenter/index.action#accountSummary). 728 | 729 | Also, it is a `Prefix` preference in the App ID description. 730 | 731 | ![App ID team prefix](docs/images/app-id-team-prefix.jpg?raw=true) 732 | 733 | If you already have `apple-app-site-association` file - then you need to add `applinks` block to it from the generated file. 734 | 735 | ##### Step 3 736 | 737 | Upload `apple-app-site-association` file in the root of your domain. 738 | 739 | **It should be downloadable from the direct link.** For example, `https://firsthost.com/apple-app-site-association`. 740 | 741 | **No redirects are allowed!** When application is launched - it downloads it from that link, so if it can't find it - Universal Links are not gonna work. 742 | 743 | That's it, you have finished configuring iOS for UL support. 744 | 745 | ### Testing iOS application 746 | 747 | Unlike Android, Apple doesn't provide any tools to test Universal Links. So you have to do all the [integration stuff](#ios-web-integration) before any real testing. So please, do that. 748 | 749 | But if you don't want to... well, there is one way to skip it. You can use [branch.io](https://branch.io) to handle all the SSL/apple-app-site-association stuff for you. How to do that - described in their [documentation](https://dev.branch.io/recipes/branch_universal_links/#enable-universal-links-on-the-branch-dashboard). From there you can skip Xcode and SDK integration stuff, because you don't need that. 750 | 751 | Step-by-step guide: 752 | 753 | 1. Go to developer console and register your App ID, as described in [Activating UL support in member center](#activate-ul-support-in-member-center). 754 | 755 | 2. Register account on [branch.io](https://dashboard.branch.io/), if you don't have it yet. 756 | 757 | 3. Login into [branch dashboard](https://dashboard.branch.io/). Go to `Settings` -> `Link Settings`, activate `Enable Universal Links`, fill in `Bundle identifier` and `Team ID`. 758 | 759 | ![App ID](docs/images/branch-io.jpg?raw=true) 760 | 761 | 4. It will take some time to update their servers, so be patient. To check if it is ready - just open [https://bnc.lt/apple-app-site-association](https://bnc.lt/apple-app-site-association) and search for your `Bundle identifier`. 762 | 763 | Pay attention for `paths` - if there is any for your app, then write it down. 764 | 765 | For example: 766 | ```json 767 | ...,"9F38WJR2U8.com.example.ul":{"paths":["/a2Be/*"]},... 768 | ``` 769 | 770 | 5. Create new Cordova iOS application and add UL plugin: 771 | 772 | ```sh 773 | cordova create TestProject com.example.ul TestProject 774 | cd ./TestProject 775 | cordova platform add ios 776 | cordova plugin add cordova-universal-links-plugin 777 | ``` 778 | 779 | 6. Add `bnc.lt` and your other hosts into `config.xml`: 780 | 781 | ```xml 782 | 783 | 784 | 785 | 786 | ``` 787 | 788 | For test purpose you can leave only `bnc.lt` in there. But if you specifying your hosts - you need to [white label](https://dev.branch.io/recipes/branch_universal_links/#white-label-domains) them. 789 | 790 | 7. Attach your real device to the computer and run application on it: 791 | 792 | ```sh 793 | cordova run ios 794 | ``` 795 | 796 | Emulator will not work. 797 | 798 | 8. Email yourself a link that need's to be tested. 799 | 800 | For example, `https://bnc.lt/a2Be/somepage.html`. As you can see, link constructed from hostname and path component, specified in `apple-app-site-association` file. This link may not even lead to the real page, it doesn't matter. It's only purpose is to open the app. 801 | 802 | Now click on your link. Application should be launched. If not - check all the steps above. Also, check your provisioning profiles in Xcode. 803 | 804 | ### Useful notes on Universal Links for iOS 805 | 806 | First of all, you need to understand how the Universal Links works. When user clicks on the link - Safari checks, if any of the installed apps can handle it. If app is found - Safari starts it, if not - link opened as usually in the browser. 807 | 808 | Now, let's assume you have a following setup in `config.xml`: 809 | ```xml 810 | 811 | 812 | 813 | 814 | 815 | ``` 816 | By this we state, that our app should handle `http://mywebsite.com/some/page.html` link. So, if user clicks on `http://mywebsite.com` - application would not launch. And this is totally as you want it to be. Now comes the interesting part: if user opens `http://mywebsite.com` in the Safari and then presses on `http://mywebsite.com/some/page.html` link - application is not gonna start, he will stay in the browser. And at the top of that page he will see a Smart Banner. To launch the application user will have to click on that banner. And this is a normal behaviour from iOS. If user already viewing your website in the browser - he doesn't want to leave it, when he clicks on some link, that leads on the page inside your site. But if he clicks on the `http://mywebsite.com/some/page.html` link from some other source - then it will start your application. 817 | 818 | Another thing that every developer should be aware of: 819 | 820 | When a user is in an app, opened by Universal Links - a return to browser option will persist at the top of the screen (i.e. `mywebsite.com`). Users who have clicked the `mywebsite.com` option will be taken to their Safari browser, and Smart Banner is persistently rendered on the top of the window. This banner has an `OPEN` call to action. For all future clicks of URLs, associated with this app via Universal Links, the app will never be launched again for the user, and the user will continue being redirected to the Safari page with the banner. If the user clicks `OPEN` - then the app will be launched, and all future clicks of the URL will deep linking the user to the app. 821 | 822 | ### Additional documentation links 823 | 824 | **Android:** 825 | - [Video tutorial on Android App Indexing](https://realm.io/news/juan-gomez-android-app-indexing/) 826 | - [Enable Deep Linking on Android](https://developer.android.com/training/app-indexing/deep-linking.html) 827 | - [Specifying App Content for Indexing](https://developer.android.com/training/app-indexing/enabling-app-indexing.html) 828 | - [Documentation on enabling App Indexing on the website](https://developers.google.com/app-indexing/android/publish#host-your-links) 829 | 830 | **iOS:** 831 | - [Apple documentation on Universal Links](https://developer.apple.com/library/ios/documentation/General/Conceptual/AppSearch/UniversalLinks.html) 832 | - [Apple documentation on apple-app-site-association file](https://developer.apple.com/library/ios/documentation/Security/Reference/SharedWebCredentialsRef/index.html) 833 | - [How to setup universal links on iOS 9](https://blog.branch.io/how-to-setup-universal-links-to-deep-link-on-apple-ios-9) 834 | - [Branch.io documentation on universal links](https://dev.branch.io/recipes/branch_universal_links/#enable-universal-links-on-the-branch-dashboard) 835 | --------------------------------------------------------------------------------