<|start_filename|>web-branch-deep-linking:/karma.conf.js<|end_filename|>
module.exports = function(config) {
	config.set({
		browserStack: {
			username: BROWSER_STACK_USERNAME,
			accessKey: BROWSER_STACK_ACCESS_KEY
		}

		customLaunchers: {
			bs_firefox_mac: {
				base: 'BrowserStack',
				browser: 'firefox',
				browser_version: '21.0',
				os: 'OS X',
				os_version: 'Mountain Lion'
			},
			bs_iphone5: {
				base: 'BrowserStack',
				device: 'iPhone 5',
				os: 'ios',
				os_version: '6.0'
			}
		},

		browsers: ['bs_firefox_mac', 'bs_iphone5']
	});
};


<|start_filename|>web-branch-deep-linking:/src/0_jsonparse.js<|end_filename|>
/**
 * The default goog.json.parse uses eval. We don't want that.
 */
'use strict';

goog.provide('safejson');

/*jshint unused:false*/
goog.require('goog.json');

safejson.parse = function(sJSON) {
	sJSON = String(sJSON);
	try {
		return JSON.parse(sJSON);
	}
	catch (e) {

	}

	throw Error("Invalid JSON string: " + sJSON);
};

safejson.stringify = function(objJSON) {
	try {
		return (typeof JSON === 'object' && typeof JSON.stringify === 'function') ? JSON.stringify(objJSON) : goog.json.serialize(objJSON);
	}
	catch (e) {

	}

	throw Error("Could not stringify object");
};



<|start_filename|>web-branch-deep-linking:/test/mocha.css<|end_filename|>
#mocha-stats {
  position: static;
  font-size: 36px;
}


<|start_filename|>web-branch-deep-linking:/test/journeys.js<|end_filename|>
'use strict';

goog.require('utils');
goog.require('Branch');
goog.require('resources');
goog.require('config');
goog.require('storage');
goog.require('session');
goog.require('branch_view');
goog.require('banner_utils');

goog.require('goog.json'); // jshint unused:false

/*globals branch_sample_key, session_id, identity_id, browser_fingerprint_id, BranchStorage */

