@thathoff/cordova-plugin-universal-links 0.3.0

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