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
+/**