describe('Branch', function() {
	var storage = new BranchStorage([ 'pojo' ]);
	var sandbox;
	var requests;

	window.sdk_version = 'web' + config.version;

	beforeEach(function() {
		testUtils.go('');
		sandbox = sinon.sandbox.create();
		localStorage.clear();
		sessionStorage.clear();
		requests = [];
	});

	function initBranch(runInit, keepStorage) {
		[ document.getElementById('branch-banner-iframe'), document.getElementById('branch-banner') ].forEach(function(el) {
			el && el.parentNode && el.parentNode.removeChild(el);
		});

		if (!keepStorage) {
			storage.clear();
		}

		sandbox.stub(utils, 'mobileUserAgent', function() {
			return 'ios';
		});

		var branch = new Branch();

		sandbox.stub(branch._server, 'request', function(resource, obj, storage, callback) {
			requests.push({
				resource: resource,
				obj: obj,
				callback: callback
			});
		});

		if (runInit) {
			branch.init(branch_sample_key);
			requests[0].callback(null, browser_fingerprint_id);
			requests[1].callback(
				null,
				{
					browser_fingerprint_id: browser_fingerprint_id,
					identity_id: identity_id,
					session_id: session_id
				}
			);
			requests[2].callback(null, {});
			requests = [];
		}

		return branch;
	}

	function basicTests(call, params) {
		it('should fail if branch not initialized', function(done) {
			var branch = initBranch(false);
			var assert = testUtils.plan(params.length * 2, done);

			function basicTest(param) {
				var p = testUtils.nulls(param);
				branch[call].apply(branch, p.concat(function(err) {
					assert.strictEqual(err.message, 'Branch SDK not initialized');
				}));
				assert.throws(function() {
					branch[call].apply(branch, p);
				}, 'Branch SDK not initialized');
			}

			for (var i = 0; i < params.length; i++) {
				basicTest(params[i]);
			}
		});
	}

	afterEach(function() {
		sandbox.restore();
	});

	describe('journeys', function() {

		it('should attempt to pass deeplink data in a banner call', function(done) {
			var branch = initBranch(false);
			var assert = testUtils.plan(3, done);

			var bannerDeeplinkData = {
				tags: [ 'custom' ],
				data: {
					mydata: 'From Banner',
					foo: 'bar',
					'$deeplink_path': 'open/item/5678'
				}
			}

			branch.init(branch_sample_key);
			branch.banner(
				{
					immediate: true,
					disableHide: true,
					forgetHide: true
				},
				bannerDeeplinkData
			);

			// _r
			requests[0].callback(null, browser_fingerprint_id);

			// v1/open
			requests[1].callback(
				null,
				{
					browser_fingerprint_id: browser_fingerprint_id,
					identity_id: identity_id,
					session_id: session_id
				}
			);

			// v1/event
			requests[2].callback(null, {
				branch_view_enabled: false
			});

			// v1/deepview
			requests[3].callback(null, {
				branch_view_enabled: false
			});

			assert.strictEqual(requests[3].resource.endpoint, '/v1/deepview', 'calling deepview');
			assert.strictEqual(JSON.parse(requests[3].obj.data)['mydata'], 'From Banner', 'deep link data was passed by banner');

			assert.strictEqual(requests.length, 4, '4 requests made');
		});

		it('should attempt to pass deeplink data to a journey in a page view event', function(done) {
			var branch = initBranch(false);
			var assert = testUtils.plan(4, done);

			var bannerDeeplinkData = {
				tags: [ 'custom' ],
				data: {
					mydata: 'From Banner',
					foo: 'bar',
					'$deeplink_path': 'open/item/5678'
				}
			}

			sandbox.stub(branch_view, 'handleBranchViewData', function(server, branchViewData, data) {
				assert.isDefined(data, 'user data has been defined');
				assert.strictEqual(true, banner_utils.shouldAppend(storage, {
					forgetHide: true,
					showiOS: true
				}), 'branch view should be displayable');
				assert.strictEqual(data.data.mydata, 'From Banner', 'deep link data was passed by banner');
			});

			branch.init(branch_sample_key);
			branch.banner(
				{
					immediate: true,
					disableHide: true,
					forgetHide: true
				},
				bannerDeeplinkData
			);

			branch.track(
				'tester-event', {
					'apple': 'seed'
				}, function(err) {
			});

			// _r
			requests[0].callback(null, browser_fingerprint_id);

			// v1/open
			requests[1].callback(
				null,
				{
					browser_fingerprint_id: browser_fingerprint_id,
					identity_id: identity_id,
					session_id: session_id
				}
			);

			// v1/event (first time)
			setTimeout(function() {
				requests[2].callback(null, {
					branch_view_enabled: true,
					branch_view_data: {
						id: '345',
						number_of_use: 1000,
						// url: 'https://api.branch.io/v1/branchview/key_live_feebgAAhbH9Tv85H5wLQhpdaefiZv5Dv/279760304565736467?v=1'
						url: 'http://localhost:8000'
					}
				});

				// v1/event (second time)
				requests[3].callback(null, {
					branch_view_enabled: true
				});

				assert.strictEqual(requests.length, 5, '5 requests made');
			}, 10);
		});

		it('should attempt to pass deeplink data to a journey in a custom event', function(done) {
			var branch = initBranch(false);
			var assert = testUtils.plan(4, done);

			var bannerDeeplinkData = {
				tags: [ 'custom' ],
				data: {
					mydata: 'From Banner',
					foo: 'bar',
					'$deeplink_path': 'open/item/5678'
				}
			}

			sandbox.stub(branch_view, 'handleBranchViewData', function(server, branchViewData, data) {
				assert.isDefined(data, 'user data has been defined');
				var test = banner_utils.shouldAppend(storage, {
					forgetHide: true,
					showiOS: true
				});
				assert.strictEqual(false, banner_utils.shouldAppend(storage, {
					forgetHide: true,
					showiOS: true
				}), 'branch view should not be displayable');
				assert.strictEqual(data.data.mydata, 'From Banner', 'deep link data was passed by banner');
			});

			branch.init(branch_sample_key);
			branch.banner(
				{
					immediate: true,
					disableHide: true,
					forgetHide: true
				},
				bannerDeeplinkData
			);

			branch.track(
				'tester-event', {
					'apple': 'seed'
				}, function(err) {
			});

			// _r
			requests[0].callback(null, browser_fingerprint_id);

			// v1/open
			requests[1].callback(
				null,
				{
					browser_fingerprint_id: browser_fingerprint_id,
					identity_id: identity_id,
					session_id: session_id
				}
			);

			// v1/event (first time)
			requests[2].callback(null, {
				branch_view_enabled: true
			});

			setTimeout(function() {
				// v1/event (second time)
				requests[3].callback(null, {
					branch_view_enabled: true,
					branch_view_data: {
						id: '345',
						number_of_use: 1000,
						// url: 'https://api.branch.io/v1/branchview/key_live_feebgAAhbH9Tv85H5wLQhpdaefiZv5Dv/279760304565736467?v=1'
						url: 'http://localhost:8000'
					}
				});

				assert.strictEqual(requests.length, 5, '5 requests made');
			}, 10);
		});

		it('should attempt to pass deeplink data in a banner call from init callback', function(done) {
			// An existing user with a branch.banner() call during the callback passed into branch.init(),
			// where a Journey view would be shown. In this case, the data most recently passed to
			// branch.banner() and stored in the data cache would be sent through to the /v1/branchview
			// call. It would be combined on the server with data set in the Dashboard.
			var branch = initBranch(false);
			var assert = testUtils.plan(4, done);

			var bannerDeeplinkData = {
				tags: [ 'custom' ],
				data: {
					mydata: 'From Banner',
					foo: 'bar',
					'$deeplink_path': 'open/item/5678'
				}
			}

			sandbox.stub(branch_view, 'handleBranchViewData', function(server, branchViewData, data) {
				assert.isDefined(data, 'user data has been defined');
				assert.strictEqual(true, banner_utils.shouldAppend(storage, {
					forgetHide: true,
					showiOS: true
				}), 'branch view should be displayable');
				assert.strictEqual(data.data.mydata, 'From Banner', 'deep link data was passed by banner');
			});

			branch.init(branch_sample_key, {}, function onInit(errorMessage, branchData) {
				branch.banner(
					{
						immediate: true,
						disableHide: true,
						forgetHide: true
					},
					bannerDeeplinkData
				);
			});


			// _r
			requests[0].callback(null, browser_fingerprint_id);

			// v1/open
			requests[1].callback(
				null,
				{
					browser_fingerprint_id: browser_fingerprint_id,
					identity_id: identity_id,
					session_id: session_id
				}
			);

			// v1/event (first time)
			setTimeout(function() {
				requests[2].callback(null, {
					branch_view_enabled: true,
					branch_view_data: {
						id: '345',
						number_of_use: 1000,
						url: 'http://localhost:8000'
					}
				});

				assert.strictEqual(requests.length, 4, '4 requests made');
			}, 10);
		});

	});
});



