diff --git a/extensions/amp-subscriptions/0.1/dialog.js b/extensions/amp-subscriptions/0.1/dialog.js index fdc80803c40d..a13ff4361a1b 100644 --- a/extensions/amp-subscriptions/0.1/dialog.js +++ b/extensions/amp-subscriptions/0.1/dialog.js @@ -162,9 +162,7 @@ export class Dialog { }, mutate: () => { this.viewport_.updatePaddingBottom(offsetHeight); - // TODO(dvoytenko, #20608): add to fixed layer, once the SwG/FL - // conflict is resolved. - // this.viewport_.addToFixedLayer(this.wrapper_, true); + this.viewport_.addToFixedLayer(this.wrapper_, true); }, }); }); diff --git a/extensions/amp-subscriptions/0.1/doc-impl.js b/extensions/amp-subscriptions/0.1/doc-impl.js index b2bee6b31844..56a9e3658532 100644 --- a/extensions/amp-subscriptions/0.1/doc-impl.js +++ b/extensions/amp-subscriptions/0.1/doc-impl.js @@ -15,6 +15,7 @@ */ import {Doc} from '../../../third_party/subscriptions-project/config'; +import {Services} from '../../../src/services'; import {dev} from '../../../src/log'; /** @@ -28,6 +29,9 @@ export class DocImpl { constructor(ampdoc) { /** @private @const {!../../../src/service/ampdoc-impl.AmpDoc} */ this.ampdoc_ = ampdoc; + + /** @private {!../../../src/service/viewport/viewport-interface.ViewportInterface} */ + this.viewport_ = Services.viewportForDoc(this.ampdoc_); } /** @override */ @@ -65,6 +69,11 @@ export class DocImpl { whenReady() { return this.ampdoc_.whenReady(); } + + /** @override */ + addToFixedLayer(element) { + return this.viewport_.addToFixedLayer(element, /* Force Transfer */ true); + } } /** diff --git a/extensions/amp-subscriptions/0.1/test/test-dialog.js b/extensions/amp-subscriptions/0.1/test/test-dialog.js index afe31b08ae5b..067d741768ae 100644 --- a/extensions/amp-subscriptions/0.1/test/test-dialog.js +++ b/extensions/amp-subscriptions/0.1/test/test-dialog.js @@ -77,11 +77,9 @@ describes.realWin('AmpSubscriptions Dialog', {amp: true}, env => { expect(styles.transform).to.not.contain('17'); expect(dialog.closeButton_).to.have.display('none'); expect(updatePaddingSpy).to.be.calledOnce.calledWith(17); - // TODO(dvoytenko, #20608): add to fixed layer, once the SwG/FL - // conflict is resolved. - // expect(addToFixedLayerSpy).to.be.calledOnce - // .calledWith(dialog.getRoot()); - expect(addToFixedLayerSpy).to.not.be.called; + expect(addToFixedLayerSpy).to.be.calledOnce.calledWith( + dialog.getRoot() + ); expect(dialog.isVisible()).to.be.true; }); }); diff --git a/src/service/fixed-layer.js b/src/service/fixed-layer.js index f36971324bb6..468ec533c064 100644 --- a/src/service/fixed-layer.js +++ b/src/service/fixed-layer.js @@ -665,7 +665,9 @@ export class FixedLayer { opt_lightboxMode ) { // Warn that pub-authored inline styles may be overriden by FixedLayer. - this.warnAboutInlineStylesIfNecessary_(element); + if (!opt_forceTransfer) { + this.warnAboutInlineStylesIfNecessary_(element); + } // Ignore lightboxes because FixedLayer can interfere with their // opening/closing animations (#19149). diff --git a/third_party/subscriptions-project/config.js b/third_party/subscriptions-project/config.js index 927d38a3ec89..60533f34360a 100644 --- a/third_party/subscriptions-project/config.js +++ b/third_party/subscriptions-project/config.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** Version: 0.1.22.64 */ +/** Version: 0.1.22.67 */ /** * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. * @@ -154,6 +154,15 @@ class Doc { * @return {!Promise} */ whenReady() {} + + /** + * Adds the element to the fixed layer. + * @param {!Element} unusedElement + * @return {!Promise} + * + * This is a no-op for except in AMP on iOS < 13.0. + */ + addToFixedLayer(unusedElement) {} } /** @implements {Doc} */ @@ -208,6 +217,11 @@ class GlobalDoc { whenReady() { return whenDocumentReady(this.doc_); } + + /** @override */ + addToFixedLayer(unusedElement) { + return Promise.resolve(); + } } /** @@ -326,6 +340,9 @@ class PageConfig { * Debug logger, only log message if #swg.log=1 * @param {...*} var_args [decription] */ + +/* eslint-disable */ + function debugLog(var_args) { if (/swg.debug=1/.test(self.location.hash)) { const logArgs = Array.prototype.slice.call(arguments, 0); diff --git a/third_party/subscriptions-project/swg.js b/third_party/subscriptions-project/swg.js index f9720547f11f..5ab8778360a5 100644 --- a/third_party/subscriptions-project/swg.js +++ b/third_party/subscriptions-project/swg.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/** Version: 0.1.22.64 */ +/** Version: 0.1.22.67 */ /** * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. * @@ -2989,6 +2989,52 @@ function map(opt_initial) { return obj; } +/** + * Copyright 2019 The Subscribe with Google Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Returns an array of random values. The length of the array is numInts. Each + * int will be >= 0 and < maxVal. + * @param {!number} numInts + * @param {!number} maxVal + */ +function getRandomInts(numInts, maxVal) { + // Ensure array type is appropriate for the max value (performance) + const arr = + maxVal < 256 + ? new Uint8Array(numInts) + : maxVal < 32768 + ? new Uint16Array(numInts) + : new Uint32Array(numInts); + + if (crypto && crypto.getRandomValues) { + crypto.getRandomValues(arr); + for (let i = arr.length - 1; i > -1; i--) { + arr[i] = arr[i] % maxVal; + } + } else { + // For older browsers + for (let i = arr.length - 1; i > -1; i--) { + arr[i] = Math.floor(Math.random() * maxVal); + } + } + + return arr; +} + /** * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. * @@ -3005,6 +3051,8 @@ function map(opt_initial) { * limitations under the License. */ +const CHARS = '0123456789ABCDEF'; + /** * Polyfill for String.prototype.startsWith. * @param {string} string @@ -3018,6 +3066,55 @@ function startsWith(string, prefix) { return string.lastIndexOf(prefix, 0) == 0; } +/** + * Ensures the passed value is safe to use for character 19 per rfc4122, + * sec. 4.1.5. "Sets the high bits of clock sequence". + * @param {!number} v + */ +function getChar19(v) { + return CHARS[(v & 0x3) | 0x8]; +} + +/** + * The returned identifier will always be an 8 digit valid hexidecimal number + * and will be unique for each MS within a given month. + * @return {string} + */ +function getMonthlyTimeIdentifier() { + const hexTime = Date.now().toString(16); + return hexTime.substring(hexTime.length - 8).toUpperCase(); +} + +/** + * Generates a RFC 4122 V4 UUID. Ex: "92329D39-6F5C-4520-ABFC-AAB64544E172" + * The first 8 digits are unique for the millisecond of the month. The rest + * are randomly generated. + */ +function getUuid() { + let uuid = getMonthlyTimeIdentifier() + '-'; + let rIndex = 0; + const rands = getRandomInts(23, 16); + for (let i = 9; i < 36; i++) { + switch (i) { + case 13: + case 18: + case 23: + uuid += '-'; + break; + case 14: + uuid += '4'; + break; + case 19: + uuid += getChar19(rands[rIndex++]); + break; + default: + uuid += CHARS[rands[rIndex++]]; + break; + } + } + return uuid; +} + /** * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. * @@ -3757,49 +3854,395 @@ function adsUrl(url) { return 'https://pubads.g.doubleclick.net' + url; } -/** - * @param {string} url Relative URL, e.g. "/offersiframe". - * @param {string=} prefix - * @return {string} The complete URL. - */ -function feUrl(url, prefix = '') { - return feCached('https://news.google.com' + prefix + '/swg/_/ui/v1' + url); -} +/** + * @param {string} url Relative URL, e.g. "/offersiframe". + * @param {string=} prefix + * @return {string} The complete URL. + */ +function feUrl(url, prefix = '') { + return feCached('https://news.google.com' + prefix + '/swg/_/ui/v1' + url); +} + +/** + * @param {string} url FE URL. + * @return {string} The complete URL including cache params. + */ +function feCached(url) { + return addQueryParam(url, '_', cacheParam('hr1')); +} + +/** + * @param {!Object} args + * @return {!Object} + */ +function feArgs(args) { + return Object.assign(args, { + '_client': 'SwG 0.1.22.67', + }); +} + +/** + * @param {string} cacheKey + * @return {string} + * @package Visible for testing only. + */ +function cacheParam(cacheKey) { + let period = CACHE_KEYS[cacheKey]; + if (period == null) { + period = 1; + } + if (period === 0) { + return '_'; + } + const now = Date.now(); + return String(period <= 1 ? now : Math.floor(now / period)); +} + +/** + * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Whether the specified error is an AbortError type. + * See https://heycam.github.io/webidl/#aborterror. + * @param {*} error + * @return {boolean} + */ +function isCancelError(error) { + return activityPorts_12(error); +} + +/** + * Creates or emulates a DOMException of AbortError type. + * See https://heycam.github.io/webidl/#aborterror. + * @param {!Window} win + * @param {string=} opt_message + * @return {!DOMException} + */ +function createCancelError(win, opt_message) { + return activityPorts_11(win, opt_message); +} + +/** + * A set of error utilities combined in a class to allow easy stubbing in tests. + */ +class ErrorUtils { + /** + * @param {!Error} error + */ + static throwAsync(error) { + setTimeout(() => { + throw error; + }); + } +} + +/** + * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview + * + * Client-side experiments in SwG. + * + * The experiments can be set in a few different ways: + * 1. By gulp build rules using `--experiments=${experimentsString}` argument. + * 2. By `#swg.experiments=${experimentsString}` parameter in the URL's + * fragment. + * 3. By `swg.configure({experiments: [array]})` call. + * + * The `${experimentsString}` is defined as following: + * - experimentString = (experimentSpec,)* + * - experimentSpec = experimentId | experimentId '=' num100 ('c')? + * + * Some examples: + * - `A,B` - defines two experiments "A" and "B" that will be turned on. + * - `A:100,B:100` - the same: "A" and "B" will be turned on. + * - `A:0` - the experiment "A" will be disabled. + * - `A:1` - enable the experiment "A" in 1% of impressions. + * - `A:10c` - enable the experiment "A" in 10% of impressions with 10% + * control. In this case, 20% of the impressions will be split into two + * categories: experiment and control. Notice, a control can be requested + * only for the fraction under 20%. + */ + +/** + * @enum {string} + */ +const Selection = { + EXPERIMENT: 'e', + CONTROL: 'c', +}; + +/** + * A comma-separated set of experiments. + * @type {string} + */ +let experimentsString = ''; + +/** + * A parsed map of experiments. + * @type {?Object} + */ +let experimentMap = null; + +/** + * Ensures that the experiments have been initialized and returns them. + * @param {!Window} win + * @return {!Object} + */ +function getExperiments(win) { + if (!experimentMap) { + experimentMap = {}; + let combinedExperimentString = experimentsString; + try { + const query = parseQueryString$1(win.location.hash); + const experimentStringFromHash = query['swg.experiments']; + if (experimentStringFromHash) { + combinedExperimentString += ',' + experimentStringFromHash; + } + } catch (e) { + // Ignore: experiment parsing cannot block runtime. + ErrorUtils.throwAsync(e); + } + + // Format: + // - experimentString = (experimentSpec,)* + combinedExperimentString.split(',').forEach(s => { + s = s.trim(); + if (!s) { + return; + } + try { + parseSetExperiment(win, experimentMap, s); + } catch (e) { + // Ignore: experiment parsing cannot block runtime. + ErrorUtils.throwAsync(e); + } + }); + } + return experimentMap; +} + +/** + * @param {!Window} win + * @param {?Object} experimentMap + * @param {string} spec + */ +function parseSetExperiment(win, experimentMap, spec) { + // Format: + // - experimentSpec = experimentId | experimentId '=' num100 ('c')? + let experimentId; + let fraction; + let control = false; + const eq = spec.indexOf(':'); + if (eq == -1) { + experimentId = spec; + fraction = 100; + control = false; + } else { + experimentId = spec.substring(0, eq).trim(); + spec = spec.substring(eq + 1); + if (spec.substring(spec.length - 1) == Selection.CONTROL) { + control = true; + spec = spec.substring(0, spec.length - 1); + } + fraction = parseInt(spec, 10); + } + if (isNaN(fraction)) { + throw new Error('invalid fraction'); + } + + // Calculate "on"/"off". + let on; + if (fraction > 99) { + // Explicitly "on". + on = true; + } else if (fraction < 1) { + // Explicitly "off". + on = false; + } else if (win.sessionStorage) { + // Fractional and possibly with the control. + // Note that: + // a. We can't do persistent experiments if storage is not available. + // b. We can't run control on more than 20%. + control = control && fraction <= 20; + try { + // Set fraction in the experiment to make it unlaunchable. + const storageKey = + 'subscribe.google.com:e:' + + experimentId + + ':' + + fraction + + (control ? 'c' : ''); + let selection = parseSelection(win.sessionStorage.getItem(storageKey)); + if (!selection) { + // Is experiment/control range? + if (win.Math.random() * 100 <= fraction * (control ? 2 : 1)) { + const inExperiment = control ? win.Math.random() <= 0.5 : true; + selection = inExperiment ? Selection.EXPERIMENT : Selection.CONTROL; + win.sessionStorage.setItem(storageKey, selection); + } + } + on = !!selection; + if (selection == Selection.CONTROL) { + experimentId = 'c-' + experimentId; + } + } catch (e) { + // Ignore: experiment parsing cannot block runtime. + on = false; + ErrorUtils.throwAsync(e); + } + } else { + on = false; + } + + experimentMap[experimentId] = on; +} + +/** + * @param {?string} s + * @return {?Selection} + */ +function parseSelection(s) { + // Do a simple if-then to inline the whole Selection enum. + return s == Selection.EXPERIMENT + ? Selection.EXPERIMENT + : s == Selection.CONTROL + ? Selection.CONTROL + : null; +} + +/** + * Whether the specified experiment is on or off. + * @param {!Window} win + * @param {string} experimentId + * @return {boolean} + */ +function isExperimentOn(win, experimentId) { + return getExperiments(win)[experimentId] || false; +} + +/** + * Toggles the experiment on or off. Returns the actual value of the experiment + * after toggling is done. + * @param {!Window} win + * @param {string} experimentId + * @param {boolean} on + */ +function setExperiment(win, experimentId, on) { + getExperiments(win)[experimentId] = on; +} + +/** + * @return {!Array} + */ +function getOnExperiments(win) { + const experimentMap = getExperiments(win); + const experiments = []; + for (const experiment in experimentMap) { + if (experimentMap[experiment]) { + experiments.push(experiment); + } + } + return experiments; +} + +/** + * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @enum {string} + */ +const ExperimentFlags = { + /** + * Enables GPay API in SwG. + * Cleanup issue: #406. + */ + GPAY_API: 'gpay-api', + + /** + * Enables GPay native support. + * Cleanup issue: #441. + */ + GPAY_NATIVE: 'gpay-native', + + /** + * Enables the feature that allows you to replace one subscription + * for another in the subscribe() API. + */ + REPLACE_SUBSCRIPTION: 'replace-subscription', + + /** + * Enables the contributions feature. + */ + CONTRIBUTIONS: 'contributions', + + /** + * Enables the Propensity feature + */ + PROPENSITY: 'propensity', + + /** + * Enables the Smartbox feature. + */ + SMARTBOX: 'smartbox', -/** - * @param {string} url FE URL. - * @return {string} The complete URL including cache params. - */ -function feCached(url) { - return addQueryParam(url, '_', cacheParam('hr1')); -} + /** + * Enables logging events logged through the propensity API to the analytics + * server. + */ + LOG_PROPENSITY_TO_SWG: 'log-propensity-to-swg', -/** - * @param {!Object} args - * @return {!Object} - */ -function feArgs(args) { - return Object.assign(args, { - '_client': 'SwG 0.1.22.64', - }); -} + /** + * Enables logging user events generated by the SwG and AMP clients to the + * publishers propensity server. + */ + LOG_SWG_TO_PROPENSITY: 'log_swg_to_propensity', -/** - * @param {string} cacheKey - * @return {string} - * @package Visible for testing only. - */ -function cacheParam(cacheKey) { - let period = CACHE_KEYS[cacheKey]; - if (period == null) { - period = 1; - } - if (period === 0) { - return '_'; - } - const now = Date.now(); - return String(period <= 1 ? now : Math.floor(now / period)); -} + /** + * Enables using new Activities APIs + */ + HEJIRA: 'hejira', +}; /** * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. @@ -3933,7 +4376,17 @@ class SmartSubscriptionButtonApi { this.activityPorts_ .openIframe(this.iframe_, this.src_, this.args_) .then(port => { - port.on(SmartBoxMessage, this.handleSmartBoxClick_.bind(this)); + if (isExperimentOn(this.win_, ExperimentFlags.HEJIRA)) { + port.on(SmartBoxMessage, this.handleSmartBoxClick_.bind(this)); + } else { + port.onMessageDeprecated(result => { + const smartBoxMessage = new SmartBoxMessage(); + if (result['clicked']) { + smartBoxMessage.setIsClicked(true); + } + this.handleSmartBoxClick_(smartBoxMessage); + }); + } }); return this.iframe_; } @@ -4503,57 +4956,6 @@ class View { hasLoadingIndicator() {} } -/** - * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Whether the specified error is an AbortError type. - * See https://heycam.github.io/webidl/#aborterror. - * @param {*} error - * @return {boolean} - */ -function isCancelError(error) { - return activityPorts_12(error); -} - -/** - * Creates or emulates a DOMException of AbortError type. - * See https://heycam.github.io/webidl/#aborterror. - * @param {!Window} win - * @param {string=} opt_message - * @return {!DOMException} - */ -function createCancelError(win, opt_message) { - return activityPorts_11(win, opt_message); -} - -/** - * A set of error utilities combined in a class to allow easy stubbing in tests. - */ -class ErrorUtils { - /** - * @param {!Error} error - */ - static throwAsync(error) { - setTimeout(() => { - throw error; - }); - } -} - /** * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. * @@ -4755,6 +5157,25 @@ class ActivityIframeView extends View { port.onMessageDeprecated(callback); }); } + /** + * @param {!function(new: T)} message + * @param {function(../proto/api_messages.Message)} callback + * @template T + */ + on(message, callback) { + this.getPortPromise_().then(port => { + port.on(message, callback); + }); + } + + /** + * @param {!../proto/api_messages.Message} request + */ + execute(request) { + this.getPortPromise_().then(port => { + port.execute(request); + }); + } /** * Accepts results from the caller. @@ -5834,6 +6255,7 @@ function defaultConfig() { windowOpenMode: WindowOpenMode.AUTO, analyticsMode: AnalyticsMode.DEFAULT, enableSwgAnalytics: false, + enablePropensity: false, }; } @@ -6291,6 +6713,27 @@ class ContributionsFlow { ); } + /** + * @param {AlreadySubscribedResponse} response + */ + handleLinkRequest_(response) { + if (response.getSubscriberOrMember()) { + this.deps_.callbacks().triggerLoginRequest({ + linkRequested: !!response.getLinkRequested(), + }); + } + } + + /** + * @param {SkuSelectedResponse} response + */ + startPayFlow_(response) { + const sku = response.getSku(); + if (sku) { + new PayStartFlow(this.deps_, sku, ProductType.UI_CONTRIBUTION).start(); + } + } + /** * Starts the contributions flow or alreadyMember flow. * @return {!Promise} @@ -6305,24 +6748,33 @@ class ContributionsFlow { .callbacks() .triggerFlowCanceled(SubscriptionFlows.SHOW_CONTRIBUTION_OPTIONS); }); - - // If result is due to OfferSelection, redirect to payments. - this.activityIframeView_.onMessageDeprecated(result => { - if (result['alreadyMember']) { - this.deps_.callbacks().triggerLoginRequest({ - linkRequested: !!result['linkRequested'], - }); - return; - } - if (result['sku']) { - new PayStartFlow( - this.deps_, - /** @type {string} */ (result['sku']), - ProductType.UI_CONTRIBUTION - ).start(); - return; - } - }); + if (isExperimentOn(this.deps_.win(), ExperimentFlags.HEJIRA)) { + this.activityIframeView_.on( + AlreadySubscribedResponse, + this.handleLinkRequest_.bind(this) + ); + this.activityIframeView_.on( + SkuSelectedResponse, + this.startPayFlow_.bind(this) + ); + } else { + // If result is due to OfferSelection, redirect to payments. + this.activityIframeView_.onMessageDeprecated(result => { + if (result['alreadyMember']) { + const alreadySubscribedResponse = new AlreadySubscribedResponse(); + alreadySubscribedResponse.setLinkRequested(result['linkRequested']); + alreadySubscribedResponse.setSubscriberOrMember(true); + this.handleLinkRequest_(alreadySubscribedResponse); + return; + } + if (result['sku']) { + const skuSelectedResponse = new SkuSelectedResponse(); + skuSelectedResponse.setSku(result['sku']); + this.startPayFlow_(skuSelectedResponse); + return; + } + }); + } return this.dialogManager_.openView(this.activityIframeView_); } @@ -6642,6 +7094,11 @@ class GlobalDoc { whenReady() { return whenDocumentReady(this.doc_); } + + /** @override */ + addToFixedLayer(unusedElement) { + return Promise.resolve(); + } } /** @@ -7141,6 +7598,7 @@ class Dialog { // Attach. this.doc_.getBody().appendChild(iframe.getElement()); // Fires onload. + this.graypane_.attach(); if (hidden) { @@ -7152,11 +7610,15 @@ class Dialog { } else { this.show_(); } - - return iframe.whenReady().then(() => { - this.buildIframe_(); - return this; - }); + return this.doc_ + .addToFixedLayer(iframe.getElement()) + .then(() => { + iframe.whenReady(); + }) + .then(() => { + this.buildIframe_(); + return this; + }); } /** @@ -8219,100 +8681,34 @@ class EntitlementsManager { let url = '/publication/' + encodeURIComponent(this.publicationId_) + - '/entitlements'; - if (opt_encryptedDocumentKey) { - //TODO(chenshay): Make this a 'Post'. - url += '?crypt=' + encodeURIComponent(opt_encryptedDocumentKey); - } - return this.fetcher_ - .fetchCredentialedJson(serviceUrl(url)) - .then(json => this.parseEntitlements(json)); - } -} - -/** - * Convert String value of isReadyToPay - * (from JSON or Cache) to a boolean value. - * @param {string} value - * @return {boolean|undefined} - * @private - */ -function irtpStringToBoolean(value) { - switch (value) { - case 'true': - return true; - case 'false': - return false; - default: - return undefined; - } -} - -/** - * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @enum {string} - */ -const ExperimentFlags = { - /** - * Enables GPay API in SwG. - * Cleanup issue: #406. - */ - GPAY_API: 'gpay-api', - - /** - * Enables GPay native support. - * Cleanup issue: #441. - */ - GPAY_NATIVE: 'gpay-native', - - /** - * Enables the feature that allows you to replace one subscription - * for another in the subscribe() API. - */ - REPLACE_SUBSCRIPTION: 'replace-subscription', - - /** - * Enables the contributions feature. - */ - CONTRIBUTIONS: 'contributions', - - /** - * Enables the Propensity feature - */ - PROPENSITY: 'propensity', - - /** - * Enables the Smartbox feature. - */ - SMARTBOX: 'smartbox', - - /** - * Enables logging events logged through the propensity API to the analytics - * server. - */ - LOG_PROPENSITY_TO_SWG: 'log-propensity-to-swg', + '/entitlements'; + if (opt_encryptedDocumentKey) { + //TODO(chenshay): Make this a 'Post'. + url += '?crypt=' + encodeURIComponent(opt_encryptedDocumentKey); + } + return this.fetcher_ + .fetchCredentialedJson(serviceUrl(url)) + .then(json => this.parseEntitlements(json)); + } +} - /** - * Enables logging user events generated by the SwG and AMP clients to the - * publishers propensity server. - */ - LOG_SWG_TO_PROPENSITY: 'log_swg_to_propensity', -}; +/** + * Convert String value of isReadyToPay + * (from JSON or Cache) to a boolean value. + * @param {string} value + * @return {boolean|undefined} + * @private + */ +function irtpStringToBoolean(value) { + switch (value) { + case 'true': + return true; + case 'false': + return false; + default: + return undefined; + } +} /** * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. @@ -11734,10 +12130,10 @@ Dual licensed under the MIT and GPL licenses. */ class Random_uuid {} // Private array of chars to use - var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); + var CHARS$1 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); Random_uuid.uuid = function (len, radix) { - var chars = CHARS, uuid = [], i; + var chars = CHARS$1, uuid = [], i; radix = radix || chars.length; if (len) { @@ -11767,7 +12163,7 @@ class Random_uuid {} // Private array of chars to use // A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance // by minimizing calls to random() Random_uuid.uuidFast = function() { - var chars = CHARS, uuid = new Array(36), rnd=0, r; + var chars = CHARS$1, uuid = new Array(36), rnd=0, r; for (var i = 0; i < 36; i++) { if (i==8 || i==13 || i==18 || i==23) { uuid[i] = '-'; @@ -12210,280 +12606,56 @@ class PaymentsAsyncClient { * @private */ onResult_(response) { - response - .then(result => { - PayFrameHelper.postMessage({ - 'eventType': PostMessageEventType.LOG_LOAD_PAYMENT_DATA_API, - 'clientLatencyStartMs': this.loadPaymentDataApiStartTimeMs_, - }); - }) - .catch(result => { - if (result['errorCode']) { - PayFrameHelper.postMessage({ - 'eventType': PostMessageEventType.LOG_LOAD_PAYMENT_DATA_API, - 'error': /** @type {!PublicErrorCode} */ (result['errorCode']), - }); - } else { - // If user closes window we don't get a error code - PayFrameHelper.postMessage({ - 'eventType': PostMessageEventType.LOG_LOAD_PAYMENT_DATA_API, - 'error': PublicErrorCode.BUYER_CANCEL, - }); - } - }); - this.onPaymentResponse_(response); - } - - /** - * @param {!PaymentDataRequest} paymentDataRequest - * @return {!PaymentDataRequest} - * @private - */ - assignInternalParams_(paymentDataRequest) { - const internalParam = { - 'startTimeMs': Date.now(), - 'googleTransactionId': PaymentsAsyncClient.googleTransactionId_, - }; - paymentDataRequest['i'] = paymentDataRequest['i'] ? - Object.assign(internalParam, paymentDataRequest['i']) : - internalParam; - return paymentDataRequest; - } -} - - -/** - * Whether the request specifies that the native support has to be disabled. - * - * @param {!IsReadyToPayRequest|!PaymentDataRequest} request - * @return {boolean} - */ -function isNativeDisabledInRequest(request) { - return (request['i'] && request['i']['disableNative']) === true; -} - -/** - * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview - * - * Client-side experiments in SwG. - * - * The experiments can be set in a few different ways: - * 1. By gulp build rules using `--experiments=${experimentsString}` argument. - * 2. By `#swg.experiments=${experimentsString}` parameter in the URL's - * fragment. - * 3. By `swg.configure({experiments: [array]})` call. - * - * The `${experimentsString}` is defined as following: - * - experimentString = (experimentSpec,)* - * - experimentSpec = experimentId | experimentId '=' num100 ('c')? - * - * Some examples: - * - `A,B` - defines two experiments "A" and "B" that will be turned on. - * - `A:100,B:100` - the same: "A" and "B" will be turned on. - * - `A:0` - the experiment "A" will be disabled. - * - `A:1` - enable the experiment "A" in 1% of impressions. - * - `A:10c` - enable the experiment "A" in 10% of impressions with 10% - * control. In this case, 20% of the impressions will be split into two - * categories: experiment and control. Notice, a control can be requested - * only for the fraction under 20%. - */ - -/** - * @enum {string} - */ -const Selection = { - EXPERIMENT: 'e', - CONTROL: 'c', -}; - -/** - * A comma-separated set of experiments. - * @type {string} - */ -let experimentsString = ''; - -/** - * A parsed map of experiments. - * @type {?Object} - */ -let experimentMap = null; - -/** - * Ensures that the experiments have been initialized and returns them. - * @param {!Window} win - * @return {!Object} - */ -function getExperiments(win) { - if (!experimentMap) { - experimentMap = {}; - let combinedExperimentString = experimentsString; - try { - const query = parseQueryString$1(win.location.hash); - const experimentStringFromHash = query['swg.experiments']; - if (experimentStringFromHash) { - combinedExperimentString += ',' + experimentStringFromHash; - } - } catch (e) { - // Ignore: experiment parsing cannot block runtime. - ErrorUtils.throwAsync(e); - } - - // Format: - // - experimentString = (experimentSpec,)* - combinedExperimentString.split(',').forEach(s => { - s = s.trim(); - if (!s) { - return; - } - try { - parseSetExperiment(win, experimentMap, s); - } catch (e) { - // Ignore: experiment parsing cannot block runtime. - ErrorUtils.throwAsync(e); - } - }); - } - return experimentMap; -} - -/** - * @param {!Window} win - * @param {?Object} experimentMap - * @param {string} spec - */ -function parseSetExperiment(win, experimentMap, spec) { - // Format: - // - experimentSpec = experimentId | experimentId '=' num100 ('c')? - let experimentId; - let fraction; - let control = false; - const eq = spec.indexOf(':'); - if (eq == -1) { - experimentId = spec; - fraction = 100; - control = false; - } else { - experimentId = spec.substring(0, eq).trim(); - spec = spec.substring(eq + 1); - if (spec.substring(spec.length - 1) == Selection.CONTROL) { - control = true; - spec = spec.substring(0, spec.length - 1); - } - fraction = parseInt(spec, 10); - } - if (isNaN(fraction)) { - throw new Error('invalid fraction'); - } - - // Calculate "on"/"off". - let on; - if (fraction > 99) { - // Explicitly "on". - on = true; - } else if (fraction < 1) { - // Explicitly "off". - on = false; - } else if (win.sessionStorage) { - // Fractional and possibly with the control. - // Note that: - // a. We can't do persistent experiments if storage is not available. - // b. We can't run control on more than 20%. - control = control && fraction <= 20; - try { - // Set fraction in the experiment to make it unlaunchable. - const storageKey = - 'subscribe.google.com:e:' + - experimentId + - ':' + - fraction + - (control ? 'c' : ''); - let selection = parseSelection(win.sessionStorage.getItem(storageKey)); - if (!selection) { - // Is experiment/control range? - if (win.Math.random() * 100 <= fraction * (control ? 2 : 1)) { - const inExperiment = control ? win.Math.random() <= 0.5 : true; - selection = inExperiment ? Selection.EXPERIMENT : Selection.CONTROL; - win.sessionStorage.setItem(storageKey, selection); - } - } - on = !!selection; - if (selection == Selection.CONTROL) { - experimentId = 'c-' + experimentId; - } - } catch (e) { - // Ignore: experiment parsing cannot block runtime. - on = false; - ErrorUtils.throwAsync(e); - } - } else { - on = false; + response + .then(result => { + PayFrameHelper.postMessage({ + 'eventType': PostMessageEventType.LOG_LOAD_PAYMENT_DATA_API, + 'clientLatencyStartMs': this.loadPaymentDataApiStartTimeMs_, + }); + }) + .catch(result => { + if (result['errorCode']) { + PayFrameHelper.postMessage({ + 'eventType': PostMessageEventType.LOG_LOAD_PAYMENT_DATA_API, + 'error': /** @type {!PublicErrorCode} */ (result['errorCode']), + }); + } else { + // If user closes window we don't get a error code + PayFrameHelper.postMessage({ + 'eventType': PostMessageEventType.LOG_LOAD_PAYMENT_DATA_API, + 'error': PublicErrorCode.BUYER_CANCEL, + }); + } + }); + this.onPaymentResponse_(response); } - experimentMap[experimentId] = on; + /** + * @param {!PaymentDataRequest} paymentDataRequest + * @return {!PaymentDataRequest} + * @private + */ + assignInternalParams_(paymentDataRequest) { + const internalParam = { + 'startTimeMs': Date.now(), + 'googleTransactionId': PaymentsAsyncClient.googleTransactionId_, + }; + paymentDataRequest['i'] = paymentDataRequest['i'] ? + Object.assign(internalParam, paymentDataRequest['i']) : + internalParam; + return paymentDataRequest; + } } -/** - * @param {?string} s - * @return {?Selection} - */ -function parseSelection(s) { - // Do a simple if-then to inline the whole Selection enum. - return s == Selection.EXPERIMENT - ? Selection.EXPERIMENT - : s == Selection.CONTROL - ? Selection.CONTROL - : null; -} /** - * Whether the specified experiment is on or off. - * @param {!Window} win - * @param {string} experimentId + * Whether the request specifies that the native support has to be disabled. + * + * @param {!IsReadyToPayRequest|!PaymentDataRequest} request * @return {boolean} */ -function isExperimentOn(win, experimentId) { - return getExperiments(win)[experimentId] || false; -} - -/** - * Toggles the experiment on or off. Returns the actual value of the experiment - * after toggling is done. - * @param {!Window} win - * @param {string} experimentId - * @param {boolean} on - */ -function setExperiment(win, experimentId, on) { - getExperiments(win)[experimentId] = on; -} - -/** - * @return {!Array} - */ -function getOnExperiments(win) { - const experimentMap = getExperiments(win); - const experiments = []; - for (const experiment in experimentMap) { - if (experimentMap[experiment]) { - experiments.push(experiment); - } - } - return experiments; +function isNativeDisabledInRequest(request) { + return (request['i'] && request['i']['disableNative']) === true; } /** @@ -13189,6 +13361,47 @@ class OffersFlow { ); } + /** + * @param {SkuSelectedResponse} response + * @private + */ + startPayFlow_(response) { + const sku = response.getSku(); + if (sku) { + this.eventManager_.logSwgEvent( + AnalyticsEvent.ACTION_OFFER_SELECTED, + true + ); + new PayStartFlow(this.deps_, sku).start(); + } + } + + /** + * @param {AlreadySubscribedResponse} response + * @private + */ + handleLinkRequest_(response) { + if (response.getSubscriberOrMember()) { + this.eventManager_.logSwgEvent( + AnalyticsEvent.ACTION_ALREADY_SUBSCRIBED, + true + ); + this.deps_.callbacks().triggerLoginRequest({ + linkRequested: !!response.getLinkRequested(), + }); + } + } + + /** + * @param {ViewSubscriptionsResponse} response + * @private + */ + startNativeFlow_(response) { + if (response.getNative()) { + this.deps_.callbacks().triggerSubscribeRequest(); + } + } + /** * Starts the offers flow or alreadySubscribed flow. * @return {!Promise} @@ -13199,27 +13412,45 @@ class OffersFlow { this.activityIframeView_.onCancel(() => { this.deps_.callbacks().triggerFlowCanceled(SubscriptionFlows.SHOW_OFFERS); }); - - // If result is due to OfferSelection, redirect to payments. - this.activityIframeView_.onMessageDeprecated(result => { - if (result['alreadySubscribed']) { - this.deps_.callbacks().triggerLoginRequest({ - linkRequested: !!result['linkRequested'], - }); - return; - } - if (result['sku']) { - new PayStartFlow( - this.deps_, - /** @type {string} */ (result['sku']) - ).start(); - return; - } - if (result['native']) { - this.deps_.callbacks().triggerSubscribeRequest(); - return; - } - }); + if (isExperimentOn(this.win_, ExperimentFlags.HEJIRA)) { + this.activityIframeView_.on( + SkuSelectedResponse, + this.startPayFlow_.bind(this) + ); + this.activityIframeView_.on( + AlreadySubscribedResponse, + this.handleLinkRequest_.bind(this) + ); + this.activityIframeView_.on( + ViewSubscriptionsResponse, + this.startNativeFlow_.bind(this) + ); + } else { + // If result is due to OfferSelection, redirect to payments. + this.activityIframeView_.onMessageDeprecated(result => { + if (result['alreadySubscribed']) { + const alreadySubscribedResponse = new AlreadySubscribedResponse(); + alreadySubscribedResponse.setSubscriberOrMember(true); + if (result['linkRequested']) { + alreadySubscribedResponse.setLinkRequested(true); + } + this.handleLinkRequest_(alreadySubscribedResponse); + return; + } + if (result['sku']) { + const skuSelectedResponse = new SkuSelectedResponse(); + skuSelectedResponse.setSku(result['sku']); + this.startPayFlow_(skuSelectedResponse); + return; + } + if (result['native']) { + const viewSubscriptionsResponse = new ViewSubscriptionsResponse(); + viewSubscriptionsResponse.setNative(true); + this.startNativeFlow_(viewSubscriptionsResponse); + return; + } + }); + } this.eventManager_.logSwgEvent(AnalyticsEvent.IMPRESSION_OFFERS); @@ -13248,6 +13479,9 @@ class SubscribeOptionFlow { /** @private @const {!../components/dialog-manager.DialogManager} */ this.dialogManager_ = deps.dialogManager(); + /** @private @const {!../runtime/client-event-manager.ClientEventManager} */ + this.eventManager_ = deps.eventManager(); + /** @private @const {!ActivityIframeView} */ this.activityIframeView_ = new ActivityIframeView( deps.win(), @@ -13278,32 +13512,51 @@ class SubscribeOptionFlow { .callbacks() .triggerFlowCanceled(SubscriptionFlows.SHOW_SUBSCRIBE_OPTION); }); - - this.activityIframeView_.onMessageDeprecated(data => { - this.maybeOpenOffersFlow_(data); - }); + if (isExperimentOn(this.deps_.win(), ExperimentFlags.HEJIRA)) { + this.activityIframeView_.on( + SubscribeResponse, + this.maybeOpenOffersFlow_.bind(this) + ); + } else { + this.activityIframeView_.onMessageDeprecated(data => { + const response = new SubscribeResponse(); + if (data['subscribe']) { + response.setSubscribe(true); + } + this.maybeOpenOffersFlow_(response); + }); + } this.activityIframeView_.acceptResult().then( result => { - this.maybeOpenOffersFlow_(result.data); + const data = result.data; + const response = new SubscribeResponse(); + if (data['subscribe']) { + response.setSubscribe(true); + } + this.maybeOpenOffersFlow_(response); }, reason => { this.dialogManager_.completeView(this.activityIframeView_); throw reason; } ); + this.eventManager_.logSwgEvent( + AnalyticsEvent.IMPRESSION_CLICK_TO_SHOW_OFFERS + ); return this.dialogManager_.openView(this.activityIframeView_); } /** - * @param {*} data + * @param {SubscribeResponse} response * @private */ - maybeOpenOffersFlow_(data) { - if (data && data['subscribe']) { + maybeOpenOffersFlow_(response) { + if (response.getSubscribe()) { const options = this.options_ || {}; if (options.isClosable == undefined) { options.isClosable = OFFERS_VIEW_CLOSABLE; } + this.eventManager_.logSwgEvent(AnalyticsEvent.ACTION_VIEW_OFFERS, true); new OffersFlow(this.deps_, options).start(); } } @@ -13334,6 +13587,9 @@ class AbbrvOfferFlow { /** @private @const {!../components/dialog-manager.DialogManager} */ this.dialogManager_ = deps.dialogManager(); + /** @private @const {!../runtime/client-event-manager.ClientEventManager} */ + this.eventManager_ = deps.eventManager(); + /** @private @const {!ActivityIframeView} */ this.activityIframeView_ = new ActivityIframeView( this.win_, @@ -13351,6 +13607,22 @@ class AbbrvOfferFlow { ); } + /** + * @param {AlreadySubscribedResponse} response + * @private + */ + handleLinkRequest_(response) { + if (response.getSubscriberOrMember()) { + this.eventManager_.logSwgEvent( + AnalyticsEvent.ACTION_ALREADY_SUBSCRIBED, + true + ); + this.deps_.callbacks().triggerLoginRequest({ + linkRequested: !!response.getLinkRequested(), + }); + } + } + /** * Starts the offers flow * @return {!Promise} @@ -13367,14 +13639,22 @@ class AbbrvOfferFlow { }); // If the user is already subscribed, trigger login flow - this.activityIframeView_.onMessageDeprecated(data => { - if (data['alreadySubscribed']) { - this.deps_.callbacks().triggerLoginRequest({ - linkRequested: !!data['linkRequested'], - }); - return; - } - }); + if (isExperimentOn(this.win_, ExperimentFlags.HEJIRA)) { + this.activityIframeView_.on( + AlreadySubscribedResponse, + this.handleLinkRequest_.bind(this) + ); + } else { + this.activityIframeView_.onMessageDeprecated(data => { + if (data['alreadySubscribed']) { + const alreadySubscrbiedResponse = new AlreadySubscribedResponse(); + alreadySubscrbiedResponse.setSubscriberOrMember(true); + alreadySubscrbiedResponse.setLinkRequested(data['linkRequested']); + this.handleLinkRequest_(alreadySubscrbiedResponse); + return; + } + }); + } // If result is due to requesting offers, redirect to offers flow this.activityIframeView_.acceptResult().then(result => { if (result.data['viewOffers']) { @@ -13382,6 +13662,7 @@ class AbbrvOfferFlow { if (options.isClosable == undefined) { options.isClosable = OFFERS_VIEW_CLOSABLE; } + this.eventManager_.logSwgEvent(AnalyticsEvent.ACTION_VIEW_OFFERS, true); new OffersFlow(this.deps_, options).start(); return; } @@ -13393,6 +13674,10 @@ class AbbrvOfferFlow { } }); + this.eventManager_.logSwgEvent( + AnalyticsEvent.IMPRESSION_CLICK_TO_SHOW_OFFERS_OR_ALREADY_SUBSCRIBED + ); + return this.dialogManager_.openView(this.activityIframeView_); } } @@ -13568,49 +13853,6 @@ function storageKey(key) { return PREFIX + ':' + key; } -/** @license -Math.uuid.js (v1.4) -http://www.broofa.com -mailto:robert@broofa.com -Copyright (c) 2010 Robert Kieffer -Dual licensed under the MIT and GPL licenses. -*/ - -/* - * Generate a random uuid. - * EXAMPLES: - * returns RFC4122, version 4 ID - * >>> uuidFast() - * "92329D39-6F5C-4520-ABFC-AAB64544E172" - * - * Note: The original code was modified to ES6 and removed other functions, - * since we are only using uuidFast(). - */ - -const CHARS$1 = - '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); - -function uuidFast() { - const uuid = new Array(36); - let rnd = 0; - let r; - for (let i = 0; i < 36; i++) { - if (i === 8 || i === 13 || i === 18 || i === 23) { - uuid[i] = '-'; - } else if (i === 14) { - uuid[i] = '4'; - } else { - if (rnd <= 0x02) { - rnd = 0x2000000 + (Math.random() * 0x1000000) | 0; - } - r = rnd & 0xf; - rnd = rnd >> 4; - uuid[i] = CHARS$1[(i == 19) ? (r & 0x3) | 0x8 : r]; - } - } - return uuid.join(''); -} - /** * Copyright 2019 The Subscribe with Google Authors. All Rights Reserved. * @@ -13904,7 +14146,7 @@ class AnalyticsService { */ this.context_ = new AnalyticsContext(); - this.context_.setTransactionId(uuidFast()); + this.context_.setTransactionId(getUuid()); /** @private {?Promise} */ this.serviceReady_ = null; @@ -14091,6 +14333,12 @@ class AnalyticsService { * @param {!../api/client-event-manager-api.ClientEvent} event */ handleClientEvent_(event) { + //this event is just used to communicate information internally. It should + //not be reported to the SwG analytics service. + if (event.eventType === AnalyticsEvent.EVENT_SUBSCRIPTION_STATE) { + return; + } + if ( ClientEventManager.isPublisherEvent(event) && !this.shouldLogPublisherEvents_() @@ -14198,15 +14446,16 @@ class PropensityServer { * is available, publication ID is therefore used * in constructor for the server interface. * @param {!Window} win - * @param {string} publicationId - * @param {!../api/client-event-manager-api.ClientEventManagerApi} eventManager + * @param {!./deps.DepsDef} deps * @param {!./fetcher.Fetcher} fetcher */ - constructor(win, publicationId, eventManager, fetcher) { + constructor(win, deps, fetcher) { /** @private @const {!Window} */ this.win_ = win; + /** @private @const {!./deps.DepsDef} */ + this.deps_ = deps; /** @private @const {string} */ - this.publicationId_ = publicationId; + this.publicationId_ = this.deps_.pageConfig().getPublicationId(); /** @private {?string} */ this.clientId_ = null; /** @private @const {!./fetcher.Fetcher} */ @@ -14214,7 +14463,9 @@ class PropensityServer { /** @private @const {number} */ this.version_ = 1; - eventManager.registerEventListener(this.handleClientEvent_.bind(this)); + this.deps_ + .eventManager() + .registerEventListener(this.handleClientEvent_.bind(this)); // TODO(mborof): b/133519525 /** @private @const {!boolean} */ @@ -14222,9 +14473,6 @@ class PropensityServer { win, ExperimentFlags.LOG_SWG_TO_PROPENSITY ); - - /** @private {!boolean} */ - this.logSwgEventsConfig_ = false; } /** @@ -14308,6 +14556,17 @@ class PropensityServer { * @param {!../api/client-event-manager-api.ClientEvent} event */ handleClientEvent_(event) { + /** + * Does a live check of the config because we don't know when publisher called to + * enable (it may be after a consent dialog) + */ + if ( + !(this.deps_.config().enablePropensity && this.logSwgEventsExperiment_) && + event.eventOriginator !== EventOriginator.PROPENSITY_CLIENT + ) { + return; + } + if (event.eventType === AnalyticsEvent.EVENT_SUBSCRIPTION_STATE) { this.sendSubscriptionState( event.additionalParameters['state'], @@ -14319,12 +14578,6 @@ class PropensityServer { if (propEvent == null) { return; } - if ( - !(this.logSwgEventsExperiment_ && this.logSwgEventsConfig_) && - event.eventOriginator !== EventOriginator.PROPENSITY_CLIENT - ) { - return; - } let additionalParameters = event.additionalParameters; if (isBoolean(event.isFromUserAction)) { @@ -14416,10 +14669,6 @@ class PropensityServer { return this.parsePropensityResponse_(response); }); } - - enableLoggingSwgEvents() { - this.logSwgEventsConfig_ = true; - } } /** @@ -14444,23 +14693,21 @@ class PropensityServer { class Propensity { /** * @param {!Window} win - * @param {!../model/page-config.PageConfig} pageConfig - * @param {!../api/client-event-manager-api.ClientEventManagerApi} eventManager + * @param {!./deps.DepsDef} deps * @param {!./fetcher.Fetcher} fetcher + * + * IMPORTANT: deps may not be full initialized config and pageConfig are + * available immediately, other function should be gated on a ready promise. + * #TODO(jpettitt) switch refactor to take out the win and use deps to get win */ - constructor(win, pageConfig, eventManager, fetcher) { + constructor(win, deps, fetcher) { /** @private @const {!Window} */ this.win_ = win; /** @private {PropensityServer} */ - this.propensityServer_ = new PropensityServer( - win, - pageConfig.getPublicationId(), - eventManager, - fetcher - ); + this.propensityServer_ = new PropensityServer(win, deps, fetcher); /** @private @const {!../api/client-event-manager-api.ClientEventManagerApi} */ - this.eventManager_ = eventManager; + this.eventManager_ = deps.eventManager(); } /** @override */ @@ -14535,14 +14782,6 @@ class Propensity { additionalParameters: data, }); } - - /** - * Enables logging events generated by the Subscribe with Google codebase - * to the Propensity to Subscribe server. - */ - enableLoggingSwgEvents() { - this.propensityServer_.enableLoggingSwgEvents(); - } } /** @@ -14709,14 +14948,6 @@ class ConfiguredRuntime { /** @private @const {!Fetcher} */ this.fetcher_ = opt_integr.fetcher || new XhrFetcher(this.win_); - /** @private @const {!Propensity} */ - this.propensityModule_ = new Propensity( - this.win_, - this.pageConfig_, - this.eventManager_, - this.fetcher_ - ); - /** @private @const {!Storage} */ this.storage_ = new Storage(this.win_); @@ -14751,7 +14982,14 @@ class ConfiguredRuntime { this.win_, this.pageConfig_, this.fetcher_, - this + this // See note about 'this' above + ); + + /** @private @const {!Propensity} */ + this.propensityModule_ = new Propensity( + this.win_, + this, // See note about 'this' above + this.fetcher_ ); /** @private @const {!OffersApi} */ @@ -14852,30 +15090,39 @@ class ConfiguredRuntime { */ configure_(config) { // Validate first. - let error = null; + let error = ''; for (const k in config) { const v = config[k]; - if (k == 'windowOpenMode') { - if (v != WindowOpenMode.AUTO && v != WindowOpenMode.REDIRECT) { - error = 'Unknown windowOpenMode: ' + v; - } - } else if (k == 'experiments') { - v.forEach(experiment => setExperiment(this.win_, experiment, true)); - } else if (k == 'analyticsMode') { - if (v != AnalyticsMode.DEFAULT && v != AnalyticsMode.IMPRESSIONS) { - error = 'Unknown analytics mode: ' + v; - } - } else if (k == 'enableSwgAnalytics') { - if (!isBoolean(v)) { - error = 'Unknown enableSwgAnalytics value: ' + v; - } - } else { - error = 'Unknown config property: ' + k; + switch (k) { + case 'windowOpenMode': + if (v != WindowOpenMode.AUTO && v != WindowOpenMode.REDIRECT) { + error = 'Unknown windowOpenMode: ' + v; + } + break; + case 'experiments': + v.forEach(experiment => setExperiment(this.win_, experiment, true)); + break; + case 'analyticsMode': + if (v != AnalyticsMode.DEFAULT && v != AnalyticsMode.IMPRESSIONS) { + error = 'Unknown analytics mode: ' + v; + } + break; + case 'enableSwgAnalytics': + if (!isBoolean(v)) { + error = 'Unknown enableSwgAnalytics value: ' + v; + } + break; + case 'enablePropensity': + if (!isBoolean(v)) { + error = 'Unknown enablePropensity value: ' + v; + } + break; + default: + error = 'Unknown config property: ' + k; } } - if (error) { - throw new Error(error); - } + // Throw error string if it's not null + assert(!error, error); // Assign. Object.assign(this.config_, config); }