Browse Source

initial commit

master
munir ishak 4 years ago
commit
5687a41cd2
50 changed files with 4591 additions and 0 deletions
  1. 84
    0
      CHANGELOG.md
  2. 22
    0
      LICENSE
  3. 899
    0
      README.md
  4. BIN
      docs/images/app-associated-domains.jpg
  5. BIN
      docs/images/app-id-team-prefix.jpg
  6. BIN
      docs/images/app-id.jpg
  7. BIN
      docs/images/branch-io.jpg
  8. BIN
      docs/images/developer-console.jpg
  9. 86
    0
      hooks/afterPrepareHook.js
  10. 55
    0
      hooks/beforePluginInstallHook.js
  11. 74
    0
      hooks/iosBeforePrepareHook.js
  12. 316
    0
      hooks/lib/android/manifestWriter.js
  13. 162
    0
      hooks/lib/android/webSiteHook.js
  14. 117
    0
      hooks/lib/configXmlHelper.js
  15. 145
    0
      hooks/lib/configXmlParser.js
  16. 173
    0
      hooks/lib/ios/appleAppSiteAssociationFile.js
  17. 174
    0
      hooks/lib/ios/projectEntitlements.js
  18. 226
    0
      hooks/lib/ios/xcodePreferences.js
  19. 57
    0
      hooks/lib/xmlHelper.js
  20. 51
    0
      package.json
  21. 106
    0
      plugin.xml
  22. 221
    0
      src/android/com/nordnetab/cordova/ul/UniversalLinksPlugin.java
  23. 19
    0
      src/android/com/nordnetab/cordova/ul/js/JSAction.java
  24. 193
    0
      src/android/com/nordnetab/cordova/ul/model/JSMessage.java
  25. 85
    0
      src/android/com/nordnetab/cordova/ul/model/ULHost.java
  26. 43
    0
      src/android/com/nordnetab/cordova/ul/model/ULPath.java
  27. 156
    0
      src/android/com/nordnetab/cordova/ul/parser/ULConfigXmlParser.java
  28. 49
    0
      src/android/com/nordnetab/cordova/ul/parser/XmlTags.java
  29. 17
    0
      src/ios/AppDelegate+CULPlugin.h
  30. 32
    0
      src/ios/AppDelegate+CULPlugin.m
  31. 38
    0
      src/ios/CULPlugin.h
  32. 176
    0
      src/ios/CULPlugin.m
  33. 21
    0
      src/ios/JS/CDVInvokedUrlCommand+CULPlugin.h
  34. 19
    0
      src/ios/JS/CDVInvokedUrlCommand+CULPlugin.m
  35. 29
    0
      src/ios/JS/CDVPluginResult+CULPlugin.h
  36. 157
    0
      src/ios/JS/CDVPluginResult+CULPlugin.m
  37. 63
    0
      src/ios/Model/CULHost.h
  38. 50
    0
      src/ios/Model/CULHost.m
  39. 36
    0
      src/ios/Model/CULPath.h
  40. 21
    0
      src/ios/Model/CULPath.m
  41. 22
    0
      src/ios/Parser/JSON/CULConfigJsonParser.h
  42. 60
    0
      src/ios/Parser/JSON/CULConfigJsonParser.m
  43. 22
    0
      src/ios/Parser/XML/CULConfigXmlParser.h
  44. 123
    0
      src/ios/Parser/XML/CULConfigXmlParser.m
  45. 54
    0
      src/ios/Parser/XML/CULXmlTags.h
  46. 22
    0
      src/ios/Parser/XML/CULXmlTags.m
  47. 22
    0
      src/ios/Utils/NSBundle+CULPlugin.h
  48. 15
    0
      src/ios/Utils/NSBundle+CULPlugin.m
  49. 23
    0
      ul_web_hooks/android_web_hook_tpl.html
  50. 56
    0
      www/universal_links.js

+ 84
- 0
CHANGELOG.md View File