<|start_filename|>web-branch-deep-linking:/src/extern.js<|end_filename|>
/**
 * This file is just for the closure compiler, to tell it the types of various
 * external functions. @type{?} means it's "optional".
 */
'use strict';

/** @type {?} */
var define;
/** @type {?} */
define.amd;

/** @type {?} */
var exports;

var module = {
	exports: {}
};

// var console = { log: function() {}, error: function() {} };

var sessionStorage = {
	clear: function() {},
	getItem: function() {},
	setItem: function() {}
};

// Used in Cordova
var require = function(module) {};

var localStorage = {
	clear: function() {},
	getItem: function() {},
	removeItem: function() {},
	setItem: function() {}
};



<|start_filename|>web-branch-deep-linking:/src/appindexing.js<|end_filename|>
'use strict';
goog.provide('appindexing');

goog.require('safejson');
goog.require('utils');


appindexing.state = {};
appindexing.state['androidAppIndexingTagsPresent'] = false;
appindexing.state['iosAppIndexingTagsPresent'] = false;
appindexing.state['androidDetailsComplete'] = false;
appindexing.state['iosDetailsComplete'] = false;

appindexing.options = {};


/** Builds the 'href' attribute of the 'link' element for insertion into the webpage **/
function addAppIndexingTag(type) {
	var href;
	if (type === 'android' && appindexing.state['androidDetailsComplete']) {
		href = 'android-app://' + appindexing.options['androidPackageName'] + '/' + appindexing.options['androidURL'];
		href = 	addBranchTrackingParams(href);
		writeToDOM(href);
	} 
	if (type === 'ios' && appindexing.state['iosDetailsComplete']) {
		href = 'ios-app://' + appindexing.options['iosAppId'] + '/' + appindexing.options['iosURL'];
		href = addBranchTrackingParams(href);
		writeToDOM(href);
	}
};

/** Updates the 'href' attribute of a 'link' element to include Branch Tracking params. */
function addBranchTrackingParams(href) {

	var branchTrackingParams = { "~channel": "Firebase App Indexing", "~feature": "Auto App Indexing", "$canonical_url": utils.getWindowLocation() }
	
	if (typeof appindexing.options['data'] === 'object') {
		for (var key in appindexing.options['data']) {
			if (appindexing.options['data'].hasOwnProperty(key) && 
				!branchTrackingParams.hasOwnProperty(key)) {

				branchTrackingParams[key] = appindexing.options['data'][key];
			}
		}
	}

	var appendQueryStringUsing = (href.indexOf('?') > -1) ? '&' : '?';

	return href + appendQueryStringUsing + 'link_click_id=a-' + btoa(safejson.stringify(branchTrackingParams));
};

/** Modifies the page's HTML to insert App Indexing tags between the <head></head> section of the webpage */
function writeToDOM(href) {
	var linkElement = document.createElement('link');
	linkElement.setAttribute('rel', 'alternate');
	linkElement.setAttribute('href', href);
	document.head.appendChild(linkElement);
};

