diff --git a/modules/freewheelSSPBidAdapter.js b/modules/freewheelSSPBidAdapter.js
new file mode 100644
index 00000000000..7c696c746e6
--- /dev/null
+++ b/modules/freewheelSSPBidAdapter.js
@@ -0,0 +1,314 @@
+import * as utils from 'src/utils';
+import { registerBidder } from 'src/adapters/bidderFactory';
+// import { config } from 'src/config';
+
+const BIDDER_CODE = 'freewheel-ssp';
+
+const PROTOCOL = getProtocol();
+const FREEWHEEL_ADSSETUP = PROTOCOL + '://ads.stickyadstv.com/www/delivery/swfIndex.php';
+const MUSTANG_URL = PROTOCOL + '://cdn.stickyadstv.com/mustang/mustang.min.js';
+const PRIMETIME_URL = PROTOCOL + '://cdn.stickyadstv.com/prime-time/';
+const USER_SYNC_URL = PROTOCOL + '://ads.stickyadstv.com/auto-user-sync';
+
+function getProtocol() {
+ if (location.protocol && location.protocol.indexOf('https') === 0) {
+ return 'https';
+ } else {
+ return 'http';
+ }
+}
+
+function isValidUrl(str) {
+ if (!str) {
+ return false;
+ }
+
+ // regExp for url validation
+ var pattern = /^(https?|ftp|file):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
+ return pattern.test(str);
+}
+
+function getBiggerSize(array) {
+ var result = [0, 0];
+ for (var i = 0; i < array.length; i++) {
+ if (array[i][0] * array[i][1] > result[0] * result[1]) {
+ result = array[i];
+ }
+ }
+ return result;
+}
+
+/*
+* read the pricing extension with this format: 1.0000
+* @return {object} pricing data in format: {currency: "EUR", price:"1.000"}
+*/
+function getPricing(xmlNode) {
+ var pricingExtNode;
+ var princingData = {};
+
+ var extensions = xmlNode.querySelectorAll('Extension');
+ extensions.forEach(function(node) {
+ if (node.getAttribute('type') === 'StickyPricing') {
+ pricingExtNode = node;
+ }
+ });
+
+ if (pricingExtNode) {
+ var priceNode = pricingExtNode.querySelector('Price');
+ princingData = {
+ currency: priceNode.getAttribute('currency'),
+ price: priceNode.textContent || priceNode.innerText
+ };
+ } else {
+ utils.logWarn('PREBID - ' + BIDDER_CODE + ': Can\'t get pricing data. Is price awareness enabled?');
+ }
+
+ return princingData;
+}
+
+function getCreativeId(xmlNode) {
+ var creaId = '';
+ var adNodes = xmlNode.querySelectorAll('Ad');
+
+ adNodes.forEach(function(el) {
+ creaId += '[' + el.getAttribute('id') + ']';
+ });
+
+ return creaId;
+}
+
+/**
+* returns the top most accessible window
+*/
+function getTopMostWindow() {
+ var res = window;
+
+ try {
+ while (top !== res) {
+ if (res.parent.location.href.length) { res = res.parent; }
+ }
+ } catch (e) {}
+
+ return res;
+}
+
+function getComponentId(inputFormat) {
+ var component = 'mustang'; // default component id
+
+ if (inputFormat && inputFormat !== 'inbanner') {
+ // format identifiers are equals to their component ids.
+ component = inputFormat;
+ }
+
+ return component;
+}
+
+function getAPIName(componentId) {
+ componentId = componentId || '';
+
+ // remove dash in componentId to get API name
+ return componentId.replace('-', '');
+}
+
+function formatAdHTML(bid, size) {
+ var integrationType = bid.params.format;
+
+ var divHtml = '
';
+
+ var script = '';
+ var libUrl = '';
+ if (integrationType && integrationType !== 'inbanner') {
+ libUrl = PRIMETIME_URL + getComponentId(bid.params.format) + '.min.js';
+ script = getOutstreamScript(bid, size);
+ } else {
+ libUrl = MUSTANG_URL;
+ script = getInBannerScript(bid, size);
+ }
+
+ return divHtml +
+ '';
+}
+
+var getInBannerScript = function(bid, size) {
+ return 'var config = {' +
+ ' preloadedVast:vast,' +
+ ' autoPlay:true' +
+ ' };' +
+ ' var ad = new window.com.stickyadstv.vpaid.Ad(document.getElementById("freewheelssp_prebid_target"),config);' +
+ ' (new window.com.stickyadstv.tools.ASLoader(' + bid.params.zoneId + ', \'' + getComponentId(bid.params.format) + '\')).registerEvents(ad);' +
+ ' ad.initAd(' + size[0] + ',' + size[1] + ',"",0,"","");';
+};
+
+var getOutstreamScript = function(bid) {
+ var placementCode = bid.adUnitCode;
+
+ var config = bid.params;
+
+ // default placement if no placement is set
+ if (!config.hasOwnProperty('domId') && !config.hasOwnProperty('auto') && !config.hasOwnProperty('p') && !config.hasOwnProperty('article')) {
+ config.domId = placementCode;
+ }
+
+ var script = 'var config = {' +
+ ' preloadedVast:vast,' +
+ ' ASLoader:new window.com.stickyadstv.tools.ASLoader(' + bid.params.zoneId + ', \'' + getComponentId(bid.params.format) + '\')';
+
+ for (var key in config) {
+ // dont' send format parameter
+ // neither zone nor vastUrlParams value as Vast is already loaded
+ if (config.hasOwnProperty(key) && key !== 'format' && key !== 'zone' && key !== 'zoneId' && key !== 'vastUrlParams') {
+ script += ',' + key + ':"' + config[key] + '"';
+ }
+ }
+ script += '};' +
+
+ 'window.com.stickyadstv.' + getAPIName(bid.params.format) + '.start(config);';
+
+ return script;
+};
+
+export const spec = {
+ code: BIDDER_CODE,
+ supportedMediaTypes: ['video'],
+ aliases: ['stickyadstv'], // former name for freewheel-ssp
+ /**
+ * Determines whether or not the given bid request is valid.
+ *
+ * @param {object} bid The bid to validate.
+ * @return boolean True if this is a valid bid, and false otherwise.
+ */
+ isBidRequestValid: function(bid) {
+ return !!(bid.params.zoneId);
+ },
+
+ /**
+ * Make a server request from the list of BidRequests.
+ *
+ * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server.
+ * @return ServerRequest Info describing the request to the server.
+ */
+ buildRequests: function(bidRequests) {
+ // var currency = config.getConfig(currency);
+
+ var currentBidRequest = bidRequests[0];
+ if (bidRequests.length > 1) {
+ utils.logMessage('Prebid.JS - freewheel bid adapter: only one ad unit is required.');
+ }
+
+ var requestParams = {
+ reqType: 'AdsSetup',
+ protocolVersion: '2.0',
+ zoneId: currentBidRequest.params.zoneId,
+ componentId: getComponentId(currentBidRequest.params.format)
+ };
+
+ var location = utils.getTopWindowUrl();
+ if (isValidUrl(location)) {
+ requestParams.loc = location;
+ }
+
+ var playerSize = getBiggerSize(currentBidRequest.sizes);
+ if (playerSize[0] > 0 || playerSize[1] > 0) {
+ requestParams.playerSize = playerSize[0] + 'x' + playerSize[1];
+ }
+
+ return {
+ method: 'GET',
+ url: FREEWHEEL_ADSSETUP,
+ data: requestParams,
+ bidRequest: currentBidRequest
+ };
+ },
+
+ /**
+ * Unpack the response from the server into a list of bids.
+ *
+ * @param {*} serverResponse A successful response from the server.
+ * @param {object} request: the built request object containing the initial bidRequest.
+ * @return {Bid[]} An array of bids which were nested inside the server.
+ */
+ interpretResponse: function(serverResponse, request) {
+ var bidrequest = request.bidRequest;
+ var playerSize = getBiggerSize(bidrequest.sizes);
+
+ if (typeof serverResponse == 'object' && typeof serverResponse.body == 'string') {
+ serverResponse = serverResponse.body;
+ }
+
+ var xmlDoc;
+ try {
+ var parser = new DOMParser();
+ xmlDoc = parser.parseFromString(serverResponse, 'application/xml');
+ } catch (err) {
+ utils.logWarn('Prebid.js - ' + BIDDER_CODE + ' : ' + err);
+ return;
+ }
+
+ const princingData = getPricing(xmlDoc);
+ const creativeId = getCreativeId(xmlDoc);
+
+ const topWin = getTopMostWindow();
+ if (!topWin.freewheelssp_cache) {
+ topWin.freewheelssp_cache = {};
+ }
+ topWin.freewheelssp_cache[bidrequest.adUnitCode] = serverResponse;
+
+ const bidResponses = [];
+
+ if (princingData.price) {
+ const bidResponse = {
+ requestId: bidrequest.bidId,
+ cpm: princingData.price,
+ width: playerSize[0],
+ height: playerSize[1],
+ creativeId: creativeId,
+ currency: princingData.currency,
+ netRevenue: true,
+ ttl: 360
+ };
+
+ var mediaTypes = bidrequest.mediaTypes || {};
+ if (mediaTypes.video) {
+ // bidResponse.vastXml = serverResponse;
+ bidResponse.mediaType = 'video';
+
+ var blob = new Blob([serverResponse], {type: 'application/xml'});
+ bidResponse.vastUrl = window.URL.createObjectURL(blob);
+ } else {
+ bidResponse.ad = formatAdHTML(bidrequest, playerSize);
+ }
+
+ bidResponses.push(bidResponse);
+ }
+
+ return bidResponses;
+ },
+
+ getUserSyncs: function(syncOptions) {
+ if (syncOptions.pixelEnabled) {
+ return [{
+ type: 'image',
+ url: USER_SYNC_URL
+ }];
+ }
+ }
+}
+registerBidder(spec);
diff --git a/modules/freewheelSSPBidAdapter.md b/modules/freewheelSSPBidAdapter.md
new file mode 100644
index 00000000000..ba7915c87e1
--- /dev/null
+++ b/modules/freewheelSSPBidAdapter.md
@@ -0,0 +1,27 @@
+# Overview
+
+Module Name: Freewheel SSP Bidder Adapter
+Module Type: Bidder Adapter
+Maintainer: clientsidesdk@freewheel.tv
+
+# Description
+
+Module that connects to Freewheel ssp's demand sources
+
+# Test Parameters
+```
+ var adUnits = [
+ {
+ code: 'test-div',
+ sizes: [[300, 250]], // a display size
+ bids: [
+ {
+ bidder: "freewheel-ssp",
+ params: {
+ zoneId : '277225'
+ }
+ }
+ ]
+ }
+ ];
+```
\ No newline at end of file
diff --git a/test/spec/modules/freewheelSSPBidAdapter_spec.js b/test/spec/modules/freewheelSSPBidAdapter_spec.js
new file mode 100644
index 00000000000..107259e9805
--- /dev/null
+++ b/test/spec/modules/freewheelSSPBidAdapter_spec.js
@@ -0,0 +1,193 @@
+import { expect } from 'chai';
+import { spec } from 'modules/freewheelSSPBidAdapter';
+import { newBidder } from 'src/adapters/bidderFactory';
+
+const ENDPOINT = '//ads.stickyadstv.com/www/delivery/swfIndex.php';
+
+describe('freewheelSSP BidAdapter Test', () => {
+ const adapter = newBidder(spec);
+
+ describe('inherited functions', () => {
+ it('exists and is a function', () => {
+ expect(adapter.callBids).to.exist.and.to.be.a('function');
+ });
+ });
+
+ describe('isBidRequestValid', () => {
+ let bid = {
+ 'bidder': 'freewheel-ssp',
+ 'params': {
+ 'zoneId': '277225'
+ },
+ 'adUnitCode': 'adunit-code',
+ 'sizes': [[300, 250], [300, 600]],
+ 'bidId': '30b31c1838de1e',
+ 'bidderRequestId': '22edbae2733bf6',
+ 'auctionId': '1d1a030790a475',
+ };
+
+ it('should return true when required params found', () => {
+ expect(spec.isBidRequestValid(bid)).to.equal(true);
+ });
+
+ it('should return false when required params are not passed', () => {
+ let bid = Object.assign({}, bid);
+ delete bid.params;
+ bid.params = {
+ wrong: 'missing zone id'
+ };
+ expect(spec.isBidRequestValid(bid)).to.equal(false);
+ });
+ });
+
+ describe('buildRequests', () => {
+ let bidRequests = [
+ {
+ 'bidder': 'freewheel-ssp',
+ 'params': {
+ 'zoneId': '277225'
+ },
+ 'adUnitCode': 'adunit-code',
+ 'sizes': [[300, 250], [300, 600]],
+ 'bidId': '30b31c1838de1e',
+ 'bidderRequestId': '22edbae2733bf6',
+ 'auctionId': '1d1a030790a475',
+ }
+ ];
+
+ it('should add parameters to the tag', () => {
+ const request = spec.buildRequests(bidRequests);
+ console.log(request.data);
+
+ const payload = request.data;
+ expect(payload.reqType).to.equal('AdsSetup');
+ expect(payload.protocolVersion).to.equal('2.0');
+ expect(payload.zoneId).to.equal('277225');
+ expect(payload.componentId).to.equal('mustang');
+ expect(payload.playerSize).to.equal('300x600');
+ });
+
+ it('sends bid request to ENDPOINT via GET', () => {
+ const request = spec.buildRequests(bidRequests);
+ expect(request.url).to.contain(ENDPOINT);
+ expect(request.method).to.equal('GET');
+ });
+ })
+
+ describe('interpretResponse', () => {
+ let bidRequests = [
+ {
+ 'bidder': 'freewheel-ssp',
+ 'params': {
+ 'zoneId': '277225'
+ },
+ 'adUnitCode': 'adunit-code',
+ 'sizes': [[300, 250], [300, 600]],
+ 'bidId': '30b31c1838de1e',
+ 'bidderRequestId': '22edbae2733bf6',
+ 'auctionId': '1d1a030790a475',
+ }
+ ];
+
+ let formattedBidRequests = [
+ {
+ 'bidder': 'freewheel-ssp',
+ 'params': {
+ 'zoneId': '277225',
+ 'format': 'floorad'
+ },
+ 'adUnitCode': 'adunit-code',
+ 'sizes': [[600, 250], [300, 600]],
+ 'bidId': '30b3other1c1838de1e',
+ 'bidderRequestId': '22edbae273other3bf6',
+ 'auctionId': '1d1a03079test0a475',
+ },
+ {
+ 'bidder': 'stickyadstv',
+ 'params': {
+ 'zoneId': '277225',
+ 'format': 'test'
+ },
+ 'adUnitCode': 'adunit-code',
+ 'sizes': [[300, 600]],
+ 'bidId': '2',
+ 'bidderRequestId': '3',
+ 'auctionId': '4',
+ }
+ ];
+
+ let response = '' +
+ '' +
+ ' ' +
+ ' Adswizz' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' 00:00:09' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' 0.2000' +
+ ' ' +
+ ' ' +
+ ' ' +
+ '';
+
+ let ad = '';
+ let formattedAd = '';
+
+ it('should get correct bid response', () => {
+ var request = spec.buildRequests(bidRequests);
+
+ let expectedResponse = [
+ {
+ requestId: '30b31c1838de1e',
+ cpm: '0.2000',
+ width: 300,
+ height: 600,
+ creativeId: '28517153',
+ currency: 'EUR',
+ netRevenue: true,
+ ttl: 360,
+ ad: ad
+ }
+ ];
+
+ let result = spec.interpretResponse(response, request);
+ expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0]));
+ });
+
+ it('should get correct bid response with formated ad', () => {
+ var request = spec.buildRequests(formattedBidRequests);
+
+ let expectedResponse = [
+ {
+ requestId: '30b31c1838de1e',
+ cpm: '0.2000',
+ width: 300,
+ height: 600,
+ creativeId: '28517153',
+ currency: 'EUR',
+ netRevenue: true,
+ ttl: 360,
+ ad: formattedAd
+ }
+ ];
+
+ let result = spec.interpretResponse(response, request);
+ expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0]));
+ });
+
+ it('handles nobid responses', () => {
+ var reqest = spec.buildRequests(formattedBidRequests);
+ let response = '';
+
+ let result = spec.interpretResponse(response, reqest);
+ expect(result.length).to.equal(0);
+ });
+ });
+});