@@ -0,0 +1,84 @@
1
+# Change Log
2
+
3
+## 1.2.1 (2016-10-23)
4
+
5
+**Bug fixes:**
6
+
7
+- [Issue #79](https://github.com/martindrapeau/cordova-universal-links-plugin/issues/79). Fixed installation error: header files were added to the compile section of the project.
8
+- [Issue #77](https://github.com/martindrapeau/cordova-universal-links-plugin/issues/77). Fixed `before_prepare` hook for iOS that crashed on several systems. Thanks to [@lunchbag](https://github.com/lunchbag) for providing a fix.
9
+
10
+**Enhancements:**
11
+
12
+- [Issue #93](https://github.com/martindrapeau/cordova-universal-links-plugin/issues/93). Fixed iOS build warnings.
13
+
14
+## 1.2.0 (2016-07-27)
15
+
16
+**Enhancements:**
17
+
18
+- [Merged pull request #56](https://github.com/martindrapeau/cordova-universal-links-plugin/pull/56). Adds support for wildcard domains. Thanks to [@schmidt](https://github.com/schmidt) for implementation.
19
+
20
+**Docs:**
21
+
22
+- [Merged pull request #67](https://github.com/martindrapeau/cordova-universal-links-plugin/pull/67). Added `Prevent Android from creating multiple app instances` section. Thanks to [@yernandus](https://github.com/yernandus).
23
+- [Merged pull request #70](https://github.com/martindrapeau/cordova-universal-links-plugin/pull/70). Added `Digital Asset Links support` section. Thanks to [@ghybs](https://github.com/ghybs).
24
+
25
+## 1.1.2 (2016-04-27)
26
+
27
+**Bug fixes:**
28
+
29
+- [Issue #27](https://github.com/martindrapeau/cordova-universal-links-plugin/issues/27). From now on dependency packages will be installed in the plugin's folder instead of the project's root folder.
30
+
31
+## 1.1.1 (2016-03-17)
32
+
33
+**Bug fixes:**
34
+
35
+- [Issue #52](https://github.com/martindrapeau/cordova-universal-links-plugin/issues/52). Fixed `config.xml` file preferences reading. Thanks to [@ikostic](https://github.com/ikostic) for providing fix.
36
+- [Issue #47](https://github.com/martindrapeau/cordova-universal-links-plugin/issues/47). If `paths` in `apple-app-site-association` file contains only `*` - we will also add `/`, so that app would be opened from root domain.
37
+- Merged [PR #42](https://github.com/martindrapeau/cordova-universal-links-plugin/pull/42). Fixed Android web integration on Android 6.0. Thanks to [@mohamed-ahmed](https://github.com/mohamed-ahmed).
38
+
39
+**Docs:**
40
+
41
+- Merged [PR #50](https://github.com/martindrapeau/cordova-universal-links-plugin/pull/50). Fixed typo in documentation. Thanks to [@rafaellop](https://github.com/rafaellop).
42
+- Merged [PR #43](https://github.com/martindrapeau/cordova-universal-links-plugin/pull/43). Updated documentation regarding `apple-app-site-association` file. Thanks to [@Chun-Yang](https://github.com/Chun-Yang).
43
+- Updated `Useful notes on Universal Links for iOS` section. Thanks to [@conor-mac-aoidh](https://github.com/conor-mac-aoidh) for providing information.
44
+
45
+## 1.1.0 (2015-12-18)
46
+
47
+**Bug fixes:**
48
+
49
+- [Issue #26](https://github.com/martindrapeau/cordova-universal-links-plugin/issues/26). Fixed support for multiple wildcards in path. Thanks to [@tdelmas](https://github.com/tdelmas) for helping with solution.
50
+- Other minor bug fixes.
51
+
52
+**Enhancements:**
53
+
54
+- [Issue #18](https://github.com/martindrapeau/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.
55
+- [Issue #20](https://github.com/martindrapeau/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.
56
+- [Issue #22](https://github.com/martindrapeau/cordova-universal-links-plugin/issues/22). Plugin now compatible with Cordova v5.4.
57
+- [Issue #24](https://github.com/martindrapeau/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.
58
+- [Issue #25](https://github.com/martindrapeau/cordova-universal-links-plugin/issues/25). Plugin now compatible with Cordova iOS platform v4.0.0.
59
+
60
+**Docs:**
61
+
62
+- Added `Migrating from previous versions` section.
63
+- Updated `Cordova config preferences` section.
64
+- Updated `Application launch handling` section.
65
+- Other minor changes because of new release.
66
+
67
+## 1.0.1 (2015-10-23)
68
+
69
+**Bug fixes:**
70
+
71
+- Android. Fixed [issue #9](https://github.com/martindrapeau/cordova-universal-links-plugin/issues/9). Now when application is resumed from the link click - appropriate event is dispatched to the JavaScript side.
72
+
73
+**Enhancements:**
74
+
75
+- iOS. [Issue #6](https://github.com/martindrapeau/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.
76
+- Merged [pull request #1](https://github.com/martindrapeau/cordova-universal-links-plugin/pull/1). Now dependency npm packages are taken from the package.json file. Thanks to [@dpa99c](https://github.com/dpa99c).
77
+
78
+**Docs:**
79
+
80
+- Added `Useful notes on Universal Links for iOS` section.
81
+- Updated `Android web integration` section. Added more information about web integration process.
82
+- Added some additional links on the Android documentation.
83
+- Fixed some broken links inside the docs.
84
+- Added CHANGELOG.md file.

+ 22
- 0
LICENSE View File

@@ -0,0 +1,22 @@
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
+

+ 899
- 0
README.md View File

@@ -0,0 +1,899 @@
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 9.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
+**iOS Note:** you can use this plugin in iOS 8 applications. It will not crash the app, but it also is not gonna handle the links, because this is iOS 9 feature.
26
+
27
+## Documentation
28
+- [Installation](#installation)
29
+- [Migrating from previous versions](#migrating-from-previous-versions)
30
+- [How to build plugin in Xcode 6](#how-to-build-plugin-in-xcode-6)
31
+- [Cordova config preferences](#cordova-config-preferences)
32
+- [Application launch handling](#application-launch-handling)
33
+- [Android web integration](#android-web-integration)
34
+  - [Modify web pages](#modify-web-pages)
35
+  - [Verify your website on Webmaster Tools](#verify-your-website-on-webmaster-tools)
36
+  - [Connect your app in the Google Play console](#connect-your-app-in-the-google-play-console)
37
+  - [Digital Asset Links support](#digital-asset-links-support)
38
+- [Testing UL for Android locally](#testing-ul-for-android-locally)
39
+- [iOS web integration](#ios-web-integration)
40
+  - [Activate UL support in member center](#activate-ul-support-in-member-center)
41
+  - [Configure apple-app-site-association file for website](#configure-apple-app-site-association-file-for-website)
42
+- [Testing iOS application](#testing-ios-application)
43
+- [Useful notes on Universal Links for iOS](#useful-notes-on-universal-links-for-ios)
44
+  - [They don't work everywhere](#they-dont-work-everywhere)
45
+  - [How links handled in Safari](#how-links-handled-in-safari)
46
+- [Additional documentation links](#additional-documentation-links)
47
+
48
+### Installation
49
+This requires cordova 5.0+ (current stable 1.2.1)
50
+
51
+```sh
52
+cordova plugin add cordova-mirtech-plugin-universal-links
53
+```
54
+
55
+It is also possible to install via repo url directly (**unstable**)
56
+
57
+```sh
58
+cordova plugin add https://github.com/martindrapeau/cordova-universal-links-plugin.git
59
+```
60
+
61
+### Migrating from previous versions
62
+
63
+##### From v1.0.x to v1.1.x
64
+
65
+In v1.0.x to capture universal links events you had to subscribe on them like so:
66
+```js
67
+document.addEventListener('eventName', didLaunchAppFromLink, false);
68
+
69
+function didLaunchAppFromLink(event) {
70
+  var urlData = event.detail;
71
+  console.log('Did launch application from the link: ' + urlData.url);
72
+  // do some work
73
+}
74
+```
75
+And there were some problems with the timing: event could be fired long before you were subscribing to it.
76
+
77
+From v1.1.0 it changes to the familiar Cordova style:
78
+```js
79
+var app = {
80
+  // Application Constructor
81
+  initialize: function() {
82
+    this.bindEvents();
83
+  },
84
+
85
+  // Bind Event Listeners
86
+  bindEvents: function() {
87
+    document.addEventListener('deviceready', this.onDeviceReady, false);
88
+  },
89
+
90
+  // deviceready Event Handler
91
+  onDeviceReady: function() {
92
+    universalLinks.subscribe('eventName', app.didLaunchAppFromLink);
93
+  },
94
+
95
+  didLaunchAppFromLink: function(eventData) {
96
+    alert('Did launch application from the link: ' + eventData.url);
97
+  }
98
+};
99
+
100
+app.initialize();
101
+```
102
+
103
+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.
104
+
105
+Also, in v1.0.x `ul_didLaunchAppFromLink` was used as a default event name. From v1.1.0 you can just do like that:
106
+```js
107
+universalLinks.subscribe(null, callbackFunction);
108
+```
109
+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`.
110
+
111
+### How to build plugin in Xcode 6
112
+
113
+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.
114
+
115
+1. Clone the `xcode6-support` branch of the plugin from the GitHub:
116
+
117
+  ```sh
118
+  mkdir /Workspace/Mobile/CordovaPlugins
119
+  cd /Workspace/Mobile/CordovaPlugins
120
+  git clone -b xcode6-support https://github.com/martindrapeau/cordova-universal-links-plugin.git
121
+  ```
122
+
123
+2. Go to your applications project and add plugin from the cloned source:
124
+
125
+  ```sh
126
+  cd /Workspace/Mobile/CoolApp
127
+  cordova plugin add /Workspace/Mobile/CordovaPlugins/cordova-mirtech-plugin-universal-links/
128
+  ```
129
+
130
+Now you can build your project in Xcode 6.
131
+
132
+### Cordova config preferences
133
+
134
+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.
135
+
136
+Those preferences are specified inside the `<universal-links>` block. For example:
137
+
138
+```xml
139
+<universal-links>
140
+    <host name="example.com">
141
+        <path url="/some/path" />
142
+    </host>
143
+</universal-links>
144
+```
145
+
146
+In it you define hosts and paths that application should handle. You can have as many hosts and paths as you like.
147
+
148
+#### host
149
+`<host />` tag lets you describe hosts, that your application supports. It can have three attributes:
150
+- `name` - hostname. **This is a required attribute.**
151
+- `scheme` - supported url scheme. Should be either `http` or `https`. If not set - `http` is used.
152
+- `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.
153
+
154
+For example,
155
+
156
+```xml
157
+<universal-links>
158
+    <host name="example.com" scheme="https" event="ul_myExampleEvent" />
159
+</universal-links>
160
+```
161
+
162
+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).
163
+
164
+You can also use wildcards for domains. For example,
165
+
166
+```xml
167
+<universal-links>
168
+    <host name="*.users.example.com" scheme="https" event="wildcardusers" />
169
+    <host name="*.example.com" scheme="https" event="wildcardmatch" />
170
+</universal-links>
171
+```
172
+
173
+Please note, that iOS will look for the `apple-app-site-association` on `https://users.example.com/apple-app-site-association` and `https://example.com/apple-app-site-association` respectively.
174
+
175
+Android will try to access the [app links file](https://developer.android.com/training/app-links/index.html#web-assoc) at `https://*.users.example.com/.well-known/assetlinks.json` and `https://*.example.com/.well-known/assetlinks.json` respectively.
176
+
177
+#### path
178
+In `<path />` tag you define which paths for the given host you want to support. If no `<path />` is set - then we want to handle all of them. If paths are defined - then application will process only those links.
179
+
180
+Supported attributes are:
181
+- `url` - path component of the url; should be relative to the host name. **This is a required attribute.**
182
+- `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.
183
+
184
+For example,
185
+
186
+```xml
187
+<universal-links>
188
+    <host name="example.com">
189
+        <path url="/some/path" />
190
+    </host>
191
+</universal-links>
192
+```
193
+
194
+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.
195
+
196
+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.
197
+
198
+In order to support all links inside `/some/path/` you can use `*` like so:
199
+
200
+```xml
201
+<universal-links>
202
+    <host name="example.com">
203
+        <path url="/some/path/*" />
204
+    </host>
205
+</universal-links>
206
+```
207
+
208
+`*` can be used only for paths, but you can place it anywhere. For example,
209
+
210
+```xml
211
+<universal-links>
212
+    <host name="example.com">
213
+        <path url="*mypath*" />
214
+    </host>
215
+</universal-links>
216
+```
217
+
218
+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.
219
+
220
+**Note:** Following configuration
221
+
222
+```xml
223
+<universal-links>
224
+    <host name="example.com" />
225
+</universal-links>
226
+```
227
+
228
+is the same as:
229
+
230
+```xml
231
+<universal-links>
232
+    <host name="example.com">
233
+      <path url="*" />
234
+    </host>
235
+</universal-links>
236
+```
237
+
238
+#### ios-team-id
239
+
240
+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:
241
+
242
+```json
243
+{
244
+  "applinks": {
245
+    "apps": [],
246
+    "details": [
247
+      {
248
+        "appID": "<TEAM_ID_FROM_MEMBER_CENTER>.<BUNDLE_ID>",
249
+        "paths": [
250
+          "/some/path/*"
251
+        ]
252
+      }
253
+    ]
254
+  }
255
+}
256
+```
257
+
258
+- `<BUNDLE_ID>` is replaced with the id, that is defined in the `widget` of your `config.xml`. For example:
259
+
260
+  ```xml
261
+  <widget id="com.example.ul" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
262
+  ```
263
+
264
+- `<TEAM_ID_FROM_MEMBER_CENTER>` - 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 `<ios-team-id>` preference in `config.xml` like so:
265
+
266
+  ```xml
267
+  <universal-links>
268
+      <ios-team-id value="<TEAM_ID_FROM_MEMBER_CENTER>" />
269
+  </universal-links>
270
+  ```
271
+
272
+For example, following `config.xml`
273
+```xml
274
+<widget id="com.example.ul" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
275
+
276
+<!-- some other cordova preferences -->
277
+
278
+<universal-links>
279
+    <ios-team-id value="1Q2WER3TY" />
280
+    <host name="mysite.com" >
281
+      <path url="/some/path/*" />
282
+    </host>
283
+</universal-links>
284
+</widget>
285
+```
286
+
287
+will result into
288
+```json
289
+{
290
+  "applinks": {
291
+    "apps": [],
292
+    "details": [
293
+      {
294
+        "appID": "1Q2WER3TY.com.example.ul",
295
+        "paths": [
296
+          "/some/path/*"
297
+        ]
298
+      }
299
+    ]
300
+  }
301
+}
302
+```
303
+
304
+This is iOS-only preference, Android doesn't need it.
305
+
306
+#### Prevent Android from creating multiple app instances
307
+
308
+When clicking on a universal link from another App (typically from an email client), Android will likely create a new instance of your app, even if it is already loaded in memory. It may even create a new instance with each click, resulting in many instances of your app in the task switcher. See details in [issue #37](https://github.com/martindrapeau/cordova-universal-links-plugin/issues/37).
309
+
310
+To force Android opening always the same app instance, a known workaround is to change the [activity launch mode](https://developer.android.com/guide/topics/manifest/activity-element.html#lmode) to `singleInstance`. To do so, you can use the following preference in Cordova `config.xml` file:
311
+```xml
312
+<preference name="AndroidLaunchMode" value="singleInstance" />
313
+```
314
+
315
+### Application launch handling
316
+
317
+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:
318
+```js
319
+universalLinks.subscribe('eventName', function (eventData) {
320
+  // do some work
321
+  console.log('Did launch application from the link: ' + eventData.url);
322
+});
323
+```
324
+
325
+If you didn't specify event name for path and host in `config.xml` - just pass `null` as a first parameter:
326
+```js
327
+universalLinks.subscribe(null, function (eventData) {
328
+  // do some work
329
+  console.log('Did launch application from the link: ' + eventData.url);
330
+});
331
+```
332
+
333
+`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:
334
+
335
+```json
336
+{
337
+  "url": "http://myhost.com/news/ul-plugin-released.html?foo=bar#cordova-news",
338
+  "scheme": "http",
339
+  "host": "myhost.com",
340
+  "path": "/news/ul-plugin-released.html",
341
+  "params": {
342
+    "foo": "bar"
343
+  },
344
+  "hash": "cordova-news"
345
+}
346
+```
347
+
348
+- `url` - original launch url;
349
+- `scheme` - url scheme;
350
+- `host` - hostname from the url;
351
+- `path` - path component of the url;
352
+- `params` - dictionary with query parameters; the ones that after `?` character;
353
+- `hash` - content after `#` character.
354
+
355
+If you want - you can also unsubscribe from the events later on:
356
+```js
357
+universalLinks.unsubscribe('eventName');
358
+```
359
+
360
+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.
361
+
362
+1. Create new Cordova application and add Android platform.
363
+
364
+  ```sh
365
+  cordova create TestAndroidApp com.example.ul TestAndroidApp
366
+  cd ./TestAndroidApp
367
+  cordova platform add android
368
+  ```
369
+
370
+2. Add UL plugin:
371
+
372
+  ```sh
373
+  cordova plugin add cordova-mirtech-plugin-universal-links
374
+  ```
375
+
376
+3. Add `<universal-links>` preference into `config.xml`:
377
+
378
+  ```xml
379
+  <!-- some other data from config.xml -->
380
+  <universal-links>
381
+   <host name="myhost.com">
382
+     <path url="/news/" event="openNewsListPage" />
383
+     <path url="/news/*" event="openNewsDetailedPage" />
384
+   </host>
385
+  </universal-links>
386
+  ```
387
+
388
+  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.
389
+
390
+4. Subscribe to `openNewsListPage` and `openNewsDetailedPage` events. For that - open `www/js/index.js` and make it look like that:
391
+
392
+  ```js
393
+  var app = {
394
+    // Application Constructor
395
+    initialize: function() {
396
+      this.bindEvents();
397
+    },
398
+
399
+    // Bind Event Listeners
400
+    bindEvents: function() {
401
+      document.addEventListener('deviceready', this.onDeviceReady, false);
402
+    },
403
+
404
+    // deviceready Event Handler
405
+    onDeviceReady: function() {
406
+      console.log('Device is ready for work');
407
+      universalLinks.subscribe('openNewsListPage', app.onNewsListPageRequested);
408
+      universalLinks.subscribe('openNewsDetailedPage', app.onNewsDetailedPageRequested);
409
+    },
410
+
411
+    // openNewsListPage Event Handler
412
+    onNewsListPageRequested: function(eventData) {
413
+      console.log('Showing list of awesome news.');
414
+
415
+      // do some work to show list of news
416
+    },
417
+
418
+    // openNewsDetailedPage Event Handler
419
+    onNewsDetailedPageRequested: function(eventData) {
420
+      console.log('Showing to user details page: ' + eventData.path);
421
+
422
+      // do some work to show detailed page
423
+    }
424
+  };
425
+
426
+  app.initialize();
427
+  ```
428
+
429
+  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.
430
+
431
+5. Build and run your application:
432
+
433
+  ```sh
434
+  cordova run android
435
+  ```
436
+
437
+6. Close your app.
438
+
439
+7. Execute in the terminal:
440
+
441
+  ```sh
442
+  adb shell am start -W -a android.intent.action.VIEW -d "http://myhost.com/news/" com.example.ul
443
+  ```
444
+
445
+  As a result, your application will be launched, and in JavaScript console you will see message:
446
+
447
+  ```
448
+  Showing to user list of awesome news.
449
+  ```
450
+
451
+  Repeat operation, but this time with the command:
452
+
453
+  ```sh
454
+  adb shell am start -W -a android.intent.action.VIEW -d "http://myhost.com/news/ul-plugin-released.html" com.example.ul
455
+  ```
456
+
457
+  Application will be launched and you will see in JS console:
458
+
459
+  ```
460
+  Showing to user details page: /news/ul-plugin-released.html
461
+  ```
462
+
463
+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:
464
+
465
+1. Open `config.xml` and change `<universal-links>` block like so:
466
+
467
+  ```xml
468
+  <universal-links>
469
+   <host name="myhost.com">
470
+     <path url="/news/" event="openNewsListPage" />
471
+     <path url="/news/*" event="openNewsDetailedPage" />
472
+     <path url="*" event="launchedAppFromLink" />
473
+   </host>
474
+  </universal-links>
475
+  ```
476
+
477
+  As you can see - we added `*` as `path`. This way we declared, that application should be launched from any `http://myhost.com` link.
478
+
479
+2. Add handling for default UL event in the `www/js/index.js`:
480
+
481
+  ```js
482
+  var app = {
483
+    // Application Constructor
484
+    initialize: function() {
485
+      this.bindEvents();
486
+    },
487
+
488
+    // Bind Event Listeners
489
+    bindEvents: function() {
490
+      document.addEventListener('deviceready', this.onDeviceReady, false);
491
+    },
492
+
493
+    // deviceready Event Handler
494
+    onDeviceReady: function() {
495
+      console.log('Handle deviceready event if you need');
496
+      universalLinks.subscribe('openNewsListPage', app.onNewsListPageRequested);
497
+      universalLinks.subscribe('openNewsDetailedPage', app.onNewsDetailedPageRequested);
498
+      universalLinks.subscribe('launchedAppFromLink', app.onApplicationDidLaunchFromLink);
499
+    },
500
+
501
+    // openNewsListPage Event Handler
502
+    onNewsListPageRequested: function(eventData) {
503
+      console.log('Showing to user list of awesome news');
504
+
505
+      // do some work to show list of news
506
+    },
507
+
508
+    // openNewsDetailedPage Event Handler
509
+    onNewsDetailedPageRequested: function(eventData) {
510
+      console.log('Showing to user details page for some news');
511
+
512
+      // do some work to show detailed page
513
+    },
514
+
515
+    // launchedAppFromLink Event Handler
516
+    onApplicationDidLaunchFromLink: function(eventData) {
517
+      console.log('Did launch app from the link: ' + eventData.url);
518
+    }
519
+  };
520
+
521
+  app.initialize();
522
+  ```
523
+
524
+That's it! Now, by default for `myhost.com` links `onApplicationDidLaunchFromLink` method will be called, but for `news` section - `onNewsListPageRequested` and `onNewsDetailedPageRequested`.
525
+
526
+### Android web integration
527
+
528
+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.
529
+
530
+Integration process consists of three steps:
531
+
532
+1. Modify your web pages by adding special `<link />` tags in the `<head />` section.
533
+2. Verify your website on Webmaster Tools.
534
+3. Connect your app in the Google Play console.
535
+
536
+#### Modify web pages
537
+
538
+To create a link between your mobile content and the page on the website you need to include proper `<link />` tags in the `<head />` section of your website.
539
+
540
+Link tag is constructed like so:
541
+
542
+```html
543
+<link rel="alternate"
544
+      href="android-app://<package_name>/<scheme>/<host><path>" />
545
+```
546
+
547
+where:
548
+- `<package_name>` - your application's package name;
549
+- `<scheme>` - url scheme;
550
+- `<host>` - hostname;
551
+- `<path>` - path component.
552
+
553
+For example, if your `config.xml` file looks like this:
554
+
555
+```xml
556
+<universal-links>
557
+ <host name="myhost.com">
558
+   <path url="/news/" />
559
+   <path url="/profile/" />
560
+ </host>
561
+</universal-links>
562
+```
563
+
564
+and a package name is `com.example.ul`, then `<head />` section on your website will be:
565
+
566
+```html
567
+<head>
568
+<link rel="alternate" href="android-app://com.example.ul/http/myhost.com/news/" />
569
+<link rel="alternate" href="android-app://com.example.ul/http/myhost.com/profile/" />
570
+
571
+<!-- Your other stuff from the head tag -->
572
+</head>
573
+```
574
+
575
+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.
576
+
577
+So, instead of manually writing them down - you can take them from that file and put on the website.
578
+
579
+#### Verify your website on Webmaster Tools
580
+
581
+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.
582
+
583
+#### Connect your app in the Google Play console
584
+
585
+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.
586
+
587
+#### Digital Asset Links support
588
+
589
+For Android version 6.0 (Marshmallow) or greater [Digital Asset Links](https://developers.google.com/digital-asset-links/v1/getting-started) can be used.
590
+
591
+Here's a very simplified example of how the website `www.example.com` could use Digital Asset Links to specify that any links to URLs in that site should open in a designated app rather than the browser:
592
+
593
+1. The website `www.example.com` publishes a statement list at `https://www.example.com/.well-known/assetlinks.json`. This is the official name and location for a statement list on a site. Statement lists in any other location, or with any other name, are not valid for this site. In our example, the statement list consists of one statement, granting its Android app the permission to open links on its site:
594
+
595
+  ```json
596
+  [{
597
+    "relation": ["delegate_permission/common.handle_all_urls"],
598
+    "target" : { "namespace": "android_app", "package_name": "com.example.app",
599
+                 "sha256_cert_fingerprints": ["hash_of_app_certificate"] }
600
+  }]
601
+  ```
602
+
603
+  A statement list supports an array of statements within the [ ] marks, but our example file contains only one statement.
604
+
605
+2. The Android app listed in the statement above has an intent filter that specifies the scheme, host, and path pattern of URLs that it wants to handle: in this case, `https://www.example.com`. The intent filter includes a special attribute `android:autoVerify`, new to Android M, which indicates that Android should verify the statement on the website, described in the intent filter when the app is installed.
606
+
607
+3. A user installs the app. Android sees the intent filter with the `autoVerify` attribute and checks for the presence of the statement list at the specified site. If present, Android checks whether that file includes a statement granting link handling to the app, and verifies the app against the statement by certificate hash. If everything checks out, Android will then forward any `https://www.example.com` intents to the `example.com` app.
608
+
609
+4. The user clicks a link to `https://www.example.com/puppies` on the device. This link could be anywhere: in a browser, in a Google Search Appliance suggestion, or anywhere else. Android forwards the intent to the `example.com` app.
610
+
611
+5. The `example.com` app receives the intent and chooses to handle it, opening the puppies page in the app. If for some reason the app had declined to handle the link, or if the app were not on the device, then the link will be send to the next default intent handler, matching that intent pattern (i.e. browser).
612
+
613
+### Testing UL for Android locally
614
+
615
+To test Android application for Deep Linking support you just need to execute the following command in the console:
616
+
617
+```sh
618
+adb shell am start
619
+        -W -a android.intent.action.VIEW
620
+        -d <URI> <PACKAGE>
621
+```
622
+
623
+where
624
+- `<URI>` - url that you want to test;
625
+- `<PACKAGE>` - your application's package name.
626
+
627
+**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.
628
+
629
+Let's create new application to play with:
630
+1. Create new Cordova project and add Android platform to it:
631
+
632
+  ```sh
633
+  cordova create TestAndroidApp com.example.ul TestAndroidApp
634
+  cd ./TestAndroidApp
635
+  cordova platform add android
636
+  ```
637
+
638
+2. Add UL plugin:
639
+
640
+  ```sh
641
+  cordova plugin add cordova-mirtech-plugin-universal-links
642
+  ```
643
+
644
+3. Add `<universal-links>` preference into `config.xml` (`TestAndroidApp/config.xml`):
645
+
646
+  ```xml
647
+  <!-- some other data from config.xml -->
648
+  <universal-links>
649
+   <host name="myhost.com" />
650
+  </universal-links>
651
+  ```
652
+
653
+4. Build and run the app:
654
+
655
+  ```sh
656
+  cordova run android
657
+  ```
658
+
659
+5. Close your application and return to console.
660
+6. Enter in console:
661
+
662
+  ```sh
663
+  adb shell am start -W -a android.intent.action.VIEW -d "http://myhost.com/any/path" com.example.ul
664
+  ```
665
+
666
+  As a result, your application will be launched and you will see in console:
667
+
668
+  ```
669
+  Starting: Intent { act=android.intent.action.VIEW dat=http://myhost.com/any/path pkg=com.example.ul }
670
+  Status: ok
671
+  Activity: com.example.ul/.MainActivity
672
+  ThisTime: 52
673
+  TotalTime: 52
674
+  Complete
675
+  ```
676
+
677
+  If you'll try to use host (or path), that is not defined in `config.xml` - you'll get a following error:
678
+
679
+  ```
680
+  Starting: Intent { act=android.intent.action.VIEW dat=http://anotherhost.com/path pkg=com.example.ul }
681
+  Error: Activity not started, unable to resolve Intent { act=android.intent.action.VIEW dat=http://anotherhost.com/path flg=0x10000000 pkg=com.example.ul }
682
+  ```
683
+
684
+This way you can experiment with your Android application and check how it corresponds to different links.
685
+
686
+### iOS web integration
687
+
688
+In the case of iOS integration of the Universal Links is a little harder. It consist of two steps:
689
+
690
+1. Register your application on [developer console](https://developer.apple.com) and enable `Associated Domains` feature. Make sure your website is SSL ready.
691
+2. Generate, and upload `apple-app-site-association` file on your website (if you don't have it yet).
692
+
693
+First one you will have to do manually, but plugin will help you with the second step.
694
+
695
+#### Activate UL support in member center
696
+
697
+1. Go to your [developer console](https://developer.apple.com). Click on `Certificate, Identifiers & Profiles` and then on `Identifiers`.
698
+
699
+  ![Developer console](docs/images/developer-console.jpg?raw=true)
700
+
701
+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`.
702
+
703
+  ![App ID](docs/images/app-id.jpg?raw=true)
704
+
705
+3. In the `Application Services` section of your App Identifier activate `Associated Domains` and save the changes.
706
+
707
+  ![App ID](docs/images/app-associated-domains.jpg?raw=true)
708
+
709
+Now your App ID is registered and has `Associated Domains` feature.
710
+
711
+#### Configure apple-app-site-association file for website
712
+
713
+In order for Universal Links to work - you need to associate your application with the certain domain. For that you need to:
714
+
715
+1. Make your site to work over `https`.
716
+2. Create `apple-app-site-association` file, containing your App ID and paths you want to handle.
717
+3. Upload `apple-app-site-association` file in the root of your website.
718
+
719
+##### Step 1
720
+
721
+We are not gonna describe stuff regarding certificate acquiring and making your website to work over `https`. You can find lots of information about that on the Internet.
722
+
723
+##### Step 2
724
+
725
+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:
726
+```
727
+<hostname>#apple-app-site-association
728
+```
729
+
730
+For example, let's say your application's bundle ID is `com.example.ul`, and `config.xml` has several hosts:
731
+
732
+```xml
733
+<universal-links>
734
+  <host name="firsthost.com">
735
+    <path url="/some/path/*" />
736
+  </host>
737
+  <host name="secondhost.com" />
738
+</universal-links>
739
+```
740
+
741
+Run `cordova build`, and then go to `ul_web_hooks/ios/` folder in your Cordova project. You will see there two files:
742
+
743
+```
744
+firsthost.com#apple-app-site-association
745
+secondhost.com#apple-app-site-association
746
+```
747
+
748
+Content of the first one is:
749
+```json
750
+{
751
+  "applinks": {
752
+    "apps": [],
753
+    "details": [
754
+      {
755
+        "appID": "<YOUR_TEAM_ID_FROM_MEMBER_CENTER>.com.example.ul",
756
+        "paths": [
757
+          "/some/path/*"
758
+        ]
759
+      }
760
+    ]
761
+  }
762
+}
763
+```
764
+
765
+And the second one:
766
+```json
767
+{
768
+  "applinks": {
769
+    "apps": [],
770
+    "details": [
771
+      {
772
+        "appID": "<YOUR_TEAM_ID_FROM_MEMBER_CENTER>.com.example.ul",
773
+        "paths": [
774
+          "*", "/"
775
+        ]
776
+      }
777
+    ]
778
+  }
779
+}
780
+```
781
+
782
+**Note:** in the second case plugin will add `/` to the paths, so the app would be opened with `https://secondhost.com.com/` links, and not only with `https://secondhost.com/some/random.html`.
783
+
784
+Before uploading them on your servers - you need to replace `<YOUR_TEAM_ID_FROM_MEMBER_CENTER>` 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).
785
+
786
+Also, it is a `Prefix` preference in the App ID description.
787
+
788
+![App ID team prefix](docs/images/app-id-team-prefix.jpg?raw=true)
789
+
790
+If you already have `apple-app-site-association` file - then you need to add `applinks` block to it from the generated file.
791
+
792
+##### Step 3
793
+
794
+Upload `apple-app-site-association` file in the root of your domain.
795
+
796
+**It should be downloadable from the direct link.** For example, `https://firsthost.com/apple-app-site-association`.
797
+
798
+**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.
799
+
800
+That's it, you have finished configuring iOS for UL support.
801
+
802
+### Testing iOS application
803
+
804
+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.
805
+
806
+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.
807
+
808
+Step-by-step guide:
809
+
810
+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).
811
+
812
+2. Register account on [branch.io](https://dashboard.branch.io/), if you don't have it yet.
813
+
814
+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`.
815
+
816
+  ![App ID](docs/images/branch-io.jpg?raw=true)
817
+
818
+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`.
819
+
820
+  Pay attention for `paths` - if there is any for your app, then write it down.
821
+
822
+  For example:
823
+  ```json
824
+  ...,"9F38WJR2U8.com.example.ul":{"paths":["/a2Be/*"]},...
825
+  ```
826
+
827
+5. Create new Cordova iOS application and add UL plugin:
828
+
829
+  ```sh
830
+  cordova create TestProject com.example.ul TestProject
831
+  cd ./TestProject
832
+  cordova platform add ios
833
+  cordova plugin add cordova-mirtech-plugin-universal-links
834
+  ```
835
+
836
+6. Add `bnc.lt` and your other hosts into `config.xml`:
837
+
838
+  ```xml
839
+  <universal-links>
840
+    <host name="bnc.lt" />
841
+    <host name="yourdomain.com" />
842
+  </universal-links>
843
+  ```
844
+
845
+  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.
846
+
847
+7. Attach your real device to the computer and run application on it:
848
+
849
+  ```sh
850
+  cordova run ios
851
+  ```
852
+
853
+  Emulator will not work.
854
+
855
+8. Email yourself a link that need's to be tested.
856
+
857
+  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.
858
+
859
+  Now click on your link. Application should be launched. If not - check all the steps above. Also, check your provisioning profiles in Xcode.
860
+
861
+### Useful notes on Universal Links for iOS
862
+
863
+#### They don't work everywhere
864
+
865
+First of all - you need to accept the fact, that Universal Links **doesn't work everywhere**. Some applications doesn't respect them. You can read more in [that article](https://blog.branch.io/ios-9.2-deep-linking-guide-transitioning-to-universal-links), section `Universal Links Still Don’t Work Everywhere`.
866
+
867
+#### How links handled in Safari
868
+
869
+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.
870
+
871
+Now, let's assume you have a following setup in `config.xml`:
872
+```xml
873
+<universal-links>
874
+  <host name="mywebsite.com">
875
+    <path url="/some/page.html" />
876
+  </host>
877
+</universal-links>
878
+```
879
+
880
+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.
881
+
882
+Another thing that every developer should be aware of:
883
+
884
+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.
885
+
886
+### Additional documentation links
887
+
888
+**Android:**
889
+- [Video tutorial on Android App Indexing](https://realm.io/news/juan-gomez-android-app-indexing/)
890
+- [Enable Deep Linking on Android](https://developer.android.com/training/app-indexing/deep-linking.html)
891
+- [Specifying App Content for Indexing](https://developer.android.com/training/app-indexing/enabling-app-indexing.html)
892
+- [Documentation on enabling App Indexing on the website](https://developers.google.com/app-indexing/android/publish#host-your-links)
893
+
894
+**iOS:**
895
+- [Apple documentation on Universal Links](https://developer.apple.com/library/ios/documentation/General/Conceptual/AppSearch/UniversalLinks.html)
896
+- [Apple documentation on apple-app-site-association file](https://developer.apple.com/library/ios/documentation/Security/Reference/SharedWebCredentialsRef/index.html)
897
+- [How to setup universal links on iOS 9](https://blog.branch.io/how-to-setup-universal-links-to-deep-link-on-apple-ios-9)
898
+- [Branch.io documentation on universal links](https://dev.branch.io/recipes/branch_universal_links/#enable-universal-links-on-the-branch-dashboard)
899
+- [Where universal links don't work](https://blog.branch.io/ios-9.2-deep-linking-guide-transitioning-to-universal-links)

BIN
docs/images/app-associated-domains.jpg View File


BIN
docs/images/app-id-team-prefix.jpg View File


BIN
docs/images/app-id.jpg View File


BIN
docs/images/branch-io.jpg View File


BIN
docs/images/developer-console.jpg View File


+ 86
- 0
hooks/afterPrepareHook.js View File

@@ -0,0 +1,86 @@
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 <universal-links>
5
+data you have specified in the projects config.xml file.
6
+*/
7
+
8
+var configParser = require('./lib/configXmlParser.js');
9
+var androidManifestWriter = require('./lib/android/manifestWriter.js');
10
+var androidWebHook = require('./lib/android/webSiteHook.js');
11
+var iosProjectEntitlements = require('./lib/ios/projectEntitlements.js');
12
+var iosAppSiteAssociationFile = require('./lib/ios/appleAppSiteAssociationFile.js');
13
+var iosProjectPreferences = require('./lib/ios/xcodePreferences.js');
14
+var ANDROID = 'android';
15
+var 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
+  var 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 <universal-links> 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 <link> 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 <universal-links> 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
+}

+ 55
- 0
hooks/beforePluginInstallHook.js View File

@@ -0,0 +1,55 @@
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 path = require('path');
7
+var fs = require('fs');
8
+var spawnSync = require('child_process').spawnSync;
9
+var pluginNpmDependencies = require('../package.json').dependencies;
10
+var INSTALLATION_FLAG_FILE_NAME = '.npmInstalled';
11
+
12
+// region mark that we installed npm packages
13
+/**
14
+ * Check if we already executed this hook.
15
+ *
16
+ * @param {Object} ctx - cordova context
17
+ * @return {Boolean} true if already executed; otherwise - false
18
+ */
19
+function isInstallationAlreadyPerformed(ctx) {
20
+  var pathToInstallFlag = path.join(ctx.opts.projectRoot, 'plugins', ctx.opts.plugin.id, INSTALLATION_FLAG_FILE_NAME);
21
+  try {
22
+    fs.accessSync(pathToInstallFlag, fs.F_OK);
23
+    return true;
24
+  } catch (err) {
25
+    return false;
26
+  }
27
+}
28
+
29
+/**
30
+ * Create empty file - indicator, that we tried to install dependency modules after installation.
31
+ * We have to do that, or this hook is gonna be called on any plugin installation.
32
+ */
33
+function createPluginInstalledFlag(ctx) {
34
+  var pathToInstallFlag = path.join(ctx.opts.projectRoot, 'plugins', ctx.opts.plugin.id, INSTALLATION_FLAG_FILE_NAME);
35
+
36
+  fs.closeSync(fs.openSync(pathToInstallFlag, 'w'));
37
+}
38
+// endregion
39
+
40
+module.exports = function(ctx) {
41
+  if (isInstallationAlreadyPerformed(ctx)) {
42
+    return;
43
+  }
44
+
45
+  console.log('Installing dependency packages: ');
46
+  console.log(JSON.stringify(pluginNpmDependencies, null, 2));
47
+
48
+  var npm = (process.platform === "win32" ? "npm.cmd" : "npm");
49
+  var result = spawnSync(npm, ['install', '--production'], { cwd: './plugins/' + ctx.opts.plugin.id });
50
+  if (result.error) {
51
+    throw result.error;
52
+  }
53
+
54
+  createPluginInstalledFlag(ctx);
55
+};

+ 74
- 0
hooks/iosBeforePrepareHook.js View File

@@ -0,0 +1,74 @@
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 will do nothing.
5
+*/
6
+
7
+var path = require('path');
8
+var fs = require('fs');
9
+var 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
+  var iosProjectFilePath = path.join(projectRoot, 'platforms', 'ios');
23
+  var configXmlHelper = new ConfigXmlHelper(ctx);
24
+  var newProjectName = configXmlHelper.getProjectName();
25
+
26
+  var oldProjectName = getOldProjectName(iosProjectFilePath);
27
+
28
+  // if name has not changed - do nothing
29
+  if (oldProjectName.length && oldProjectName === newProjectName) {
30
+    return;
31
+  }
32
+
33
+  console.log('Project name has changed. Renaming .entitlements file.');
34
+
35
+  // if it does - rename it
36
+  var oldEntitlementsFilePath = path.join(iosProjectFilePath, oldProjectName, 'Resources', oldProjectName + '.entitlements');
37
+  var newEntitlementsFilePath = path.join(iosProjectFilePath, oldProjectName, 'Resources', newProjectName + '.entitlements');
38
+
39
+  try {
40
+    fs.renameSync(oldEntitlementsFilePath, newEntitlementsFilePath);
41
+  } catch (err) {
42
+    console.warn('Failed to rename .entitlements file.');
43
+    console.warn(err);
44
+  }
45
+}
46
+
47
+// region Private API
48
+
49
+/**
50
+ * Get old name of the project.
51
+ * Name is detected by the name of the .xcodeproj file.
52
+ *
53
+ * @param {String} projectDir absolute path to ios project directory
54
+ * @return {String} old project name
55
+ */
56
+function getOldProjectName(projectDir) {
57
+  var files = [];
58
+  try {
59
+    files = fs.readdirSync(projectDir);
60
+  } catch (err) {
61
+    return '';
62
+  }
63
+
64
+  var projectFile = '';
65
+  files.forEach(function(fileName) {
66
+    if (path.extname(fileName) === '.xcodeproj') {
67
+      projectFile = path.basename(fileName, '.xcodeproj');
68
+    }
69
+  });
70
+
71
+  return projectFile;
72
+}
73
+
74
+// endregion

+ 316
- 0
hooks/lib/android/manifestWriter.js View File

@@ -0,0 +1,316 @@
1
+/**
2
+Class injects plugin preferences into AndroidManifest.xml file.
3
+*/
4
+
5
+var path = require('path');
6
+var xmlHelper = require('../xmlHelper.js');
7
+
8
+module.exports = {
9
+  writePreferences: writePreferences
10
+};
11
+
12
+// region Public API
13
+
14
+/**
15
+ * Inject preferences into AndroidManifest.xml file.
16
+ *
17
+ * @param {Object} cordovaContext - cordova context object
18
+ * @param {Object} pluginPreferences - plugin preferences as JSON object; already parsed
19
+ */
20
+function writePreferences(cordovaContext, pluginPreferences) {
21
+  var pathToManifest = path.join(cordovaContext.opts.projectRoot, 'platforms', 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
22
+  var manifestSource = xmlHelper.readXmlAsJson(pathToManifest);
23
+  var cleanManifest;
24
+  var updatedManifest;
25
+
26
+  // remove old intent-filters
27
+  cleanManifest = removeOldOptions(manifestSource);
28
+
29
+  // inject intent-filters based on plugin preferences
30
+  updatedManifest = injectOptions(cleanManifest, pluginPreferences);
31
+
32
+  // save new version of the AndroidManifest
33
+  xmlHelper.writeJsonAsXml(updatedManifest, pathToManifest);
34
+}
35
+
36
+// endregion
37
+
38
+// region Manifest cleanup methods
39
+
40
+/**
41
+ * Remove old intent-filters from the manifest file.
42
+ *
43
+ * @param {Object} manifestData - manifest content as JSON object
44
+ * @return {Object} manifest data without old intent-filters
45
+ */
46
+function removeOldOptions(manifestData) {
47
+  var cleanManifest = manifestData;
48
+  var activities = manifestData['manifest']['application'][0]['activity'];
49
+
50
+  activities.forEach(removeIntentFiltersFromActivity);
51
+  cleanManifest['manifest']['application'][0]['activity'] = activities;
52
+
53
+  return cleanManifest;
54
+}
55
+
56
+/**
57
+ * Remove old intent filters from the given activity.
58
+ *
59
+ * @param {Object} activity - activity, from which we need to remove intent-filters.
60
+ *                            Changes applied to the passed object.
61
+ */
62
+function removeIntentFiltersFromActivity(activity) {
63
+  var oldIntentFilters = activity['intent-filter'];
64
+  var newIntentFilters = [];
65
+
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
+  var categories = intentFilter['category'];
88
+  var data = intentFilter['data'];
89
+
90
+  return isActionForUniversalLinks(actions) &&
91
+    isCategoriesForUniversalLinks(categories) &&
92
+    isDataTagForUniversalLinks(data);
93
+}
94
+
95
+/**
96
+ * Check if actions from the intent-filter corresponds to actions for Universal Links.
97
+ *
98
+ * @param {Array} actions - list of actions in the intent-filter
99
+ * @return {Boolean} true - if action for Universal Links; otherwise - false
100
+ */
101
+function isActionForUniversalLinks(actions) {
102
+  // there can be only 1 action
103
+  if (actions == null || actions.length != 1) {
104
+    return false;
105
+  }
106
+
107
+  var action = actions[0]['$']['android:name'];
108
+
109
+  return ('android.intent.action.VIEW' === action);
110
+}
111
+
112
+/**
113
+ * Check if categories in the intent-filter corresponds to categories for Universal Links.
114
+ *
115
+ * @param {Array} categories - list of categories in the intent-filter
116
+ * @return {Boolean} true - if action for Universal Links; otherwise - false
117
+ */
118
+function isCategoriesForUniversalLinks(categories) {
119
+  // there can be only 2 categories
120
+  if (categories == null || categories.length != 2) {
121
+    return false;
122
+  }
123
+
124
+  var isBrowsable = false;
125
+  var isDefault = false;
126
+
127
+  // check intent categories
128
+  categories.forEach(function(category) {
129
+    var categoryName = category['$']['android:name'];
130
+    if (!isBrowsable) {
131
+      isBrowsable = 'android.intent.category.BROWSABLE' === categoryName;
132
+    }
133
+
134
+    if (!isDefault) {
135
+      isDefault = 'android.intent.category.DEFAULT' === categoryName;
136
+    }
137
+  });
138
+
139
+  return isDefault && isBrowsable;
140
+}
141
+
142
+/**
143
+ * Check if data tag from intent-filter corresponds to data for Universal Links.
144
+ *
145
+ * @param {Array} data - list of data tags in the intent-filter
146
+ * @return {Boolean} true - if data tag for Universal Links; otherwise - false
147
+ */
148
+function isDataTagForUniversalLinks(data) {
149
+  // can have only 1 data tag in the intent-filter
150
+  if (data == null || data.length != 1) {
151
+    return false;
152
+  }
153
+
154
+  var dataHost = data[0]['$']['android:host'];
155
+  var dataScheme = data[0]['$']['android:scheme'];
156
+  var hostIsSet = dataHost != null && dataHost.length > 0;
157
+  var schemeIsSet = dataScheme != null && dataScheme.length > 0;
158
+
159
+  return hostIsSet && schemeIsSet;
160
+}
161
+
162
+// endregion
163
+
164
+// region Methods to inject preferences into AndroidManifest.xml file
165
+
166
+/**
167
+ * Inject options into manifest file.
168
+ *
169
+ * @param {Object} manifestData - manifest content where preferences should be injected
170
+ * @param {Object} pluginPreferences - plugin preferences from config.xml; already parsed
171
+ * @return {Object} updated manifest data with corresponding intent-filters
172
+ */
173
+function injectOptions(manifestData, pluginPreferences) {
174
+  var changedManifest = manifestData;
175
+  var activitiesList = changedManifest['manifest']['application'][0]['activity'];
176
+  var launchActivityIndex = getMainLaunchActivityIndex(activitiesList);
177
+  var ulIntentFilters = [];
178
+  var launchActivity;
179
+
180
+  if (launchActivityIndex < 0) {
181
+    console.warn('Could not find launch activity in the AndroidManifest file. Can\'t inject Universal Links preferences.');
182
+    return;
183
+  }
184
+
185
+  // get launch activity
186
+  launchActivity = activitiesList[launchActivityIndex];
187
+
188
+  // generate intent-filters
189
+  pluginPreferences.hosts.forEach(function(host) {
190
+    host.paths.forEach(function(hostPath) {
191
+      ulIntentFilters.push(createIntentFilter(host.name, host.scheme, hostPath));
192
+    });
193
+  });
194
+
195
+  // add Universal Links intent-filters to the launch activity
196
+  launchActivity['intent-filter'] = launchActivity['intent-filter'].concat(ulIntentFilters);
197
+
198
+  return changedManifest;
199
+}
200
+
201
+/**
202
+ * Find index of the applications launcher activity.
203
+ *
204
+ * @param {Array} activities - list of all activities in the app
205
+ * @return {Integer} index of the launch activity; -1 - if none was found
206
+ */
207
+function getMainLaunchActivityIndex(activities) {
208
+  var launchActivityIndex = -1;
209
+  activities.some(function(activity, index) {
210
+    if (isLaunchActivity(activity)) {
211
+      launchActivityIndex = index;
212
+      return true;
213
+    }
214
+
215
+    return false;
216
+  });
217
+
218
+  return launchActivityIndex;
219
+}
220
+
221
+/**
222
+ * Check if the given actvity is a launch activity.
223
+ *
224
+ * @param {Object} activity - activity to check
225
+ * @return {Boolean} true - if this is a launch activity; otherwise - false
226
+ */
227
+function isLaunchActivity(activity) {
228
+  var intentFilters = activity['intent-filter'];
229
+  var isLauncher = false;
230
+
231
+  if (intentFilters == null || intentFilters.length == 0) {
232
+    return false;
233
+  }
234
+
235
+  isLauncher = intentFilters.some(function(intentFilter) {
236
+    var action = intentFilter['action'];
237
+    var category = intentFilter['category'];
238
+
239
+    if (action == null || action.length != 1 || category == null || category.length != 1) {
240
+      return false;
241
+    }
242
+
243
+    var isMainAction = ('android.intent.action.MAIN' === action[0]['$']['android:name']);
244
+    var isLauncherCategory = ('android.intent.category.LAUNCHER' === category[0]['$']['android:name']);
245
+
246
+    return isMainAction && isLauncherCategory;
247
+  });
248
+
249
+  return isLauncher;
250
+}
251
+
252
+/**
253
+ * Create JSON object that represent intent-filter for universal link.
254
+ *
255
+ * @param {String} host - host name
256
+ * @param {String} scheme - host scheme
257
+ * @param {String} pathName - host path
258
+ * @return {Object} intent-filter as a JSON object
259
+ */
260
+function createIntentFilter(host, scheme, pathName) {
261
+  var intentFilter = {
262
+    '$': {
263
+      'android:autoVerify': 'true'
264
+    },
265
+    'action': [{
266
+      '$': {
267
+        'android:name': 'android.intent.action.VIEW'
268
+      }
269
+    }],
270
+    'category': [{
271
+      '$': {
272
+        'android:name': 'android.intent.category.DEFAULT'
273
+      }
274
+    }, {
275
+      '$': {
276
+        'android:name': 'android.intent.category.BROWSABLE'
277
+      }
278
+    }],
279
+    'data': [{
280
+      '$': {
281
+        'android:host': host,
282
+        'android:scheme': scheme
283
+      }
284
+    }]
285
+  };
286
+
287
+  injectPathComponentIntoIntentFilter(intentFilter, pathName);
288
+
289
+  return intentFilter;
290
+}
291
+
292
+/**
293
+ * Inject host path into provided intent-filter.
294
+ *
295
+ * @param {Object} intentFilter - intent-filter object where path component should be injected
296
+ * @param {String} pathName - host path to inject
297
+ */
298
+function injectPathComponentIntoIntentFilter(intentFilter, pathName) {
299
+  if (pathName == null || pathName === '*') {
300
+    return;
301
+  }
302
+
303
+  var attrKey = 'android:path';
304
+  if (pathName.indexOf('*') >= 0) {
305
+    attrKey = 'android:pathPattern';
306
+    pathName = pathName.replace(/\*/g, '.*');
307
+  }
308
+
309
+  if (pathName.indexOf('/') != 0) {
310
+    pathName = '/' + pathName;
311
+  }
312
+
313
+  intentFilter['data'][0]['$'][attrKey] = pathName;
314
+}
315
+
316
+// endregion

+ 162
- 0
hooks/lib/android/webSiteHook.js View File

@@ -0,0 +1,162 @@
1
+/*
2
+Class creates android_web_hook.html file in your Cordova project root folder.
3
+File holds <link /> 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
+
10
+var fs = require('fs');
11
+var path = require('path');
12
+var mkpath = require('mkpath');
13
+var ConfigXmlHelper = require('../configXmlHelper.js');
14
+var WEB_HOOK_FILE_PATH = path.join('ul_web_hooks', 'android', 'android_web_hook.html');
15
+var WEB_HOOK_TPL_FILE_PATH = path.join('plugins', 'cordova-mirtech-plugin-universal-links', 'ul_web_hooks', 'android_web_hook_tpl.html');
16
+var LINK_PLACEHOLDER = '[__LINKS__]';
17
+var LINK_TEMPLATE = '<link rel="alternate" href="android-app://<package_name>/<scheme>/<host><path>" />';
18
+
19
+module.exports = {
20
+  generate: generateWebHook
21
+};
22
+
23
+// region Public API
24
+
25
+/**
26
+ * Generate website hook for android application.
27
+ *
28
+ * @param {Object} cordovaContext - cordova context object
29
+ * @param {Object} pluginPreferences - plugin preferences from config.xml file; already parsed
30
+ */
31
+function generateWebHook(cordovaContext, pluginPreferences) {
32
+  var projectRoot = cordovaContext.opts.projectRoot;
33
+  var configXmlHelper = new ConfigXmlHelper(cordovaContext);
34
+  var packageName = configXmlHelper.getPackageName('android');
35
+  var template = readTemplate(projectRoot);
36
+
37
+  // if template was not found - exit
38
+  if (template == null || template.length == 0) {
39
+    return;
40
+  }
41
+
42
+  // generate hook content
43
+  var linksToInsert = generateLinksSet(projectRoot, packageName, pluginPreferences);
44
+  var hookContent = template.replace(LINK_PLACEHOLDER, linksToInsert);
45
+
46
+  // save hook
47
+  saveWebHook(projectRoot, hookContent);
48
+}
49
+
50
+// endregion
51
+
52
+// region Public API
53
+
54
+/**
55
+ * Read hook teplate from plugin directory.
56
+ *
57
+ * @param {String} projectRoot - absolute path to cordova's project root
58
+ * @return {String} data from the template file
59
+ */
60
+function readTemplate(projectRoot) {
61
+  var filePath = path.join(projectRoot, WEB_HOOK_TPL_FILE_PATH);
62
+  var tplData = null;
63
+
64
+  try {
65
+    tplData = fs.readFileSync(filePath, 'utf8');
66
+  } catch (err) {
67
+    console.warn('Template file for android web hook is not found!');
68
+    console.warn(err);
69
+  }
70
+
71
+  return tplData;
72
+}
73
+
74
+/**
75
+ * Generate list of <link /> tags based on plugin preferences.
76
+ *
77
+ * @param {String} projectRoot - absolute path to cordova's project root
78
+ * @param {String} packageName - android application package name
79
+ * @param {Object} pluginPreferences - plugin preferences, defined in config.xml; already parsed
80
+ * @return {String} list of <link /> tags
81
+ */
82
+function generateLinksSet(projectRoot, packageName, pluginPreferences) {
83
+  var linkTpl = LINK_TEMPLATE.replace('<package_name>', packageName);
84
+  var content = '';
85
+
86
+  pluginPreferences.hosts.forEach(function(host) {
87
+    host.paths.forEach(function(hostPath) {
88
+      content += generateLinkTag(linkTpl, host.scheme, host.name, hostPath) + '\n';
89
+    });
90
+  });
91
+
92
+  return content;
93
+}
94
+
95
+/**
96
+ * Generate <link /> tag.
97
+ *
98
+ * @param {String} linkTpl - template to use for tag generation
99
+ * @param {String} scheme - host scheme
100
+ * @param {String} host - host name
101
+ * @param {String} path - host path
102
+ * @return {String} <link /> tag
103
+ */
104
+function generateLinkTag(linkTpl, scheme, host, path) {
105
+  linkTpl = linkTpl.replace('<scheme>', scheme).replace('<host>', host);
106
+  if (path == null || path === '*') {
107
+    return linkTpl.replace('<path>', '');
108
+  }
109
+
110
+  // for android we need to replace * with .* for pattern matching
111
+  if (path.indexOf('*') >= 0) {
112
+    path = path.replace(/\*/g, '.*');
113
+  }
114
+
115
+  // path should start with /
116
+  if (path.indexOf('/') != 0) {
117
+    path = '/' + path;
118
+  }
119
+
120
+  return linkTpl.replace('<path>', path);
121
+}
122
+
123
+/**
124
+ * Save data to website hook file.
125
+ *
126
+ * @param {String} projectRoot - absolute path to project root
127
+ * @param {String} hookContent - data to save
128
+ * @return {boolean} true - if data was saved; otherwise - false;
129
+ */
130
+function saveWebHook(projectRoot, hookContent) {
131
+  var filePath = path.join(projectRoot, WEB_HOOK_FILE_PATH);
132
+  var isSaved = true;
133
+
134
+  // ensure directory exists
135
+  createDirectoryIfNeeded(path.dirname(filePath));
136
+
137
+  // write data to file
138
+  try {
139
+    fs.writeFileSync(filePath, hookContent, 'utf8');
140
+  } catch (err) {
141
+    console.warn('Failed to create android web hook!');
142
+    console.warn(err);
143
+    isSaved = false;
144
+  }
145
+
146
+  return isSaved;
147
+}
148
+
149
+/**
150
+ * Create directory if it doesn't exist yet.
151
+ *
152
+ * @param {String} dir - absolute path to directory
153
+ */
154
+function createDirectoryIfNeeded(dir) {
155
+  try {
156
+    mkpath.sync(dir);
157
+  } catch (err) {
158
+    console.log(err);
159
+  }
160
+}
161
+
162
+// endregion

+ 117
- 0
hooks/lib/configXmlHelper.js View File

@@ -0,0 +1,117 @@
1
+/*
2
+Helper class to read data from config.xml file.
3
+*/
4
+var path = require('path');
5
+var xmlHelper = require('./xmlHelper.js');
6
+var ANDROID = 'android';
7
+var IOS = 'ios';
8
+var CONFIG_FILE_NAME = 'config.xml';
9
+var context;
10
+var projectRoot;
11
+
12
+module.exports = ConfigXmlHelper;
13
+
14
+// region public API
15
+
16
+/**
17
+ * Constructor.
18
+ *
19
+ * @param {Object} cordovaContext - cordova context object
20
+ */
21
+function ConfigXmlHelper(cordovaContext) {
22
+  context = cordovaContext;
23
+  projectRoot = context.opts.projectRoot;
24
+}
25
+
26
+/**
27
+ * Read config.xml data as JSON object.
28
+ *
29
+ * @return {Object} JSON object with data from config.xml
30
+ */
31
+ConfigXmlHelper.prototype.read = function() {
32
+  var filePath = getConfigXmlFilePath();
33
+
34
+  return xmlHelper.readXmlAsJson(filePath);
35
+}
36
+
37
+/**
38
+ * Get package name for the application. Depends on the platform.
39
+ *
40
+ * @param {String} platform - 'ios' or 'android'; for what platform we need a package name
41
+ * @return {String} package/bundle name
42
+ */
43
+ConfigXmlHelper.prototype.getPackageName = function(platform) {
44
+  var configFilePath = getConfigXmlFilePath();
45
+  var config = getCordovaConfigParser(configFilePath);
46
+  var packageName;
47
+
48
+  switch (platform) {
49
+    case ANDROID:
50
+      {
51
+        packageName = config.android_packageName();
52
+        break;
53
+      }
54
+    case IOS:
55
+      {
56
+        packageName = config.ios_CFBundleIdentifier();
57
+        break;
58
+      }
59
+  }
60
+  if (packageName === undefined || packageName.length == 0) {
61
+    packageName = config.packageName();
62
+  }
63
+
64
+  return packageName;
65
+}
66
+
67
+/**
68
+ * Get name of the current project.
69
+ *
70
+ * @return {String} name of the project
71
+ */
72
+ConfigXmlHelper.prototype.getProjectName = function() {
73
+  return getProjectName();
74
+}
75
+
76
+// endregion
77
+
78
+// region Private API
79
+
80
+/**
81
+ * Get config parser from cordova library.
82
+ *
83
+ * @param {String} configFilePath absolute path to the config.xml file
84
+ * @return {Object}
85
+ */
86
+function getCordovaConfigParser(configFilePath) {
87
+  var ConfigParser;
88
+
89
+  // If we are running Cordova 5.4 or abova - use parser from cordova-common.
90
+  // Otherwise - from cordova-lib.
91
+  try {
92
+    ConfigParser = context.requireCordovaModule('cordova-common/src/ConfigParser/ConfigParser');
93
+  } catch (e) {
94
+    ConfigParser = context.requireCordovaModule('cordova-lib/src/configparser/ConfigParser')
95
+  }
96
+
97
+  return new ConfigParser(configFilePath);
98
+}
99
+
100
+/**
101
+ * Get absolute path to the config.xml.
102
+ */
103
+function getConfigXmlFilePath() {
104
+  return path.join(projectRoot, CONFIG_FILE_NAME);
105
+}
106
+
107
+/**
108
+ * Get project name from config.xml
109
+ */
110
+function getProjectName() {
111
+  var configFilePath = getConfigXmlFilePath();
112
+  var config = getCordovaConfigParser(configFilePath);
113
+
114
+  return config.name();
115
+}
116
+
117
+// endregion

+ 145
- 0
hooks/lib/configXmlParser.js View File

@@ -0,0 +1,145 @@
1
+/*
2
+Parser for config.xml file. Read plugin-specific preferences (from <universal-links> tag) as JSON object.
3
+*/
4
+var path = require('path');
5
+var ConfigXmlHelper = require('./configXmlHelper.js');
6
+var DEFAULT_SCHEME = 'http';
7
+
8
+module.exports = {
9
+  readPreferences: readPreferences
10
+};
11
+
12
+// region Public API
13
+
14
+/**
15
+ * Read plugin preferences from the config.xml file.
16
+ *
17
+ * @param {Object} cordovaContext - cordova context object
18
+ * @return {Array} list of host objects
19
+ */
20
+function readPreferences(cordovaContext) {
21
+  // read data from projects root config.xml file
22
+  var configXml = new ConfigXmlHelper(cordovaContext).read();
23
+  if (configXml == null) {
24
+    console.warn('config.xml not found! Please, check that it exist\'s in your project\'s root directory.');
25
+    return null;
26
+  }
27
+
28
+  // look for data from the <universal-links> tag
29
+  var ulXmlPreferences = configXml.widget['universal-links'];
30
+  if (ulXmlPreferences == null || ulXmlPreferences.length == 0) {
31
+    console.warn('<universal-links> tag is not set in the config.xml. Universal Links plugin is not going to work.');
32
+    return null;
33
+  }
34
+
35
+  var xmlPreferences = ulXmlPreferences[0];
36
+
37
+  // read hosts
38
+  var hosts = constructHostsList(xmlPreferences);
39
+
40
+  // read ios team ID
41
+  var iosTeamId = getTeamIdPreference(xmlPreferences);
42
+
43
+  return {
44
+    'hosts': hosts,
45
+    'iosTeamId': iosTeamId
46
+  };
47
+}
48
+
49
+// endregion
50
+
51
+// region Private API
52
+
53
+function getTeamIdPreference(xmlPreferences) {
54
+  if (xmlPreferences.hasOwnProperty('ios-team-id')) {
55
+    return xmlPreferences['ios-team-id'][0]['$']['value'];
56
+  }
57
+
58
+  return null;
59
+}
60
+
61
+/**
62
+ * Construct list of host objects, defined in xml file.
63
+ *
64
+ * @param {Object} xmlPreferences - plugin preferences from config.xml as JSON object
65
+ * @return {Array} array of JSON objects, where each entry defines host data from config.xml.
66
+ */
67
+function constructHostsList(xmlPreferences) {
68
+  var hostsList = [];
69
+
70
+  // look for defined hosts
71
+  var xmlHostList = xmlPreferences['host'];
72
+  if (xmlHostList == null || xmlHostList.length == 0) {
73
+    return [];
74
+  }
75
+
76
+  xmlHostList.forEach(function(xmlElement) {
77
+    var host = constructHostEntry(xmlElement);
78
+    if (host) {
79
+      hostsList.push(host);
80
+    }
81
+  });
82
+
83
+  return hostsList;
84
+}
85
+
86
+/**
87
+ * Construct host object from xml data.
88
+ *
89
+ * @param {Object} xmlElement - xml data to process.
90
+ * @return {Object} host entry as JSON object
91
+ */
92
+function constructHostEntry(xmlElement) {
93
+  var host = {
94
+      scheme: DEFAULT_SCHEME,
95
+      name: '',
96
+      paths: []
97
+    };
98
+  var hostProperties = xmlElement['$'];
99
+
100
+  if (hostProperties == null || hostProperties.length == 0) {
101
+    return null;
102
+  }
103
+
104
+  // read host name
105
+  host.name = hostProperties.name;
106
+
107
+  // read scheme if defined
108
+  if (hostProperties['scheme'] != null) {
109
+    host.scheme = hostProperties.scheme;
110
+  }
111
+
112
+  // construct paths list, defined for the given host
113
+  host.paths = constructPaths(xmlElement);
114
+
115
+  return host;
116
+}
117
+
118
+/**
119
+ * Construct list of path objects from the xml data.
120
+ *
121
+ * @param {Object} xmlElement - xml data to process
122
+ * @return {Array} list of path entries, each on is a JSON object
123
+ */
124
+function constructPaths(xmlElement) {
125
+  if (xmlElement['path'] == null) {
126
+    return ['*'];
127
+  }
128
+
129
+  var paths = [];
130
+  xmlElement.path.some(function(pathElement) {
131
+    var url = pathElement['$']['url'];
132
+
133
+    // Ignore explicit paths if '*' is defined
134
+    if (url === '*') {
135
+      paths = ['*'];
136
+      return true;
137
+    }
138
+
139
+    paths.push(url);
140
+  });
141
+
142
+  return paths;
143
+}
144
+
145
+// endregion

+ 173
- 0
hooks/lib/ios/appleAppSiteAssociationFile.js View File

@@ -0,0 +1,173 @@
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 <YOUR_TEAM_ID_FROM_MEMBER_CENTER> with the real team id from the member center, if <ios-team-id> 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
+
20
+var path = require('path');
21
+var mkpath = require('mkpath');
22
+var fs = require('fs');
23
+var rimraf = require('rimraf');
24
+var ConfigXmlHelper = require('../configXmlHelper.js');
25
+var IOS_TEAM_ID = '<YOUR_TEAM_ID_FROM_MEMBER_CENTER>';
26
+var ASSOCIATION_FILE_NAME = 'apple-app-site-association';
27
+var bundleId;
28
+var context;
29
+
30
+module.exports = {
31
+  generate: generate
32
+};
33
+
34
+// region Public API
35
+
36
+/**
37
+ * Generate apple-app-site-association files.
38
+ *
39
+ * @param {Object} cordovaContext - cordova context object
40
+ * @param {Object} pluginPreferences - list of hosts from the config.xml; already parsed
41
+ */
42
+function generate(cordovaContext, pluginPreferences) {
43
+  context = cordovaContext;
44
+  removeOldFiles();
45
+  createNewAssociationFiles(pluginPreferences);
46
+}
47
+
48
+// endregion
49
+
50
+// region Content generation
51
+
52
+/**
53
+ * Remove old files from ul_web_hooks/ios folder.
54
+ */
55
+function removeOldFiles() {
56
+  rimraf.sync(getWebHookDirectory());
57
+}
58
+
59
+/**
60
+ * Generate new set of apple-app-site-association files.
61
+ *
62
+ * @param {Object} pluginPreferences - list of hosts from config.xml
63
+ */
64
+function createNewAssociationFiles(pluginPreferences) {
65
+  var teamId = pluginPreferences.iosTeamId;
66
+  if (!teamId) {
67
+    teamId = IOS_TEAM_ID;
68
+  }
69
+
70
+  pluginPreferences.hosts.forEach(function(host) {
71
+    var content = generateFileContentForHost(host, teamId);
72
+    saveContentToFile(host.name, content);
73
+  });
74
+}
75
+
76
+/**
77
+ * Generate content of the apple-app-site-association file for the specific host.
78
+ *
79
+ * @param {Object} host - host information
80
+ * @return {Object} content of the file as JSON object
81
+ */
82
+function generateFileContentForHost(host, teamId) {
83
+  var appID = teamId + '.' + getBundleId();
84
+  var paths = host.paths.slice();
85
+
86
+  // if paths are '*' - we should add '/' to it to support root domains.
87
+  // https://github.com/martindrapeau/cordova-universal-links-plugin/issues/46
88
+  if (paths.length == 1 && paths[0] === '*') {
89
+    paths.push('/');
90
+  }
91
+
92
+  return {
93
+    "applinks": {
94
+      "apps": [],
95
+      "details": [{
96
+        "appID": appID,
97
+        "paths": paths
98
+      }]
99
+    }
100
+  };
101
+}
102
+
103
+/**
104
+ * Save data to the the apple-app-site-association file.
105
+ *
106
+ * @param {String} filePrefix - prefix for the generated file; usually - hostname
107
+ * @param {Object} content - file content as JSON object
108
+ */
109
+function saveContentToFile(filePrefix, content) {
110
+  var dirPath = getWebHookDirectory();
111
+  var filePath = path.join(dirPath, filePrefix + '#' + ASSOCIATION_FILE_NAME);
112
+
113
+  // create all directories from file path
114
+  createDirectoriesIfNeeded(dirPath);
115
+
116
+  // write content to the file
117
+  try {
118
+    fs.writeFileSync(filePath, JSON.stringify(content, null, 2), 'utf8');
119
+  } catch (err) {
120
+    console.log(err);
121
+  }
122
+}
123
+
124
+/**
125
+ * Create all directories from the given path.
126
+ *
127
+ * @param {String} dirPath - full path to directory
128
+ */
129
+function createDirectoriesIfNeeded(dirPath) {
130
+  try {
131
+    mkpath.sync(dirPath);
132
+  } catch (err) {
133
+    console.log(err);
134
+  }
135
+}
136
+
137
+// endregion
138
+
139
+// region Support methods
140
+
141
+/**
142
+ * Path to the ios web hook directory.
143
+ *
144
+ * @return {String} path to web hook directory
145
+ */
146
+function getWebHookDirectory() {
147
+  return path.join(getProjectRoot(), 'ul_web_hooks', 'ios');
148
+}
149
+
150
+/**
151
+ * Project root directory
152
+ *
153
+ * @return {String} absolute path to project root
154
+ */
155
+function getProjectRoot() {
156
+  return context.opts.projectRoot;
157
+}
158
+
159
+/**
160
+ * Get bundle id from the config.xml file.
161
+ *
162
+ * @return {String} bundle id
163
+ */
164
+function getBundleId() {
165
+  if (bundleId === undefined) {
166
+    var configXmlHelper = new ConfigXmlHelper(context);
167
+    bundleId = configXmlHelper.getPackageName('ios');
168
+  }
169
+
170
+  return bundleId;
171
+}
172
+
173
+// endregion

+ 174
- 0
hooks/lib/ios/projectEntitlements.js View File

@@ -0,0 +1,174 @@
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
+
9
+var path = require('path');
10
+var fs = require('fs');
11
+var plist = require('plist');
12
+var mkpath = require('mkpath');
13
+var ConfigXmlHelper = require('../configXmlHelper.js');
14
+var ASSOCIATED_DOMAINS = 'com.apple.developer.associated-domains';
15
+var context;
16
+var projectRoot;
17
+var projectName;
18
+var entitlementsFilePath;
19
+
20
+module.exports = {
21
+  generateAssociatedDomainsEntitlements: generateEntitlements
22
+};
23
+
24
+// region Public API
25
+
26
+/**
27
+ * Generate entitlements file content.
28
+ *
29
+ * @param {Object} cordovaContext - cordova context object
30
+ * @param {Object} pluginPreferences - plugin preferences from config.xml; already parsed
31
+ */
32
+function generateEntitlements(cordovaContext, pluginPreferences) {
33
+  context = cordovaContext;
34
+
35
+  var currentEntitlements = getEntitlementsFileContent();
36
+  var newEntitlements = injectPreferences(currentEntitlements, pluginPreferences);
37
+
38
+  saveContentToEntitlementsFile(newEntitlements);
39
+}
40
+
41
+// endregion
42
+
43
+// region Work with entitlements file
44
+
45
+/**
46
+ * Save data to entitlements file.
47
+ *
48
+ * @param {Object} content - data to save; JSON object that will be transformed into xml
49
+ */
50
+function saveContentToEntitlementsFile(content) {
51
+  var plistContent = plist.build(content);
52
+  var filePath = pathToEntitlementsFile();
53
+
54
+  // ensure that file exists
55
+  mkpath.sync(path.dirname(filePath));
56
+
57
+  // save it's content
58
+  fs.writeFileSync(filePath, plistContent, 'utf8');
59
+}
60
+
61
+/**
62
+ * Read data from existing entitlements file. If none exist - default value is returned
63
+ *
64
+ * @return {String} entitlements file content
65
+ */
66
+function getEntitlementsFileContent() {
67
+  var pathToFile = pathToEntitlementsFile();
68
+  var content;
69
+
70
+  try {
71
+    content = fs.readFileSync(pathToFile, 'utf8');
72
+  } catch (err) {
73
+    return defaultEntitlementsFile();
74
+  }
75
+
76
+  return plist.parse(content);
77
+}
78
+
79
+/**
80
+ * Get content for an empty entitlements file.
81
+ *
82
+ * @return {String} default entitlements file content
83
+ */
84
+function defaultEntitlementsFile() {
85
+  return {};
86
+}
87
+
88
+/**
89
+ * Inject list of hosts into entitlements file.
90
+ *
91
+ * @param {Object} currentEntitlements - entitlements where to inject preferences
92
+ * @param {Object} pluginPreferences - list of hosts from config.xml
93
+ * @return {Object} new entitlements content
94
+ */
95
+function injectPreferences(currentEntitlements, pluginPreferences) {
96
+  var newEntitlements = currentEntitlements;
97
+  var content = generateAssociatedDomainsContent(pluginPreferences);
98
+
99
+  newEntitlements[ASSOCIATED_DOMAINS] = content;
100
+
101
+  return newEntitlements;
102
+}
103
+
104
+/**
105
+ * Generate content for associated-domains dictionary in the entitlements file.
106
+ *
107
+ * @param {Object} pluginPreferences - list of hosts from conig.xml
108
+ * @return {Object} associated-domains dictionary content
109
+ */
110
+function generateAssociatedDomainsContent(pluginPreferences) {
111
+  var domainsList = [];
112
+
113
+  // generate list of host links
114
+  pluginPreferences.hosts.forEach(function(host) {
115
+    var link = domainsListEntryForHost(host);
116
+    if (domainsList.indexOf(link) == -1) {
117
+      domainsList.push(link);
118
+    }
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

+ 226
- 0
hooks/lib/ios/xcodePreferences.js View File

@@ -0,0 +1,226 @@
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
+var path = require('path');
10
+var compare = require('node-version-compare');
11
+var ConfigXmlHelper = require('../configXmlHelper.js');
12
+var IOS_DEPLOYMENT_TARGET = '8.0';
13
+var COMMENT_KEY = /_comment$/;
14
+var context;
15
+
16
+module.exports = {
17
+  enableAssociativeDomainsCapability: enableAssociativeDomainsCapability
18
+}
19
+
20
+// region Public API
21
+
22
+/**
23
+ * Activate associated domains capability for the application.
24
+ *
25
+ * @param {Object} cordovaContext - cordova context object
26
+ */
27
+function enableAssociativeDomainsCapability(cordovaContext) {
28
+  context = cordovaContext;
29
+
30
+  var projectFile = loadProjectFile();
31
+
32
+  // adjust preferences
33
+  activateAssociativeDomains(projectFile.xcode);
34
+
35
+  // add entitlements file to pbxfilereference
36
+  addPbxReference(projectFile.xcode);
37
+
38
+  // save changes
39
+  projectFile.write();
40
+}
41
+
42
+// endregion
43
+
44
+// region Alter project file preferences
45
+
46
+/**
47
+ * Activate associated domains support in the xcode project file:
48
+ * - set deployment target to ios 9;
49
+ * - add .entitlements file to Code Sign Entitlements preference.
50
+ *
51
+ * @param {Object} xcodeProject - xcode project preferences; all changes are made in that instance
52
+ */
53
+function activateAssociativeDomains(xcodeProject) {
54
+  var configurations = nonComments(xcodeProject.pbxXCBuildConfigurationSection());
55
+  var entitlementsFilePath = pathToEntitlementsFile();
56
+  var config;
57
+  var buildSettings;
58
+  var deploymentTargetIsUpdated;
59
+
60
+  for (config in configurations) {
61
+    buildSettings = configurations[config].buildSettings;
62
+    buildSettings['CODE_SIGN_ENTITLEMENTS'] = '"' + entitlementsFilePath + '"';
63
+
64
+    // if deployment target is less then the required one - increase it
65
+    if (buildSettings['IPHONEOS_DEPLOYMENT_TARGET']) {
66
+      if (compare(buildSettings['IPHONEOS_DEPLOYMENT_TARGET'], IOS_DEPLOYMENT_TARGET) == -1) {
67
+        buildSettings['IPHONEOS_DEPLOYMENT_TARGET'] = IOS_DEPLOYMENT_TARGET;
68
+        deploymentTargetIsUpdated = true;
69
+      }
70
+    } else {
71
+      buildSettings['IPHONEOS_DEPLOYMENT_TARGET'] = IOS_DEPLOYMENT_TARGET;
72
+      deploymentTargetIsUpdated = true;
73
+    }
74
+  }
75
+
76
+  if (deploymentTargetIsUpdated) {
77
+    console.log('IOS project now has deployment target set as: ' + IOS_DEPLOYMENT_TARGET);
78
+  }
79
+
80
+  console.log('IOS project Code Sign Entitlements now set to: ' + entitlementsFilePath);
81
+}
82
+
83
+// endregion
84
+
85
+// region PBXReference methods
86
+
87
+/**
88
+ * Add .entitlemets file into the project.
89
+ *
90
+ * @param {Object} xcodeProject - xcode project preferences; all changes are made in that instance
91
+ */
92
+function addPbxReference(xcodeProject) {
93
+  var fileReferenceSection = nonComments(xcodeProject.pbxFileReferenceSection());
94
+  var entitlementsFileName = path.basename(pathToEntitlementsFile());
95
+
96
+  if (isPbxReferenceAlreadySet(fileReferenceSection, entitlementsFileName)) {
97
+    console.log('Entitlements file is in reference section.');
98
+    return;
99
+  }
100
+
101
+  console.log('Entitlements file is not in references section, adding it');
102
+  xcodeProject.addResourceFile(entitlementsFileName);
103
+}
104
+
105
+/**
106
+ * Check if .entitlemets file reference already set.
107
+ *
108
+ * @param {Object} fileReferenceSection - PBXFileReference section
109
+ * @param {String} entitlementsRelativeFilePath - relative path to entitlements file
110
+ * @return true - if reference is set; otherwise - false
111
+ */
112
+function isPbxReferenceAlreadySet(fileReferenceSection, entitlementsRelativeFilePath) {
113
+  var isAlreadyInReferencesSection = false;
114
+  var uuid;
115
+  var fileRefEntry;
116
+
117
+  for (uuid in fileReferenceSection) {
118
+    fileRefEntry = fileReferenceSection[uuid];
119
+    if (fileRefEntry.path && fileRefEntry.path.indexOf(entitlementsRelativeFilePath) > -1) {
120
+      isAlreadyInReferencesSection = true;
121
+      break;
122
+    }
123
+  }
124
+
125
+  return isAlreadyInReferencesSection;
126
+}
127
+
128
+// region Xcode project file helpers
129
+
130
+/**
131
+ * Load iOS project file from platform specific folder.
132
+ *
133
+ * @return {Object} projectFile - project file information
134
+ */
135
+function loadProjectFile() {
136
+  var platform_ios;
137
+  var projectFile;
138
+
139
+  try {
140
+    // try pre-5.0 cordova structure
141
+    platform_ios = context.requireCordovaModule('cordova-lib/src/plugman/platforms')['ios'];
142
+    projectFile = platform_ios.parseProjectFile(iosPlatformPath());
143
+  } catch (e) {
144
+    try {
145
+      // let's try cordova 5.0 structure
146
+      platform_ios = context.requireCordovaModule('cordova-lib/src/plugman/platforms/ios');
147
+      projectFile = platform_ios.parseProjectFile(iosPlatformPath());
148
+    } catch (e) {
149
+      // Then cordova 7.0
150
+      var project_files = context.requireCordovaModule('glob').sync(path.join(iosPlatformPath(), '*.xcodeproj', 'project.pbxproj'));
151
+      
152
+      if (project_files.length === 0) {
153
+        throw new Error('does not appear to be an xcode project (no xcode project file)');
154
+      }
155
+      
156
+      var pbxPath = project_files[0];
157
+      
158
+      var xcodeproj = context.requireCordovaModule('xcode').project(pbxPath);
159
+      xcodeproj.parseSync();
160
+      
161
+      projectFile = {
162
+        'xcode': xcodeproj,
163
+        write: function () {
164
+          var fs = context.requireCordovaModule('fs');
165
+            
166
+          var frameworks_file = path.join(iosPlatformPath(), 'frameworks.json');
167
+          var frameworks = {};
168
+          try {
169
+            frameworks = context.requireCordovaModule(frameworks_file);
170
+          } catch (e) { }
171
+        
172
+          fs.writeFileSync(pbxPath, xcodeproj.writeSync());
173
+          if (Object.keys(frameworks).length === 0){
174
+            // If there is no framework references remain in the project, just remove this file
175
+            context.requireCordovaModule('shelljs').rm('-rf', frameworks_file);
176
+            return;
177
+          }
178
+          fs.writeFileSync(frameworks_file, JSON.stringify(this.frameworks, null, 4));
179
+        }
180
+      };
181
+    }
182
+  }
183
+
184
+  return projectFile;
185
+}
186
+
187
+/**
188
+ * Remove comments from the file.
189
+ *
190
+ * @param {Object} obj - file object
191
+ * @return {Object} file object without comments
192
+ */
193
+function nonComments(obj) {
194
+  var keys = Object.keys(obj);
195
+  var newObj = {};
196
+
197
+  for (var i = 0, len = keys.length; i < len; i++) {
198
+    if (!COMMENT_KEY.test(keys[i])) {
199
+      newObj[keys[i]] = obj[keys[i]];
200
+    }
201
+  }
202
+
203
+  return newObj;
204
+}
205
+
206
+// endregion
207
+
208
+// region Path helpers
209
+
210
+function iosPlatformPath() {
211
+  return path.join(projectRoot(), 'platforms', 'ios');
212
+}
213
+
214
+function projectRoot() {
215
+  return context.opts.projectRoot;
216
+}
217
+
218
+function pathToEntitlementsFile() {
219
+  var configXmlHelper = new ConfigXmlHelper(context),
220
+    projectName = configXmlHelper.getProjectName(),
221
+    fileName = projectName + '.entitlements';
222
+
223
+  return path.join(projectName, 'Resources', fileName);
224
+}
225
+
226
+// endregion

+ 57
- 0
hooks/lib/xmlHelper.js View File

@@ -0,0 +1,57 @@
1
+/*
2
+Small helper class to read/write from/to xml file.
3
+*/
4
+
5
+var fs = require('fs');
6
+var xml2js = require('xml2js');
7
+
8
+module.exports = {
9
+  readXmlAsJson: readXmlAsJson,
10
+  writeJsonAsXml: writeJsonAsXml
11
+};
12
+
13
+/**
14
+ * Read data from the xml file as JSON object.
15
+ *
16
+ * @param {String} filePath - absolute path to xml file
17
+ * @return {Object} JSON object with the contents of the xml file
18
+ */
19
+function readXmlAsJson(filePath) {
20
+  var xmlData;
21
+  var xmlParser;
22
+  var parsedData;
23
+
24
+  try {
25
+    xmlData = fs.readFileSync(filePath, 'utf8');
26
+    xmlParser = new xml2js.Parser();
27
+    xmlParser.parseString(xmlData, function(err, data) {
28
+      if (data) {
29
+        parsedData = data;
30
+      }
31
+    });
32
+  } catch (err) {}
33
+
34
+  return parsedData;
35
+}
36
+
37
+/**
38
+ * Write JSON object as xml into the specified file.
39
+ *
40
+ * @param {Object} jsData - JSON object to write
41
+ * @param {String} filePath - path to the xml file where data should be saved
42
+ * @return {boolean} true - if data saved to file; false - otherwise
43
+ */
44
+function writeJsonAsXml(jsData, filePath, options) {
45
+  var xmlBuilder = new xml2js.Builder(options);
46
+  var changedXmlData = xmlBuilder.buildObject(jsData);
47
+  var isSaved = true;
48
+
49
+  try {
50
+    fs.writeFileSync(filePath, changedXmlData, 'utf8');
51
+  } catch (err) {
52
+    console.log(err);
53
+    isSaved = false;
54
+  }
55
+
56
+  return isSaved;
57
+}

+ 51
- 0
package.json View File

@@ -0,0 +1,51 @@
1
+{
2
+  "name": "cordova-mirtech-plugin-universal-links",
3
+  "version": "1.2.1",
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-mirtech-plugin-universal-links",
7
+    "platforms": [
8
+      "ios",
9
+      "android"
10
+    ]
11
+  },
12
+  "repository": {
13
+    "type": "git",
14
+    "url": "git+https://github.com/martindrapeau/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/martindrapeau/cordova-universal-links-plugin/issues"
49
+  },
50
+  "homepage": "https://github.com/martindrapeau/cordova-universal-links-plugin#readme"
51
+}

+ 106
- 0
plugin.xml View File

@@ -0,0 +1,106 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+
3
+<plugin id="cordova-mirtech-plugin-universal-links" version="1.2.1" xmlns="http://apache.org/cordova/ns/plugins/1.0">
4
+
5
+  <name>Universal Links Plugin</name>
6
+  <description>
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
+  </description>
10
+  <license>MIT</license>
11
+  <keywords>cordova,links,universal</keywords>
12
+
13
+  <repo>https://github.com/martindrapeau/cordova-universal-links-plugin</repo>
14
+  <issue>https://github.com/martindrapeau/cordova-universal-links-plugin/issues</issue>
15
+
16
+  <engines>
17
+    <engine name="cordova-ios" version=">=3.8"></engine>
18
+    <engine name="cordova-android" version=">=4"></engine>
19
+  </engines>
20
+
21
+<!-- JavaScrip Library Sources -->
22
+  <js-module name="universalLinks" src="www/universal_links.js">
23
+    <clobbers target="universalLinks"/>
24
+  </js-module>
25
+
26
+<!-- Hooks -->
27
+  <hook src="hooks/afterPrepareHook.js" type="after_prepare"/>
28
+  <hook src="hooks/beforePluginInstallHook.js" type="before_plugin_install" />
29
+
30
+  <platform name="ios">
31
+    <hook src="hooks/iosBeforePrepareHook.js" type="before_prepare" />
32
+
33
+<!-- Plugin inclusion in Cordova config.xml  -->
34
+    <config-file parent="/*" target="config.xml">
35
+      <feature name="UniversalLinks">
36
+        <param name="ios-package" value="CULPlugin"/>
37
+        <param name="onload" value="true"/>
38
+      </feature>
39
+    </config-file>
40
+
41
+<!-- Objective-C Sources -->
42
+
43
+    <source-file src="src/ios/CULPlugin.m"/>
44
+    <header-file src="src/ios/CULPlugin.h"/>
45
+
46
+    <source-file src="src/ios/AppDelegate+CULPlugin.m"/>
47
+    <header-file src="src/ios/AppDelegate+CULPlugin.h"/>
48
+
49
+<!-- sources for JS folder -->
50
+    <source-file src="src/ios/JS/CDVPluginResult+CULPlugin.m" target-dir="JS/"/>
51
+    <header-file src="src/ios/JS/CDVPluginResult+CULPlugin.h" target-dir="JS/"/>
52
+
53
+    <source-file src="src/ios/JS/CDVInvokedUrlCommand+CULPlugin.m" target-dir="JS/"/>
54
+    <header-file src="src/ios/JS/CDVInvokedUrlCommand+CULPlugin.h" target-dir="JS/"/>
55
+
56
+<!-- sources for Model folder -->
57
+    <source-file src="src/ios/Model/CULHost.m" target-dir="Model/"/>
58
+    <header-file src="src/ios/Model/CULHost.h" target-dir="Model/"/>
59
+
60
+    <source-file src="src/ios/Model/CULPath.m" target-dir="Model/"/>
61
+    <header-file src="src/ios/Model/CULPath.h" target-dir="Model/"/>
62
+
63
+<!-- sources for XML Parser folder -->
64
+    <source-file src="src/ios/Parser/XML/CULXmlTags.m" target-dir="Parser/XML/"/>
65
+    <header-file src="src/ios/Parser/XML/CULXmlTags.h" target-dir="Parser/XML/"/>
66
+
67
+    <source-file src="src/ios/Parser/XML/CULConfigXmlParser.m" target-dir="Parser/XML/"/>
68
+    <header-file src="src/ios/Parser/XML/CULConfigXmlParser.h" target-dir="Parser/XML/"/>
69
+
70
+<!-- sources for JSON Parser folder -->
71
+    <source-file src="src/ios/Parser/JSON/CULConfigJsonParser.m" target-dir="Parser/JSON/"/>
72
+    <header-file src="src/ios/Parser/JSON/CULConfigJsonParser.h" target-dir="Parser/JSON/"/>
73
+
74
+<!-- sources for Utils folder -->
75
+    <source-file src="src/ios/Utils/NSBundle+CULPlugin.m" target-dir="Utils/"/>
76
+    <header-file src="src/ios/Utils/NSBundle+CULPlugin.h" target-dir="Utils/"/>
77
+
78
+  </platform>
79
+
80
+  <platform name="android">
81
+<!-- Plugin inclusion in Cordova config.xml -->
82
+    <config-file parent="/*" target="res/xml/config.xml">
83
+      <feature name="UniversalLinks">
84
+        <param name="android-package" value="com.nordnetab.cordova.ul.UniversalLinksPlugin"/>
85
+        <param name="onload" value="true"/>
86
+      </feature>
87
+    </config-file>
88
+
89
+<!-- Java Library Sources -->
90
+<!-- sources for package: com.nordnetab.cordova.ul.js -->
91
+    <source-file src="src/android/com/nordnetab/cordova/ul/js/JSAction.java" target-dir="src/com/nordnetab/cordova/ul/js/"/>
92
+
93
+<!-- sources for package: com.nordnetab.cordova.ul.model -->
94
+    <source-file src="src/android/com/nordnetab/cordova/ul/model/JSMessage.java" target-dir="src/com/nordnetab/cordova/ul/model/"/>
95
+    <source-file src="src/android/com/nordnetab/cordova/ul/model/ULHost.java" target-dir="src/com/nordnetab/cordova/ul/model/"/>
96
+    <source-file src="src/android/com/nordnetab/cordova/ul/model/ULPath.java" target-dir="src/com/nordnetab/cordova/ul/model/"/>
97
+
98
+<!-- sources for package: com.nordnetab.cordova.ul.parser -->
99
+    <source-file src="src/android/com/nordnetab/cordova/ul/parser/ULConfigXmlParser.java" target-dir="src/com/nordnetab/cordova/ul/parser/"/>
100
+    <source-file src="src/android/com/nordnetab/cordova/ul/parser/XmlTags.java" target-dir="src/com/nordnetab/cordova/ul/parser/"/>
101
+
102
+<!-- sources for package: com.nordnetab.cordova.ul -->
103
+    <source-file src="src/android/com/nordnetab/cordova/ul/UniversalLinksPlugin.java" target-dir="src/com/nordnetab/cordova/ul/"/>
104
+
105
+  </platform>
106
+</plugin>

+ 221
- 0
src/android/com/nordnetab/cordova/ul/UniversalLinksPlugin.java View File

@@ -0,0 +1,221 @@
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
+ * <p/>
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<ULHost> supportedHosts;
36
+
37
+    // list of subscribers
38
+    private Map<String, CallbackContext> 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<String, CallbackContext>();
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; <code>null</code> 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<Map.Entry<String, CallbackContext>> subscribersSet = subscribers.entrySet();
142
+        for (Map.Entry<String, CallbackContext> 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; <code>null</code> - if none were found
205
+     */
206
+    private ULHost findHostByUrl(Uri url) {
207
+        ULHost host = null;
208
+        final String launchHost = url.getHost().toLowerCase();
209
+        for (ULHost supportedHost : supportedHosts) {
210
+            if (supportedHost.getName().equals(launchHost) ||
211
+                    supportedHost.getName().startsWith("*.") && launchHost.endsWith(supportedHost.getName().substring(1))) {
212
+                host = supportedHost;
213
+                break;
214
+            }
215
+        }
216
+
217
+        return host;
218
+    }
219
+
220
+    // endregion
221
+}

+ 19
- 0
src/android/com/nordnetab/cordova/ul/js/JSAction.java View File

@@ -0,0 +1,19 @@
1
+package com.nordnetab.cordova.ul.js;
2
+
3
+/**
4
+ * Created by Nikolay Demyankov on 09.09.15.
5
+ * <p/>
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
+}

+ 193
- 0
src/android/com/nordnetab/cordova/ul/model/JSMessage.java View File

@@ -0,0 +1,193 @@
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
+ * <p/>
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().toLowerCase();
111
+        final List<ULPath> 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<String> 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
+}

+ 85
- 0
src/android/com/nordnetab/cordova/ul/model/ULHost.java View File

@@ -0,0 +1,85 @@
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
+ * <p/>
9
+ * Model for <host /> 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<ULPath> 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.toLowerCase();
33
+        this.scheme = (scheme == null) ? DEFAULT_SCHEME : scheme;
34
+        this.event = (event == null) ? DEFAULT_EVENT : event;
35
+        this.paths = new ArrayList<ULPath>();
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<ULPath> 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
+}

+ 43
- 0
src/android/com/nordnetab/cordova/ul/model/ULPath.java View File

@@ -0,0 +1,43 @@
1
+package com.nordnetab.cordova.ul.model;
2
+
3
+/**
4
+ * Created by Nikolay Demyankov on 09.09.15.
5
+ * <p/>
6
+ * Model for <path /> 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("*", "(.*)").toLowerCase();
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
+}

+ 156
- 0
src/android/com/nordnetab/cordova/ul/parser/ULConfigXmlParser.java View File

@@ -0,0 +1,156 @@
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
+ * <p/>
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<ULHost> 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<ULHost> 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 <host />
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 <path />
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<ULHost>();
149
+        isInsideMainTag = false;
150
+        didParseMainBlock = false;
151
+        isInsideHostBlock = false;
152
+        processedHost = null;
153
+    }
154
+
155
+    // endregion
156
+}

+ 49
- 0
src/android/com/nordnetab/cordova/ul/parser/XmlTags.java View File

@@ -0,0 +1,49 @@
1
+package com.nordnetab.cordova.ul.parser;
2
+
3
+/**
4
+ * Created by Nikolay Demyankov on 10.09.15.
5
+ * <p/>
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
+}

+ 17
- 0
src/ios/AppDelegate+CULPlugin.h View File

@@ -0,0 +1,17 @@
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 *))restorationHandler;
16
+
17
+@end

+ 32
- 0
src/ios/AppDelegate+CULPlugin.m View File

@@ -0,0 +1,32 @@
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 continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler {
18
+    // ignore activities that are not for Universal Links
19
+    if (![userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb] || userActivity.webpageURL == nil) {
20
+        return NO;
21
+    }
22
+    
23
+    // get instance of the plugin and let it handle the userActivity object
24
+    CULPlugin *plugin = [self.viewController getCommandInstance:PLUGIN_NAME];
25
+    if (plugin == nil) {
26
+        return NO;
27
+    }
28
+    
29
+    return [plugin handleUserActivity:userActivity];
30
+}
31
+
32
+@end

+ 38
- 0
src/ios/CULPlugin.h View File

@@ -0,0 +1,38 @@
1
+//
2
+//  CULPlugin.h
3
+//
4
+//  Created by Nikolay Demyankov on 14.09.15.
5
+//
6
+
7
+#import <Foundation/Foundation.h>
8
+#import <Cordova/CDVPlugin.h>
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 <code>true</code> - if this is a universal link and it is defined in config.xml; otherwise - <code>false</code>
35
+ */
36
+- (BOOL)handleUserActivity:(NSUserActivity *)userActivity;
37
+
38
+@end

+ 176
- 0
src/ios/CULPlugin.m View File

@@ -0,0 +1,176 @@
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
+#import "CULConfigJsonParser.h"
14
+
15
+@interface CULPlugin() {
16
+    NSArray *_supportedHosts;
17
+    CDVPluginResult *_storedEvent;
18
+    NSMutableDictionary<NSString *, NSString *> *_subscribers;
19
+}
20
+
21
+@end
22
+
23
+@implementation CULPlugin
24
+
25
+#pragma mark Public API
26
+
27
+- (void)pluginInitialize {
28
+    [self localInit];
29
+    // Can be used for testing.
30
+    // Just uncomment, close the app and reopen it. That will simulate application launch from the link.
31
+//    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResume:) name:UIApplicationWillEnterForegroundNotification object:nil];
32
+}
33
+
34
+//- (void)onResume:(NSNotification *)notification {
35
+//    NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];
36
+//    [activity setWebpageURL:[NSURL URLWithString:@"http://site2.com/news/page?q=1&v=2#myhash"]];
37
+//    
38
+//    [self handleUserActivity:activity];
39
+//}
40
+
41
+- (void)handleOpenURL:(NSNotification*)notification {
42
+    id url = notification.object;
43
+    if (![url isKindOfClass:[NSURL class]]) {
44
+        return;
45
+    }
46
+    
47
+    CULHost *host = [self findHostByURL:url];
48
+    if (host) {
49
+        [self storeEventWithHost:host originalURL:url];
50
+    }
51
+}
52
+
53
+- (BOOL)handleUserActivity:(NSUserActivity *)userActivity {
54
+    [self localInit];
55
+    
56
+    NSURL *launchURL = userActivity.webpageURL;
57
+    CULHost *host = [self findHostByURL:launchURL];
58
+    if (host == nil) {
59
+        return NO;
60
+    }
61
+    
62
+    [self storeEventWithHost:host originalURL:launchURL];
63
+    
64
+    return YES;
65
+}
66
+
67
+- (void)onAppTerminate {
68
+    _supportedHosts = nil;
69
+    _subscribers = nil;
70
+    _storedEvent = nil;
71
+    
72
+    [super onAppTerminate];
73
+}
74
+
75
+#pragma mark Private API
76
+
77
+- (void)localInit {
78
+    if (_supportedHosts) {
79
+        return;
80
+    }
81
+    
82
+    _subscribers = [[NSMutableDictionary alloc] init];
83
+    
84
+    // Get supported hosts from the config.xml or www/ul.json.
85
+    // For now priority goes to json config.
86
+    _supportedHosts = [self getSupportedHostsFromPreferences];
87
+}
88
+
89
+- (NSArray<CULHost *> *)getSupportedHostsFromPreferences {
90
+    NSString *jsonConfigPath = [[NSBundle mainBundle] pathForResource:@"ul" ofType:@"json" inDirectory:@"www"];
91
+    if (jsonConfigPath) {
92
+        return [CULConfigJsonParser parseConfig:jsonConfigPath];
93
+    }
94
+    
95
+    return [CULConfigXmlParser parse];
96
+}
97
+
98
+/**
99
+ *  Store event data for future use.
100
+ *  If we are resuming the app - try to consume it.
101
+ *
102
+ *  @param host        host that matches the launch url
103
+ *  @param originalUrl launch url
104
+ */
105
+- (void)storeEventWithHost:(CULHost *)host originalURL:(NSURL *)originalUrl {
106
+    _storedEvent = [CDVPluginResult resultWithHost:host originalURL:originalUrl];
107
+    [self tryToConsumeEvent];
108
+}
109
+
110
+/**
111
+ *  Find host entry that corresponds to launch url.
112
+ *
113
+ *  @param  launchURL url that launched the app
114
+ *  @return host entry; <code>nil</code> if none is found
115
+ */
116
+- (CULHost *)findHostByURL:(NSURL *)launchURL {
117
+    NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:launchURL resolvingAgainstBaseURL:YES];
118
+    CULHost *host = nil;
119
+    for (CULHost *supportedHost in _supportedHosts) {
120
+        NSPredicate *pred = [NSPredicate predicateWithFormat:@"self LIKE[c] %@", supportedHost.name];
121
+        if ([pred evaluateWithObject:urlComponents.host]) {
122
+            host = supportedHost;
123
+            break;
124
+        }
125
+    }
126
+    
127
+    return host;
128
+}
129
+
130
+#pragma mark Methods to send data to JavaScript
131
+
132
+/**
133
+ *  Try to send event to the web page.
134
+ *  If there is a subscriber for the event - it will be consumed. 
135
+ *  If not - it will stay until someone subscribes to it.
136
+ */
137
+- (void)tryToConsumeEvent {
138
+    if (_subscribers.count == 0 || _storedEvent == nil) {
139
+        return;
140
+    }
141
+    
142
+    NSString *storedEventName = [_storedEvent eventName];
143
+    for (NSString *eventName in _subscribers) {
144
+        if ([storedEventName isEqualToString:eventName]) {
145
+            NSString *callbackID = _subscribers[eventName];
146
+            [self.commandDelegate sendPluginResult:_storedEvent callbackId:callbackID];
147
+            _storedEvent = nil;
148
+            break;
149
+        }
150
+    }
151
+}
152
+
153
+#pragma mark Methods, available from JavaScript side
154
+
155
+- (void)jsSubscribeForEvent:(CDVInvokedUrlCommand *)command {
156
+    NSString *eventName = [command eventName];
157
+    if (eventName.length == 0) {
158
+        return;
159
+    }
160
+    
161
+    _subscribers[eventName] = command.callbackId;
162
+    [self tryToConsumeEvent];
163
+}
164
+
165
+- (void)jsUnsubscribeFromEvent:(CDVInvokedUrlCommand *)command {
166
+    NSString *eventName = [command eventName];
167
+    if (eventName.length == 0) {
168
+        return;
169
+    }
170
+    
171
+    [_subscribers removeObjectForKey:eventName];
172
+}
173
+
174
+
175
+
176
+@end

+ 21
- 0
src/ios/JS/CDVInvokedUrlCommand+CULPlugin.h View File

@@ -0,0 +1,21 @@
1
+//
2
+//  CDVInvokedUrlCommand+CULPlugin.h
3
+//
4
+//  Created by Nikolay Demyankov on 08.12.15.
5
+//
6
+
7
+#import <Cordova/CDVPlugin.h>
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

+ 19
- 0
src/ios/JS/CDVInvokedUrlCommand+CULPlugin.m View File

@@ -0,0 +1,19 @@
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

+ 29
- 0
src/ios/JS/CDVPluginResult+CULPlugin.h View File

@@ -0,0 +1,29 @@
1
+//
2
+//  CDVPluginResult+CULPlugin.h
3
+//
4
+//  Created by Nikolay Demyankov on 15.09.15.
5
+//
6
+
7
+#import <Cordova/CDVPlugin.h>
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

+ 157
- 0
src/ios/JS/CDVPluginResult+CULPlugin.m View File

@@ -0,0 +1,157 @@
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<CULPath *> *hostPaths = host.paths;
106
+    NSString *originalPath = urlComponents.path.lowercaseString;
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<NSURLQueryItem *> *queryItems = originalURLComponents.queryItems;
147
+    NSMutableDictionary<NSString *, NSString *> *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

+ 63
- 0
src/ios/Model/CULHost.h View File

@@ -0,0 +1,63 @@
1
+//
2
+//  CULHost.h
3
+//
4
+//  Created by Nikolay Demyankov on 15.09.15.
5
+//
6
+
7
+#import <Foundation/Foundation.h>
8
+#import "CULPath.h"
9
+
10
+/**
11
+ *  Model for <host /> 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<CULPath *> *paths;
37
+
38
+/**
39
+ *  Constructor.
40
+ *
41
+ *  @param name   host name
42
+ *  @param scheme scheme; if <code>nil</code> - http will be used
43
+ *  @param event  event name; if <code>nil</code> - 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
+/**
57
+ *  Add list of paths.
58
+ * 
59
+ *  @param paths paths to add
60
+ */
61
+- (void)addAllPaths:(NSArray<CULPath *> *)paths;
62
+
63
+@end

+ 50
- 0
src/ios/Model/CULHost.m View File

@@ -0,0 +1,50 @@
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<CULPath *> *_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.lowercaseString;
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
+- (void)addAllPaths:(NSArray<CULPath *> *)paths {
41
+    if (paths) {
42
+        [_paths addObjectsFromArray:paths];
43
+    }
44
+}
45
+
46
+- (NSArray<CULPath *> *)paths {
47
+    return _paths;
48
+}
49
+
50
+@end

+ 36
- 0
src/ios/Model/CULPath.h View File

@@ -0,0 +1,36 @@
1
+//
2
+//  CULPath.h
3
+//
4
+//  Created by Nikolay Demyankov on 15.09.15.
5
+//
6
+
7
+#import <Foundation/Foundation.h>
8
+
9
+/**
10
+ *  Model for <path /> 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

+ 21
- 0
src/ios/Model/CULPath.m View File

@@ -0,0 +1,21 @@
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:@".*"].lowercaseString;
15
+        _event = event;
16
+    }
17
+    
18
+    return self;
19
+}
20
+
21
+@end

+ 22
- 0
src/ios/Parser/JSON/CULConfigJsonParser.h View File

@@ -0,0 +1,22 @@
1
+//
2
+//  CULConfigJsonParser.h
3
+//
4
+//  Created by Nikolay Demyankov on 29.01.17.
5
+//
6
+
7
+#import <Foundation/Foundation.h>
8
+#import "CULHost.h"
9
+
10
+/**
11
+ *  JSON parser for plugin's preferences.
12
+ */
13
+@interface CULConfigJsonParser : NSObject
14
+
15
+/**
16
+ *  Parse JSON config.
17
+ *
18
+ *  @return list of hosts, defined in the config file
19
+ */
20
++ (NSArray<CULHost *> *)parseConfig:(NSString *)pathToJsonConfig;
21
+
22
+@end

+ 60
- 0
src/ios/Parser/JSON/CULConfigJsonParser.m View File

@@ -0,0 +1,60 @@
1
+//
2
+//  CULConfigJsonParser.m
3
+//
4
+//  Created by Nikolay Demyankov on 29.01.17.
5
+//
6
+
7
+#import "CULConfigJsonParser.h"
8
+
9
+@implementation CULConfigJsonParser
10
+
11
++ (NSArray<CULHost *> *)parseConfig:(NSString *)pathToJsonConfig {
12
+    NSData *ulData = [NSData dataWithContentsOfFile:pathToJsonConfig];
13
+    if (!ulData) {
14
+        return nil;
15
+    }
16
+    
17
+    NSError *error = nil;
18
+    NSArray *jsonObject = [NSJSONSerialization JSONObjectWithData:ulData options:kNilOptions error:&error];
19
+    if (error) {
20
+        return nil;
21
+    }
22
+    
23
+    NSMutableArray<CULHost *> *preferences = [[NSMutableArray alloc] init];
24
+    for (NSDictionary *jsonEntry in jsonObject) {
25
+        CULHost *host = [[CULHost alloc] initWithHostName:jsonEntry[@"host"]
26
+                                                   scheme:jsonEntry[@"scheme"]
27
+                                                    event:jsonEntry[@"event"]];
28
+        NSArray<CULPath *> *paths = [self parsePathsFromJson:jsonEntry[@"path"] forHost:host];
29
+        [host addAllPaths:paths];
30
+        
31
+        [preferences addObject:host];
32
+    }
33
+    
34
+    return preferences;
35
+}
36
+
37
++ (NSArray<CULPath *> *)parsePathsFromJson:(NSArray *)jsonArray forHost:(CULHost *)host {
38
+    if (!jsonArray || !jsonArray.count) {
39
+        return nil;
40
+    }
41
+    
42
+    NSMutableArray<CULPath *> *paths = [[NSMutableArray alloc] initWithCapacity:jsonArray.count];
43
+    for (NSDictionary *entry in jsonArray) {
44
+        NSString *urlPath = entry[@"url"];
45
+        NSString *pathEvent = entry[@"event"] ? entry[@"event"] : host.event;
46
+        
47
+        // ignore '*' paths; we don't need them here
48
+        if ([urlPath isEqualToString:@"*"] || [urlPath isEqualToString:@".*"]) {
49
+            continue;
50
+        }
51
+        
52
+        // create path entry
53
+        CULPath *path = [[CULPath alloc] initWithUrlPath:urlPath andEvent:pathEvent];
54
+        [paths addObject:path];
55
+    }
56
+    
57
+    return paths;
58
+}
59
+
60
+@end

+ 22
- 0
src/ios/Parser/XML/CULConfigXmlParser.h View File

@@ -0,0 +1,22 @@
1
+//
2
+//  CULConfigXmlParser.h
3
+//
4
+//  Created by Nikolay Demyankov on 15.09.15.
5
+//
6
+
7
+#import <Foundation/Foundation.h>
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<CULHost *> *)parse;
21
+
22
+@end

+ 123
- 0
src/ios/Parser/XML/CULConfigXmlParser.m View File

@@ -0,0 +1,123 @@
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() <NSXMLParserDelegate> {
13
+    NSMutableArray<CULHost *> *_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<CULHost *> *)parse {
27
+    CULConfigXmlParser *parser = [[CULConfigXmlParser alloc] init];
28
+    
29
+    return [parser parseConfig];
30
+}
31
+
32
+- (NSArray<CULHost *> *)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<NSString *,NSString *> *)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<NSString *, NSString *> *)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<NSString *, NSString *> *)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

+ 54
- 0
src/ios/Parser/XML/CULXmlTags.h View File

@@ -0,0 +1,54 @@
1
+//
2
+//  CULXmlTags.h
3
+//
4
+//  Created by Nikolay Demyankov on 15.09.15.
5
+//
6
+
7
+#import <Foundation/Foundation.h>
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

+ 22
- 0
src/ios/Parser/XML/CULXmlTags.m View File

@@ -0,0 +1,22 @@
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

+ 22
- 0
src/ios/Utils/NSBundle+CULPlugin.h View File

@@ -0,0 +1,22 @@
1
+//
2
+//  NSBundle+CULPlugin.h
3
+//
4
+//  Created by Nikolay Demyankov on 15.09.15.
5
+//
6
+
7
+#import <Foundation/Foundation.h>
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

+ 15
- 0
src/ios/Utils/NSBundle+CULPlugin.m View File

@@ -0,0 +1,15 @@
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

+ 23
- 0
ul_web_hooks/android_web_hook_tpl.html View File

@@ -0,0 +1,23 @@
1
+<!--
2
+Besides activating Deep Links in the application - you need to enable indexing for your web pages.
3
+For that you should add <link /> tags in the <head> section of the web pages that you specified in config.xml.
4
+Doing so, you declare that this page should be openned in the mobile application instead of the browser.
5
+
6
+Link tag is look like this:
7
+  <link rel="alternate" href="android-app://<package_name>/<scheme>/<host>/<path>" />
8
+
9
+More documentation could be found here:
10
+https://developer.android.com/training/app-indexing/enabling-app-indexing.html
11
+
12
+For convenience, plugin generates the list of <link /> tags based on what you specified in the config.xml.
13
+You can just copy-paste them in the <header> section of the appropriate pages of your website.
14
+
15
+-->
16
+
17
+<head>
18
+[__LINKS__]
19
+<!-- Your other stuff from the head tag -->
20
+</head>
21
+<body>
22
+<!-- Your page content -->
23
+</body>

+ 56
- 0
www/universal_links.js View File

@@ -0,0 +1,56 @@
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;

Loading…
Cancel
Save