/** Scans through all 'link' tags then updates their 'href' values for those that contain 
 * "ios-app" or "android-app" to include Branch tracking parameters.
 * (Stage 1) 
 */
appindexing.updateAppIndexingTagsIfPresent = function() {
	var linkTags = document.getElementsByTagName('link');
	var numLinkTags = linkTags.length;

	if (!numLinkTags) { return; }

	for (var counter = 0; counter < numLinkTags; counter++) {

		var currLinkTag = linkTags[counter];
		var currLinkTagHref = currLinkTag.href;

		if (!currLinkTagHref) { continue; }


		if (currLinkTagHref.includes('ios-app')) {
			appindexing.state['iosAppIndexingTagsPresent'] = true;
			currLinkTag.setAttribute('href',  addBranchTrackingParams(currLinkTagHref));
		}
		if (currLinkTagHref.includes('android-app')) {
			appindexing.state['androidAppIndexingTagsPresent'] = true;
			currLinkTag.setAttribute('href',  addBranchTrackingParams(currLinkTagHref));
		}
	}
};

/** Checks whether config contains appropriate per platform params. For Android, config needs to contain
  * both androidPackageName and androidURL. For ios both, iosAppId and iosURL. Once config is correctly set for
  * ios or Android, an App Indexing tag is built and inserted into the webpage.
  * (Stage 2)
  */
appindexing.insertAppIndexingTagsFromConfig = function(type) {

	if (type === 'android' && 
		typeof appindexing.options['androidPackageName'] === 'string' &&
		typeof appindexing.options['androidURL'] === 'string' ) {
		appindexing.state['androidDetailsComplete'] = true;
		addAppIndexingTag('android');
	}
	
	if (type === 'ios' && 
		typeof appindexing.options['iosAppId'] === 'string' &&
		typeof appindexing.options['iosURL'] === 'string' ) {
		appindexing.state['iosDetailsComplete'] = true;
		addAppIndexingTag('ios');
	}
};

/** If App Indexing tags do not exist and if the original config passed into autoAppIndex call does 
  * not contain appropriate data, then as a last resort, this function will try to update the config from page's App Links tags.
  * Once config is built it is validated and then if appropriate, App Indexing tags are built and inserted into the webpage.
  * (Stage 3)
 */
appindexing.populateConfigFromAppLinksTags = function(type) {
	var metaTags = document.getElementsByTagName('meta');

	for (var counter = 0; counter < metaTags.length; counter++) {

		var currMetaTag = metaTags[counter];

		if (type === 'ios' && currMetaTag.getAttribute('property') === 'al:ios:app_store_id') {
			appindexing.options['iosAppId'] = currMetaTag.getAttribute('content');
		}
		if (type === 'ios' && currMetaTag.getAttribute('property') === 'al:ios:url') {
			appindexing.options['iosURL'] = currMetaTag.getAttribute('content').replace('://','/');
		}
		if (type === 'android' && currMetaTag.getAttribute('property') === 'al:android:package') {
			appindexing.options['androidPackageName'] = currMetaTag.getAttribute('content');
		}
		if (type === 'android' && currMetaTag.getAttribute('property') === 'al:android:url') {
			appindexing.options['androidURL'] = currMetaTag.getAttribute('content').replace('://','/');
		}
	}
	appindexing.insertAppIndexingTagsFromConfig(type);
};

<|start_filename|>web-branch-deep-linking:/test/0_queue.js<|end_filename|>
'use strict';

goog.require('task_queue');

describe('task_queue', function() {
	var queue;
	var orderCalled;
	var clock;
	var assert = testUtils.unplanned();
	beforeEach(function() {
		queue = task_queue();
		clock = sinon.useFakeTimers();
		orderCalled = [];
		queue(function(next) {
			setTimeout(function() {
				orderCalled.push(0);
				next();
			}, 10);
		});
		assert.strictEqual(orderCalled[0], undefined, 'Has not yet called function');
	});

	it('should queue a function and call it', function(done) {
		clock.tick(11);
		assert.strictEqual(orderCalled[0], 0, 'Function called');
		done();
	});

	it('should enqueue two functions, and call them in order', function(done) {
		queue(function(next) {
			setTimeout(function() {
				orderCalled.push(1);
				next();
			}, 10);
		});
		clock.tick(11);
		assert.strictEqual(orderCalled[0], 0, 'Called first function');
		assert.strictEqual(orderCalled[1], undefined, 'Has not yet called second function');
		clock.tick(11);
		assert.strictEqual(orderCalled[1], 1, 'Called second function');
		done();
	});
});
