{"version":3,"file":"swg.js","sources":["src/constants.ts","src/components/view.ts","src/utils/activity-utils.ts","src/utils/dom.ts","src/utils/errors.ts","src/ui/activity-iframe-view.ts","src/proto/api_messages.ts","src/api/subscriptions.ts","src/utils/log.ts","src/utils/bytes.ts","src/utils/json.ts","src/utils/jwt.ts","src/api/subscribe-response.ts","src/utils/constants.ts","src/api/user-data.ts","src/utils/url.ts","src/runtime/services.ts","src/runtime/pay-flow.ts","src/runtime/offers-flow.ts","../node_modules/web-activities/activity-ports.js","src/components/activities.ts","src/api/client-event-manager-api.ts","src/utils/types.ts","src/runtime/client-event-manager.ts","src/runtime/experiments.ts","src/utils/string.ts","src/utils/style.ts","src/utils/date-utils.ts","src/runtime/analytics-service.ts","src/utils/random.ts","src/runtime/experiment-flags.ts","src/i18n/swg-strings.ts","src/runtime/smart-button-api.ts","src/utils/i18n.ts","src/runtime/button-api.ts","src/runtime/callbacks.ts","src/model/attribution-params.ts","src/model/auto-prompt-config.ts","src/model/client-config.ts","src/runtime/client-config-manager.ts","src/runtime/contributions-flow.ts","src/ui/ui-css.ts","src/api/deferred-account-creation.ts","src/runtime/deferred-account-flow.ts","src/utils/document-ready.ts","src/model/doc.ts","src/components/friendly-iframe.ts","src/utils/animation.ts","src/components/graypane.ts","src/ui/loading-view.ts","src/components/dialog.ts","src/components/dialog-manager.ts","src/api/basic-subscriptions.ts","src/api/logger-api.ts","src/runtime/event-type-mapping.ts","src/runtime/google-analytics-event-listener.ts","src/ui/toast.ts","src/runtime/audience-action-flow.ts","src/utils/assets.ts","src/runtime/audience-action-local-ui.ts","src/api/intervention-type.ts","src/runtime/fetcher.ts","../node_modules/safevalues/dist/mjs/internals/secrets.js","../node_modules/safevalues/dist/mjs/internals/trusted_types.js","../node_modules/safevalues/dist/mjs/internals/html_impl.js","../node_modules/safevalues/dist/mjs/builders/html_builders.js","src/runtime/audience-action-local-flow.ts","src/api/available-intervention.ts","src/api/entitlements.ts","src/api/metering.ts","src/runtime/meter-toast-api.ts","src/runtime/extended-access/utils.ts","src/runtime/entitlements-manager.ts","src/runtime/jserror.ts","src/runtime/link-accounts-flow.ts","src/runtime/logger.ts","src/runtime/login-notification-api.ts","src/runtime/login-prompt-api.ts","src/runtime/offers-api.ts","src/model/page-config.ts","src/model/page-config-resolver.ts","../third_party/gpay/src/constants.js","../third_party/gpay/src/post_message_service.js","../third_party/gpay/src/pay_frame_helper.js","../third_party/gpay/src/payments_request_delegate.js","../third_party/gpay/src/graypane.js","../third_party/gpay/src/validator.js","../third_party/gpay/src/payments_web_activity_delegate.js","../third_party/gpay/src/element_injector.js","../third_party/gpay/src/upi_handler.js","../third_party/gpay/third_party/random_uuid/Random.uuid.js","../third_party/gpay/src/payjs_async.js","../third_party/gpay/src/utils.js","src/utils/preconnect.ts","src/runtime/pay-client.ts","src/api/propensity-api.ts","src/runtime/propensity-server.ts","src/runtime/propensity.ts","src/runtime/storage.ts","src/runtime/subscription-linking-flow.ts","src/runtime/wait-for-subscription-lookup-api.ts","src/runtime/runtime.ts","src/main.ts"],"sourcesContent":["/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * This file contains a set of variables the compiler will overwrite.\n * The initial values are just placeholders for tests.\n */\n\nexport const FRONTEND = 'https://news.google.com';\nexport const PAY_ENVIRONMENT = 'TEST';\nexport const PLAY_ENVIRONMENT = 'STAGING';\nexport const FRONTEND_CACHE = 'nocache';\nexport const INTERNAL_RUNTIME_VERSION = '0.0.0';\nexport const ASSETS = '/assets';\nexport const ADS_SERVER = 'https://pubads.g.doubleclick.net';\nexport const EXPERIMENTS = '';\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {Dialog} from './dialog';\n\n/**\n * abstract View Class. Used to render the content within the Dialog. The\n * extended class has actual content.\n */\nexport abstract class View {\n /**\n * Empty constructor.\n */\n constructor() {}\n\n /**\n * Gets the iframe element.\n */\n abstract getElement(): HTMLIFrameElement;\n\n abstract init(dialog: Dialog): Promise;\n\n /**\n * Resizes the content.\n */\n resized() {\n // Do nothing by default. Override if needed.\n }\n\n /**\n * Accept the result.\n */\n abstract whenComplete(): Promise;\n\n abstract shouldFadeBody(): boolean;\n\n abstract hasLoadingIndicator(): boolean;\n\n abstract shouldAnimateFade(): boolean;\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {ActivityPortDef} from '../components/activities';\n\nexport async function acceptPortResultData(\n port: ActivityPortDef,\n requireOrigin: string,\n requireOriginVerified: boolean,\n requireSecureChannel: boolean\n): Promise {\n const result = await port.acceptResult();\n if (\n result.origin != requireOrigin ||\n (requireOriginVerified && !result.originVerified) ||\n (requireSecureChannel && !result.secureChannel)\n ) {\n throw new Error('channel mismatch');\n }\n return result.data;\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {Doc} from '../model/doc';\n\nexport const styleType = 'text/css';\n\n/**\n * Add attributes to an element.\n */\nfunction addAttributesToElement(\n element: Element,\n attributes: {[key: string]: string} = {}\n) {\n for (const [key, value] of Object.entries(attributes)) {\n element.setAttribute(key, value);\n }\n}\n\n/**\n * Create a new element on document with specified tagName and attributes.\n */\nexport function createElement(\n doc: Document,\n tagName: string,\n attributes: {[key: string]: string},\n content?: string\n): T {\n const element = doc.createElement(tagName) as T;\n\n addAttributesToElement(element, attributes);\n\n if (content) {\n element.textContent = content;\n }\n\n return element;\n}\n\n/**\n * Removes the element.\n */\nexport function removeElement(element: Element) {\n if (element.parentElement) {\n element.parentElement.removeChild(element);\n }\n}\n\n/**\n * Removes all children from the parent element.\n */\nexport function removeChildren(parent: Element) {\n parent.textContent = '';\n}\n\n/**\n * Injects the provided styles in the HEAD section of the document.\n * @param doc The document object.\n * @param styleText The style string.\n */\nexport function injectStyleSheet(doc: Doc, styleText: string): Element {\n const styleElement: HTMLStyleElement = createElement(\n doc.getWin().document,\n 'style',\n {\n 'type': styleType,\n }\n );\n styleElement.textContent = styleText;\n doc.getHead()?.appendChild(styleElement);\n return styleElement;\n}\n\n/**\n * Whether the node has a next node in the document order.\n * This means either:\n * a. The node itself has a nextSibling.\n * b. Any of the node ancestors has a nextSibling.\n */\nexport function hasNextNodeInDocumentOrder(node: Node): boolean {\n let currentNode: Node | null = node;\n do {\n if (currentNode.nextSibling) {\n return true;\n }\n } while ((currentNode = currentNode.parentNode));\n return false;\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Whether the specified error is an AbortError type.\n * See https://heycam.github.io/webidl/#aborterror.\n */\nexport function isCancelError(error: Error): boolean {\n return error?.name === 'AbortError';\n}\n\n/**\n * Creates an Error of AbortError type.\n * See https://heycam.github.io/webidl/#aborterror.\n */\nexport function createCancelError(message: string): Error {\n const error = new Error('AbortError: ' + message);\n error.name = 'AbortError';\n return error;\n}\n\n/**\n * A set of error utilities combined in a class to allow easy stubbing in tests.\n */\nexport class ErrorUtils {\n static throwAsync(error: Error) {\n setTimeout(() => {\n throw error;\n });\n }\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {ActivityIframePort, ActivityPorts} from '../components/activities';\nimport {ActivityResult} from 'web-activities/activity-ports';\nimport {Dialog} from '../components/dialog';\nimport {Message} from '../proto/api_messages';\nimport {View} from '../components/view';\nimport {acceptPortResultData} from '../utils/activity-utils';\nimport {createElement} from '../utils/dom';\nimport {isCancelError} from '../utils/errors';\n\nconst iframeAttributes = {\n 'frameborder': '0',\n 'scrolling': 'no',\n};\n\n/**\n * Class to build and render Activity iframe view.\n */\nexport class ActivityIframeView extends View {\n private readonly doc_: Document;\n private readonly iframe_: HTMLIFrameElement;\n private port_: ActivityIframePort | null = null;\n private portResolver_?: (\n port: ActivityIframePort | Promise\n ) => void;\n private readonly portPromise_: Promise;\n\n constructor(\n private readonly win_: Window,\n private readonly activityPorts_: ActivityPorts,\n private readonly src_: string,\n /** Additional data to be passed to the iframe. */\n private readonly args_: {[key: string]: string},\n private readonly shouldFadeBody_: boolean = false,\n private readonly hasLoadingIndicator_: boolean = false,\n private readonly shouldAnimateFade_: boolean = true\n ) {\n super();\n\n this.doc_ = this.win_.document;\n\n this.iframe_ = createElement(this.doc_, 'iframe', iframeAttributes);\n\n this.portPromise_ = new Promise((resolve) => {\n this.portResolver_ = resolve;\n });\n }\n\n getElement() {\n return this.iframe_;\n }\n\n async init(dialog: Dialog) {\n const port = await this.activityPorts_.openIframe(\n this.iframe_,\n this.src_,\n this.args_\n );\n return this.onOpenIframeResponse_(port, dialog);\n }\n\n /**\n * Returns if document should fade for this view.\n */\n shouldFadeBody(): boolean {\n return this.shouldFadeBody_;\n }\n\n /**\n * Returns if the view shows loading indicator.\n */\n hasLoadingIndicator(): boolean {\n return this.hasLoadingIndicator_;\n }\n\n private onOpenIframeResponse_(\n port: ActivityIframePort,\n dialog: Dialog\n ): Promise {\n this.port_ = port;\n this.portResolver_!(port);\n\n this.port_.onResizeRequest((height) => {\n dialog.resizeView(this, height);\n });\n\n return this.port_.whenReady();\n }\n\n private getPortPromise_(): Promise {\n return this.portPromise_;\n }\n\n async on(\n message: new (data?: unknown[], includesLabel?: boolean) => T,\n callback: (p1: T) => void\n ): Promise {\n const port = await this.getPortPromise_();\n port.on(message, callback);\n }\n\n async execute(request: Message) {\n const port = await this.getPortPromise_();\n port.execute(request);\n }\n\n /**\n * Accepts results from the caller.\n */\n async acceptResult(): Promise {\n const port = await this.getPortPromise_();\n return port.acceptResult();\n }\n\n /**\n * Accepts results from the caller and verifies origin.\n */\n async acceptResultAndVerify(\n requireOrigin: string,\n requireOriginVerified: boolean,\n requireSecureChannel: boolean\n ): Promise {\n const port = await this.getPortPromise_();\n return acceptPortResultData(\n port,\n requireOrigin,\n requireOriginVerified,\n requireSecureChannel\n );\n }\n\n /**\n * Completes the flow.\n */\n async whenComplete(): Promise {\n await this.acceptResult();\n }\n\n async onCancel(callback: () => void): Promise {\n try {\n await this.acceptResult();\n } catch (err) {\n if (isCancelError(err as Error)) {\n callback();\n }\n throw err;\n }\n }\n\n resized() {\n if (this.port_) {\n this.port_.resized();\n }\n }\n\n shouldAnimateFade(): boolean {\n return this.shouldAnimateFade_;\n }\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @fileoverview Protos for SwG client/iframe messaging\n * Auto generated, do not edit\n */\n\n/* tslint:disable:enforce-name-casing */\n/* tslint:disable:strip-private-property-underscore */\n\n// clang-format off\n\n/** Carries information relating to RRM. */\nexport interface Message {\n label(): string;\n\n toArray(includeLabel?: boolean): unknown[];\n}\n\n/** Constructor for a message that carries information relating to RRM. */\ninterface MessageConstructor {\n new (data?: unknown[], includesLabel?: boolean): Message;\n}\n\n/** */\nexport enum ActionType {\n ACTION_TYPE_UNKNOWN = 0,\n ACTION_TYPE_RELOAD_PAGE = 1,\n ACTION_TYPE_UPDATE_COUNTER = 2,\n}\n\n/** */\nexport enum AnalyticsEvent {\n UNKNOWN = 0,\n IMPRESSION_PAYWALL = 1,\n IMPRESSION_AD = 2,\n IMPRESSION_OFFERS = 3,\n IMPRESSION_SUBSCRIBE_BUTTON = 4,\n IMPRESSION_SMARTBOX = 5,\n IMPRESSION_SWG_BUTTON = 6,\n IMPRESSION_CLICK_TO_SHOW_OFFERS = 7,\n IMPRESSION_CLICK_TO_SHOW_OFFERS_OR_ALREADY_SUBSCRIBED = 8,\n IMPRESSION_SUBSCRIPTION_COMPLETE = 9,\n IMPRESSION_ACCOUNT_CHANGED = 10,\n IMPRESSION_PAGE_LOAD = 11,\n IMPRESSION_LINK = 12,\n IMPRESSION_SAVE_SUBSCR_TO_GOOGLE = 13,\n IMPRESSION_GOOGLE_UPDATED = 14,\n IMPRESSION_SHOW_OFFERS_SMARTBOX = 15,\n IMPRESSION_SHOW_OFFERS_SWG_BUTTON = 16,\n IMPRESSION_SELECT_OFFER_SMARTBOX = 17,\n IMPRESSION_SELECT_OFFER_SWG_BUTTON = 18,\n IMPRESSION_SHOW_CONTRIBUTIONS_SWG_BUTTON = 19,\n IMPRESSION_SELECT_CONTRIBUTION_SWG_BUTTON = 20,\n IMPRESSION_METER_TOAST = 21,\n IMPRESSION_REGWALL = 22,\n IMPRESSION_SHOWCASE_REGWALL = 23,\n IMPRESSION_SWG_SUBSCRIPTION_MINI_PROMPT = 24,\n IMPRESSION_SWG_CONTRIBUTION_MINI_PROMPT = 25,\n IMPRESSION_CONTRIBUTION_OFFERS = 26,\n IMPRESSION_TWG_COUNTER = 27,\n IMPRESSION_TWG_SITE_SUPPORTER_WALL = 28,\n IMPRESSION_TWG_PUBLICATION = 29,\n IMPRESSION_TWG_STATIC_BUTTON = 30,\n IMPRESSION_TWG_DYNAMIC_BUTTON = 31,\n IMPRESSION_TWG_STICKER_SELECTION_SCREEN = 32,\n IMPRESSION_TWG_PUBLICATION_NOT_SET_UP = 33,\n IMPRESSION_REGWALL_OPT_IN = 34,\n IMPRESSION_NEWSLETTER_OPT_IN = 35,\n IMPRESSION_SUBSCRIPTION_OFFERS_ERROR = 36,\n IMPRESSION_CONTRIBUTION_OFFERS_ERROR = 37,\n IMPRESSION_TWG_SHORTENED_STICKER_FLOW = 38,\n IMPRESSION_SUBSCRIPTION_LINKING_LOADING = 39,\n IMPRESSION_SUBSCRIPTION_LINKING_COMPLETE = 40,\n IMPRESSION_SUBSCRIPTION_LINKING_ERROR = 41,\n IMPRESSION_SURVEY = 42,\n IMPRESSION_REGWALL_ERROR = 43,\n IMPRESSION_NEWSLETTER_ERROR = 44,\n IMPRESSION_SURVEY_ERROR = 45,\n IMPRESSION_METER_TOAST_ERROR = 46,\n IMPRESSION_MINI_PROMPT = 47,\n IMPRESSION_MINI_PROMPT_ERROR = 48,\n IMPRESSION_REWARDED_AD = 49,\n IMPRESSION_BYOP_NEWSLETTER_OPT_IN = 50,\n IMPRESSION_REWARDED_AD_ERROR = 51,\n IMPRESSION_HOSTED_PAGE_SUBSCRIPTION_OFFERS = 52,\n IMPRESSION_HOSTED_PAGE_CONTRIBUTION_OFFERS = 53,\n IMPRESSION_HOSTED_PAGE_SUBSCRIPTION_OFFERS_ERROR = 54,\n IMPRESSION_HOSTED_PAGE_CONTRIBUTION_OFFERS_ERROR = 55,\n IMPRESSION_BYO_CTA = 56,\n IMPRESSION_BYO_CTA_ERROR = 57,\n ACTION_SUBSCRIBE = 1000,\n ACTION_PAYMENT_COMPLETE = 1001,\n ACTION_ACCOUNT_CREATED = 1002,\n ACTION_ACCOUNT_ACKNOWLEDGED = 1003,\n ACTION_SUBSCRIPTIONS_LANDING_PAGE = 1004,\n ACTION_PAYMENT_FLOW_STARTED = 1005,\n ACTION_OFFER_SELECTED = 1006,\n ACTION_SWG_BUTTON_CLICK = 1007,\n ACTION_VIEW_OFFERS = 1008,\n ACTION_ALREADY_SUBSCRIBED = 1009,\n ACTION_NEW_DEFERRED_ACCOUNT = 1010,\n ACTION_LINK_CONTINUE = 1011,\n ACTION_LINK_CANCEL = 1012,\n ACTION_GOOGLE_UPDATED_CLOSE = 1013,\n ACTION_USER_CANCELED_PAYFLOW = 1014,\n ACTION_SAVE_SUBSCR_TO_GOOGLE_CONTINUE = 1015,\n ACTION_SAVE_SUBSCR_TO_GOOGLE_CANCEL = 1016,\n ACTION_SWG_BUTTON_SHOW_OFFERS_CLICK = 1017,\n ACTION_SWG_BUTTON_SELECT_OFFER_CLICK = 1018,\n ACTION_SWG_BUTTON_SHOW_CONTRIBUTIONS_CLICK = 1019,\n ACTION_SWG_BUTTON_SELECT_CONTRIBUTION_CLICK = 1020,\n ACTION_USER_CONSENT_DEFERRED_ACCOUNT = 1021,\n ACTION_USER_DENY_DEFERRED_ACCOUNT = 1022,\n ACTION_DEFERRED_ACCOUNT_REDIRECT = 1023,\n ACTION_GET_ENTITLEMENTS = 1024,\n ACTION_METER_TOAST_SUBSCRIBE_CLICK = 1025,\n ACTION_METER_TOAST_EXPANDED = 1026,\n ACTION_METER_TOAST_CLOSED_BY_ARTICLE_INTERACTION = 1027,\n ACTION_METER_TOAST_CLOSED_BY_SWIPE_DOWN = 1028,\n ACTION_METER_TOAST_CLOSED_BY_X_CLICKED = 1029,\n ACTION_SWG_SUBSCRIPTION_MINI_PROMPT_CLICK = 1030,\n ACTION_SWG_CONTRIBUTION_MINI_PROMPT_CLICK = 1031,\n ACTION_SWG_SUBSCRIPTION_MINI_PROMPT_CLOSE = 1032,\n ACTION_SWG_CONTRIBUTION_MINI_PROMPT_CLOSE = 1033,\n ACTION_CONTRIBUTION_OFFER_SELECTED = 1034,\n ACTION_SHOWCASE_REGWALL_GSI_CLICK = 1035,\n ACTION_SHOWCASE_REGWALL_EXISTING_ACCOUNT_CLICK = 1036,\n ACTION_SUBSCRIPTION_OFFERS_CLOSED = 1037,\n ACTION_CONTRIBUTION_OFFERS_CLOSED = 1038,\n ACTION_TWG_STATIC_CTA_CLICK = 1039,\n ACTION_TWG_DYNAMIC_CTA_CLICK = 1040,\n ACTION_TWG_SITE_LEVEL_SUPPORTER_WALL_CTA_CLICK = 1041,\n ACTION_TWG_DIALOG_SUPPORTER_WALL_CTA_CLICK = 1042,\n ACTION_TWG_COUNTER_CLICK = 1043,\n ACTION_TWG_SITE_SUPPORTER_WALL_ALL_THANKS_CLICK = 1044,\n ACTION_TWG_PAID_STICKER_SELECTED_SCREEN_CLOSE_CLICK = 1045,\n ACTION_TWG_PAID_STICKER_SELECTION_CLICK = 1046,\n ACTION_TWG_FREE_STICKER_SELECTION_CLICK = 1047,\n ACTION_TWG_MINI_SUPPORTER_WALL_CLICK = 1048,\n ACTION_TWG_CREATOR_BENEFIT_CLICK = 1049,\n ACTION_TWG_FREE_TRANSACTION_START_NEXT_BUTTON_CLICK = 1050,\n ACTION_TWG_PAID_TRANSACTION_START_NEXT_BUTTON_CLICK = 1051,\n ACTION_TWG_STICKER_SELECTION_SCREEN_CLOSE_CLICK = 1052,\n ACTION_TWG_ARTICLE_LEVEL_SUPPORTER_WALL_CTA_CLICK = 1053,\n ACTION_REGWALL_OPT_IN_BUTTON_CLICK = 1054,\n ACTION_REGWALL_ALREADY_OPTED_IN_CLICK = 1055,\n ACTION_NEWSLETTER_OPT_IN_BUTTON_CLICK = 1056,\n ACTION_NEWSLETTER_ALREADY_OPTED_IN_CLICK = 1057,\n ACTION_REGWALL_OPT_IN_CLOSE = 1058,\n ACTION_NEWSLETTER_OPT_IN_CLOSE = 1059,\n ACTION_SHOWCASE_REGWALL_SIWG_CLICK = 1060,\n ACTION_TWG_CHROME_APP_MENU_ENTRY_POINT_CLICK = 1061,\n ACTION_TWG_DISCOVER_FEED_MENU_ENTRY_POINT_CLICK = 1062,\n ACTION_SHOWCASE_REGWALL_3P_BUTTON_CLICK = 1063,\n ACTION_SUBSCRIPTION_OFFERS_RETRY = 1064,\n ACTION_CONTRIBUTION_OFFERS_RETRY = 1065,\n ACTION_TWG_SHORTENED_STICKER_FLOW_STICKER_SELECTION_CLICK = 1066,\n ACTION_INITIATE_UPDATED_SUBSCRIPTION_LINKING = 1067,\n ACTION_SURVEY_SUBMIT_CLICK = 1068,\n ACTION_SURVEY_CLOSED = 1069,\n ACTION_SURVEY_DATA_TRANSFER = 1070,\n ACTION_REGWALL_PAGE_REFRESH = 1071,\n ACTION_NEWSLETTER_PAGE_REFRESH = 1072,\n ACTION_SURVEY_PAGE_REFRESH = 1073,\n ACTION_METER_TOAST_PAGE_REFRESH = 1074,\n ACTION_MINI_PROMPT_INTERACTION = 1075,\n ACTION_SURVEY_PREVIOUS_BUTTON_CLICK = 1076,\n ACTION_SURVEY_NEXT_BUTTON_CLICK = 1077,\n ACTION_REWARDED_AD_VIEW = 1078,\n ACTION_REWARDED_AD_CLOSE = 1079,\n ACTION_REWARDED_AD_CLOSE_AD = 1080,\n ACTION_REWARDED_AD_SIGN_IN = 1081,\n ACTION_REWARDED_AD_SUPPORT = 1082,\n ACTION_BACK_TO_HOMEPAGE = 1083,\n ACTION_BYOP_NEWSLETTER_OPT_IN_CLOSE = 1084,\n ACTION_BYOP_NEWSLETTER_OPT_IN_SUBMIT = 1085,\n ACTION_SUBSCRIPTION_LINKING_CLOSE = 1086,\n ACTION_BYO_CTA_CLOSE = 1087,\n ACTION_BYO_CTA_BUTTON_CLICK = 1088,\n EVENT_PAYMENT_FAILED = 2000,\n EVENT_REGWALL_OPT_IN_FAILED = 2001,\n EVENT_NEWSLETTER_OPT_IN_FAILED = 2002,\n EVENT_REGWALL_ALREADY_OPT_IN = 2003,\n EVENT_NEWSLETTER_ALREADY_OPT_IN = 2004,\n EVENT_SUBSCRIPTION_LINKING_FAILED = 2005,\n EVENT_SURVEY_ALREADY_SUBMITTED = 2006,\n EVENT_SURVEY_COMPLETION_RECORD_FAILED = 2007,\n EVENT_SURVEY_DATA_TRANSFER_FAILED = 2008,\n EVENT_BYO_CTA_COMPLETION_RECORD_FAILED = 2009,\n EVENT_CUSTOM = 3000,\n EVENT_CONFIRM_TX_ID = 3001,\n EVENT_CHANGED_TX_ID = 3002,\n EVENT_GPAY_NO_TX_ID = 3003,\n EVENT_GPAY_CANNOT_CONFIRM_TX_ID = 3004,\n EVENT_GOOGLE_UPDATED = 3005,\n EVENT_NEW_TX_ID = 3006,\n EVENT_UNLOCKED_BY_SUBSCRIPTION = 3007,\n EVENT_UNLOCKED_BY_METER = 3008,\n EVENT_NO_ENTITLEMENTS = 3009,\n EVENT_HAS_METERING_ENTITLEMENTS = 3010,\n EVENT_OFFERED_METER = 3011,\n EVENT_UNLOCKED_FREE_PAGE = 3012,\n EVENT_INELIGIBLE_PAYWALL = 3013,\n EVENT_UNLOCKED_FOR_CRAWLER = 3014,\n EVENT_TWG_COUNTER_VIEW = 3015,\n EVENT_TWG_SITE_SUPPORTER_WALL_VIEW = 3016,\n EVENT_TWG_STATIC_BUTTON_VIEW = 3017,\n EVENT_TWG_DYNAMIC_BUTTON_VIEW = 3018,\n EVENT_TWG_PRE_TRANSACTION_PRIVACY_SETTING_PRIVATE = 3019,\n EVENT_TWG_POST_TRANSACTION_SETTING_PRIVATE = 3020,\n EVENT_TWG_PRE_TRANSACTION_PRIVACY_SETTING_PUBLIC = 3021,\n EVENT_TWG_POST_TRANSACTION_SETTING_PUBLIC = 3022,\n EVENT_REGWALL_OPTED_IN = 3023,\n EVENT_NEWSLETTER_OPTED_IN = 3024,\n EVENT_SHOWCASE_METERING_INIT = 3025,\n EVENT_DISABLE_MINIPROMPT_DESKTOP = 3026,\n EVENT_SUBSCRIPTION_LINKING_SUCCESS = 3027,\n EVENT_SURVEY_SUBMITTED = 3028,\n EVENT_LINK_ACCOUNT_SUCCESS = 3029,\n EVENT_SAVE_SUBSCRIPTION_SUCCESS = 3030,\n EVENT_SURVEY_DATA_TRANSFER_COMPLETE = 3031,\n EVENT_RUNTIME_IS_READY = 3032,\n EVENT_START_API = 3033,\n EVENT_SHOW_OFFERS_API = 3034,\n EVENT_SHOW_CONTRIBUTION_OPTIONS_API = 3035,\n EVENT_REWARDED_AD_FLOW_INIT = 3048,\n EVENT_REWARDED_AD_READY = 3036,\n EVENT_REWARDED_AD_GPT_MISSING_ERROR = 3037,\n EVENT_REWARDED_AD_CONFIG_ERROR = 3038,\n EVENT_REWARDED_AD_PAGE_ERROR = 3039,\n EVENT_REWARDED_AD_GPT_ERROR = 3040,\n EVENT_REWARDED_AD_GRANTED = 3041,\n EVENT_REWARDED_AD_NOT_FILLED = 3049,\n EVENT_GLOBAL_FREQUENCY_CAP_MET = 3042,\n EVENT_PROMPT_FREQUENCY_CAP_MET = 3043,\n EVENT_ACTION_IMPRESSIONS_STORAGE_KEY_NOT_FOUND_ERROR = 3044,\n EVENT_LOCAL_STORAGE_TIMESTAMPS_PARSING_ERROR = 3052,\n EVENT_FREQUENCY_CAP_CONFIG_NOT_FOUND_ERROR = 3045,\n EVENT_PROMPT_FREQUENCY_CONFIG_NOT_FOUND = 3053,\n EVENT_BYOP_NEWSLETTER_OPT_IN_CONFIG_ERROR = 3046,\n EVENT_BYOP_NEWSLETTER_OPT_IN_CODE_SNIPPET_ERROR = 3047,\n EVENT_SUBSCRIPTION_PAYMENT_COMPLETE = 3050,\n EVENT_CONTRIBUTION_PAYMENT_COMPLETE = 3051,\n EVENT_HOSTED_PAGE_SUBSCRIPTION_PAYMENT_COMPLETE = 3054,\n EVENT_HOSTED_PAGE_CONTRIBUTION_PAYMENT_COMPLETE = 3055,\n EVENT_COMPLETION_COUNT_FOR_REPEATABLE_ACTION_MISSING_ERROR = 3056,\n EVENT_SUBSCRIPTION_STATE = 4000,\n}\n\n/** */\nexport enum EntitlementResult {\n UNKNOWN_ENTITLEMENT_RESULT = 0,\n UNLOCKED_SUBSCRIBER = 1001,\n UNLOCKED_FREE = 1002,\n UNLOCKED_METER = 1003,\n LOCKED_REGWALL = 2001,\n LOCKED_PAYWALL = 2002,\n INELIGIBLE_PAYWALL = 2003,\n}\n\n/** */\nexport enum EntitlementSource {\n UNKNOWN_ENTITLEMENT_SOURCE = 0,\n GOOGLE_SUBSCRIBER_ENTITLEMENT = 1001,\n GOOGLE_SHOWCASE_METERING_SERVICE = 2001,\n SUBSCRIBE_WITH_GOOGLE_METERING_SERVICE = 2002,\n PUBLISHER_ENTITLEMENT = 3001,\n}\n\n/** */\nexport enum EventOriginator {\n UNKNOWN_CLIENT = 0,\n SWG_CLIENT = 1,\n AMP_CLIENT = 2,\n PROPENSITY_CLIENT = 3,\n SWG_SERVER = 4,\n PUBLISHER_CLIENT = 5,\n SHOWCASE_CLIENT = 6,\n}\n\n/** */\nexport enum ReaderSurfaceType {\n READER_SURFACE_TYPE_UNSPECIFIED = 0,\n READER_SURFACE_WORDPRESS = 1,\n READER_SURFACE_CHROME = 2,\n READER_SURFACE_TENOR = 3,\n}\n\n/** */\nexport class AccountCreationRequest implements Message {\n private complete_: boolean | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.complete_ = data[base] == null ? null : (data[base] as boolean);\n }\n\n getComplete(): boolean | null {\n return this.complete_;\n }\n\n setComplete(value: boolean): void {\n this.complete_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.complete_, // field 1 - complete\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'AccountCreationRequest';\n }\n}\n\n/** */\nexport class ActionRequest implements Message {\n private action_: ActionType | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.action_ = data[base] == null ? null : (data[base] as ActionType);\n }\n\n getAction(): ActionType | null {\n return this.action_;\n }\n\n setAction(value: ActionType): void {\n this.action_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.action_, // field 1 - action\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'ActionRequest';\n }\n}\n\n/** */\nexport class AlreadySubscribedResponse implements Message {\n private subscriberOrMember_: boolean | null;\n private linkRequested_: boolean | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.subscriberOrMember_ =\n data[base] == null ? null : (data[base] as boolean);\n\n this.linkRequested_ =\n data[1 + base] == null ? null : (data[1 + base] as boolean);\n }\n\n getSubscriberOrMember(): boolean | null {\n return this.subscriberOrMember_;\n }\n\n setSubscriberOrMember(value: boolean): void {\n this.subscriberOrMember_ = value;\n }\n\n getLinkRequested(): boolean | null {\n return this.linkRequested_;\n }\n\n setLinkRequested(value: boolean): void {\n this.linkRequested_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.subscriberOrMember_, // field 1 - subscriber_or_member\n this.linkRequested_, // field 2 - link_requested\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'AlreadySubscribedResponse';\n }\n}\n\n/** */\nexport class AnalyticsContext implements Message {\n private embedderOrigin_: string | null;\n private transactionId_: string | null;\n private referringOrigin_: string | null;\n private utmSource_: string | null;\n private utmCampaign_: string | null;\n private utmMedium_: string | null;\n private sku_: string | null;\n private readyToPay_: boolean | null;\n private label_: string[] | null;\n private clientVersion_: string | null;\n private url_: string | null;\n private clientTimestamp_: Timestamp | null;\n private readerSurfaceType_: ReaderSurfaceType | null;\n private integrationVersion_: string | null;\n private pageLoadBeginTimestamp_: Timestamp | null;\n private loadEventStartDelay_: Duration | null;\n private runtimeCreationTimestamp_: Timestamp | null;\n private isLockedContent_: boolean | null;\n private urlFromMarkup_: string | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.embedderOrigin_ = data[base] == null ? null : (data[base] as string);\n\n this.transactionId_ =\n data[1 + base] == null ? null : (data[1 + base] as string);\n\n this.referringOrigin_ =\n data[2 + base] == null ? null : (data[2 + base] as string);\n\n this.utmSource_ =\n data[3 + base] == null ? null : (data[3 + base] as string);\n\n this.utmCampaign_ =\n data[4 + base] == null ? null : (data[4 + base] as string);\n\n this.utmMedium_ =\n data[5 + base] == null ? null : (data[5 + base] as string);\n\n this.sku_ = data[6 + base] == null ? null : (data[6 + base] as string);\n\n this.readyToPay_ =\n data[7 + base] == null ? null : (data[7 + base] as boolean);\n\n this.label_ = (data[8 + base] as string[]) || [];\n\n this.clientVersion_ =\n data[9 + base] == null ? null : (data[9 + base] as string);\n\n this.url_ = data[10 + base] == null ? null : (data[10 + base] as string);\n\n this.clientTimestamp_ =\n data[11 + base] == null\n ? null\n : new Timestamp(data[11 + base] as unknown[], includesLabel);\n\n this.readerSurfaceType_ =\n data[12 + base] == null ? null : (data[12 + base] as ReaderSurfaceType);\n\n this.integrationVersion_ =\n data[13 + base] == null ? null : (data[13 + base] as string);\n\n this.pageLoadBeginTimestamp_ =\n data[14 + base] == null\n ? null\n : new Timestamp(data[14 + base] as unknown[], includesLabel);\n\n this.loadEventStartDelay_ =\n data[15 + base] == null\n ? null\n : new Duration(data[15 + base] as unknown[], includesLabel);\n\n this.runtimeCreationTimestamp_ =\n data[16 + base] == null\n ? null\n : new Timestamp(data[16 + base] as unknown[], includesLabel);\n\n this.isLockedContent_ =\n data[17 + base] == null ? null : (data[17 + base] as boolean);\n\n this.urlFromMarkup_ =\n data[18 + base] == null ? null : (data[18 + base] as string);\n }\n\n getEmbedderOrigin(): string | null {\n return this.embedderOrigin_;\n }\n\n setEmbedderOrigin(value: string): void {\n this.embedderOrigin_ = value;\n }\n\n getTransactionId(): string | null {\n return this.transactionId_;\n }\n\n setTransactionId(value: string): void {\n this.transactionId_ = value;\n }\n\n getReferringOrigin(): string | null {\n return this.referringOrigin_;\n }\n\n setReferringOrigin(value: string): void {\n this.referringOrigin_ = value;\n }\n\n getUtmSource(): string | null {\n return this.utmSource_;\n }\n\n setUtmSource(value: string): void {\n this.utmSource_ = value;\n }\n\n getUtmCampaign(): string | null {\n return this.utmCampaign_;\n }\n\n setUtmCampaign(value: string): void {\n this.utmCampaign_ = value;\n }\n\n getUtmMedium(): string | null {\n return this.utmMedium_;\n }\n\n setUtmMedium(value: string): void {\n this.utmMedium_ = value;\n }\n\n getSku(): string | null {\n return this.sku_;\n }\n\n setSku(value: string): void {\n this.sku_ = value;\n }\n\n getReadyToPay(): boolean | null {\n return this.readyToPay_;\n }\n\n setReadyToPay(value: boolean): void {\n this.readyToPay_ = value;\n }\n\n getLabelList(): string[] | null {\n return this.label_;\n }\n\n setLabelList(value: string[]): void {\n this.label_ = value;\n }\n\n getClientVersion(): string | null {\n return this.clientVersion_;\n }\n\n setClientVersion(value: string): void {\n this.clientVersion_ = value;\n }\n\n getUrl(): string | null {\n return this.url_;\n }\n\n setUrl(value: string): void {\n this.url_ = value;\n }\n\n getClientTimestamp(): Timestamp | null {\n return this.clientTimestamp_;\n }\n\n setClientTimestamp(value: Timestamp): void {\n this.clientTimestamp_ = value;\n }\n\n getReaderSurfaceType(): ReaderSurfaceType | null {\n return this.readerSurfaceType_;\n }\n\n setReaderSurfaceType(value: ReaderSurfaceType): void {\n this.readerSurfaceType_ = value;\n }\n\n getIntegrationVersion(): string | null {\n return this.integrationVersion_;\n }\n\n setIntegrationVersion(value: string): void {\n this.integrationVersion_ = value;\n }\n\n getPageLoadBeginTimestamp(): Timestamp | null {\n return this.pageLoadBeginTimestamp_;\n }\n\n setPageLoadBeginTimestamp(value: Timestamp): void {\n this.pageLoadBeginTimestamp_ = value;\n }\n\n getLoadEventStartDelay(): Duration | null {\n return this.loadEventStartDelay_;\n }\n\n setLoadEventStartDelay(value: Duration): void {\n this.loadEventStartDelay_ = value;\n }\n\n getRuntimeCreationTimestamp(): Timestamp | null {\n return this.runtimeCreationTimestamp_;\n }\n\n setRuntimeCreationTimestamp(value: Timestamp): void {\n this.runtimeCreationTimestamp_ = value;\n }\n\n getIsLockedContent(): boolean | null {\n return this.isLockedContent_;\n }\n\n setIsLockedContent(value: boolean): void {\n this.isLockedContent_ = value;\n }\n\n getUrlFromMarkup(): string | null {\n return this.urlFromMarkup_;\n }\n\n setUrlFromMarkup(value: string): void {\n this.urlFromMarkup_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.embedderOrigin_, // field 1 - embedder_origin\n this.transactionId_, // field 2 - transaction_id\n this.referringOrigin_, // field 3 - referring_origin\n this.utmSource_, // field 4 - utm_source\n this.utmCampaign_, // field 5 - utm_campaign\n this.utmMedium_, // field 6 - utm_medium\n this.sku_, // field 7 - sku\n this.readyToPay_, // field 8 - ready_to_pay\n this.label_, // field 9 - label\n this.clientVersion_, // field 10 - client_version\n this.url_, // field 11 - url\n this.clientTimestamp_ ? this.clientTimestamp_.toArray(includeLabel) : [], // field 12 - client_timestamp\n this.readerSurfaceType_, // field 13 - reader_surface_type\n this.integrationVersion_, // field 14 - integration_version\n this.pageLoadBeginTimestamp_\n ? this.pageLoadBeginTimestamp_.toArray(includeLabel)\n : [], // field 15 - page_load_begin_timestamp\n this.loadEventStartDelay_\n ? this.loadEventStartDelay_.toArray(includeLabel)\n : [], // field 16 - load_event_start_delay\n this.runtimeCreationTimestamp_\n ? this.runtimeCreationTimestamp_.toArray(includeLabel)\n : [], // field 17 - runtime_creation_timestamp\n this.isLockedContent_, // field 18 - is_locked_content\n this.urlFromMarkup_, // field 19 - url_from_markup\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'AnalyticsContext';\n }\n}\n\n/** */\nexport class AnalyticsEventMeta implements Message {\n private eventOriginator_: EventOriginator | null;\n private isFromUserAction_: boolean | null;\n private configurationId_: string | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.eventOriginator_ =\n data[base] == null ? null : (data[base] as EventOriginator);\n\n this.isFromUserAction_ =\n data[1 + base] == null ? null : (data[1 + base] as boolean);\n\n this.configurationId_ =\n data[2 + base] == null ? null : (data[2 + base] as string);\n }\n\n getEventOriginator(): EventOriginator | null {\n return this.eventOriginator_;\n }\n\n setEventOriginator(value: EventOriginator): void {\n this.eventOriginator_ = value;\n }\n\n getIsFromUserAction(): boolean | null {\n return this.isFromUserAction_;\n }\n\n setIsFromUserAction(value: boolean): void {\n this.isFromUserAction_ = value;\n }\n\n getConfigurationId(): string | null {\n return this.configurationId_;\n }\n\n setConfigurationId(value: string): void {\n this.configurationId_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.eventOriginator_, // field 1 - event_originator\n this.isFromUserAction_, // field 2 - is_from_user_action\n this.configurationId_, // field 3 - configuration_id\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'AnalyticsEventMeta';\n }\n}\n\n/** */\nexport class AnalyticsRequest implements Message {\n private context_: AnalyticsContext | null;\n private event_: AnalyticsEvent | null;\n private meta_: AnalyticsEventMeta | null;\n private params_: EventParams | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.context_ =\n data[base] == null\n ? null\n : new AnalyticsContext(data[base] as unknown[], includesLabel);\n\n this.event_ =\n data[1 + base] == null ? null : (data[1 + base] as AnalyticsEvent);\n\n this.meta_ =\n data[2 + base] == null\n ? null\n : new AnalyticsEventMeta(data[2 + base] as unknown[], includesLabel);\n\n this.params_ =\n data[3 + base] == null\n ? null\n : new EventParams(data[3 + base] as unknown[], includesLabel);\n }\n\n getContext(): AnalyticsContext | null {\n return this.context_;\n }\n\n setContext(value: AnalyticsContext): void {\n this.context_ = value;\n }\n\n getEvent(): AnalyticsEvent | null {\n return this.event_;\n }\n\n setEvent(value: AnalyticsEvent): void {\n this.event_ = value;\n }\n\n getMeta(): AnalyticsEventMeta | null {\n return this.meta_;\n }\n\n setMeta(value: AnalyticsEventMeta): void {\n this.meta_ = value;\n }\n\n getParams(): EventParams | null {\n return this.params_;\n }\n\n setParams(value: EventParams): void {\n this.params_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.context_ ? this.context_.toArray(includeLabel) : [], // field 1 - context\n this.event_, // field 2 - event\n this.meta_ ? this.meta_.toArray(includeLabel) : [], // field 3 - meta\n this.params_ ? this.params_.toArray(includeLabel) : [], // field 4 - params\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'AnalyticsRequest';\n }\n}\n\n/** */\nexport class AudienceActivityClientLogsRequest implements Message {\n private event_: AnalyticsEvent | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.event_ = data[base] == null ? null : (data[base] as AnalyticsEvent);\n }\n\n getEvent(): AnalyticsEvent | null {\n return this.event_;\n }\n\n setEvent(value: AnalyticsEvent): void {\n this.event_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.event_, // field 1 - event\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'AudienceActivityClientLogsRequest';\n }\n}\n\n/** */\nexport class CompleteAudienceActionResponse implements Message {\n private swgUserToken_: string | null;\n private actionCompleted_: boolean | null;\n private userEmail_: string | null;\n private alreadyCompleted_: boolean | null;\n private displayName_: string | null;\n private givenName_: string | null;\n private familyName_: string | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.swgUserToken_ = data[base] == null ? null : (data[base] as string);\n\n this.actionCompleted_ =\n data[1 + base] == null ? null : (data[1 + base] as boolean);\n\n this.userEmail_ =\n data[2 + base] == null ? null : (data[2 + base] as string);\n\n this.alreadyCompleted_ =\n data[3 + base] == null ? null : (data[3 + base] as boolean);\n\n this.displayName_ =\n data[4 + base] == null ? null : (data[4 + base] as string);\n\n this.givenName_ =\n data[5 + base] == null ? null : (data[5 + base] as string);\n\n this.familyName_ =\n data[6 + base] == null ? null : (data[6 + base] as string);\n }\n\n getSwgUserToken(): string | null {\n return this.swgUserToken_;\n }\n\n setSwgUserToken(value: string): void {\n this.swgUserToken_ = value;\n }\n\n getActionCompleted(): boolean | null {\n return this.actionCompleted_;\n }\n\n setActionCompleted(value: boolean): void {\n this.actionCompleted_ = value;\n }\n\n getUserEmail(): string | null {\n return this.userEmail_;\n }\n\n setUserEmail(value: string): void {\n this.userEmail_ = value;\n }\n\n getAlreadyCompleted(): boolean | null {\n return this.alreadyCompleted_;\n }\n\n setAlreadyCompleted(value: boolean): void {\n this.alreadyCompleted_ = value;\n }\n\n getDisplayName(): string | null {\n return this.displayName_;\n }\n\n setDisplayName(value: string): void {\n this.displayName_ = value;\n }\n\n getGivenName(): string | null {\n return this.givenName_;\n }\n\n setGivenName(value: string): void {\n this.givenName_ = value;\n }\n\n getFamilyName(): string | null {\n return this.familyName_;\n }\n\n setFamilyName(value: string): void {\n this.familyName_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.swgUserToken_, // field 1 - swg_user_token\n this.actionCompleted_, // field 2 - action_completed\n this.userEmail_, // field 3 - user_email\n this.alreadyCompleted_, // field 4 - already_completed\n this.displayName_, // field 5 - display_name\n this.givenName_, // field 6 - given_name\n this.familyName_, // field 7 - family_name\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'CompleteAudienceActionResponse';\n }\n}\n\n/** */\nexport class Duration implements Message {\n private seconds_: number | null;\n private nanos_: number | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.seconds_ = data[base] == null ? null : (data[base] as number);\n\n this.nanos_ = data[1 + base] == null ? null : (data[1 + base] as number);\n }\n\n getSeconds(): number | null {\n return this.seconds_;\n }\n\n setSeconds(value: number): void {\n this.seconds_ = value;\n }\n\n getNanos(): number | null {\n return this.nanos_;\n }\n\n setNanos(value: number): void {\n this.nanos_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.seconds_, // field 1 - seconds\n this.nanos_, // field 2 - nanos\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'Duration';\n }\n}\n\n/** */\nexport class EntitlementJwt implements Message {\n private jwt_: string | null;\n private source_: string | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.jwt_ = data[base] == null ? null : (data[base] as string);\n\n this.source_ = data[1 + base] == null ? null : (data[1 + base] as string);\n }\n\n getJwt(): string | null {\n return this.jwt_;\n }\n\n setJwt(value: string): void {\n this.jwt_ = value;\n }\n\n getSource(): string | null {\n return this.source_;\n }\n\n setSource(value: string): void {\n this.source_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.jwt_, // field 1 - jwt\n this.source_, // field 2 - source\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'EntitlementJwt';\n }\n}\n\n/** */\nexport class EntitlementsRequest implements Message {\n private usedEntitlement_: EntitlementJwt | null;\n private clientEventTime_: Timestamp | null;\n private entitlementSource_: EntitlementSource | null;\n private entitlementResult_: EntitlementResult | null;\n private token_: string | null;\n private isUserRegistered_: boolean | null;\n private subscriptionTimestamp_: Timestamp | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.usedEntitlement_ =\n data[base] == null\n ? null\n : new EntitlementJwt(data[base] as unknown[], includesLabel);\n\n this.clientEventTime_ =\n data[1 + base] == null\n ? null\n : new Timestamp(data[1 + base] as unknown[], includesLabel);\n\n this.entitlementSource_ =\n data[2 + base] == null ? null : (data[2 + base] as EntitlementSource);\n\n this.entitlementResult_ =\n data[3 + base] == null ? null : (data[3 + base] as EntitlementResult);\n\n this.token_ = data[4 + base] == null ? null : (data[4 + base] as string);\n\n this.isUserRegistered_ =\n data[5 + base] == null ? null : (data[5 + base] as boolean);\n\n this.subscriptionTimestamp_ =\n data[6 + base] == null\n ? null\n : new Timestamp(data[6 + base] as unknown[], includesLabel);\n }\n\n getUsedEntitlement(): EntitlementJwt | null {\n return this.usedEntitlement_;\n }\n\n setUsedEntitlement(value: EntitlementJwt): void {\n this.usedEntitlement_ = value;\n }\n\n getClientEventTime(): Timestamp | null {\n return this.clientEventTime_;\n }\n\n setClientEventTime(value: Timestamp): void {\n this.clientEventTime_ = value;\n }\n\n getEntitlementSource(): EntitlementSource | null {\n return this.entitlementSource_;\n }\n\n setEntitlementSource(value: EntitlementSource): void {\n this.entitlementSource_ = value;\n }\n\n getEntitlementResult(): EntitlementResult | null {\n return this.entitlementResult_;\n }\n\n setEntitlementResult(value: EntitlementResult): void {\n this.entitlementResult_ = value;\n }\n\n getToken(): string | null {\n return this.token_;\n }\n\n setToken(value: string): void {\n this.token_ = value;\n }\n\n getIsUserRegistered(): boolean | null {\n return this.isUserRegistered_;\n }\n\n setIsUserRegistered(value: boolean): void {\n this.isUserRegistered_ = value;\n }\n\n getSubscriptionTimestamp(): Timestamp | null {\n return this.subscriptionTimestamp_;\n }\n\n setSubscriptionTimestamp(value: Timestamp): void {\n this.subscriptionTimestamp_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.usedEntitlement_ ? this.usedEntitlement_.toArray(includeLabel) : [], // field 1 - used_entitlement\n this.clientEventTime_ ? this.clientEventTime_.toArray(includeLabel) : [], // field 2 - client_event_time\n this.entitlementSource_, // field 3 - entitlement_source\n this.entitlementResult_, // field 4 - entitlement_result\n this.token_, // field 5 - token\n this.isUserRegistered_, // field 6 - is_user_registered\n this.subscriptionTimestamp_\n ? this.subscriptionTimestamp_.toArray(includeLabel)\n : [], // field 7 - subscription_timestamp\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'EntitlementsRequest';\n }\n}\n\n/** */\nexport class EntitlementsResponse implements Message {\n private jwt_: string | null;\n private swgUserToken_: string | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.jwt_ = data[base] == null ? null : (data[base] as string);\n\n this.swgUserToken_ =\n data[1 + base] == null ? null : (data[1 + base] as string);\n }\n\n getJwt(): string | null {\n return this.jwt_;\n }\n\n setJwt(value: string): void {\n this.jwt_ = value;\n }\n\n getSwgUserToken(): string | null {\n return this.swgUserToken_;\n }\n\n setSwgUserToken(value: string): void {\n this.swgUserToken_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.jwt_, // field 1 - jwt\n this.swgUserToken_, // field 2 - swg_user_token\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'EntitlementsResponse';\n }\n}\n\n/** */\nexport class EventParams implements Message {\n private smartboxMessage_: string | null;\n private gpayTransactionId_: string | null;\n private hadLogged_: boolean | null;\n private sku_: string | null;\n private oldTransactionId_: string | null;\n private isUserRegistered_: boolean | null;\n private subscriptionFlow_: string | null;\n private subscriptionTimestamp_: Timestamp | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.smartboxMessage_ = data[base] == null ? null : (data[base] as string);\n\n this.gpayTransactionId_ =\n data[1 + base] == null ? null : (data[1 + base] as string);\n\n this.hadLogged_ =\n data[2 + base] == null ? null : (data[2 + base] as boolean);\n\n this.sku_ = data[3 + base] == null ? null : (data[3 + base] as string);\n\n this.oldTransactionId_ =\n data[4 + base] == null ? null : (data[4 + base] as string);\n\n this.isUserRegistered_ =\n data[5 + base] == null ? null : (data[5 + base] as boolean);\n\n this.subscriptionFlow_ =\n data[6 + base] == null ? null : (data[6 + base] as string);\n\n this.subscriptionTimestamp_ =\n data[7 + base] == null\n ? null\n : new Timestamp(data[7 + base] as unknown[], includesLabel);\n }\n\n getSmartboxMessage(): string | null {\n return this.smartboxMessage_;\n }\n\n setSmartboxMessage(value: string): void {\n this.smartboxMessage_ = value;\n }\n\n getGpayTransactionId(): string | null {\n return this.gpayTransactionId_;\n }\n\n setGpayTransactionId(value: string): void {\n this.gpayTransactionId_ = value;\n }\n\n getHadLogged(): boolean | null {\n return this.hadLogged_;\n }\n\n setHadLogged(value: boolean): void {\n this.hadLogged_ = value;\n }\n\n getSku(): string | null {\n return this.sku_;\n }\n\n setSku(value: string): void {\n this.sku_ = value;\n }\n\n getOldTransactionId(): string | null {\n return this.oldTransactionId_;\n }\n\n setOldTransactionId(value: string): void {\n this.oldTransactionId_ = value;\n }\n\n getIsUserRegistered(): boolean | null {\n return this.isUserRegistered_;\n }\n\n setIsUserRegistered(value: boolean): void {\n this.isUserRegistered_ = value;\n }\n\n getSubscriptionFlow(): string | null {\n return this.subscriptionFlow_;\n }\n\n setSubscriptionFlow(value: string): void {\n this.subscriptionFlow_ = value;\n }\n\n getSubscriptionTimestamp(): Timestamp | null {\n return this.subscriptionTimestamp_;\n }\n\n setSubscriptionTimestamp(value: Timestamp): void {\n this.subscriptionTimestamp_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.smartboxMessage_, // field 1 - smartbox_message\n this.gpayTransactionId_, // field 2 - gpay_transaction_id\n this.hadLogged_, // field 3 - had_logged\n this.sku_, // field 4 - sku\n this.oldTransactionId_, // field 5 - old_transaction_id\n this.isUserRegistered_, // field 6 - is_user_registered\n this.subscriptionFlow_, // field 7 - subscription_flow\n this.subscriptionTimestamp_\n ? this.subscriptionTimestamp_.toArray(includeLabel)\n : [], // field 8 - subscription_timestamp\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'EventParams';\n }\n}\n\n/** */\nexport class FinishedLoggingResponse implements Message {\n private complete_: boolean | null;\n private error_: string | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.complete_ = data[base] == null ? null : (data[base] as boolean);\n\n this.error_ = data[1 + base] == null ? null : (data[1 + base] as string);\n }\n\n getComplete(): boolean | null {\n return this.complete_;\n }\n\n setComplete(value: boolean): void {\n this.complete_ = value;\n }\n\n getError(): string | null {\n return this.error_;\n }\n\n setError(value: string): void {\n this.error_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.complete_, // field 1 - complete\n this.error_, // field 2 - error\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'FinishedLoggingResponse';\n }\n}\n\n/** */\nexport class LinkSaveTokenRequest implements Message {\n private authCode_: string | null;\n private token_: string | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.authCode_ = data[base] == null ? null : (data[base] as string);\n\n this.token_ = data[1 + base] == null ? null : (data[1 + base] as string);\n }\n\n getAuthCode(): string | null {\n return this.authCode_;\n }\n\n setAuthCode(value: string): void {\n this.authCode_ = value;\n }\n\n getToken(): string | null {\n return this.token_;\n }\n\n setToken(value: string): void {\n this.token_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.authCode_, // field 1 - auth_code\n this.token_, // field 2 - token\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'LinkSaveTokenRequest';\n }\n}\n\n/** */\nexport class LinkingInfoResponse implements Message {\n private requested_: boolean | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.requested_ = data[base] == null ? null : (data[base] as boolean);\n }\n\n getRequested(): boolean | null {\n return this.requested_;\n }\n\n setRequested(value: boolean): void {\n this.requested_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.requested_, // field 1 - requested\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'LinkingInfoResponse';\n }\n}\n\n/** */\nexport class OpenDialogRequest implements Message {\n private urlPath_: string | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.urlPath_ = data[base] == null ? null : (data[base] as string);\n }\n\n getUrlPath(): string | null {\n return this.urlPath_;\n }\n\n setUrlPath(value: string): void {\n this.urlPath_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.urlPath_, // field 1 - url_path\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'OpenDialogRequest';\n }\n}\n\n/** */\nexport class SkuSelectedResponse implements Message {\n private sku_: string | null;\n private oldSku_: string | null;\n private oneTime_: boolean | null;\n private playOffer_: string | null;\n private oldPlayOffer_: string | null;\n private customMessage_: string | null;\n private anonymous_: boolean | null;\n private sharingPolicyEnabled_: boolean | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.sku_ = data[base] == null ? null : (data[base] as string);\n\n this.oldSku_ = data[1 + base] == null ? null : (data[1 + base] as string);\n\n this.oneTime_ = data[2 + base] == null ? null : (data[2 + base] as boolean);\n\n this.playOffer_ =\n data[3 + base] == null ? null : (data[3 + base] as string);\n\n this.oldPlayOffer_ =\n data[4 + base] == null ? null : (data[4 + base] as string);\n\n this.customMessage_ =\n data[5 + base] == null ? null : (data[5 + base] as string);\n\n this.anonymous_ =\n data[6 + base] == null ? null : (data[6 + base] as boolean);\n\n this.sharingPolicyEnabled_ =\n data[7 + base] == null ? null : (data[7 + base] as boolean);\n }\n\n getSku(): string | null {\n return this.sku_;\n }\n\n setSku(value: string): void {\n this.sku_ = value;\n }\n\n getOldSku(): string | null {\n return this.oldSku_;\n }\n\n setOldSku(value: string): void {\n this.oldSku_ = value;\n }\n\n getOneTime(): boolean | null {\n return this.oneTime_;\n }\n\n setOneTime(value: boolean): void {\n this.oneTime_ = value;\n }\n\n getPlayOffer(): string | null {\n return this.playOffer_;\n }\n\n setPlayOffer(value: string): void {\n this.playOffer_ = value;\n }\n\n getOldPlayOffer(): string | null {\n return this.oldPlayOffer_;\n }\n\n setOldPlayOffer(value: string): void {\n this.oldPlayOffer_ = value;\n }\n\n getCustomMessage(): string | null {\n return this.customMessage_;\n }\n\n setCustomMessage(value: string): void {\n this.customMessage_ = value;\n }\n\n getAnonymous(): boolean | null {\n return this.anonymous_;\n }\n\n setAnonymous(value: boolean): void {\n this.anonymous_ = value;\n }\n\n getSharingPolicyEnabled(): boolean | null {\n return this.sharingPolicyEnabled_;\n }\n\n setSharingPolicyEnabled(value: boolean): void {\n this.sharingPolicyEnabled_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.sku_, // field 1 - sku\n this.oldSku_, // field 2 - old_sku\n this.oneTime_, // field 3 - one_time\n this.playOffer_, // field 4 - play_offer\n this.oldPlayOffer_, // field 5 - old_play_offer\n this.customMessage_, // field 6 - custom_message\n this.anonymous_, // field 7 - anonymous\n this.sharingPolicyEnabled_, // field 8 - sharing_policy_enabled\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'SkuSelectedResponse';\n }\n}\n\n/** */\nexport class SmartBoxMessage implements Message {\n private isClicked_: boolean | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.isClicked_ = data[base] == null ? null : (data[base] as boolean);\n }\n\n getIsClicked(): boolean | null {\n return this.isClicked_;\n }\n\n setIsClicked(value: boolean): void {\n this.isClicked_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.isClicked_, // field 1 - is_clicked\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'SmartBoxMessage';\n }\n}\n\n/** */\nexport class SubscribeResponse implements Message {\n private subscribe_: boolean | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.subscribe_ = data[base] == null ? null : (data[base] as boolean);\n }\n\n getSubscribe(): boolean | null {\n return this.subscribe_;\n }\n\n setSubscribe(value: boolean): void {\n this.subscribe_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.subscribe_, // field 1 - subscribe\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'SubscribeResponse';\n }\n}\n\n/** */\nexport class SubscriptionLinkingCompleteResponse implements Message {\n private publisherProvidedId_: string | null;\n private success_: boolean | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.publisherProvidedId_ =\n data[base] == null ? null : (data[base] as string);\n\n this.success_ = data[1 + base] == null ? null : (data[1 + base] as boolean);\n }\n\n getPublisherProvidedId(): string | null {\n return this.publisherProvidedId_;\n }\n\n setPublisherProvidedId(value: string): void {\n this.publisherProvidedId_ = value;\n }\n\n getSuccess(): boolean | null {\n return this.success_;\n }\n\n setSuccess(value: boolean): void {\n this.success_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.publisherProvidedId_, // field 1 - publisher_provided_id\n this.success_, // field 2 - success\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'SubscriptionLinkingCompleteResponse';\n }\n}\n\n/** */\nexport class SubscriptionLinkingResponse implements Message {\n private publisherProvidedId_: string | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.publisherProvidedId_ =\n data[base] == null ? null : (data[base] as string);\n }\n\n getPublisherProvidedId(): string | null {\n return this.publisherProvidedId_;\n }\n\n setPublisherProvidedId(value: string): void {\n this.publisherProvidedId_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.publisherProvidedId_, // field 1 - publisher_provided_id\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'SubscriptionLinkingResponse';\n }\n}\n\n/** */\nexport class SurveyAnswer implements Message {\n private answerId_: number | null;\n private answerText_: string | null;\n private answerCategory_: string | null;\n private ppsValue_: string | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.answerId_ = data[base] == null ? null : (data[base] as number);\n\n this.answerText_ =\n data[1 + base] == null ? null : (data[1 + base] as string);\n\n this.answerCategory_ =\n data[2 + base] == null ? null : (data[2 + base] as string);\n\n this.ppsValue_ = data[3 + base] == null ? null : (data[3 + base] as string);\n }\n\n getAnswerId(): number | null {\n return this.answerId_;\n }\n\n setAnswerId(value: number): void {\n this.answerId_ = value;\n }\n\n getAnswerText(): string | null {\n return this.answerText_;\n }\n\n setAnswerText(value: string): void {\n this.answerText_ = value;\n }\n\n getAnswerCategory(): string | null {\n return this.answerCategory_;\n }\n\n setAnswerCategory(value: string): void {\n this.answerCategory_ = value;\n }\n\n getPpsValue(): string | null {\n return this.ppsValue_;\n }\n\n setPpsValue(value: string): void {\n this.ppsValue_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.answerId_, // field 1 - answer_id\n this.answerText_, // field 2 - answer_text\n this.answerCategory_, // field 3 - answer_category\n this.ppsValue_, // field 4 - pps_value\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'SurveyAnswer';\n }\n}\n\n/** */\nexport class SurveyDataTransferRequest implements Message {\n private surveyQuestions_: SurveyQuestion[] | null;\n private storePpsInLocalStorage_: boolean | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.surveyQuestions_ = ((data[base] as unknown[][]) || []).map(\n (item) => new SurveyQuestion(item, includesLabel)\n );\n\n this.storePpsInLocalStorage_ =\n data[1 + base] == null ? null : (data[1 + base] as boolean);\n }\n\n getSurveyQuestionsList(): SurveyQuestion[] | null {\n return this.surveyQuestions_;\n }\n\n setSurveyQuestionsList(value: SurveyQuestion[]): void {\n this.surveyQuestions_ = value;\n }\n\n getStorePpsInLocalStorage(): boolean | null {\n return this.storePpsInLocalStorage_;\n }\n\n setStorePpsInLocalStorage(value: boolean): void {\n this.storePpsInLocalStorage_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.surveyQuestions_\n ? this.surveyQuestions_.map((item) => item.toArray(includeLabel))\n : [], // field 1 - survey_questions\n this.storePpsInLocalStorage_, // field 2 - store_pps_in_local_storage\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'SurveyDataTransferRequest';\n }\n}\n\n/** */\nexport class SurveyDataTransferResponse implements Message {\n private success_: boolean | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.success_ = data[base] == null ? null : (data[base] as boolean);\n }\n\n getSuccess(): boolean | null {\n return this.success_;\n }\n\n setSuccess(value: boolean): void {\n this.success_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.success_, // field 1 - success\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'SurveyDataTransferResponse';\n }\n}\n\n/** */\nexport class SurveyQuestion implements Message {\n private questionId_: number | null;\n private questionText_: string | null;\n private questionCategory_: string | null;\n private surveyAnswers_: SurveyAnswer[] | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.questionId_ = data[base] == null ? null : (data[base] as number);\n\n this.questionText_ =\n data[1 + base] == null ? null : (data[1 + base] as string);\n\n this.questionCategory_ =\n data[2 + base] == null ? null : (data[2 + base] as string);\n\n this.surveyAnswers_ = ((data[3 + base] as unknown[][]) || []).map(\n (item) => new SurveyAnswer(item, includesLabel)\n );\n }\n\n getQuestionId(): number | null {\n return this.questionId_;\n }\n\n setQuestionId(value: number): void {\n this.questionId_ = value;\n }\n\n getQuestionText(): string | null {\n return this.questionText_;\n }\n\n setQuestionText(value: string): void {\n this.questionText_ = value;\n }\n\n getQuestionCategory(): string | null {\n return this.questionCategory_;\n }\n\n setQuestionCategory(value: string): void {\n this.questionCategory_ = value;\n }\n\n getSurveyAnswersList(): SurveyAnswer[] | null {\n return this.surveyAnswers_;\n }\n\n setSurveyAnswersList(value: SurveyAnswer[]): void {\n this.surveyAnswers_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.questionId_, // field 1 - question_id\n this.questionText_, // field 2 - question_text\n this.questionCategory_, // field 3 - question_category\n this.surveyAnswers_\n ? this.surveyAnswers_.map((item) => item.toArray(includeLabel))\n : [], // field 4 - survey_answers\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'SurveyQuestion';\n }\n}\n\n/** */\nexport class Timestamp implements Message {\n private seconds_: number | null;\n private nanos_: number | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.seconds_ = data[base] == null ? null : (data[base] as number);\n\n this.nanos_ = data[1 + base] == null ? null : (data[1 + base] as number);\n }\n\n getSeconds(): number | null {\n return this.seconds_;\n }\n\n setSeconds(value: number): void {\n this.seconds_ = value;\n }\n\n getNanos(): number | null {\n return this.nanos_;\n }\n\n setNanos(value: number): void {\n this.nanos_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.seconds_, // field 1 - seconds\n this.nanos_, // field 2 - nanos\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'Timestamp';\n }\n}\n\n/** */\nexport class ToastCloseRequest implements Message {\n private close_: boolean | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.close_ = data[base] == null ? null : (data[base] as boolean);\n }\n\n getClose(): boolean | null {\n return this.close_;\n }\n\n setClose(value: boolean): void {\n this.close_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.close_, // field 1 - close\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'ToastCloseRequest';\n }\n}\n\n/** */\nexport class ViewSubscriptionsResponse implements Message {\n private native_: boolean | null;\n\n constructor(data: unknown[] = [], includesLabel = true) {\n const base = includesLabel ? 1 : 0;\n\n this.native_ = data[base] == null ? null : (data[base] as boolean);\n }\n\n getNative(): boolean | null {\n return this.native_;\n }\n\n setNative(value: boolean): void {\n this.native_ = value;\n }\n\n toArray(includeLabel = true): unknown[] {\n const arr: unknown[] = [\n this.native_, // field 1 - native\n ];\n if (includeLabel) {\n arr.unshift(this.label());\n }\n return arr;\n }\n\n label(): string {\n return 'ViewSubscriptionsResponse';\n }\n}\n\nconst PROTO_MAP: {[key: string]: MessageConstructor} = {\n 'AccountCreationRequest': AccountCreationRequest,\n 'ActionRequest': ActionRequest,\n 'AlreadySubscribedResponse': AlreadySubscribedResponse,\n 'AnalyticsContext': AnalyticsContext,\n 'AnalyticsEventMeta': AnalyticsEventMeta,\n 'AnalyticsRequest': AnalyticsRequest,\n 'AudienceActivityClientLogsRequest': AudienceActivityClientLogsRequest,\n 'CompleteAudienceActionResponse': CompleteAudienceActionResponse,\n 'Duration': Duration,\n 'EntitlementJwt': EntitlementJwt,\n 'EntitlementsRequest': EntitlementsRequest,\n 'EntitlementsResponse': EntitlementsResponse,\n 'EventParams': EventParams,\n 'FinishedLoggingResponse': FinishedLoggingResponse,\n 'LinkSaveTokenRequest': LinkSaveTokenRequest,\n 'LinkingInfoResponse': LinkingInfoResponse,\n 'OpenDialogRequest': OpenDialogRequest,\n 'SkuSelectedResponse': SkuSelectedResponse,\n 'SmartBoxMessage': SmartBoxMessage,\n 'SubscribeResponse': SubscribeResponse,\n 'SubscriptionLinkingCompleteResponse': SubscriptionLinkingCompleteResponse,\n 'SubscriptionLinkingResponse': SubscriptionLinkingResponse,\n 'SurveyAnswer': SurveyAnswer,\n 'SurveyDataTransferRequest': SurveyDataTransferRequest,\n 'SurveyDataTransferResponse': SurveyDataTransferResponse,\n 'SurveyQuestion': SurveyQuestion,\n 'Timestamp': Timestamp,\n 'ToastCloseRequest': ToastCloseRequest,\n 'ViewSubscriptionsResponse': ViewSubscriptionsResponse,\n};\n\n/**\n * Utility to deserialize a buffer\n */\nexport function deserialize(data: unknown[]): Message {\n const key = data ? (data[0] as string) : null;\n if (key) {\n const ctor = PROTO_MAP[key];\n if (ctor) {\n return new ctor(data);\n }\n }\n throw new Error(`Deserialization failed for ${data}`);\n}\n\n/**\n * Gets a message's label.\n */\nexport function getLabel(messageType: MessageConstructor): string {\n return messageType.prototype.label();\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {AvailableIntervention} from './available-intervention';\nimport {ClientEventManagerApi} from './client-event-manager-api';\nimport {\n DeferredAccountCreationRequest,\n DeferredAccountCreationResponse,\n} from './deferred-account-creation';\nimport {Entitlements} from './entitlements';\nimport {LoggerApi} from './logger-api';\nimport {Offer} from './offer';\nimport {PropensityApi} from './propensity-api';\nimport {SubscribeResponse} from './subscribe-response';\n\nexport interface Subscriptions {\n /**\n * Optionally initializes the subscriptions runtime with publication or\n * product ID. If not called, the runtime will look for the initialization\n * parameters in the page's markup.\n */\n init(productOrPublicationId: string): void;\n\n /**\n * Optionally configures the runtime with non-default properties. See\n * `Config` definition for details.\n */\n configure(config: Config): Promise | void;\n\n /**\n * Starts the entitlement flow.\n */\n start(): Promise | void;\n\n /**\n * Resets the entitlements that can be fetched again.\n */\n reset(): Promise | void;\n\n /**\n * Resets the entitlements and clears all of the caches.\n */\n clear(): Promise | void;\n\n getEntitlements(\n params?: GetEntitlementsParamsExternalDef\n ): Promise;\n\n /**\n * Set the subscribe callback.\n */\n setOnEntitlementsResponse(\n callback: (entitlements: Promise) => void\n ): Promise | void;\n\n /**\n * Returns a set of offers.\n */\n getOffers(options?: {productId?: string}): Promise;\n\n /**\n * Starts the Offers flow.\n */\n showOffers(options?: OffersRequest): Promise;\n\n /**\n * Starts the Offers flow for a subscription update.\n */\n showUpdateOffers(options?: OffersRequest): Promise;\n\n /**\n * Show subscription option.\n */\n showSubscribeOption(options?: OffersRequest): Promise;\n\n /**\n * Show abbreviated offers.\n */\n showAbbrvOffer(options?: OffersRequest): Promise;\n\n /**\n * Show contribution options for the users to select from.\n * The options are grouped together by periods (Weekly, Monthly, etc.).\n * User can select the amount to contribute to from available options\n * to the publisher. These options are based on the SKUs defined in the Play\n * console for a given publication.\n * Each SKU has Amount, Period, SKUId and other attributes.\n */\n showContributionOptions(options?: OffersRequest): Promise;\n\n /**\n * Set the callback for the native subscribe request. Setting this callback\n * triggers the \"native\" option in the offers flow.\n */\n setOnNativeSubscribeRequest(callback: () => void): Promise | void;\n\n /**\n * Set the subscribe complete callback.\n */\n setOnSubscribeResponse(\n callback: (subscribeResponse: Promise) => void\n ): Promise | void;\n\n /**\n * Starts subscription purchase flow.\n */\n subscribe(sku: string): Promise;\n\n /**\n * Starts subscription purchase flow.\n */\n updateSubscription(subscriptionRequest: SubscriptionRequest): Promise;\n\n /**\n * Set the contribution complete callback.\n */\n setOnContributionResponse(\n callback: (subscribeResponsePromise: Promise) => void\n ): Promise | void;\n\n /**\n * Set the payment complete callback.\n */\n setOnPaymentResponse(\n callback: (subscribeResponsePromise: Promise) => void\n ): Promise | void;\n\n /**\n * Starts contributions purchase flow.\n */\n contribute(\n skuOrSubscriptionRequest: string | SubscriptionRequest\n ): Promise;\n\n /**\n * Starts the deferred account creation flow.\n * See `DeferredAccountCreationRequest` for more details.\n */\n completeDeferredAccountCreation(\n options?: DeferredAccountCreationRequest | null\n ): Promise;\n\n setOnLoginRequest(\n callback: (loginRequest: LoginRequest) => void\n ): Promise | void;\n\n triggerLoginRequest(request: LoginRequest): Promise | void;\n\n /**\n * Starts the login prompt flow.\n */\n showLoginPrompt(): Promise;\n\n /**\n * Starts the login notification flow.\n */\n showLoginNotification(): Promise;\n\n setOnLinkComplete(callback: () => void): Promise | void;\n\n waitForSubscriptionLookup(accountPromise: Promise): Promise;\n\n /**\n * Starts the Account linking flow.\n * TODO(dparikh): decide if it's only exposed for testing or PROD purposes.\n */\n linkAccount(params?: {ampReaderId?: string}): Promise;\n\n /**\n * Notifies the client that a flow has been started. The name of the flow\n * is passed as the callback argument. The flow name corresponds to the\n * method name in this interface, such as \"showOffers\", or \"subscribe\".\n * See `SubscriptionFlows` for the full list.\n *\n * Also see `setOnFlowCanceled` method.\n */\n setOnFlowStarted(\n callback: (params: {flow: string; data: object}) => void\n ): Promise | void;\n\n /**\n * Notifies the client that a flow has been canceled. The name of the flow\n * is passed as the callback argument. The flow name corresponds to the\n * method name in this interface, such as \"showOffers\", or \"subscribe\".\n * See `SubscriptionFlows` for the full list.\n *\n * Notice that some of the flows, such as \"subscribe\", could additionally\n * have their own \"cancel\" events.\n *\n * Also see `setOnFlowStarted` method.\n */\n setOnFlowCanceled(\n callback: (params: {flow: string; data: object}) => void\n ): Promise | void;\n\n /**\n * Starts the save subscriptions flow.\n * @return a promise indicating whether the flow completed successfully.\n */\n saveSubscription(\n requestCallback: SaveSubscriptionRequestCallback\n ): Promise;\n\n /**\n * Starts the subscription linking flow.\n * @return promise indicating result of the operation\n */\n linkSubscription(\n linkSubscriptionRequest: LinkSubscriptionRequest\n ): Promise;\n\n /**\n * Creates an element with the SwG button style and the provided callback.\n * The default theme is \"light\".\n */\n createButton(\n optionsOrCallback: ButtonOptions | (() => void),\n callback?: () => void\n ): Element;\n\n /**\n * Attaches the SwG button style and the provided callback to an existing\n * DOM element. The default theme is \"light\".\n */\n attachButton(\n button: HTMLElement,\n optionsOrCallback: ButtonOptions | (() => void),\n callback?: () => void\n ): void;\n\n /**\n * Attaches smartButton element and the provided callback.\n * The default theme is \"light\".\n */\n attachSmartButton(\n button: HTMLElement,\n optionsOrCallback: SmartButtonOptions | (() => void),\n callback?: () => void\n ): void;\n\n /**\n * Retrieves the propensity module that provides APIs to\n * get propensity scores based on user state and events\n */\n getPropensityModule(): Promise;\n\n getLogger(): Promise;\n\n getEventManager(): Promise;\n\n /**\n * Publishers participating in Showcase should call this with their own entitlements\n * and entitlement related UI events. SwG will automatically do this for Google\n * sourced subscriptions and meters.\n */\n setShowcaseEntitlement(entitlement: PublisherEntitlement): Promise;\n\n /**\n * Publishers, who both (1) participate in Showcase and (2) use server-side paywalls,\n * should call this method to consume Showcase entitlements.\n */\n consumeShowcaseEntitlementJwt(\n showcaseEntitlementJwt: string,\n onCloseDialog?: () => void | null\n ): Promise | void;\n\n /**\n * Intelligently returns the most interesting action to the\n * reader based on different different user status. For\n * instance, a new user may get free metering by simply\n * clicking 'follow-publisher' action, and a frequently\n * visiting user may be shown a 'creating an account' action.\n * TODO(moonbong): Implement this function.\n */\n showBestAudienceAction(): void;\n\n /**\n * Sets the publisherProvidedId.\n */\n setPublisherProvidedId(publisherProvidedId: string): Promise | void;\n\n /**\n * Returns a list of available interventions. If there are no interventions available\n * an empty array is returned. If the article does not exist, null is returned.\n */\n getAvailableInterventions(): Promise;\n}\n\nexport enum ShowcaseEvent {\n // Events indicating content could potentially be unlocked:\n\n /** This event is only required if the user can choose not to use a publisher meter. */\n EVENT_SHOWCASE_METER_OFFERED = 'EVENT_SHOWCASE_METER_OFFERED',\n\n // Events indicating content was unlocked:\n\n /** Publisher managed subscriptions only. */\n EVENT_SHOWCASE_UNLOCKED_BY_SUBSCRIPTION = 'EVENT_SHOWCASE_UNLOCKED_BY_SUBSCRIPTION',\n /** Publisher managed meters only. */\n EVENT_SHOWCASE_UNLOCKED_BY_METER = 'EVENT_SHOWCASE_UNLOCKED_BY_METER',\n /** When the article is free for any reason (lead article, etc). */\n EVENT_SHOWCASE_UNLOCKED_FREE_PAGE = 'EVENT_SHOWCASE_UNLOCKED_FREE_PAGE',\n\n // Events indicating the user must take action to view content:\n\n /** When the user must register (or log in) to view the article. */\n EVENT_SHOWCASE_NO_ENTITLEMENTS_REGWALL = 'EVENT_SHOWCASE_NO_ENTITLEMENTS_REGWALL',\n\n // Events indicating the user must subscribe to view content:\n\n /** When the user is not eligible for showcase entitlements. */\n EVENT_SHOWCASE_INELIGIBLE_PAYWALL = 'EVENT_SHOWCASE_INELIGIBLE_PAYWALL',\n /** When the user has no remaining showcase entitlements. */\n EVENT_SHOWCASE_NO_ENTITLEMENTS_PAYWALL = 'EVENT_SHOWCASE_NO_ENTITLEMENTS_PAYWALL',\n}\n\n/**\n * In order to participate in News Showcase, publishers must report information about their entitlements.\n */\nexport interface PublisherEntitlement {\n /** Is the user registered currently? */\n isUserRegistered: boolean;\n /** Publisher entitlement event type. */\n entitlement: ShowcaseEvent;\n /** Timestamp(in millisecond) when the user converted to a subscriber. Null if the user is not a subscriber. */\n subscriptionTimestamp: number | null;\n}\n\nexport enum SubscriptionFlows {\n SHOW_OFFERS = 'showOffers',\n SHOW_SUBSCRIBE_OPTION = 'showSubscribeOption',\n SHOW_ABBRV_OFFER = 'showAbbrvOffer',\n SHOW_CONTRIBUTION_OPTIONS = 'showContributionOptions',\n SUBSCRIBE = 'subscribe',\n CONTRIBUTE = 'contribute',\n COMPLETE_DEFERRED_ACCOUNT_CREATION = 'completeDeferredAccountCreation',\n LINK_ACCOUNT = 'linkAccount',\n SHOW_LOGIN_PROMPT = 'showLoginPrompt',\n SHOW_LOGIN_NOTIFICATION = 'showLoginNotification',\n SHOW_METER_TOAST = 'showMeterToast',\n}\n\nexport interface Config {\n experiments?: string[];\n /**\n * Either \"auto\" or \"redirect\". The \"redirect\" value will\n * force redirect flow for any window.open operation, including payments.\n * The \"auto\" value either uses a redirect or a popup flow depending on\n * what's possible on a specific environment. Defaults to \"auto\".\n */\n windowOpenMode?: WindowOpenMode;\n analyticsMode?: AnalyticsMode;\n /**\n * If set to true then events logged by the publisher's\n * client will be sent to Google's SwG analytics service. This information is\n * used to compare the effectiveness of Google's buy-flow events to those\n * generated by the publisher's client code. This includes events sent to\n * both PropensityApi and LoggerApi.\n */\n enableSwgAnalytics?: boolean;\n /**\n * If true events from the logger api are sent to the\n * propensity server. Note events from the legacy propensity endpoint are\n * always sent.\n */\n enablePropensity?: boolean;\n publisherProvidedId?: string;\n paySwgVersion?: string;\n}\n\n/**\n * Params for GetEntitlements requests to SwG Client.\n * swg-js constructs objects of this type, but publisher JS won't.\n * swg-js converts these params to a Base64 JSON string\n * before sending them to SwG Client.\n */\nexport interface GetEntitlementsParamsInternalDef {\n metering?: GetEntitlementsMeteringParamsInternal;\n}\n\n/**\n * Encryption params for GetEntitlements requests.\n */\nexport interface GetEntitlementsEncryptionParams {\n encryptedDocumentKey: string;\n}\n\n/**\n * Metering params for GetEntitlements requests to SwG Client.\n * swg-js constructs objects of this type, but publisher JS won't.\n */\nexport interface GetEntitlementsMeteringParamsInternal {\n clientTypes?: number[];\n owner?: string;\n state?: {\n id: string;\n attributes: {\n name: string;\n timestamp: number;\n }[];\n };\n token?: string;\n resource: {\n hashedCanonicalUrl: string;\n };\n}\n\n/**\n * Params for `getEntitlements` calls from publisher JS.\n * swg-js converts objects of this type to GetEntitlementsParamsInternal.\n */\nexport interface GetEntitlementsParamsExternalDef {\n encryption?: GetEntitlementsEncryptionParams;\n metering?: GetEntitlementsMeteringParamsExternal;\n publisherProvidedId?: string;\n}\n\n/**\n * Params for `getEntitlements` calls from publisher JS.\n * swg-js converts objects of this type to GetEntitlementsMeteringParamsInternal.\n */\nexport interface GetEntitlementsMeteringParamsExternal {\n clientTypes?: number[];\n owner?: string;\n state: {\n id: string;\n standardAttributes: {\n [key: string]: {\n timestamp: number;\n };\n };\n customAttributes?: {\n [key: string]: {\n timestamp: number;\n };\n };\n };\n resource?: {\n hashedCanonicalUrl: string;\n };\n}\n\nexport enum AnalyticsMode {\n DEFAULT = 0,\n IMPRESSIONS = 1,\n}\n\nexport enum WindowOpenMode {\n AUTO = 'auto',\n REDIRECT = 'redirect',\n}\n\nexport enum ReplaceSkuProrationMode {\n /**\n * The replacement takes effect immediately, and the remaining time will\n * be prorated and credited to the user. This is the current default\n * behavior.\n */\n IMMEDIATE_WITH_TIME_PRORATION = 'IMMEDIATE_WITH_TIME_PRORATION',\n}\n\n/**\n * The Offers/Contributions UI is rendered differently based on the\n * ProductType. The ProductType parameter is passed to the Payments flow, and\n * then passed back to the Payments confirmation page to render messages/text\n * based on the ProductType.\n */\nexport enum ProductType {\n SUBSCRIPTION = 'SUBSCRIPTION',\n UI_CONTRIBUTION = 'UI_CONTRIBUTION',\n}\n\nexport enum ClientTheme {\n LIGHT = 'light',\n DARK = 'dark',\n}\n\nexport function defaultConfig(): Config {\n return {\n windowOpenMode: WindowOpenMode.AUTO,\n analyticsMode: AnalyticsMode.DEFAULT,\n enableSwgAnalytics: false,\n enablePropensity: false,\n };\n}\n\nexport interface OffersRequest {\n /**\n * A list of SKUs to return from the defined or default list. The\n * order is preserved. Required if oldSku is specified (to indicate which\n * SKUs the user can upgrade or downgrade to).\n */\n skus?: string[];\n\n /**\n * A predefined list of SKUs. Use of this property is uncommon.\n * Possible values are \"default\" and \"amp\". Default is \"default\".\n */\n list?: string;\n\n /** A boolean value to determine whether the view is closable. */\n isClosable?: boolean;\n\n /**\n * Optional. The SKU to replace. For example, if a user wants to\n * upgrade or downgrade their current subscription.\n */\n oldSku?: string;\n\n /**\n * Optional. Disables the fade in animation if set to false. Defaults to true\n * if unset.\n * TODO: b/304803271 - remove this field from the api.\n */\n shouldAnimateFade?: boolean;\n}\n\nexport interface LoginRequest {\n linkRequested: boolean;\n}\n\n/**\n * Properties:\n * - one and only one of \"token\" or \"authCode\"\n * AuthCode reference: https://developers.google.com/actions/identity/oauth2-code-flow\n * Token reference: https://developers.google.com/actions/identity/oauth2-implicit-flow\n */\nexport interface SaveSubscriptionRequest {\n token?: string;\n authCode?: string;\n}\n\n/**\n * Callback for retrieving subscription request\n */\nexport type SaveSubscriptionRequestCallback = () =>\n | Promise\n | SaveSubscriptionRequest;\n\nexport interface ButtonOptions {\n /** Sets the button SVG and title. Default is \"en\". */\n theme?: string;\n /** \"Light\" or \"dark\". Default is \"light\". */\n lang?: string;\n /** Whether to enable the button. */\n enable?: boolean;\n}\n\nexport interface SmartButtonOptions {\n /** Sets the button SVG and title. Default is \"en\". */\n theme?: string;\n /** \"Light\" or \"dark\". Default is \"light\". */\n lang?: string;\n /** Overrides theme color for message text. (ex: \"#09f\") */\n messageTextColor?: string;\n}\n\nexport interface SubscriptionRequest {\n /** Required. Sku to add to the user's subscriptions. */\n skuId: string;\n /**\n * Optional. This is if you want to replace one sku with another. For\n * example, if a user wants to upgrade or downgrade their current subscription.\n */\n oldSku?: string;\n /**\n * Optional. When replacing a subscription you can decide on a\n * specific proration mode to charge the user.\n * The default is IMMEDIATE_WITH_TIME_PRORATION.\n */\n replaceSkuProrationMode?: ReplaceSkuProrationMode;\n /**\n * Optional. When a user chooses a contribution, they have the option\n * to make it non-recurring.\n */\n oneTime?: boolean;\n /**\n * Optional. Extra data relating to the request.\n */\n metadata?: object;\n}\n\nexport interface LinkSubscriptionRequest {\n publisherProvidedId: string;\n}\n\nexport interface LinkSubscriptionResult {\n publisherProvidedId?: string | null;\n success: boolean;\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Debug logger, only log message if #swg.log=1 exists in URL.\n */\nexport function debugLog(...args: unknown[]) {\n if (/swg.debug=1/.test(self.location.hash)) {\n args.unshift('[Subscriptions]');\n log(...args);\n }\n}\n\nexport function log(...args: unknown[]) {\n // eslint-disable-next-line no-console\n console /*OK*/\n .log(...args);\n}\n\nexport function warn(...args: unknown[]) {\n // eslint-disable-next-line no-console\n console /*OK*/\n .warn(...args);\n}\n\n/**\n * Throws an error if the first argument isn't trueish.\n *\n * Supports argument substitution into the message via %s placeholders.\n * @param shouldBeTrueish The value to assert. The assert fails if it does\n * not evaluate to true.\n * @param message The assertion message\n * @param args Arguments substituted into %s in the message.\n */\nexport function assert(\n shouldBeTrueish: unknown,\n message = 'Assertion failed',\n ...args: unknown[]\n): void {\n if (shouldBeTrueish) {\n return;\n }\n\n const splitMessage = message.split('%s');\n const first = splitMessage.shift();\n let formatted = first;\n for (const arg of args) {\n const nextConstant = splitMessage.shift();\n formatted += toString(arg) + nextConstant;\n }\n throw new Error(formatted);\n}\n\nfunction toString(val: unknown): string {\n // Do check equivalent to `val instanceof Element` without cross-window bug\n const possibleElement = val as Element;\n if (possibleElement?.nodeType == 1) {\n return (\n possibleElement.tagName.toLowerCase() +\n (possibleElement.id ? '#' + possibleElement.id : '')\n );\n }\n return String(val);\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {assert} from './log';\n\n/**\n * Converts a string which holds 8-bit code points, such as the result of atob,\n * into a Uint8Array with the corresponding bytes.\n * If you have a string of characters, you probably want to be using utf8Encode.\n */\nexport function stringToBytes(str: string): Uint8Array {\n const bytes = new Uint8Array(str.length);\n for (let i = 0; i < str.length; i++) {\n const charCode = str.charCodeAt(i);\n assert(charCode <= 255, 'Characters must be in range [0,255]');\n bytes[i] = charCode;\n }\n return bytes;\n}\n\n/**\n * Converts a 8-bit bytes array into a string\n */\nexport function bytesToString(bytes: Uint8Array): string {\n // Intentionally avoids String.fromCharCode.apply so we don't suffer a\n // stack overflow. #10495, https://jsperf.com/bytesToString-2\n const array = new Array(bytes.length);\n for (let i = 0; i < bytes.length; i++) {\n array[i] = String.fromCharCode(bytes[i]);\n }\n return array.join('');\n}\n\n/**\n * Interpret a byte array as a UTF-8 string.\n */\nexport function utf8DecodeSync(bytes: Uint8Array): string {\n if (typeof TextDecoder !== 'undefined') {\n return new TextDecoder('utf-8').decode(bytes);\n }\n const asciiString = bytesToString(new Uint8Array(bytes));\n return decodeURIComponent(escape(asciiString));\n}\n\n/**\n * Turn a string into UTF-8 bytes.\n */\nexport function utf8EncodeSync(string: string): Uint8Array {\n if (typeof TextEncoder !== 'undefined') {\n return new TextEncoder().encode(string);\n }\n return stringToBytes(unescape(encodeURIComponent(string)));\n}\n\n/**\n * Converts a string which is in base64url encoding into a Uint8Array\n * containing the decoded value.\n */\nexport function base64UrlDecodeToBytes(str: string): Uint8Array {\n const encoded = atob(str.replaceAll('-', '+').replaceAll('_', '/'));\n return stringToBytes(encoded);\n}\n\n/**\n * Converts a bytes array into base64url encoded string.\n * base64url is defined in RFC 4648. It is sometimes referred to as \"web safe\".\n */\nexport function base64UrlEncodeFromBytes(bytes: Uint8Array): string {\n const str = bytesToString(bytes);\n return btoa(str)\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replaceAll('=', '');\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Parses the given `json` string without throwing an exception if not valid.\n * Returns `undefined` if parsing fails.\n * Returns the `Object` corresponding to the JSON string when parsing succeeds.\n * @param json JSON string to parse\n * @param onFailed Optional function that will be called\n * with the error if parsing fails.\n * @return Value parsed from JSON.\n */\nexport function tryParseJson(\n json: string,\n onFailed?: (err: Error) => void\n): unknown {\n try {\n return JSON.parse(json);\n } catch (err: unknown) {\n if (onFailed) {\n onFailed(err as Error);\n }\n return undefined;\n }\n}\n\n/**\n * Converts the passed string into a JSON object (if possible) and returns the\n * value of the propertyName on that object.\n */\nexport function getPropertyFromJsonString(\n jsonString: string,\n propertyName: string\n): unknown {\n const json = tryParseJson(jsonString) as {[key: string]: unknown};\n return json?.[propertyName];\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {base64UrlDecodeToBytes, utf8DecodeSync} from './bytes';\nimport {tryParseJson} from './json';\n\ninterface JwtTokenInternalDef {\n header: unknown;\n payload: unknown;\n verifiable: string;\n sig: string;\n}\n\n/**\n * Provides helper methods to decode and verify JWT tokens.\n */\nexport class JwtHelper {\n constructor() {}\n\n /**\n * Decodes JWT token and returns its payload.\n */\n decode(encodedToken: string): unknown {\n return this.decodeInternal_(encodedToken).payload;\n }\n\n private decodeInternal_(encodedToken: string): JwtTokenInternalDef {\n // See https://jwt.io/introduction/\n /**\n * Throws error about invalid token.\n */\n function invalidToken() {\n throw new Error(`Invalid token: \"${encodedToken}\"`);\n }\n\n // Encoded token has three parts: header.payload.sig\n // Note! The padding is not allowed by JWT spec:\n // http://self-issued.info/docs/draft-goland-json-web-token-00.html#rfc.section.5\n const parts = encodedToken.split('.');\n if (parts.length != 3) {\n invalidToken();\n }\n const headerUtf8Bytes = base64UrlDecodeToBytes(parts[0]);\n const payloadUtf8Bytes = base64UrlDecodeToBytes(parts[1]);\n return {\n header: tryParseJson(utf8DecodeSync(headerUtf8Bytes), invalidToken),\n payload: tryParseJson(utf8DecodeSync(payloadUtf8Bytes), invalidToken),\n verifiable: `${parts[0]}.${parts[1]}`,\n sig: parts[2],\n };\n }\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {Entitlements} from './entitlements';\nimport {UserData} from './user-data';\n\nexport class SubscribeResponse {\n constructor(\n readonly raw: string,\n readonly purchaseData: PurchaseData,\n readonly userData: UserData | null,\n readonly entitlements: Entitlements | null,\n readonly productType: string,\n private readonly completeHandler_: () => Promise,\n readonly oldSku: string | null = null,\n readonly swgUserToken: string | null = null,\n readonly paymentRecurrence: number | null = null,\n readonly requestMetadata: unknown = null\n ) {}\n\n clone(): SubscribeResponse {\n return new SubscribeResponse(\n this.raw,\n this.purchaseData,\n this.userData,\n this.entitlements,\n this.productType,\n this.completeHandler_,\n this.oldSku,\n this.swgUserToken\n );\n }\n\n json() {\n return {\n 'purchaseData': this.purchaseData.json(),\n 'userData': this.userData ? this.userData.json() : null,\n 'entitlements': this.entitlements ? this.entitlements.json() : null,\n 'oldSku': this.oldSku,\n 'productType': this.productType,\n 'swgUserToken': this.swgUserToken,\n };\n }\n\n /**\n * Allows the receiving site to complete/acknowledge that it registered\n * the subscription purchase. The typical action would be to create an\n * account (or match an existing one) and associated the purchase with\n * that account.\n *\n * SwG will display progress indicator until this method is called and\n * upon receiving this call will show the confirmation to the user.\n * The promise returned by this method will yield once the user closes\n * the confirmation.\n */\n complete(): Promise {\n return this.completeHandler_();\n }\n}\n\nexport class PurchaseData {\n readonly data: string;\n\n constructor(readonly raw: string, readonly signature: string) {\n this.data = raw;\n }\n\n clone(): PurchaseData {\n return new PurchaseData(this.raw, this.signature);\n }\n\n json() {\n return {\n 'data': this.raw,\n 'signature': this.signature,\n };\n }\n}\n","/**\n * Copyright 2021 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst Constants = {\n /**\n * IAB Audience taxonomy version for logging PPS values to localStorage.\n * Value mapped to googletag.enums.Taxonomy.IAB_AUDIENCE_1_1.\n */\n PPS_AUDIENCE_TAXONOMY_KEY: 1,\n};\n\nconst StorageKeys = {\n /**\n * Local storage key for swgUserToken.\n */\n USER_TOKEN: 'USER_TOKEN',\n\n /**\n * Local storage key for read time.\n */\n READ_TIME: 'READ_TIME',\n\n /**\n * Local storage key for cacheable entitlements.\n */\n ENTITLEMENTS: 'ents',\n\n /**\n * Local storage key for whether credential isReadyToPay.\n */\n IS_READY_TO_PAY: 'isreadytopay',\n\n /**\n * Local storage key for redirect.\n */\n REDIRECT: 'subscribe.google.com:rk',\n\n /**\n * Local storage key for survey completed timestamps.\n */\n SURVEY_COMPLETED: 'surveycompleted',\n\n /**\n * Local storage key for survey data transfer failure timestamps.\n */\n SURVEY_DATA_TRANSFER_FAILED: 'surveydatatransferfailed',\n\n /**\n * Local storage key for whether toast was shown.\n */\n TOAST: 'toast',\n\n /**\n * Local storage key for frequency capping timestamps.\n */\n TIMESTAMPS: 'tsp',\n};\n\nconst StorageKeysWithoutPublicationIdSuffix = {\n /**\n * Local storage key for IAB Audience Taxonomy values. It must take on the\n * 'values' as defined by the PPS GPT API.\n * In order to maintain legacy code snippet that's provided to publishers without publicationId suffix, the stroage key should remain as \"subscribe.google.com:ppstaxonomies\".\n */\n PPS_TAXONOMIES: 'ppstaxonomies',\n};\n\nexport {Constants, StorageKeys, StorageKeysWithoutPublicationIdSuffix};\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport class UserData {\n id: string;\n email: string;\n emailVerified: string;\n name: string;\n givenName: string;\n familyName: string;\n pictureUrl: string;\n\n constructor(\n public readonly idToken: string,\n public readonly data: {[key: string]: string}\n ) {\n this.id = data['sub'];\n this.email = data['email'];\n this.emailVerified = data['email_verified'];\n this.name = data['name'];\n this.givenName = data['given_name'];\n this.familyName = data['family_name'];\n this.pictureUrl = data['picture'];\n }\n\n clone(): UserData {\n return new UserData(this.idToken, this.data);\n }\n\n json() {\n return {\n 'id': this.id,\n 'email': this.email,\n 'emailVerified': this.emailVerified,\n 'name': this.name,\n 'givenName': this.givenName,\n 'familyName': this.familyName,\n 'pictureUrl': this.pictureUrl,\n };\n }\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {Doc} from '../model/doc';\nimport {Message} from '../proto/api_messages';\nimport {warn} from './log';\n\n// NOTE: This regex was copied from SwG's AMP extension. https://github.com/ampproject/amphtml/blob/c23bf281f817a2ee5df73f6fd45e9f4b71bb68b6/extensions/amp-subscriptions-google/0.1/amp-subscriptions-google.js#L56\nconst GOOGLE_DOMAIN_RE = /(^|\\.)google\\.(com?|[a-z]{2}|com?\\.[a-z]{2}|cat)$/;\n\ninterface Location {\n href: string;\n protocol: string;\n host: string;\n hostname: string;\n port: string;\n pathname: string;\n search: string;\n hash: string;\n origin: string;\n}\n\n/**\n * Cached a-tag to avoid memory allocation during URL parsing.\n */\nconst a = self.document.createElement('a');\n\n/**\n * We cached all parsed URLs. As of now there are no use cases\n * of AMP docs that would ever parse an actual large number of URLs,\n * but we often parse the same one over and over again.\n */\nconst cache: {[url: string]: Location} = {};\n\n/**\n * Returns a Location-like object for the given URL. If it is relative,\n * the URL gets resolved.\n * Consider the returned object immutable. This is enforced during\n * testing by freezing the object.\n */\nexport function parseUrl(url: string): Location {\n const fromCache = cache[url];\n if (fromCache) {\n return fromCache;\n }\n\n const info = parseUrlWithA(a, url);\n\n return (cache[url] = info);\n}\n\n/**\n * Returns a Location-like object for the given URL. If it is relative,\n * the URL gets resolved.\n */\nfunction parseUrlWithA(a: HTMLAnchorElement, url: string): Location {\n a.href = url;\n\n const info: Location = {\n href: a.href,\n protocol: a.protocol,\n host: a.host,\n hostname: a.hostname,\n port: a.port == '0' ? '' : a.port,\n pathname: a.pathname,\n search: a.search,\n hash: a.hash,\n origin: a.protocol + '//' + a.host,\n };\n\n // For data URI a.origin is equal to the string 'null' which is not useful.\n // We instead return the actual origin which is the full URL.\n if (a.origin && a.origin !== 'null') {\n info.origin = a.origin;\n } else if (info.protocol === 'data:' || !info.host) {\n info.origin = info.href;\n }\n return info;\n}\n\n/**\n * Parses and builds Object of URL query string.\n * @param query The URL query string.\n */\nexport function parseQueryString(query: string): {[key: string]: string} {\n if (!query) {\n return {};\n }\n return (/^[?#]/.test(query) ? query.slice(1) : query)\n .split('&')\n .reduce((params, param) => {\n const item = param.split('=');\n try {\n const key = decodeURIComponent(item[0] || '');\n const value = decodeURIComponent(item[1] || '');\n if (key) {\n params[key] = value;\n }\n } catch (err) {\n // eslint-disable-next-line no-console\n warn(`SwG could not parse a URL query param: ${item[0]}`);\n }\n return params;\n }, {} as {[key: string]: string});\n}\n\n/**\n * Adds a parameter to a query string.\n */\nexport function addQueryParam(\n url: string,\n param: string,\n value: string\n): string {\n const queryIndex = url.indexOf('?');\n const fragmentIndex = url.indexOf('#');\n let fragment = '';\n if (fragmentIndex != -1) {\n fragment = url.substring(fragmentIndex);\n url = url.substring(0, fragmentIndex);\n }\n if (queryIndex == -1) {\n url += '?';\n } else if (queryIndex < url.length - 1) {\n url += '&';\n }\n url += encodeURIComponent(param) + '=' + encodeURIComponent(value);\n\n return url + fragment;\n}\n\nexport function serializeProtoMessageForUrl(message: Message): string {\n return JSON.stringify(message.toArray(false));\n}\n\nexport function getCanonicalTag(doc: Doc): string | undefined {\n const rootNode = doc.getRootNode();\n const canonicalTag = rootNode.querySelector(\n \"link[rel='canonical']\"\n ) as HTMLLinkElement;\n return canonicalTag?.href;\n}\n\n/**\n * Returns the canonical URL from the canonical tag. If the canonical tag is\n * not present, treat the doc URL itself as canonical.\n */\nexport function getCanonicalUrl(doc: Doc): string {\n const rootNode = doc.getRootNode();\n return (\n getCanonicalTag(doc) ||\n rootNode.location.origin + rootNode.location.pathname\n );\n}\n\nconst PARSED_URL = parseUrl(self.window.location.href);\nconst PARSED_REFERRER = parseUrl(self.document.referrer);\n\n/**\n * True for Google domains\n * @param parsedUrl Defaults to the current page's URL\n */\nfunction isGoogleDomain(parsedUrl: Location): boolean {\n return GOOGLE_DOMAIN_RE.test(parsedUrl.hostname);\n}\n\n/**\n * True for HTTPS URLs\n * @param parsedUrl Defaults to the current page's URL\n */\nexport function isSecure(parsedUrl = PARSED_URL): boolean {\n return parsedUrl.protocol === 'https' || parsedUrl.protocol === 'https:';\n}\n\n/**\n * True when the page is rendered within a secure Google application or\n * was linked to from a secure Google domain.\n * @param parsedReferrer Defaults to the current page's referrer\n */\nexport function wasReferredByGoogle(parsedReferrer = PARSED_REFERRER): boolean {\n return isSecure(parsedReferrer) && isGoogleDomain(parsedReferrer);\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ADS_SERVER,\n FRONTEND,\n FRONTEND_CACHE,\n INTERNAL_RUNTIME_VERSION,\n PAY_ENVIRONMENT,\n PLAY_ENVIRONMENT,\n} from '../constants';\nimport {addQueryParam, parseQueryString, parseUrl} from '../utils/url';\n\n/** Possible cache keys which can influence how Swgjs busts caches for iframes. */\ntype CacheKey = 'zero' | 'nocache' | 'hr1' | 'hr12';\n\n/**\n * Have to put these in the map to avoid compiler optimization. Due to\n * optimization issues, this map only allows property-style keys. E.g. \"hr1\",\n * as opposed to \"1hr\".\n */\nexport const CACHE_KEYS: {[key in CacheKey]: string} = {\n 'zero': '0', //testing value\n 'nocache': '1',\n 'hr1': '3600000', // 1hr = 1000 * 60 * 60\n 'hr12': '43200000', // 12hr = 1000 * 60 * 60 * 12\n};\n\ninterface OperatingMode {\n frontEnd: string;\n payEnv: string;\n playEnv: string;\n feCache: CacheKey;\n}\n\n/**\n * Default operating Mode\n */\nexport const DEFAULT: OperatingMode = {\n frontEnd: FRONTEND,\n payEnv: PAY_ENVIRONMENT,\n playEnv: PLAY_ENVIRONMENT,\n feCache: FRONTEND_CACHE,\n};\n\n/**\n * Default operating Mode\n */\nconst PROD: OperatingMode = {\n frontEnd: 'https://news.google.com',\n payEnv: 'PRODUCTION',\n playEnv: 'PROD',\n feCache: 'nocache',\n};\n\n/**\n * Default operating Mode\n */\nconst AUTOPUSH: OperatingMode = {\n frontEnd: 'https://subscribe-autopush.sandbox.google.com',\n payEnv: 'PRODUCTION',\n playEnv: 'AUTOPUSH',\n feCache: 'nocache',\n};\n\n/**\n * Default operating Mode\n */\nconst QUAL: OperatingMode = {\n frontEnd: 'https://subscribe-qual.sandbox.google.com',\n payEnv: 'SANDBOX',\n playEnv: 'STAGING',\n feCache: 'nocache',\n};\n\n/**\n * Operating modes, only runtime switchable modes are here.\n * Build time modes set the default and are configured in prepare.sh.\n *\n * IMPORTANT: modes other than prod will only work on Google internal networks!\n */\nexport const MODES: {[key: string]: OperatingMode} = {\n 'default': DEFAULT,\n 'prod': PROD,\n 'autopush': AUTOPUSH,\n 'qual': QUAL,\n};\n\n/**\n * Check for swg.mode= in url fragment. If it exists, use it,\n * otherwise use the default build mode.\n */\nexport function getSwgMode(): OperatingMode {\n const query = parseQueryString(self.location.hash);\n const swgMode = query['swg.mode'];\n if (swgMode && MODES[swgMode]) {\n return MODES[swgMode];\n }\n return MODES['default'];\n}\n\nexport function feOrigin(): string {\n return parseUrl(getSwgMode().frontEnd).origin;\n}\n\n/**\n * @param url Relative URL, e.g. \"/service1\".\n * @return The complete URL.\n */\nexport function serviceUrl(url: string): string {\n // Allows us to make API calls with enabled experiments.\n const query = parseQueryString(self.location.hash);\n const experiments = query['swg.experiments'];\n if (experiments !== undefined) {\n url = addQueryParam(url, 'e', experiments);\n }\n\n return `${getSwgMode().frontEnd}/swg/_/api/v1` + url;\n}\n\n/**\n * @param url Relative URL, e.g. \"/service1\".\n * @return The complete URL.\n */\nexport function adsUrl(url: string): string {\n return ADS_SERVER + url;\n}\n\n/**\n * @param url Relative URL, e.g. \"/offersiframe\".\n * @param params List of extra params to append to the URL.\n * @param prefix\n * @return The complete URL.\n */\nexport function feUrl(\n url: string,\n params: {[key: string]: string} = {},\n prefix = ''\n): string {\n // Add cache param.\n const prefixed = prefix ? `swg/${prefix}` : 'swg';\n url = feCached(`${getSwgMode().frontEnd}/${prefixed}/ui/v1${url}`);\n\n // Optionally add jsmode param. This allows us to test against \"aggressively\" compiled Boq JS.\n const query = parseQueryString(self.location.hash);\n const boqJsMode = query['swg.boqjsmode'];\n if (boqJsMode !== undefined) {\n url = addQueryParam(url, 'jsmode', boqJsMode);\n }\n\n // Allows us to open iframes with enabled experiments.\n const experiments = query['swg.experiments'];\n if (experiments !== undefined) {\n url = addQueryParam(url, 'e', experiments);\n }\n\n for (const param in params) {\n url = addQueryParam(url, param, params[param]);\n }\n\n return url;\n}\n\n/**\n * @param url FE URL.\n * @return The complete URL including cache param.\n */\nexport function feCached(url: string): string {\n return addQueryParam(url, '_', cacheParam(getSwgMode().feCache));\n}\n\nexport function feArgs(args: {}): {} {\n return Object.assign(args, {\n '_client': `SwG ${INTERNAL_RUNTIME_VERSION}`,\n });\n}\n\nexport function cacheParam(cacheKey: CacheKey): string {\n const period = Number(CACHE_KEYS[cacheKey] || 1);\n if (period === 0) {\n return '_';\n }\n const now = Date.now();\n return String(period <= 1 ? now : Math.floor(now / period));\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * The Flow goes like this:\n * a. Start Payments\n * b. Complete Payments\n * c. Create Account\n * d. Acknowledge Account\n *\n * In other words, Flow = Payments + Account Creation.\n */\n\nimport {\n AccountCreationRequest,\n EntitlementsResponse,\n} from '../proto/api_messages';\nimport {ActivityIframeView} from '../ui/activity-iframe-view';\nimport {ActivityPorts} from '../components/activities';\nimport {AnalyticsEvent, EventParams} from '../proto/api_messages';\nimport {AnalyticsService} from './analytics-service';\nimport {ClientConfigManager} from './client-config-manager';\nimport {ClientEventManager} from './client-event-manager';\nimport {Deps} from './deps';\nimport {DialogManager} from '../components/dialog-manager';\nimport {Entitlements} from '../api/entitlements';\nimport {JwtHelper} from '../utils/jwt';\nimport {PageConfig} from '../model/page-config';\nimport {PayClient, PaymentCancelledError} from './pay-client';\nimport {\n PaymentData,\n SwgCallbackData,\n} from '../../third_party/gpay/src/payjs_async';\nimport {\n ProductType,\n SubscriptionFlows,\n SubscriptionRequest,\n WindowOpenMode,\n} from '../api/subscriptions';\nimport {PurchaseData, SubscribeResponse} from '../api/subscribe-response';\nimport {StorageKeys} from '../utils/constants';\nimport {UserData} from '../api/user-data';\nimport {feArgs, feUrl} from './services';\nimport {getPropertyFromJsonString} from '../utils/json';\nimport {getSwgMode} from './services';\nimport {isCancelError} from '../utils/errors';\n\n/**\n * Subscribe with Google request to pass to payments.\n */\nexport interface SwgPaymentRequest {\n skuId: string;\n publicationId?: string;\n oldSku?: string;\n replaceSkuProrationMode?: number;\n paymentRecurrence?: number;\n swgVersion?: string;\n metadata?: object;\n}\n\n/**\n * String values input by the publisher are mapped to the number values.\n */\nexport const ReplaceSkuProrationModeMapping: {[key: string]: number} = {\n // The replacement takes effect immediately, and the remaining time will\n // be prorated and credited to the user. This is the current default\n // behavior.\n 'IMMEDIATE_WITH_TIME_PRORATION': 1,\n};\n\nexport const RecurrenceMapping = {\n 'AUTO': 1,\n 'ONE_TIME': 2,\n};\n\nfunction getEventParams(\n sku: string,\n subscriptionFlow: string | null = null\n): EventParams {\n return new EventParams([, , , , sku, , , subscriptionFlow]);\n}\n\n/**\n * The flow to initiate payment process.\n */\nexport class PayStartFlow {\n private readonly analyticsService_: AnalyticsService;\n private readonly clientConfigManager_: ClientConfigManager;\n private readonly eventManager_: ClientEventManager;\n private readonly pageConfig_: PageConfig;\n private readonly payClient_: PayClient;\n\n constructor(\n private readonly deps_: Deps,\n private readonly subscriptionRequest_: SubscriptionRequest,\n private readonly productType_: ProductType = ProductType.SUBSCRIPTION\n ) {\n this.payClient_ = deps_.payClient();\n\n this.pageConfig_ = deps_.pageConfig();\n\n this.analyticsService_ = deps_.analytics();\n\n this.eventManager_ = deps_.eventManager();\n\n this.clientConfigManager_ = deps_.clientConfigManager();\n }\n\n /**\n * Starts the payments flow.\n */\n async start(): Promise {\n // Get the paySwgVersion for buyflow.\n const clientConfig = await this.clientConfigManager_.getClientConfig();\n this.start_(clientConfig.paySwgVersion);\n }\n\n /**\n * Starts the payments flow for the given version.\n */\n private start_(paySwgVersion?: string): void {\n const swgPaymentRequest: SwgPaymentRequest = {\n 'skuId': this.subscriptionRequest_['skuId'],\n 'publicationId': this.pageConfig_.getPublicationId(),\n };\n\n if (paySwgVersion) {\n swgPaymentRequest['swgVersion'] = paySwgVersion;\n }\n\n if (this.subscriptionRequest_['oldSku']) {\n swgPaymentRequest['oldSku'] = this.subscriptionRequest_['oldSku'];\n // Map the proration mode to the enum value (if proration exists).\n const prorationMode =\n this.subscriptionRequest_['replaceSkuProrationMode'];\n if (prorationMode) {\n swgPaymentRequest['replaceSkuProrationMode'] =\n ReplaceSkuProrationModeMapping[prorationMode];\n } else {\n swgPaymentRequest['replaceSkuProrationMode'] =\n ReplaceSkuProrationModeMapping['IMMEDIATE_WITH_TIME_PRORATION'];\n }\n this.analyticsService_.setSku(swgPaymentRequest['oldSku']);\n }\n\n // Assign one-time recurrence enum if applicable\n if (this.subscriptionRequest_['oneTime']) {\n swgPaymentRequest['paymentRecurrence'] = RecurrenceMapping['ONE_TIME'];\n }\n\n // Assign additional metadata if available.\n if (this.subscriptionRequest_['metadata']) {\n swgPaymentRequest['metadata'] = this.subscriptionRequest_['metadata'];\n }\n\n // Start/cancel events.\n const flow =\n this.productType_ == ProductType.UI_CONTRIBUTION\n ? SubscriptionFlows.CONTRIBUTE\n : SubscriptionFlows.SUBSCRIBE;\n\n this.deps_.callbacks().triggerFlowStarted(flow, this.subscriptionRequest_);\n\n this.eventManager_.logSwgEvent(\n AnalyticsEvent.ACTION_PAYMENT_FLOW_STARTED,\n true,\n getEventParams(swgPaymentRequest['skuId'])\n );\n PayCompleteFlow.waitingForPayClient = true;\n this.payClient_.start(\n {\n 'apiVersion': 1,\n 'allowedPaymentMethods': ['CARD'],\n 'environment': getSwgMode().payEnv,\n 'playEnvironment': getSwgMode().playEnv,\n 'swg': swgPaymentRequest,\n 'i': {\n 'startTimeMs': Date.now(),\n 'productType': this.productType_,\n },\n },\n {\n forceRedirect:\n this.deps_.config().windowOpenMode == WindowOpenMode.REDIRECT,\n // SwG basic does not support native.\n forceDisableNative: paySwgVersion == '2',\n }\n );\n }\n}\n\n/**\n * The flow for successful payments completion.\n */\nexport class PayCompleteFlow {\n static waitingForPayClient = false;\n\n static configurePending(deps: Deps): void {\n const eventManager = deps.eventManager();\n\n deps.payClient().onResponse(async (payPromise) => {\n deps.entitlementsManager().blockNextNotification();\n const flow = new PayCompleteFlow(deps);\n const promise = validatePayResponse(\n deps,\n payPromise,\n flow.complete.bind(flow)\n );\n deps.callbacks().triggerPaymentResponse(promise);\n\n try {\n const response = await promise;\n const sku = parseSkuFromPurchaseDataSafe(response.purchaseData);\n deps.analytics().setSku(sku || '');\n eventManager.logSwgEvent(\n AnalyticsEvent.ACTION_PAYMENT_COMPLETE,\n true,\n getEventParams(\n sku || '',\n response.productType == ProductType.UI_CONTRIBUTION\n ? SubscriptionFlows.CONTRIBUTE\n : SubscriptionFlows.SUBSCRIBE\n )\n );\n if (response.productType == ProductType.UI_CONTRIBUTION) {\n eventManager.logSwgEvent(\n AnalyticsEvent.EVENT_CONTRIBUTION_PAYMENT_COMPLETE,\n true,\n getEventParams(sku || '', SubscriptionFlows.CONTRIBUTE)\n );\n } else if (response.productType == ProductType.SUBSCRIPTION) {\n eventManager.logSwgEvent(\n AnalyticsEvent.EVENT_SUBSCRIPTION_PAYMENT_COMPLETE,\n true,\n getEventParams(sku || '', SubscriptionFlows.SUBSCRIBE)\n );\n }\n flow.start(response);\n } catch (err) {\n const reason = err as PaymentCancelledError;\n if (isCancelError(reason as Error)) {\n const productType = reason['productType'];\n const flow =\n productType == ProductType.UI_CONTRIBUTION\n ? SubscriptionFlows.CONTRIBUTE\n : SubscriptionFlows.SUBSCRIBE;\n deps.callbacks().triggerFlowCanceled(flow);\n deps\n .eventManager()\n .logSwgEvent(AnalyticsEvent.ACTION_USER_CANCELED_PAYFLOW, true);\n } else {\n deps\n .eventManager()\n .logSwgEvent(AnalyticsEvent.EVENT_PAYMENT_FAILED, false);\n deps.jserror().error('Pay failed', reason as Error);\n throw reason;\n }\n }\n });\n }\n\n private readonly win_: Window;\n private readonly activityPorts_: ActivityPorts;\n private readonly dialogManager_: DialogManager;\n private readonly eventManager_: ClientEventManager;\n private readonly clientConfigManager_: ClientConfigManager;\n\n private activityIframeView_: ActivityIframeView | null = null;\n private readyPromise_: Promise | null = null;\n private sku_: string | null = null;\n\n constructor(private readonly deps_: Deps) {\n this.win_ = deps_.win();\n\n this.activityPorts_ = deps_.activities();\n\n this.dialogManager_ = deps_.dialogManager();\n\n this.eventManager_ = deps_.eventManager();\n\n this.clientConfigManager_ = deps_.clientConfigManager();\n }\n\n /**\n * Starts the payments completion flow.\n */\n async start(response: SubscribeResponse): Promise {\n this.sku_ = parseSkuFromPurchaseDataSafe(response.purchaseData);\n this.eventManager_.logSwgEvent(\n AnalyticsEvent.IMPRESSION_ACCOUNT_CHANGED,\n true,\n getEventParams(this.sku_ || '')\n );\n this.deps_.entitlementsManager().reset(true);\n // TODO(dianajing): future-proof isOneTime flag\n const args: {[key: string]: string | boolean | undefined} = {\n 'publicationId': this.deps_.pageConfig().getPublicationId(),\n 'productType': response['productType'],\n 'isSubscriptionUpdate': !!response['oldSku'],\n 'isOneTime': !!response['paymentRecurrence'],\n };\n\n // TODO(dvoytenko, #400): cleanup once entitlements is launched everywhere.\n if (response.userData && response.entitlements) {\n args['idToken'] = response.userData.idToken;\n this.deps_\n .entitlementsManager()\n .pushNextEntitlements(response.entitlements.raw);\n // Persist swgUserToken in local storage\n if (response.swgUserToken) {\n this.deps_\n .storage()\n .set(StorageKeys.USER_TOKEN, response.swgUserToken, true);\n }\n } else {\n args['loginHint'] = response.userData?.email;\n }\n\n const urlParams: {[key: string]: string} = {};\n if (this.clientConfigManager_.shouldForceLangInIframes()) {\n urlParams.hl = this.clientConfigManager_.getLanguage();\n }\n const confirmFeUrl = feUrl('/payconfirmiframe', urlParams);\n\n const clientConfig = await this.clientConfigManager_.getClientConfig();\n\n args['useUpdatedConfirmUi'] = clientConfig.useUpdatedOfferFlows;\n args['skipAccountCreationScreen'] = clientConfig.skipAccountCreationScreen;\n this.activityIframeView_ = new ActivityIframeView(\n this.win_,\n this.activityPorts_,\n confirmFeUrl,\n feArgs(args),\n /* shouldFadeBody */ true\n );\n this.activityIframeView_.on(\n EntitlementsResponse,\n this.handleEntitlementsResponse_.bind(this)\n );\n\n this.activityIframeView_.acceptResult().then(() => {\n // The flow is complete.\n this.dialogManager_.completeView(this.activityIframeView_);\n });\n\n this.readyPromise_ = this.dialogManager_.openView(this.activityIframeView_);\n\n this.readyPromise_.then(() => {\n this.deps_.callbacks().triggerPayConfirmOpened(this.activityIframeView_!);\n });\n }\n\n private handleEntitlementsResponse_(response: EntitlementsResponse): void {\n const jwt = response.getJwt();\n if (jwt) {\n this.deps_.entitlementsManager().pushNextEntitlements(jwt);\n }\n }\n\n async complete(): Promise {\n this.eventManager_.logSwgEvent(\n AnalyticsEvent.ACTION_ACCOUNT_CREATED,\n true,\n getEventParams(this.sku_ || '')\n );\n\n const now = Date.now().toString();\n this.deps_\n .storage()\n .set(StorageKeys.READ_TIME, now, /*useLocalStorage=*/ false);\n\n this.deps_.entitlementsManager().unblockNextNotification();\n\n const clientConfig = await this.clientConfigManager_.getClientConfig();\n\n await this.readyPromise_;\n\n // Skip account creation screen if requested (needed for AMP)\n if (!clientConfig.skipAccountCreationScreen) {\n const accountCompletionRequest = new AccountCreationRequest();\n accountCompletionRequest.setComplete(true);\n this.activityIframeView_!.execute(accountCompletionRequest);\n }\n\n try {\n await this.activityIframeView_!.acceptResult();\n } catch (err) {\n // Ignore errors.\n }\n\n if (!clientConfig.skipAccountCreationScreen) {\n this.eventManager_.logSwgEvent(\n AnalyticsEvent.ACTION_ACCOUNT_ACKNOWLEDGED,\n true,\n getEventParams(this.sku_ || '')\n );\n }\n\n this.deps_.entitlementsManager().setToastShown(true);\n }\n}\n\nasync function validatePayResponse(\n deps: Deps,\n payPromise: Promise,\n completeHandler: () => Promise\n): Promise {\n const wasRedirect = !PayCompleteFlow.waitingForPayClient;\n PayCompleteFlow.waitingForPayClient = false;\n const data = await payPromise;\n // 1) We log against a random TX ID which is how we track a specific user\n // anonymously.\n // 2) If there was a redirect to gPay, we may have lost our stored TX ID.\n // 3) Pay service is supposed to give us the TX ID it logged against.\n let eventType = AnalyticsEvent.UNKNOWN;\n let eventParams = undefined;\n if (typeof data !== 'object' || !data['googleTransactionId']) {\n // If gPay doesn't give us a TX ID it means that something may\n // be wrong. If we previously logged then we are at least continuing to\n // log against the same TX ID. If we didn't previously log then we have\n // lost all connection to the events that preceded the payment event and\n // we at least want to know why that data was lost.\n eventParams = new EventParams();\n eventParams.setHadLogged(!wasRedirect);\n eventType = AnalyticsEvent.EVENT_GPAY_NO_TX_ID;\n } else {\n const oldTxId = deps.analytics().getTransactionId();\n const newTxId = data['googleTransactionId'];\n\n if (wasRedirect) {\n // This is the expected case for full redirects. It may be happening\n // unexpectedly at other times too though and we want to be aware of\n // it if it does.\n deps.analytics().setTransactionId(newTxId);\n eventType = AnalyticsEvent.EVENT_GPAY_CANNOT_CONFIRM_TX_ID;\n } else {\n if (oldTxId === newTxId) {\n // This is the expected case for non-redirect pay events\n eventType = AnalyticsEvent.EVENT_CONFIRM_TX_ID;\n } else {\n // This is an unexpected case: gPay rejected our TX ID and created\n // its own. Log the gPay TX ID but keep our logging consistent.\n eventParams = new EventParams();\n eventParams.setGpayTransactionId(newTxId);\n eventType = AnalyticsEvent.EVENT_CHANGED_TX_ID;\n }\n }\n }\n deps.eventManager().logSwgEvent(eventType, true, eventParams);\n return parseSubscriptionResponse(deps, data, completeHandler);\n}\n\nexport function parseSubscriptionResponse(\n deps: Deps,\n data: PaymentData,\n completeHandler: () => Promise\n): SubscribeResponse {\n let swgData: SwgCallbackData | null = null;\n let raw: string | null = null;\n let productType = ProductType.SUBSCRIPTION;\n let oldSku = null;\n let paymentRecurrence = null;\n let requestMetadata = null;\n\n if (data) {\n if (typeof data === 'string') {\n raw = data;\n } else {\n // Assume it's a json object in the format:\n // `{integratorClientCallbackData: \"...\"}` or `{swgCallbackData: \"...\"}`.\n const json = data;\n if (json['swgCallbackData']) {\n swgData = json['swgCallbackData'];\n } else if (json['integratorClientCallbackData']) {\n raw = json['integratorClientCallbackData'];\n }\n if (data['paymentRequest']) {\n const swgObj = data['paymentRequest']['swg'] || {};\n oldSku = swgObj['oldSku'];\n paymentRecurrence = swgObj['paymentRecurrence'];\n requestMetadata = swgObj['metadata'];\n productType =\n data['paymentRequest']['i']?.['productType'] ||\n ProductType.SUBSCRIPTION;\n }\n // Set productType if paymentRequest is not present, which happens\n // if the pay flow was opened in redirect mode.\n else if (data['productType']) {\n productType = data['productType'];\n }\n }\n }\n if (raw && !swgData) {\n raw = atob(raw);\n if (raw) {\n const parsed = JSON.parse(raw);\n swgData = parsed['swgCallbackData'];\n }\n }\n if (!swgData) {\n throw new Error('unexpected payment response');\n }\n raw = JSON.stringify(swgData);\n return new SubscribeResponse(\n raw,\n parsePurchaseData(swgData),\n parseUserData(swgData),\n parseEntitlements(deps, swgData),\n productType,\n completeHandler,\n oldSku,\n swgData['swgUserToken'],\n paymentRecurrence,\n requestMetadata\n );\n}\n\nfunction parsePurchaseData(swgData: SwgCallbackData): PurchaseData {\n const raw = swgData['purchaseData'];\n const signature = swgData['purchaseDataSignature'];\n return new PurchaseData(raw, signature);\n}\n\n/**\n * Visible for testing.\n */\nexport function parseUserData(swgData: SwgCallbackData): UserData | null {\n const idToken = swgData['idToken'];\n if (!idToken) {\n return null;\n }\n const jwt = new JwtHelper().decode(idToken);\n return new UserData(idToken, jwt as {[key: string]: string});\n}\n\n/**\n * Visible for testing.\n */\nexport function parseEntitlements(\n deps: Deps,\n swgData: SwgCallbackData\n): Entitlements | null {\n if (swgData['signedEntitlements']) {\n return deps.entitlementsManager().parseEntitlements(swgData);\n }\n return null;\n}\n\nfunction parseSkuFromPurchaseDataSafe(\n purchaseData: PurchaseData\n): string | null {\n return (\n (getPropertyFromJsonString(purchaseData.raw, 'productId') as string) || null\n );\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {ActivityIframeView} from '../ui/activity-iframe-view';\nimport {ActivityPorts} from '../components/activities';\nimport {\n AlreadySubscribedResponse,\n EntitlementsResponse,\n SkuSelectedResponse,\n SubscribeResponse,\n ViewSubscriptionsResponse,\n} from '../proto/api_messages';\nimport {AnalyticsEvent, EventParams} from '../proto/api_messages';\nimport {ClientConfig} from '../model/client-config';\nimport {ClientConfigManager} from './client-config-manager';\nimport {ClientEventManager} from './client-event-manager';\nimport {Deps} from './deps';\nimport {DialogConfig} from '../components/dialog';\nimport {DialogManager} from '../components/dialog-manager';\nimport {\n OffersRequest,\n ProductType,\n SubscriptionFlows,\n} from '../api/subscriptions';\nimport {PageConfig} from '../model/page-config';\nimport {PayStartFlow} from './pay-flow';\nimport {SubscriptionRequest} from '../api/subscriptions';\nimport {assert} from '../utils/log';\nimport {feArgs, feUrl} from './services';\nimport {parseQueryString} from '../utils/url';\n\nfunction getEventParams(sku: string): EventParams {\n return new EventParams([, , , , sku]);\n}\n\n/**\n * Offers view is closable when request was originated from 'AbbrvOfferFlow'\n * or from 'SubscribeOptionFlow'.\n */\nconst OFFERS_VIEW_CLOSABLE = true;\n\n// The value logged when the offers screen shows all available SKUs.\nconst ALL_SKUS = '*';\n\n/**\n * The class for Offers flow.\n */\nexport class OffersFlow {\n private activityIframeView_: ActivityIframeView | null = null;\n\n private readonly win_: Window;\n private readonly activityPorts_: ActivityPorts;\n private readonly dialogManager_: DialogManager;\n private readonly eventManager_: ClientEventManager;\n private readonly clientConfigManager_: ClientConfigManager;\n private readonly skus_?: string[];\n private readonly clientConfigPromise_?: Promise;\n private readonly activityIframeViewPromise_?: Promise;\n private readonly shouldAnimateFade_: boolean;\n private readonly isClosable_?: boolean;\n\n constructor(private readonly deps_: Deps, options?: OffersRequest) {\n this.win_ = deps_.win();\n\n this.activityPorts_ = deps_.activities();\n\n this.dialogManager_ = deps_.dialogManager();\n\n this.eventManager_ = deps_.eventManager();\n\n this.clientConfigManager_ = deps_.clientConfigManager();\n\n this.shouldAnimateFade_ =\n options?.shouldAnimateFade === undefined\n ? true\n : options?.shouldAnimateFade;\n\n // Default to hiding close button.\n this.isClosable_ = options?.isClosable ?? false;\n\n const feArgsObj: OffersRequest = deps_.activities().addDefaultArguments({\n 'showNative': deps_.callbacks().hasSubscribeRequestCallback(),\n 'productType': ProductType.SUBSCRIPTION,\n 'list': options?.list || 'default',\n 'skus': options?.skus || null,\n 'isClosable': this.isClosable_,\n });\n\n if (options?.oldSku) {\n feArgsObj['oldSku'] = options.oldSku;\n assert(feArgsObj['skus'], 'Need a sku list if old sku is provided!');\n\n // Remove old sku from offers if in list.\n let skuList = feArgsObj['skus']!;\n const oldSku = feArgsObj['oldSku'];\n skuList = skuList.filter((sku) => sku !== oldSku);\n\n assert(\n skuList.length > 0,\n 'Sku list only contained offer user already has'\n );\n feArgsObj['skus'] = skuList;\n }\n\n // Redirect to payments if only one upgrade option is passed.\n if (feArgsObj['skus'] && feArgsObj['skus'].length === 1) {\n const sku = feArgsObj['skus'][0];\n const oldSku = feArgsObj['oldSku'];\n // Update subscription triggers experimental flag if oldSku is passed,\n // so we need to check for oldSku to decide if it needs to be sent.\n // Otherwise we might accidentally block a regular subscription request.\n if (oldSku) {\n const skuSelectedResponse = new SkuSelectedResponse();\n skuSelectedResponse.setSku(sku);\n skuSelectedResponse.setOldSku(oldSku);\n this.startPayFlow_(skuSelectedResponse);\n return;\n }\n }\n\n this.skus_ = feArgsObj['skus'] || [ALL_SKUS];\n\n this.clientConfigPromise_ = this.clientConfigManager_.getClientConfig();\n\n this.activityIframeViewPromise_ = this.createActivityIframeView_(feArgsObj);\n }\n\n private async createActivityIframeView_(\n args: OffersRequest\n ): Promise {\n const clientConfig = await this.clientConfigPromise_!;\n\n return new ActivityIframeView(\n this.win_,\n this.activityPorts_,\n this.getUrl_(clientConfig, this.deps_.pageConfig()),\n args as {[key: string]: string},\n /* shouldFadeBody */ true,\n /* hasLoadingIndicator_ */ false,\n /* shouldAnimateFade */ this.shouldAnimateFade_\n );\n }\n\n private startPayFlow_(response: SkuSelectedResponse): void {\n const sku = response.getSku();\n if (sku) {\n const subscriptionRequest: SubscriptionRequest = {\n 'skuId': sku,\n };\n const oldSku = response.getOldSku();\n if (oldSku) {\n subscriptionRequest['oldSku'] = oldSku;\n this.deps_.analytics().setSku(oldSku);\n }\n this.eventManager_.logSwgEvent(\n AnalyticsEvent.ACTION_OFFER_SELECTED,\n true,\n getEventParams(sku)\n );\n new PayStartFlow(this.deps_, subscriptionRequest).start();\n }\n }\n\n private handleLinkRequest_(response: AlreadySubscribedResponse): void {\n if (response.getSubscriberOrMember()) {\n this.eventManager_.logSwgEvent(\n AnalyticsEvent.ACTION_ALREADY_SUBSCRIBED,\n true\n );\n this.deps_.callbacks().triggerLoginRequest({\n linkRequested: !!response.getLinkRequested(),\n });\n }\n }\n\n private startNativeFlow_(response: ViewSubscriptionsResponse): void {\n if (response.getNative()) {\n this.deps_.callbacks().triggerSubscribeRequest();\n }\n }\n\n /**\n * Starts the offers flow or alreadySubscribed flow.\n */\n async start(): Promise {\n this.activityIframeView_ = await this.activityIframeViewPromise_!;\n if (!this.activityIframeView_) {\n return;\n }\n\n // So no error if skipped to payment screen.\n // Start/cancel events.\n // The second parameter is required by Propensity in AMP.\n this.deps_.callbacks().triggerFlowStarted(SubscriptionFlows.SHOW_OFFERS, {\n skus: this.skus_,\n source: 'SwG',\n });\n this.activityIframeView_.onCancel(() => {\n this.deps_.callbacks().triggerFlowCanceled(SubscriptionFlows.SHOW_OFFERS);\n });\n this.activityIframeView_.on(\n SkuSelectedResponse,\n this.startPayFlow_.bind(this)\n );\n this.activityIframeView_.on(\n AlreadySubscribedResponse,\n this.handleLinkRequest_.bind(this)\n );\n this.activityIframeView_.on(\n ViewSubscriptionsResponse,\n this.startNativeFlow_.bind(this)\n );\n\n const clientConfig = await this.clientConfigPromise_!;\n return this.dialogManager_.openView(\n this.activityIframeView_,\n /* hidden */ false,\n this.getDialogConfig_(\n clientConfig,\n this.clientConfigManager_.shouldAllowScroll()\n )\n );\n }\n\n /**\n * Gets display configuration options for the opened dialog. Uses the\n * responsive desktop design properties if the updated offer flows UI (for\n * SwG Basic) is enabled. Permits override to allow scrolling.\n */\n getDialogConfig_(\n clientConfig: ClientConfig,\n shouldAllowScroll: boolean\n ): DialogConfig {\n return clientConfig.useUpdatedOfferFlows\n ? {\n desktopConfig: {isCenterPositioned: true, supportsWideScreen: true},\n shouldDisableBodyScrolling: !shouldAllowScroll,\n closeOnBackgroundClick: this.isClosable_,\n }\n : {};\n }\n\n /**\n * Returns the full URL that should be used for the activity iFrame view.\n */\n private getUrl_(clientConfig: ClientConfig, pageConfig: PageConfig): string {\n if (!clientConfig.useUpdatedOfferFlows) {\n const offerCardParam = parseQueryString(this.win_.location.hash)[\n 'swg.newoffercard'\n ];\n const params: {[key: string]: string} = offerCardParam\n ? {'useNewOfferCard': offerCardParam}\n : {};\n return feUrl('/offersiframe', params);\n }\n\n const params: {[key: string]: string} = {\n 'publicationId': pageConfig.getPublicationId(),\n };\n\n if (this.clientConfigManager_.shouldForceLangInIframes()) {\n params['hl'] = this.clientConfigManager_.getLanguage();\n }\n\n if (clientConfig.uiPredicates?.purchaseUnavailableRegion) {\n params['purchaseUnavailableRegion'] = 'true';\n }\n\n return feUrl('/subscriptionoffersiframe', params);\n }\n\n /**\n * Shows \"no subscription found\" on activity iFrame view.\n */\n showNoEntitlementFoundToast() {\n if (this.activityIframeView_) {\n this.activityIframeView_.execute(new EntitlementsResponse());\n }\n }\n}\n\n/**\n * The class for subscribe option flow.\n */\nexport class SubscribeOptionFlow {\n private readonly activityPorts_: ActivityPorts;\n private readonly dialogManager_: DialogManager;\n private readonly eventManager_: ClientEventManager;\n private readonly activityIframeView_: ActivityIframeView;\n\n constructor(\n private readonly deps_: Deps,\n private readonly options_?: OffersRequest\n ) {\n this.activityPorts_ = deps_.activities();\n\n this.dialogManager_ = deps_.dialogManager();\n\n this.eventManager_ = deps_.eventManager();\n\n this.activityIframeView_ = new ActivityIframeView(\n deps_.win(),\n this.activityPorts_,\n feUrl('/optionsiframe'),\n feArgs({\n 'publicationId': deps_.pageConfig().getPublicationId(),\n 'productId': deps_.pageConfig().getProductId(),\n 'list': options_?.list || 'default',\n 'skus': options_?.skus || null,\n 'isClosable': true,\n }),\n /* shouldFadeBody */ false\n );\n }\n\n /**\n * Starts the offers flow or alreadySubscribed flow.\n */\n start(): Promise {\n // Start/cancel events.\n this.deps_\n .callbacks()\n .triggerFlowStarted(SubscriptionFlows.SHOW_SUBSCRIBE_OPTION);\n this.activityIframeView_.onCancel(() => {\n this.deps_\n .callbacks()\n .triggerFlowCanceled(SubscriptionFlows.SHOW_SUBSCRIBE_OPTION);\n });\n this.activityIframeView_.on(\n SubscribeResponse,\n this.maybeOpenOffersFlow_.bind(this)\n );\n\n this.activityIframeView_.acceptResult().then(\n (result) => {\n const data = result.data as {subscribe?: boolean};\n const response = new SubscribeResponse();\n if (data['subscribe']) {\n response.setSubscribe(true);\n }\n this.maybeOpenOffersFlow_(response);\n },\n (reason) => {\n this.dialogManager_.completeView(this.activityIframeView_);\n throw reason;\n }\n );\n this.eventManager_.logSwgEvent(\n AnalyticsEvent.IMPRESSION_CLICK_TO_SHOW_OFFERS\n );\n return this.dialogManager_.openView(this.activityIframeView_);\n }\n\n private maybeOpenOffersFlow_(response: SubscribeResponse): void {\n if (response.getSubscribe()) {\n const options = this.options_ || {};\n if (options.isClosable == undefined) {\n options.isClosable = OFFERS_VIEW_CLOSABLE;\n }\n this.eventManager_.logSwgEvent(AnalyticsEvent.ACTION_VIEW_OFFERS, true);\n new OffersFlow(this.deps_, options).start();\n }\n }\n}\n\n/**\n * The class for Abbreviated Offer flow.\n */\nexport class AbbrvOfferFlow {\n private readonly win_: Window;\n private readonly activityPorts_: ActivityPorts;\n private readonly dialogManager_: DialogManager;\n private readonly eventManager_: ClientEventManager;\n private readonly activityIframeView_: ActivityIframeView;\n\n constructor(\n private readonly deps_: Deps,\n private readonly options_: OffersRequest = {}\n ) {\n this.win_ = deps_.win();\n\n this.activityPorts_ = deps_.activities();\n\n this.dialogManager_ = deps_.dialogManager();\n\n this.eventManager_ = deps_.eventManager();\n\n this.activityIframeView_ = new ActivityIframeView(\n this.win_,\n this.activityPorts_,\n feUrl('/abbrvofferiframe'),\n feArgs({\n 'publicationId': deps_.pageConfig().getPublicationId(),\n 'productId': deps_.pageConfig().getProductId(),\n 'showNative': deps_.callbacks().hasSubscribeRequestCallback(),\n 'list': options_.list || 'default',\n 'skus': options_.skus || null,\n 'isClosable': true,\n }),\n /* shouldFadeBody */ false\n );\n }\n\n private handleLinkRequest_(response: AlreadySubscribedResponse): void {\n if (response.getSubscriberOrMember()) {\n this.eventManager_.logSwgEvent(\n AnalyticsEvent.ACTION_ALREADY_SUBSCRIBED,\n true\n );\n this.deps_.callbacks().triggerLoginRequest({\n linkRequested: !!response.getLinkRequested(),\n });\n }\n }\n\n /**\n * Starts the offers flow\n */\n start(): Promise {\n // Start/cancel events.\n this.deps_\n .callbacks()\n .triggerFlowStarted(SubscriptionFlows.SHOW_ABBRV_OFFER);\n this.activityIframeView_.onCancel(() => {\n this.deps_\n .callbacks()\n .triggerFlowCanceled(SubscriptionFlows.SHOW_ABBRV_OFFER);\n });\n\n // If the user is already subscribed, trigger login flow\n this.activityIframeView_.on(\n AlreadySubscribedResponse,\n this.handleLinkRequest_.bind(this)\n );\n\n // If result is due to requesting offers, redirect to offers flow\n this.activityIframeView_.acceptResult().then((result) => {\n const data = result.data as {native?: boolean; viewOffers?: boolean};\n if (data['viewOffers']) {\n if (this.options_.isClosable == undefined) {\n this.options_.isClosable = OFFERS_VIEW_CLOSABLE;\n }\n this.eventManager_.logSwgEvent(AnalyticsEvent.ACTION_VIEW_OFFERS, true);\n new OffersFlow(this.deps_, this.options_).start();\n return;\n }\n if (data['native']) {\n this.deps_.callbacks().triggerSubscribeRequest();\n // The flow is complete.\n this.dialogManager_.completeView(this.activityIframeView_);\n return;\n }\n });\n\n this.eventManager_.logSwgEvent(\n AnalyticsEvent.IMPRESSION_CLICK_TO_SHOW_OFFERS_OR_ALREADY_SUBSCRIBED\n );\n\n return this.dialogManager_.openView(this.activityIframeView_);\n }\n}\n","/**\n * @license\n * Copyright 2017 The Web Activities Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n /** Version: 1.24 */\n'use strict';\n\n/*eslint no-unused-vars: 0*/\n\n\n/**\n * @enum {string}\n */\nconst ActivityMode = {\n IFRAME: 'iframe',\n POPUP: 'popup',\n REDIRECT: 'redirect',\n};\n\n\n/**\n * The result code used for `ActivityResult`.\n * @enum {string}\n */\nconst ActivityResultCode = {\n OK: 'ok',\n CANCELED: 'canceled',\n FAILED: 'failed',\n};\n\n\n/**\n * The result of an activity. The activity implementation returns this object\n * for a successful result, a cancelation or a failure.\n * @struct\n */\nclass ActivityResult {\n /**\n * @param {!ActivityResultCode} code\n * @param {*} data\n * @param {!ActivityMode} mode\n * @param {string} origin\n * @param {boolean} originVerified\n * @param {boolean} secureChannel\n */\n constructor(code, data, mode, origin, originVerified, secureChannel) {\n /** @const {!ActivityResultCode} */\n this.code = code;\n /** @const {*} */\n this.data = code == ActivityResultCode.OK ? data : null;\n /** @const {!ActivityMode} */\n this.mode = mode;\n /** @const {string} */\n this.origin = origin;\n /** @const {boolean} */\n this.originVerified = originVerified;\n /** @const {boolean} */\n this.secureChannel = secureChannel;\n /** @const {boolean} */\n this.ok = code == ActivityResultCode.OK;\n /** @const {?Error} */\n this.error = code == ActivityResultCode.FAILED ?\n new Error(String(data) || '') :\n null;\n }\n}\n\n\n/**\n * The activity request that different types of hosts can be started with.\n * @typedef {{\n * requestId: string,\n * returnUrl: string,\n * args: ?Object,\n * origin: (string|undefined),\n * originVerified: (boolean|undefined),\n * }}\n */\nlet ActivityRequest;\n\n\n/**\n * The activity \"open\" options used for popups and redirects.\n *\n * - returnUrl: override the return URL. By default, the current URL will be\n * used.\n * - skipRequestInUrl: removes the activity request from the URL, in case\n * redirect is used. By default, the activity request is appended to the\n * activity URL. This option can be used if the activity request is passed\n * to the activity by some alternative means.\n * - disableRedirectFallback: disallows popup fallback to redirect. By default\n * the redirect fallback is allowed. This option has to be used very carefully\n * because there are many user agents that may fail to open a popup and it\n * won't be always possible for the opener window to even be aware of such\n * failures.\n *\n * @typedef {{\n * returnUrl: (string|undefined),\n * skipRequestInUrl: (boolean|undefined),\n * disableRedirectFallback: (boolean|undefined),\n * width: (number|undefined),\n * height: (number|undefined),\n * }}\n */\nlet ActivityOpenOptions;\n\n\n/**\n * Activity client-side binding. The port provides limited ways to communicate\n * with the activity and receive signals and results from it. Not every type\n * of activity exposes a port.\n *\n * @interface\n */\nclass ActivityPort {\n\n /**\n * Returns the mode of the activity: iframe, popup or redirect.\n * @return {!ActivityMode}\n */\n getMode() {}\n\n /**\n * Accepts the result when ready. The client should verify the activity's\n * mode, origin, verification and secure channel flags before deciding\n * whether or not to trust the result.\n *\n * Returns the promise that yields when the activity has been completed and\n * either a result, a cancelation or a failure has been returned.\n *\n * @return {!Promise}\n */\n acceptResult() {}\n}\n\n\n/**\n * Activity client-side binding for messaging.\n *\n * Whether the host can or cannot receive a message depends on the type of\n * host and its state. Ensure that the code has an alternative path if\n * messaging is not available.\n *\n * @interface\n */\nclass ActivityMessagingPort {\n\n /**\n * Returns the target window where host is loaded. May be unavailable.\n * @return {?Window}\n */\n getTargetWin() {}\n\n /**\n * Sends a message to the host.\n * @param {!Object} payload\n */\n message(payload) {}\n\n /**\n * Registers a callback to receive messages from the host.\n * @param {function(!Object)} callback\n */\n onMessage(callback) {}\n\n /**\n * Creates a new communication channel or returns an existing one.\n * @param {string=} opt_name\n * @return {!Promise}\n */\n messageChannel(opt_name) {}\n}\n\n\n\n/** DOMException.ABORT_ERR name */\nconst ABORT_ERR_NAME = 'AbortError';\n\n/** DOMException.ABORT_ERR = 20 */\nconst ABORT_ERR_CODE = 20;\n\n/** @type {?HTMLAnchorElement} */\nlet aResolver;\n\n\n/**\n * @param {string} urlString\n * @return {!HTMLAnchorElement}\n */\nfunction parseUrl(urlString) {\n if (!aResolver) {\n aResolver = /** @type {!HTMLAnchorElement} */ (document.createElement('a'));\n }\n aResolver.href = urlString;\n return /** @type {!HTMLAnchorElement} */ (aResolver);\n}\n\n\n/**\n * @param {!Location|!URL|!HTMLAnchorElement} loc\n * @return {string}\n */\nfunction getOrigin(loc) {\n if (loc.origin) {\n return loc.origin;\n }\n // Make sure that the origin is normalized. Specifically on IE, host sometimes\n // includes the default port, which is not per standard.\n const protocol = loc.protocol;\n let host = loc.host;\n if (protocol == 'https:' && host.indexOf(':443') == host.length - 4) {\n host = host.replace(':443', '');\n } else if (protocol == 'http:' && host.indexOf(':80') == host.length - 3) {\n host = host.replace(':80', '');\n }\n return protocol + '//' + host;\n}\n\n\n/**\n * @param {string} urlString\n * @return {string}\n */\nfunction getOriginFromUrl(urlString) {\n return getOrigin(parseUrl(urlString));\n}\n\n\n/**\n * @param {string} urlString\n * @return {string}\n */\nfunction removeFragment(urlString) {\n const index = urlString.indexOf('#');\n if (index == -1) {\n return urlString;\n }\n return urlString.substring(0, index);\n}\n\n\n/**\n * Parses and builds Object of URL query string.\n * @param {string} query The URL query string.\n * @return {!Object}\n */\nfunction parseQueryString(query) {\n if (!query) {\n return {};\n }\n return (/^[?#]/.test(query) ? query.slice(1) : query)\n .split('&')\n .reduce((params, param) => {\n const item = param.split('=');\n const key = decodeURIComponent(item[0] || '');\n const value = decodeURIComponent(item[1] || '');\n if (key) {\n params[key] = value;\n }\n return params;\n }, {});\n}\n\n\n/**\n * @param {string} queryString A query string in the form of \"a=b&c=d\". Could\n * be optionally prefixed with \"?\" or \"#\".\n * @param {string} param The param to get from the query string.\n * @return {?string}\n */\nfunction getQueryParam(queryString, param) {\n return parseQueryString(queryString)[param];\n}\n\n\n/**\n * Add a query-like parameter to the fragment string.\n * @param {string} url\n * @param {string} param\n * @param {string} value\n * @return {string}\n */\nfunction addFragmentParam(url, param, value) {\n return url +\n (url.indexOf('#') == -1 ? '#' : '&') +\n encodeURIComponent(param) + '=' + encodeURIComponent(value);\n}\n\n\n/**\n * @param {string} queryString A query string in the form of \"a=b&c=d\". Could\n * be optionally prefixed with \"?\" or \"#\".\n * @param {string} param The param to remove from the query string.\n * @return {?string}\n */\nfunction removeQueryParam(queryString, param) {\n if (!queryString) {\n return queryString;\n }\n const search = encodeURIComponent(param) + '=';\n let index = -1;\n do {\n index = queryString.indexOf(search, index);\n if (index != -1) {\n const prev = index > 0 ? queryString.substring(index - 1, index) : '';\n if (prev == '' || prev == '?' || prev == '#' || prev == '&') {\n let end = queryString.indexOf('&', index + 1);\n if (end == -1) {\n end = queryString.length;\n }\n queryString =\n queryString.substring(0, index) +\n queryString.substring(end + 1);\n } else {\n index++;\n }\n }\n } while (index != -1 && index < queryString.length);\n return queryString;\n}\n\n\n/**\n * @param {!ActivityRequest} request\n * @return {string}\n */\nfunction serializeRequest(request) {\n const map = {\n 'requestId': request.requestId,\n 'returnUrl': request.returnUrl,\n 'args': request.args,\n };\n if (request.origin !== undefined) {\n map['origin'] = request.origin;\n }\n if (request.originVerified !== undefined) {\n map['originVerified'] = request.originVerified;\n }\n return JSON.stringify(map);\n}\n\n\n/**\n * @param {*} error\n * @return {boolean}\n */\nfunction isAbortError(error) {\n if (!error || typeof error != 'object') {\n return false;\n }\n return (error['name'] === ABORT_ERR_NAME);\n}\n\n\n/**\n * Creates or emulates a DOMException of AbortError type.\n * See https://heycam.github.io/webidl/#aborterror.\n * @param {!Window} win\n * @param {string=} opt_message\n * @return {!DOMException}\n */\nfunction createAbortError(win, opt_message) {\n const message = 'AbortError' + (opt_message ? ': ' + opt_message : '');\n let error = null;\n if (typeof win['DOMException'] == 'function') {\n // TODO(dvoytenko): remove typecast once externs are fixed.\n const constr = /** @type {function(new:DOMException, string, string)} */ (\n win['DOMException']);\n try {\n error = new constr(message, ABORT_ERR_NAME);\n } catch (e) {\n // Ignore. In particular, `new DOMException()` fails in Edge.\n }\n }\n if (!error) {\n // TODO(dvoytenko): remove typecast once externs are fixed.\n const constr = /** @type {function(new:DOMException, string)} */ (\n Error);\n error = new constr(message);\n error.name = ABORT_ERR_NAME;\n error.code = ABORT_ERR_CODE;\n }\n return error;\n}\n\n\n/**\n * Resolves the activity result as a promise:\n * - `OK` result is yielded as the promise's payload;\n * - `CANCEL` result is rejected with the `AbortError`;\n * - `FAILED` result is rejected with the embedded error.\n *\n * @param {!Window} win\n * @param {!ActivityResult} result\n * @param {function((!ActivityResult|!Promise))} resolver\n */\nfunction resolveResult(win, result, resolver) {\n if (result.ok) {\n resolver(result);\n } else {\n const error = result.error || createAbortError(win);\n error.activityResult = result;\n resolver(Promise.reject(error));\n }\n}\n\n\n/**\n * @param {!Window} win\n * @return {boolean}\n */\nfunction isIeBrowser(win) {\n // MSIE and Trident are typical user agents for IE browsers.\n const nav = win.navigator;\n return /Trident|MSIE|IEMobile/i.test(nav && nav.userAgent);\n}\n\n\n/**\n * @param {!Window} win\n * @return {boolean}\n */\nfunction isEdgeBrowser(win) {\n const nav = win.navigator;\n return /Edge/i.test(nav && nav.userAgent);\n}\n\n\n/**\n * @param {!Error} e\n */\nfunction throwAsync(e) {\n setTimeout(() => {throw e;});\n}\n\n\n/**\n * Polyfill of the `Node.isConnected` API. See\n * https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected.\n * @param {!Node} node\n * @return {boolean}\n */\nfunction isNodeConnected(node) {\n // Ensure that node is attached if specified. This check uses a new and\n // fast `isConnected` API and thus only checked on platforms that have it.\n // See https://www.chromestatus.com/feature/5676110549352448.\n if ('isConnected' in node) {\n return node['isConnected'];\n }\n // Polyfill.\n const root = node.ownerDocument && node.ownerDocument.documentElement;\n return (root && root.contains(node)) || false;\n}\n\n\n\nconst SENTINEL = '__ACTIVITIES__';\n\n\n/**\n * The messenger helper for activity's port and host.\n */\nclass Messenger {\n\n /**\n * @param {!Window} win\n * @param {!Window|function():?Window} targetOrCallback\n * @param {?string} targetOrigin\n * @param {boolean} requireTarget\n */\n constructor(win, targetOrCallback, targetOrigin, requireTarget) {\n /** @private @const {!Window} */\n this.win_ = win;\n\n /** @private @const {!Window|function():?Window} */\n this.targetOrCallback_ = targetOrCallback;\n\n /**\n * May start as unknown (`null`) until received in the first message.\n * @private {?string}\n */\n this.targetOrigin_ = targetOrigin;\n\n /** @private @const {boolean} */\n this.requireTarget_ = requireTarget;\n\n /** @private {?Window} */\n this.target_ = null;\n\n /** @private {boolean} */\n this.acceptsChannel_ = false;\n\n /** @private {?MessagePort} */\n this.port_ = null;\n\n /** @private {?function(string, ?Object)} */\n this.onCommand_ = null;\n\n /** @private {?function(!Object)} */\n this.onCustomMessage_ = null;\n\n /**\n * @private {?Object}\n */\n this.channels_ = null;\n\n /** @private @const */\n this.boundHandleEvent_ = this.handleEvent_.bind(this);\n }\n\n /**\n * Connect the port to the host or vice versa.\n * @param {function(string, ?Object)} onCommand\n */\n connect(onCommand) {\n if (this.onCommand_) {\n throw new Error('already connected');\n }\n this.onCommand_ = onCommand;\n this.win_.addEventListener('message', this.boundHandleEvent_);\n }\n\n /**\n * Disconnect messenger.\n */\n disconnect() {\n if (this.onCommand_) {\n this.onCommand_ = null;\n if (this.port_) {\n closePort(this.port_);\n this.port_ = null;\n }\n this.win_.removeEventListener('message', this.boundHandleEvent_);\n if (this.channels_) {\n for (const k in this.channels_) {\n const channelObj = this.channels_[k];\n if (channelObj.port1) {\n closePort(channelObj.port1);\n }\n if (channelObj.port2) {\n closePort(channelObj.port2);\n }\n }\n this.channels_ = null;\n }\n }\n }\n\n /**\n * Returns whether the messenger has been connected already.\n * @return {boolean}\n */\n isConnected() {\n return this.targetOrigin_ != null;\n }\n\n /**\n * Returns the messaging target. Only available when connection has been\n * establihsed.\n * @return {!Window}\n */\n getTarget() {\n const target = this.getOptionalTarget_();\n if (!target) {\n throw new Error('not connected');\n }\n return target;\n }\n\n /**\n * @return {?Window}\n * @private\n */\n getOptionalTarget_() {\n if (this.onCommand_ && !this.target_) {\n if (typeof this.targetOrCallback_ == 'function') {\n this.target_ = this.targetOrCallback_();\n } else {\n this.target_ = /** @type {!Window} */ (this.targetOrCallback_);\n }\n }\n return this.target_;\n }\n\n /**\n * Returns the messaging origin. Only available when connection has been\n * establihsed.\n * @return {string}\n */\n getTargetOrigin() {\n if (this.targetOrigin_ == null) {\n throw new Error('not connected');\n }\n return this.targetOrigin_;\n }\n\n /**\n * The host sends this message to the client to indicate that it's ready to\n * start communicating. The client is expected to respond back with the\n * \"start\" command. See `sendStartCommand` method.\n */\n sendConnectCommand() {\n // TODO(dvoytenko): MessageChannel is critically necessary for IE/Edge,\n // since window messaging doesn't always work. It's also preferred as an API\n // for other browsers: it's newer, cleaner and arguably more secure.\n // Unfortunately, browsers currently do not propagate user gestures via\n // MessageChannel, only via window messaging. This should be re-enabled\n // once browsers fix user gesture propagation.\n // See:\n // Safari: https://bugs.webkit.org/show_bug.cgi?id=186593\n // Chrome: https://bugs.chromium.org/p/chromium/issues/detail?id=851493\n // Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1469422\n const acceptsChannel = isIeBrowser(this.win_) || isEdgeBrowser(this.win_);\n this.sendCommand('connect', {'acceptsChannel': acceptsChannel});\n }\n\n /**\n * The client sends this message to the host upon receiving the \"connect\"\n * message to start the main communication channel. As a payload, the message\n * will contain the provided start arguments.\n * @param {?Object} args\n */\n sendStartCommand(args) {\n let channel = null;\n if (this.acceptsChannel_ && typeof this.win_.MessageChannel == 'function') {\n channel = new this.win_.MessageChannel();\n }\n if (channel) {\n this.sendCommand('start', args, [channel.port2]);\n // It's critical to switch to port messaging only after \"start\" has been\n // sent. Otherwise, it won't be delivered.\n this.switchToChannel_(channel.port1);\n } else {\n this.sendCommand('start', args);\n }\n }\n\n /**\n * Sends the specified command from the port to the host or vice versa.\n * @param {string} cmd\n * @param {?Object=} opt_payload\n * @param {?Array=} opt_transfer\n */\n sendCommand(cmd, opt_payload, opt_transfer) {\n const data = {\n 'sentinel': SENTINEL,\n 'cmd': cmd,\n 'payload': opt_payload || null,\n };\n if (this.port_) {\n this.port_.postMessage(data, opt_transfer || undefined);\n } else {\n const target = this.getTarget();\n // Only \"connect\" command is allowed to use `targetOrigin == '*'`\n const targetOrigin =\n cmd == 'connect' ?\n (this.targetOrigin_ != null ? this.targetOrigin_ : '*') :\n this.getTargetOrigin();\n target.postMessage(data, targetOrigin, opt_transfer || undefined);\n }\n }\n\n /**\n * Sends a message to the client.\n * @param {!Object} payload\n */\n customMessage(payload) {\n this.sendCommand('msg', payload);\n }\n\n /**\n * Registers a callback to receive messages from the client.\n * @param {function(!Object)} callback\n */\n onCustomMessage(callback) {\n this.onCustomMessage_ = callback;\n }\n\n /**\n * @param {string=} opt_name\n * @return {!Promise}\n */\n startChannel(opt_name) {\n const name = opt_name || '';\n const channelObj = this.getChannelObj_(name);\n if (!channelObj.port1) {\n const channel = new this.win_.MessageChannel();\n channelObj.port1 = channel.port1;\n channelObj.port2 = channel.port2;\n channelObj.resolver(channelObj.port1);\n }\n if (channelObj.port2) {\n // Not yet sent.\n this.sendCommand('cnset', {'name': name}, [channelObj.port2]);\n channelObj.port2 = null;\n }\n return channelObj.promise;\n }\n\n /**\n * @param {string=} opt_name\n * @return {!Promise}\n */\n askChannel(opt_name) {\n const name = opt_name || '';\n const channelObj = this.getChannelObj_(name);\n if (!channelObj.port1) {\n this.sendCommand('cnget', {'name': name});\n }\n return channelObj.promise;\n }\n\n /**\n * @param {string} name\n * @param {!MessagePort} port\n * @private\n */\n receiveChannel_(name, port) {\n const channelObj = this.getChannelObj_(name);\n channelObj.port1 = port;\n channelObj.resolver(port);\n }\n\n /**\n * @param {string} name\n * @return {!ChannelHolder}\n */\n getChannelObj_(name) {\n if (!this.channels_) {\n this.channels_ = {};\n }\n let channelObj = this.channels_[name];\n if (!channelObj) {\n let resolver;\n const promise = new Promise(resolve => {\n resolver = resolve;\n });\n channelObj = {\n port1: null,\n port2: null,\n resolver,\n promise,\n };\n this.channels_[name] = channelObj;\n }\n return channelObj;\n }\n\n /**\n * @param {!MessagePort} port\n * @private\n */\n switchToChannel_(port) {\n if (this.port_) {\n closePort(this.port_);\n }\n this.port_ = port;\n this.port_.onmessage = event => {\n const data = event.data;\n const cmd = data && data['cmd'];\n const payload = data && data['payload'] || null;\n if (cmd) {\n this.handleCommand_(cmd, payload, event);\n }\n };\n // Even though all messaging will switch to ports, the window-based message\n // listener will be preserved just in case the host is refreshed and needs\n // another connection.\n }\n\n /**\n * @param {!MessageEvent} event\n * @private\n */\n handleEvent_(event) {\n if (this.requireTarget_ && this.getOptionalTarget_() != event.source) {\n // When target is required, confirm it against the event.source. This\n // is normally only needed for ports where a single window can include\n // multiple iframes to match the event to a specific iframe. Otherwise,\n // the origin checks below are sufficient.\n return;\n }\n const data = event.data;\n if (!data || data['sentinel'] != SENTINEL) {\n return;\n }\n const cmd = data['cmd'];\n if (this.port_ && cmd != 'connect' && cmd != 'start') {\n // Messaging channel has already taken over. However, the \"connect\" and\n // \"start\" commands are allowed to proceed in case re-connection is\n // requested.\n return;\n }\n const origin = /** @type {string} */ (event.origin);\n const payload = data['payload'] || null;\n if (this.targetOrigin_ == null && cmd == 'start') {\n this.targetOrigin_ = origin;\n }\n if (this.targetOrigin_ == null && event.source) {\n if (this.getOptionalTarget_() == event.source) {\n this.targetOrigin_ = origin;\n }\n }\n // Notice that event.source may differ from the target because of\n // friendly-iframe intermediaries.\n if (origin != this.targetOrigin_) {\n return;\n }\n this.handleCommand_(cmd, payload, event);\n }\n\n /**\n * @param {string} cmd\n * @param {?Object} payload\n * @param {!MessageEvent} event\n * @private\n */\n handleCommand_(cmd, payload, event) {\n if (cmd == 'connect') {\n if (this.port_) {\n // In case the port has already been open - close it to reopen it\n // again later.\n closePort(this.port_);\n this.port_ = null;\n }\n this.acceptsChannel_ = payload && payload['acceptsChannel'] || false;\n this.onCommand_(cmd, payload);\n } else if (cmd == 'start') {\n const port = event.ports && event.ports[0];\n if (port) {\n this.switchToChannel_(port);\n }\n this.onCommand_(cmd, payload);\n } else if (cmd == 'msg') {\n if (this.onCustomMessage_ != null && payload != null) {\n this.onCustomMessage_(payload);\n }\n } else if (cmd == 'cnget') {\n const name = payload['name'];\n this.startChannel(name);\n } else if (cmd == 'cnset') {\n const name = payload['name'];\n const port = event.ports[0];\n this.receiveChannel_(name, /** @type {!MessagePort} */ (port));\n } else {\n this.onCommand_(cmd, payload);\n }\n }\n}\n\n\n/**\n * @param {!MessagePort} port\n */\nfunction closePort(port) {\n try {\n port.close();\n } catch (e) {\n // Ignore.\n }\n}\n\n\n\n\n/**\n * The `ActivityPort` implementation for the iframe case. Unlike other types\n * of activities, iframe-based activities are always connected and can react\n * to size requests.\n *\n * @implements {ActivityPort}\n * @implements {ActivityMessagingPort}\n */\nclass ActivityIframePort {\n\n /**\n * @param {!HTMLIFrameElement} iframe\n * @param {string} url\n * @param {?Object=} opt_args\n */\n constructor(iframe, url, opt_args) {\n /** @private @const {!HTMLIFrameElement} */\n this.iframe_ = iframe;\n /** @private @const {string} */\n this.url_ = url;\n /** @private @const {?Object} */\n this.args_ = opt_args || null;\n\n /** @private @const {!Window} */\n this.win_ = /** @type {!Window} */ (this.iframe_.ownerDocument.defaultView);\n\n /** @private @const {string} */\n this.targetOrigin_ = getOriginFromUrl(url);\n\n /** @private {boolean} */\n this.connected_ = false;\n\n /** @private {?function()} */\n this.connectedResolver_ = null;\n\n /** @private @const {!Promise} */\n this.connectedPromise_ = new Promise(resolve => {\n this.connectedResolver_ = resolve;\n });\n\n /** @private {?function()} */\n this.readyResolver_ = null;\n\n /** @private @const {!Promise} */\n this.readyPromise_ = new Promise(resolve => {\n this.readyResolver_ = resolve;\n });\n\n /** @private {?function((!ActivityResult|!Promise))} */\n this.resultResolver_ = null;\n\n /** @private @const {!Promise} */\n this.resultPromise_ = new Promise(resolve => {\n this.resultResolver_ = resolve;\n });\n\n /** @private {?function(number)} */\n this.onResizeRequest_ = null;\n\n /** @private {?number} */\n this.requestedHeight_ = null;\n\n /** @private @const {!Messenger} */\n this.messenger_ = new Messenger(\n this.win_,\n () => this.iframe_.contentWindow,\n this.targetOrigin_,\n /* requireTarget */ true);\n }\n\n /** @override */\n getMode() {\n return ActivityMode.IFRAME;\n }\n\n /**\n * Waits until the activity port is connected to the host.\n * @return {!Promise}\n */\n connect() {\n if (!isNodeConnected(this.iframe_)) {\n throw new Error('iframe must be in DOM');\n }\n this.messenger_.connect(this.handleCommand_.bind(this));\n this.iframe_.src = this.url_;\n return this.connectedPromise_;\n }\n\n /**\n * Disconnect the activity binding and cleanup listeners.\n */\n disconnect() {\n this.connected_ = false;\n this.messenger_.disconnect();\n }\n\n /** @override */\n acceptResult() {\n return this.resultPromise_;\n }\n\n /** @override */\n getTargetWin() {\n return this.iframe_.contentWindow || null;\n }\n\n /** @override */\n message(payload) {\n this.messenger_.customMessage(payload);\n }\n\n /** @override */\n onMessage(callback) {\n this.messenger_.onCustomMessage(callback);\n }\n\n /** @override */\n messageChannel(opt_name) {\n return this.messenger_.askChannel(opt_name);\n }\n\n /**\n * Returns a promise that yields when the iframe is ready to be interacted\n * with.\n * @return {!Promise}\n */\n whenReady() {\n return this.readyPromise_;\n }\n\n /**\n * Register a callback to handle resize requests. Once successfully resized,\n * ensure to call `resized()` method.\n * @param {function(number)} callback\n */\n onResizeRequest(callback) {\n this.onResizeRequest_ = callback;\n Promise.resolve().then(() => {\n if (this.requestedHeight_ != null) {\n callback(this.requestedHeight_);\n }\n });\n }\n\n /**\n * Signals back to the activity implementation that the client has updated\n * the activity's size.\n */\n resized() {\n if (!this.connected_) {\n return;\n }\n const height = this.iframe_.offsetHeight;\n this.messenger_.sendCommand('resized', {'height': height});\n }\n\n /**\n * @param {string} cmd\n * @param {?Object} payload\n * @private\n */\n handleCommand_(cmd, payload) {\n if (cmd == 'connect') {\n // First ever message. Indicates that the receiver is listening.\n this.connected_ = true;\n this.messenger_.sendStartCommand(this.args_);\n this.connectedResolver_();\n } else if (cmd == 'result') {\n // The last message. Indicates that the result has been received.\n if (this.resultResolver_) {\n const code = /** @type {!ActivityResultCode} */ (payload['code']);\n const data =\n code == ActivityResultCode.FAILED ?\n new Error(payload['data'] || '') :\n payload['data'];\n const result = new ActivityResult(\n code,\n data,\n ActivityMode.IFRAME,\n this.messenger_.getTargetOrigin(),\n /* originVerified */ true,\n /* secureChannel */ true);\n resolveResult(this.win_, result, this.resultResolver_);\n this.resultResolver_ = null;\n this.messenger_.sendCommand('close');\n this.disconnect();\n }\n } else if (cmd == 'ready') {\n if (this.readyResolver_) {\n this.readyResolver_();\n this.readyResolver_ = null;\n }\n } else if (cmd == 'resize') {\n this.requestedHeight_ = /** @type {number} */ (payload['height']);\n if (this.onResizeRequest_) {\n this.onResizeRequest_(this.requestedHeight_);\n }\n }\n }\n}\n\n\n\n\n/**\n * The `ActivityPort` implementation for the standalone window activity\n * client executed as a popup.\n *\n * @implements {ActivityPort}\n * @implements {ActivityMessagingPort}\n */\nclass ActivityWindowPort {\n\n /**\n * @param {!Window} win\n * @param {string} requestId\n * @param {string} url\n * @param {string} target\n * @param {?Object=} opt_args\n * @param {?ActivityOpenOptions=} opt_options\n */\n constructor(win, requestId, url, target, opt_args, opt_options) {\n const isValidTarget =\n target &&\n (target == '_blank' || target == '_top' || target[0] != '_');\n if (!isValidTarget) {\n throw new Error('The only allowed targets are \"_blank\", \"_top\"' +\n ' and name targets');\n }\n\n /** @private @const {!Window} */\n this.win_ = win;\n /** @private @const {string} */\n this.requestId_ = requestId;\n /** @private @const {string} */\n this.url_ = url;\n /** @private @const {string} */\n this.openTarget_ = target;\n /** @private @const {?Object} */\n this.args_ = opt_args || null;\n /** @private @const {!ActivityOpenOptions} */\n this.options_ = opt_options || {};\n\n /** @private {?function()} */\n this.connectedResolver_ = null;\n\n /** @private @const {!Promise} */\n this.connectedPromise_ = new Promise(resolve => {\n this.connectedResolver_ = resolve;\n });\n\n /** @private {?function((!ActivityResult|!Promise))} */\n this.resultResolver_ = null;\n\n /** @private @const {!Promise} */\n this.resultPromise_ = new Promise(resolve => {\n this.resultResolver_ = resolve;\n });\n\n /** @private {?Window} */\n this.targetWin_ = null;\n\n /** @private {?number} */\n this.heartbeatInterval_ = null;\n\n /** @private {?Messenger} */\n this.messenger_ = null;\n }\n\n /** @override */\n getMode() {\n return this.openTarget_ == '_top' ?\n ActivityMode.REDIRECT :\n ActivityMode.POPUP;\n }\n\n /**\n * Opens the activity in a window, either as a popup or via redirect.\n *\n * Returns the promise that will yield when the window returns or closed.\n * Notice, that this promise may never complete if \"redirect\" mode was used.\n *\n * @return {!Promise}\n */\n open() {\n return this.openInternal_();\n }\n\n /**\n * Waits until the activity port is connected to the host.\n * @return {!Promise}\n */\n whenConnected() {\n return this.connectedPromise_;\n }\n\n /**\n * Disconnect the activity binding and cleanup listeners.\n */\n disconnect() {\n if (this.heartbeatInterval_) {\n this.win_.clearInterval(this.heartbeatInterval_);\n this.heartbeatInterval_ = null;\n }\n if (this.messenger_) {\n this.messenger_.disconnect();\n this.messenger_ = null;\n }\n if (this.targetWin_) {\n // Try to close the popup window. The host will also try to do the same.\n try {\n this.targetWin_.close();\n } catch (e) {\n // Ignore.\n }\n this.targetWin_ = null;\n }\n this.resultResolver_ = null;\n }\n\n /** @override */\n getTargetWin() {\n return this.targetWin_;\n }\n\n /** @override */\n acceptResult() {\n return this.resultPromise_;\n }\n\n /**\n * Sends a message to the host.\n * Whether the host can or cannot receive a message depends on the type of\n * host and its state. Ensure that the code has an alternative path if\n * messaging is not available.\n * @override\n */\n message(payload) {\n this.messenger_.customMessage(payload);\n }\n\n /**\n * Registers a callback to receive messages from the host.\n * Whether the host can or cannot receive a message depends on the type of\n * host and its state. Ensure that the code has an alternative path if\n * messaging is not available.\n * @override\n */\n onMessage(callback) {\n this.messenger_.onCustomMessage(callback);\n }\n\n /**\n * Creates a new communication channel or returns an existing one.\n * Whether the host can or cannot receive a message depends on the type of\n * host and its state. Ensure that the code has an alternative path if\n * messaging is not available.\n * @override\n */\n messageChannel(opt_name) {\n return this.messenger_.askChannel(opt_name);\n }\n\n /**\n * This method wraps around window's open method. It first tries to execute\n * `open` call with the provided target and if it fails, it retries the call\n * with the `_top` target. This is necessary given that in some embedding\n * scenarios, such as iOS' WKWebView, navigation to `_blank` and other targets\n * is blocked by default.\n * @return {!Promise}\n * @private\n */\n openInternal_() {\n const featuresStr = this.buildFeatures_();\n\n // Protectively, the URL will contain the request payload, unless explicitly\n // directed not to via `skipRequestInUrl` option.\n let url = this.url_;\n if (!this.options_.skipRequestInUrl) {\n const returnUrl =\n this.options_.returnUrl ||\n removeFragment(this.win_.location.href);\n const requestString = serializeRequest({\n requestId: this.requestId_,\n returnUrl,\n args: this.args_,\n });\n url = addFragmentParam(url, '__WA__', requestString);\n }\n\n // Open the window.\n let targetWin;\n let openTarget = this.openTarget_;\n // IE does not support CORS popups - the popup has to fallback to redirect\n // mode.\n if (openTarget != '_top') {\n if (isIeBrowser(this.win_)) {\n openTarget = '_top';\n }\n }\n // Try first with the specified target. If we're inside the WKWebView or\n // a similar environments, this method is expected to fail by default for\n // all targets except `_top`.\n try {\n targetWin = this.win_.open(url, openTarget, featuresStr);\n } catch (e) {\n // Ignore.\n }\n // Then try with `_top` target.\n if (!targetWin &&\n openTarget != '_top' &&\n !this.options_.disableRedirectFallback) {\n openTarget = '_top';\n try {\n targetWin = this.win_.open(url, openTarget);\n } catch (e) {\n // Ignore.\n }\n }\n\n // Setup the target window.\n if (targetWin) {\n this.targetWin_ = targetWin;\n if (openTarget != '_top') {\n this.setupPopup_();\n }\n } else {\n this.disconnectWithError_(new Error('failed to open window'));\n }\n\n // Return result promise, even though it may never complete.\n return this.resultPromise_.catch(() => {\n // Ignore. Call to the `acceptResult()` should fail if needed.\n });\n }\n\n /**\n * @return {string}\n * @private\n */\n buildFeatures_() {\n // The max width and heights are calculated as following:\n // MaxSize = AvailSize - ControlsSize\n // ControlsSize = OuterSize - InnerSize\n const screen = this.win_.screen;\n const availWidth = screen.availWidth || screen.width;\n const availHeight = screen.availHeight || screen.height;\n const isTop = this.isTopWindow_();\n const isEdge = isEdgeBrowser(this.win_);\n // Limit controls to 100px width and height. Notice that it's only\n // possible to calculate controls size in the top window, not in iframes.\n // Notice that the Edge behavior is somewhat unique. If we can't find the\n // right width/height, it will launch in the full-screen. Other browsers\n // deal with such cases more gracefully.\n const controlsWidth =\n isTop && this.win_.outerWidth > this.win_.innerWidth ?\n Math.min(100, this.win_.outerWidth - this.win_.innerWidth) :\n (isEdge ? 100 : 0);\n const controlsHeight =\n isTop && this.win_.outerHeight > this.win_.innerHeight ?\n Math.min(100, this.win_.outerHeight - this.win_.innerHeight) :\n (isEdge ? 100 : 0);\n // With all the adjustments, at least 50% of the available width/height\n // should be made available to a popup.\n const maxWidth = Math.max(availWidth - controlsWidth, availWidth * 0.5);\n const maxHeight = Math.max(availHeight - controlsHeight, availHeight * 0.5);\n let w = Math.floor(Math.min(600, maxWidth * 0.9));\n let h = Math.floor(Math.min(600, maxHeight * 0.9));\n if (this.options_.width) {\n w = Math.min(this.options_.width, maxWidth);\n }\n if (this.options_.height) {\n h = Math.min(this.options_.height, maxHeight);\n }\n const x = Math.floor((screen.width - w) / 2);\n const y = Math.floor((screen.height - h) / 2);\n const features = {\n 'height': h,\n 'width': w,\n 'resizable': 'yes',\n 'scrollbars': 'yes',\n };\n // Do not set left/top in Edge: it fails.\n if (!isEdge) {\n features['left'] = x;\n features['top'] = y;\n }\n let featuresStr = '';\n for (const f in features) {\n if (featuresStr) {\n featuresStr += ',';\n }\n featuresStr += `${f}=${features[f]}`;\n }\n return featuresStr;\n }\n\n /**\n * This method only exists to make iframe/top emulation possible in tests.\n * Otherwise `window.top` cannot be overridden.\n * @return {boolean}\n * @private\n */\n isTopWindow_() {\n return this.win_ == this.win_.top;\n }\n\n /** @private */\n setupPopup_() {\n // Keep alive to catch the window closing, which would indicate\n // \"cancel\" signal.\n this.heartbeatInterval_ = this.win_.setInterval(() => {\n this.check_(/* delayCancel */ true);\n }, 500);\n\n // Start up messaging. The messaging is explicitly allowed to proceed\n // without origin check b/c all arguments have already been passed in\n // the URL and special handling is enforced when result is delivered.\n this.messenger_ = new Messenger(\n this.win_,\n /** @type {!Window} */ (this.targetWin_),\n /* targetOrigin */ null,\n /* requireTarget */ true);\n this.messenger_.connect(this.handleCommand_.bind(this));\n }\n\n /**\n * @param {boolean=} opt_delayCancel\n * @private\n */\n check_(opt_delayCancel) {\n if (!this.targetWin_ || this.targetWin_.closed) {\n if (this.heartbeatInterval_) {\n this.win_.clearInterval(this.heartbeatInterval_);\n this.heartbeatInterval_ = null;\n }\n // Give a chance for the result to arrive, but otherwise consider the\n // responce to be empty.\n this.win_.setTimeout(() => {\n try {\n this.result_(ActivityResultCode.CANCELED, /* data */ null);\n } catch (e) {\n this.disconnectWithError_(e);\n }\n }, opt_delayCancel ? 3000 : 0);\n }\n }\n\n /**\n * @param {!Error} reason\n * @private\n */\n disconnectWithError_(reason) {\n if (this.resultResolver_) {\n this.resultResolver_(Promise.reject(reason));\n }\n this.disconnect();\n }\n\n /**\n * @param {!ActivityResultCode} code\n * @param {*} data\n * @private\n */\n result_(code, data) {\n if (this.resultResolver_) {\n const isConnected = this.messenger_.isConnected();\n const result = new ActivityResult(\n code,\n data,\n ActivityMode.POPUP,\n isConnected ?\n this.messenger_.getTargetOrigin() :\n getOriginFromUrl(this.url_),\n /* originVerified */ isConnected,\n /* secureChannel */ isConnected);\n resolveResult(this.win_, result, this.resultResolver_);\n this.resultResolver_ = null;\n }\n if (this.messenger_) {\n this.messenger_.sendCommand('close');\n }\n this.disconnect();\n }\n\n /**\n * @param {string} cmd\n * @param {?Object} payload\n * @private\n */\n handleCommand_(cmd, payload) {\n if (cmd == 'connect') {\n // First ever message. Indicates that the receiver is listening.\n this.messenger_.sendStartCommand(this.args_);\n this.connectedResolver_();\n } else if (cmd == 'result') {\n // The last message. Indicates that the result has been received.\n const code = /** @type {!ActivityResultCode} */ (payload['code']);\n const data =\n code == ActivityResultCode.FAILED ?\n new Error(payload['data'] || '') :\n payload['data'];\n this.result_(code, data);\n } else if (cmd == 'check') {\n this.win_.setTimeout(() => this.check_(), 200);\n }\n }\n}\n\n\n/**\n * @param {!Window} win\n * @param {string} fragment\n * @param {string} requestId\n * @return {?ActivityPort}\n */\nfunction discoverRedirectPort(win, fragment, requestId) {\n // Try to find the result in the fragment.\n const paramName = '__WA_RES__';\n const fragmentParam = getQueryParam(fragment, paramName);\n if (!fragmentParam) {\n return null;\n }\n const response = /** @type {?Object} */ (JSON.parse(fragmentParam));\n if (!response || response['requestId'] != requestId) {\n return null;\n }\n\n // Remove the found param from the fragment.\n const cleanFragment = removeQueryParam(win.location.hash, paramName) || '';\n if (cleanFragment != win.location.hash) {\n if (win.history && win.history.replaceState) {\n try {\n win.history.replaceState(win.history.state, '', cleanFragment);\n } catch (e) {\n // Ignore.\n }\n }\n }\n\n const code = response['code'];\n const data = response['data'];\n const origin = response['origin'];\n const referrerOrigin = win.document.referrer &&\n getOriginFromUrl(win.document.referrer);\n const originVerified = origin == referrerOrigin;\n return new ActivityWindowRedirectPort(\n win,\n code,\n data,\n origin,\n originVerified);\n}\n\n\n/**\n * The `ActivityPort` implementation for the standalone window activity\n * client executed as a popup.\n *\n * @implements {ActivityPort}\n */\nclass ActivityWindowRedirectPort {\n\n /**\n * @param {!Window} win\n * @param {!ActivityResultCode} code\n * @param {*} data\n * @param {string} targetOrigin\n * @param {boolean} targetOriginVerified\n */\n constructor(win, code, data, targetOrigin, targetOriginVerified) {\n /** @private @const {!Window} */\n this.win_ = win;\n /** @private @const {!ActivityResultCode} */\n this.code_ = code;\n /** @private @const {*} */\n this.data_ = data;\n /** @private {string} */\n this.targetOrigin_ = targetOrigin;\n /** @private {boolean} */\n this.targetOriginVerified_ = targetOriginVerified;\n }\n\n /** @override */\n getMode() {\n return ActivityMode.REDIRECT;\n }\n\n /** @override */\n acceptResult() {\n const result = new ActivityResult(\n this.code_,\n this.data_,\n ActivityMode.REDIRECT,\n this.targetOrigin_,\n this.targetOriginVerified_,\n /* secureChannel */ false);\n return new Promise(resolve => {\n resolveResult(this.win_, result, resolve);\n });\n }\n}\n\n\n\n\n/**\n * The page-level activities manager ports. This class is intended to be used\n * as a singleton. It can start activities of all modes: iframe, popup, and\n * redirect.\n */\nclass ActivityPorts {\n\n /**\n * @param {!Window} win\n */\n constructor(win) {\n /** @const {string} */\n this.version = '1.24';\n\n /** @private @const {!Window} */\n this.win_ = win;\n\n /** @private @const {string} */\n this.fragment_ = win.location.hash;\n\n /**\n * @private @const {!Object>}\n */\n this.requestHandlers_ = {};\n\n /**\n * The result buffer is indexed by `requestId`.\n * @private @const {!Object}\n */\n this.resultBuffer_ = {};\n\n /** @private {?function(!Error)} */\n this.redirectErrorResolver_ = null;\n\n /** @private {!Promise} */\n this.redirectErrorPromise_ = new Promise(resolve => {\n this.redirectErrorResolver_ = resolve;\n });\n }\n\n /**\n * Start an activity within the specified iframe.\n * @param {!HTMLIFrameElement} iframe\n * @param {string} url\n * @param {?Object=} opt_args\n * @return {!Promise}\n */\n openIframe(iframe, url, opt_args) {\n const port = new ActivityIframePort(iframe, url, opt_args);\n return port.connect().then(() => port);\n }\n\n /**\n * Start an activity in a separate window. The result will be delivered\n * to the `onResult` callback.\n *\n * The activity can be opened in two modes: \"popup\" and \"redirect\". This\n * depends on the `target` value, but also on the browser/environment.\n *\n * The allowed `target` values are `_blank`, `_top` and name targets. The\n * `_self`, `_parent` and similar targets are not allowed.\n *\n * The `_top` target indicates that the activity should be opened as a\n * \"redirect\", while other targets indicate that the activity should be\n * opened as a popup. The activity client will try to honor the requested\n * target. However, it's not always possible. Some environments do not\n * allow popups and they either force redirect or fail the window open\n * request. In this case, the activity will try to fallback to the \"redirect\"\n * mode.\n *\n * @param {string} requestId\n * @param {string} url\n * @param {string} target\n * @param {?Object=} opt_args\n * @param {?ActivityOpenOptions=} opt_options\n * @return {{targetWin: ?Window}}\n */\n open(requestId, url, target, opt_args, opt_options) {\n const port = this.openWin_(requestId, url, target, opt_args, opt_options);\n return {targetWin: port.getTargetWin()};\n }\n\n /**\n * Start an activity in a separate window and tries to setup messaging with\n * this window.\n *\n * See `open()` method for more details, including `onResult` callback.\n *\n * @param {string} requestId\n * @param {string} url\n * @param {string} target\n * @param {?Object=} opt_args\n * @param {?ActivityOpenOptions=} opt_options\n * @return {!Promise}\n */\n openWithMessaging(requestId, url, target, opt_args, opt_options) {\n const port = this.openWin_(requestId, url, target, opt_args, opt_options);\n return port.whenConnected().then(() => port);\n }\n\n /**\n * Registers the callback for the result of the activity opened with the\n * specified `requestId` (see the `open()` method). The callback is a\n * function that takes a single `ActivityPort` argument. The client\n * can use this object to verify the port using it's origin, verified and\n * secure channel flags. Then the client can call\n * `ActivityPort.acceptResult()` method to accept the result.\n *\n * The activity result is handled via a separate callback because of a\n * possible redirect. So use of direct callbacks and/or promises is not\n * possible in that case.\n *\n * A typical implementation would look like:\n * ```\n * ports.onResult('request1', function(port) {\n * port.acceptResult().then(function(result) {\n * // Only verified origins are allowed.\n * if (result.origin == expectedOrigin &&\n * result.originVerified &&\n * result.secureChannel) {\n * handleResultForRequest1(result);\n * }\n * });\n * })\n *\n * ports.open('request1', request1Url, '_blank');\n * ```\n *\n * @param {string} requestId\n * @param {function(!ActivityPort)} callback\n */\n onResult(requestId, callback) {\n let handlers = this.requestHandlers_[requestId];\n if (!handlers) {\n handlers = [];\n this.requestHandlers_[requestId] = handlers;\n }\n handlers.push(callback);\n\n // Consume available result.\n const availableResult = this.discoverResult_(requestId);\n if (availableResult) {\n this.consumeResult_(availableResult, callback);\n }\n }\n\n /**\n * @param {function(!Error)} handler\n */\n onRedirectError(handler) {\n this.redirectErrorPromise_.then(handler);\n }\n\n /**\n * @param {string} requestId\n * @param {string} url\n * @param {string} target\n * @param {?Object=} opt_args\n * @param {?ActivityOpenOptions=} opt_options\n * @return {!ActivityWindowPort}\n */\n openWin_(requestId, url, target, opt_args, opt_options) {\n const port = new ActivityWindowPort(\n this.win_, requestId, url, target, opt_args, opt_options);\n port.open().then(() => {\n // Await result if possible. Notice that when falling back to \"redirect\",\n // the result will never arrive through this port.\n this.consumeResultAll_(requestId, port);\n });\n return port;\n }\n\n /**\n * @param {string} requestId\n * @return {?ActivityPort}\n * @private\n */\n discoverResult_(requestId) {\n let port = this.resultBuffer_[requestId];\n if (!port && this.fragment_) {\n try {\n port = discoverRedirectPort(\n this.win_, this.fragment_, requestId);\n } catch (e) {\n throwAsync(e);\n this.redirectErrorResolver_(e);\n }\n if (port) {\n this.resultBuffer_[requestId] = port;\n }\n }\n return port;\n }\n\n /**\n * @param {!ActivityPort} port\n * @param {function(!ActivityPort)} callback\n * @private\n */\n consumeResult_(port, callback) {\n Promise.resolve().then(() => {\n callback(port);\n });\n }\n\n /**\n * @param {string} requestId\n * @param {!ActivityPort} port\n * @private\n */\n consumeResultAll_(requestId, port) {\n // Find and execute handlers.\n const handlers = this.requestHandlers_[requestId];\n if (handlers) {\n handlers.forEach(handler => {\n this.consumeResult_(port, handler);\n });\n }\n // Buffer the result for callbacks that may arrive in the future.\n this.resultBuffer_[requestId] = port;\n }\n}\n\n\n\nmodule.exports = {\n ActivityPorts,\n ActivityIframePort,\n ActivityMessagingPort,\n ActivityMode,\n ActivityOpenOptions,\n ActivityPort,\n ActivityRequest,\n ActivityResult,\n ActivityResultCode,\n ActivityWindowPort,\n createAbortError,\n isAbortError,\n};\n","/**\n * Copyright 2019 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n AnalyticsRequest,\n EventOriginator,\n Message,\n deserialize,\n getLabel,\n} from '../proto/api_messages';\nimport {INTERNAL_RUNTIME_VERSION} from '../constants';\n\nimport {StorageKeys} from '../utils/constants';\nimport {addQueryParam} from '../utils/url';\n\nimport {\n ActivityMode,\n ActivityOpenOptions,\n ActivityResult,\n ActivityIframePort as WebActivityIframePort,\n ActivityPort as WebActivityPort,\n ActivityPorts as WebActivityPorts,\n} from 'web-activities/activity-ports';\nimport {Deps} from '../runtime/deps';\n\nexport interface ActivityPortDef {\n acceptResult(): Promise;\n}\n\nexport interface ActivityPort extends ActivityPortDef {\n /**\n * Returns the mode of the activity: iframe, popup or redirect.\n */\n getMode(): ActivityMode;\n\n /**\n * Accepts the result when ready. The client should verify the activity's\n * mode, origin, verification and secure channel flags before deciding\n * whether or not to trust the result.\n *\n * Returns the promise that yields when the activity has been completed and\n * either a result, a cancelation or a failure has been returned.\n */\n acceptResult(): Promise;\n\n /**\n * Returns a promise that yields when the iframe is ready to be interacted\n * with.\n */\n whenReady(): Promise;\n\n /**\n * Waits until the activity port is connected to the host.\n */\n connect(): Promise;\n\n /**\n * Disconnect the activity binding and cleanup listeners.\n */\n disconnect(): void;\n\n /**\n * Register a callback to handle resize requests. Once successfully resized,\n * ensure to call `resized()` method.\n */\n onResizeRequest(callback: (size: number) => void): void;\n\n execute(request: Message): void;\n\n on(\n messageType: new (data?: unknown[], includesLabel?: boolean) => T,\n callback: (p1: T) => void\n ): void;\n\n /**\n * Signals back to the activity implementation that the client has updated\n * the activity's size.\n */\n resized(): void;\n}\n\nclass ActivityPortDeprecated implements ActivityPortDef {\n constructor(private readonly port_: WebActivityPort) {}\n\n acceptResult(): Promise {\n return this.port_.acceptResult();\n }\n}\n\nexport class ActivityIframePort implements ActivityPortDef {\n private readonly iframePort_: WebActivityIframePort;\n private readonly callbackMap_: {[key: string]: (message: Message) => void};\n\n constructor(\n iframe: HTMLIFrameElement,\n url: string,\n private readonly deps_: Deps,\n args?: unknown\n ) {\n this.iframePort_ = new WebActivityIframePort(iframe, url, args);\n this.callbackMap_ = {};\n }\n\n /**\n * Returns a promise that yields when the iframe is ready to be interacted\n * with.\n */\n whenReady(): Promise {\n return this.iframePort_.whenReady();\n }\n\n /**\n * Waits until the activity port is connected to the host.\n */\n async connect(): Promise {\n await this.iframePort_.connect();\n\n // Attach a callback to receive messages after connection complete\n this.iframePort_.onMessage((data) => {\n const response = data?.['RESPONSE'];\n if (!response) {\n return;\n }\n const cb = this.callbackMap_[response[0] as string];\n if (cb) {\n const message = deserialize(response);\n cb(message);\n }\n });\n\n if (this.deps_ && this.deps_.eventManager()) {\n this.on(AnalyticsRequest, (request) => {\n this.deps_.eventManager().logEvent({\n eventType: request.getEvent(),\n eventOriginator: EventOriginator.SWG_SERVER,\n isFromUserAction: request.getMeta()?.getIsFromUserAction(),\n additionalParameters: request.getParams(),\n configurationId: request.getMeta()?.getConfigurationId(),\n });\n });\n }\n }\n\n /**\n * Disconnect the activity binding and cleanup listeners.\n */\n disconnect() {\n this.iframePort_.disconnect();\n }\n\n /**\n * Returns the mode of the activity: iframe, popup or redirect.\n */\n getMode(): ActivityMode {\n return this.iframePort_.getMode();\n }\n\n /**\n * Accepts the result when ready. The client should verify the activity's\n * mode, origin, verification and secure channel flags before deciding\n * whether or not to trust the result.\n *\n * Returns the promise that yields when the activity has been completed and\n * either a result, a cancelation or a failure has been returned.\n */\n acceptResult(): Promise {\n return this.iframePort_.acceptResult();\n }\n\n /**\n * Register a callback to handle resize requests. Once successfully resized,\n * ensure to call `resized()` method.\n */\n onResizeRequest(callback: (size: number) => void) {\n return this.iframePort_.onResizeRequest(callback);\n }\n\n execute(request: Message) {\n this.iframePort_.message({'REQUEST': request.toArray()});\n }\n\n on(\n message: new (data?: unknown[], includesLabel?: boolean) => T,\n callback: (p1: T) => void\n ) {\n let label = null;\n try {\n label = getLabel(message);\n } catch (ex) {\n // Thrown if message is not a proto object and has no label\n label = null;\n }\n if (!label) {\n throw new Error('Invalid data type');\n } else if (this.callbackMap_[label]) {\n throw new Error('Invalid type or duplicate callback for ' + label);\n }\n this.callbackMap_[label] = callback as (p1: Message) => void;\n }\n\n /**\n * Signals back to the activity implementation that the client has updated\n * the activity's size.\n */\n resized() {\n this.iframePort_.resized();\n }\n}\n\nexport class ActivityPorts {\n activityPorts_: WebActivityPorts;\n\n constructor(private readonly deps_: Deps) {\n this.activityPorts_ = new WebActivityPorts(deps_.win());\n }\n\n /**\n * Adds client version, publication, product and logging context information.\n */\n addDefaultArguments(args?: {} | null): {} {\n const deps = this.deps_;\n const pageConfig = deps.pageConfig();\n const context = deps.analytics().getContext();\n return Object.assign(\n {\n 'analyticsContext': context.toArray(),\n 'publicationId': pageConfig.getPublicationId(),\n 'productId': pageConfig.getProductId(),\n '_client': `SwG ${INTERNAL_RUNTIME_VERSION}`,\n 'supportsEventManager': true,\n },\n args || {}\n );\n }\n\n /*\n * Start an activity within the specified iframe.\n */\n private async openActivityIframePort_(\n iframe: HTMLIFrameElement,\n url: string,\n args?: unknown\n ) {\n const activityPort = new ActivityIframePort(iframe, url, this.deps_, args);\n await activityPort.connect();\n return activityPort;\n }\n\n /**\n * Start an activity within the specified iframe.\n */\n async openIframe(\n iframe: HTMLIFrameElement,\n url: string,\n args?: {} | null,\n addDefaultArguments = false\n ): Promise {\n if (addDefaultArguments) {\n args = this.addDefaultArguments(args);\n }\n\n const swgUserToken = await this.deps_\n .storage()\n .get(StorageKeys.USER_TOKEN, /* useLocalStorage= */ true);\n\n const queryParams = new URL(url).searchParams;\n if (swgUserToken && !queryParams.has('sut')) {\n url = addQueryParam(url, 'sut', swgUserToken);\n }\n\n const pubId = this.deps_.pageConfig().getPublicationId();\n if (pubId && !queryParams.has('publicationId')) {\n url = addQueryParam(url, 'publicationId', pubId);\n }\n\n return this.openActivityIframePort_(iframe, url, args);\n }\n\n /**\n * Start an activity in a separate window. The result will be delivered\n * to the `onResult` callback.\n *\n * The activity can be opened in two modes: \"popup\" and \"redirect\". This\n * depends on the `target` value, but also on the browser/environment.\n *\n * The allowed `target` values are `_blank`, `_top` and name targets. The\n * `_self`, `_parent` and similar targets are not allowed.\n *\n * The `_top` target indicates that the activity should be opened as a\n * \"redirect\", while other targets indicate that the activity should be\n * opened as a popup. The activity client will try to honor the requested\n * target. However, it's not always possible. Some environments do not\n * allow popups and they either force redirect or fail the window open\n * request. In this case, the activity will try to fallback to the \"redirect\"\n * mode.\n */\n open(\n requestId: string,\n url: string,\n target: string,\n args?: {} | null,\n options?: ActivityOpenOptions | null,\n addDefaultArguments = false\n ): {targetWin: Window | null} {\n if (addDefaultArguments) {\n args = this.addDefaultArguments(args);\n }\n return this.activityPorts_.open(requestId, url, target, args, options);\n }\n\n /**\n * Registers the callback for the result of the activity opened with the\n * specified `requestId` (see the `open()` method). The callback is a\n * function that takes a single `ActivityPort` argument. The client\n * can use this object to verify the port using it's origin, verified and\n * secure channel flags. Then the client can call\n * `ActivityPort.acceptResult()` method to accept the result.\n *\n * The activity result is handled via a separate callback because of a\n * possible redirect. So use of direct callbacks and/or promises is not\n * possible in that case.\n *\n * A typical implementation would look like:\n * ```\n * ports.onResult('request1', async (port) => {\n * const result = await port.acceptResult();\n * // Only verified origins are allowed.\n * if (result.origin == expectedOrigin &&\n * result.originVerified &&\n * result.secureChannel) {\n * handleResultForRequest1(result);\n * }\n * })\n *\n * ports.open('request1', request1Url, '_blank');\n * ```\n */\n onResult(requestId: string, callback: (port: ActivityPortDef) => void): void {\n this.activityPorts_.onResult(requestId, (port) => {\n callback(new ActivityPortDeprecated(port));\n });\n }\n\n onRedirectError(handler: (error: Error) => void) {\n this.activityPorts_.onRedirectError(handler);\n }\n\n getOriginalWebActivityPorts(): WebActivityPorts {\n return this.activityPorts_;\n }\n}\n","/**\n * Copyright 2019 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AnalyticsEvent as AnalyticsEventDef,\n EventOriginator as EventOriginatorDef,\n} from '../proto/api_messages';\n\nexport enum FilterResult {\n /** The event is allowed to proceed to the listeners. */\n PROCESS_EVENT = 0,\n /** The event is canceled and the listeners are not informed about it. */\n CANCEL_EVENT = 1,\n}\n\n/** Defines a client event in SwG. */\nexport interface ClientEvent {\n /** Required. The AnalyticsEvent type that occurred. */\n eventType: AnalyticsEventDef | null;\n /** Required. The codebase that initiated the event. */\n eventOriginator: EventOriginatorDef;\n /** Optional. True if the user took an action to generate the event. */\n isFromUserAction?: boolean | null;\n /** Optional. A JSON object to store generic data. */\n additionalParameters?: {} | null;\n /** Optional. When the event happened. */\n timestamp?: number;\n /** Optional. ID of the associated action configuration. */\n configurationId?: string | null;\n}\n\nexport interface ClientEventManagerApi {\n /**\n * Call this function to log an event. The registered listeners will be\n * invoked unless the event is filtered.\n */\n registerEventListener(\n listener: (\n clientEvent: ClientEvent,\n clientEventParams?: ClientEventParams\n ) => void\n ): void;\n\n /**\n * Register a filterer for events if you need to potentially prevent the\n * listeners from hearing about it. A filterer should return\n * FilterResult.CANCEL_EVENT to prevent listeners from hearing about the\n * event.\n */\n registerEventFilterer(\n filterer: (clientEvent: ClientEvent) => FilterResult\n ): void;\n\n /**\n * Call this function to log an event. It will immediately throw an error if\n * the event is invalid. It will then asynchronously call the filterers and\n * stop the event if a filterer cancels it. After that, it will call each\n * listener asynchronously.\n */\n logEvent(event: ClientEvent, eventParams?: ClientEventParams): void;\n}\n\n/**\n * Event Properties to handle for a specific event. For example, GA Event\n * properties to skip SwG logging but still be handled via callback.\n */\nexport interface ClientEventParams {\n googleAnalyticsParameters?: GoogleAnalyticsParameters;\n}\n\nexport interface GoogleAnalyticsParameters {\n /* eslint-disable google-camelcase/google-camelcase */\n event_category?: string;\n survey_question?: string;\n survey_answer_category?: string;\n event_label?: string;\n /* eslint-enable google-camelcase/google-camelcase */\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Determines if value is actually an Object.\n */\nexport function isObject(value: unknown): boolean {\n const str = Object.prototype.toString.call(value);\n return str === '[object Object]';\n}\n\n/**\n * Checks whether `enumObj` has a given `value`.\n */\nexport function isEnumValue(enumObj: object, value: unknown): boolean {\n return Object.values(enumObj).includes(value);\n}\n\n/**\n * True if the value is a function.\n */\nexport function isFunction(value: unknown): boolean {\n return typeof value === 'function';\n}\n\n/**\n * True if the value is either true or false.\n */\nexport function isBoolean(value: unknown): boolean {\n return typeof value === 'boolean';\n}\n","/**\n * Copyright 2019 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AnalyticsEvent,\n EventOriginator,\n EventParams,\n} from '../proto/api_messages';\nimport {\n ClientEvent,\n ClientEventManagerApi,\n ClientEventParams,\n FilterResult,\n} from '../api/client-event-manager-api';\nimport {isBoolean, isEnumValue, isFunction, isObject} from '../utils/types';\nimport {log} from '../utils/log';\n\n/**\n * Helper function to describe an issue with an event object\n * @param {!string} valueName\n * @param {unknown} value\n * @returns {!string}\n */\nfunction createEventErrorMessage(valueName: string, value: unknown): string {\n return 'Event has an invalid ' + valueName + '(' + value + ')';\n}\n\n/**\n * Throws an error if the event is invalid.\n */\nfunction validateEvent(event: ClientEvent) {\n if (!isObject(event)) {\n throw new Error('Event must be a valid object');\n }\n\n if (!isEnumValue(AnalyticsEvent, event.eventType)) {\n throw new Error(createEventErrorMessage('eventType', event.eventType));\n }\n\n if (!isEnumValue(EventOriginator, event.eventOriginator)) {\n throw new Error(\n createEventErrorMessage('eventOriginator', event.eventOriginator)\n );\n }\n\n if (\n !isObject(event.additionalParameters) &&\n event.additionalParameters != null\n ) {\n throw new Error(\n createEventErrorMessage(\n 'additionalParameters',\n event.additionalParameters\n )\n );\n }\n\n if (event.isFromUserAction != null && !isBoolean(event.isFromUserAction)) {\n throw new Error(\n createEventErrorMessage('isFromUserAction', event.isFromUserAction)\n );\n }\n}\n\nexport class ClientEventManager implements ClientEventManagerApi {\n static isPublisherEvent(event: ClientEvent): boolean {\n return (\n event.eventOriginator === EventOriginator.PROPENSITY_CLIENT ||\n event.eventOriginator === EventOriginator.PUBLISHER_CLIENT\n );\n }\n\n private listeners_: ((\n clientEvent: ClientEvent,\n params?: ClientEventParams\n ) => void)[] = [];\n private filterers_: ((clientEvent: ClientEvent) => FilterResult)[] = [];\n\n /** Visible for testing. */\n lastAction: Promise | null = null;\n\n constructor(private readonly isReadyPromise_: Promise) {}\n\n registerEventListener(\n listener: (\n clientEvent: ClientEvent,\n clientEventParams?: ClientEventParams\n ) => void\n ) {\n if (!isFunction(listener)) {\n throw new Error('Event manager listeners must be a function');\n }\n this.listeners_.push(listener);\n }\n\n registerEventFilterer(filterer: (clientEvent: ClientEvent) => FilterResult) {\n if (!isFunction(filterer)) {\n throw new Error('Event manager filterers must be a function');\n }\n this.filterers_.push(filterer);\n }\n\n logEvent(\n event: ClientEvent,\n eventParams?: ClientEventParams,\n eventTime?: number\n ) {\n validateEvent(event);\n\n // Use provided timestamp or current if not provided.\n event.timestamp = eventTime ?? Date.now();\n\n this.lastAction = this.handleEvent_(event, eventParams);\n }\n\n /**\n * Triggers event listeners, unless filterers cancel the event.\n */\n private async handleEvent_(\n event: ClientEvent,\n eventParams?: ClientEventParams\n ) {\n await this.isReadyPromise_;\n\n // Bail if a filterer cancels the event.\n for (const filterer of this.filterers_) {\n try {\n if (filterer(event) === FilterResult.CANCEL_EVENT) {\n return;\n }\n } catch (e) {\n log(e);\n }\n }\n\n // Trigger listeners.\n for (const listener of this.listeners_) {\n try {\n listener(event, eventParams);\n } catch (e) {\n log(e);\n }\n }\n }\n\n /**\n * Creates an event with the arguments provided and calls logEvent.\n */\n logSwgEvent(\n eventType: AnalyticsEvent,\n isFromUserAction: boolean | null = false,\n eventParams: EventParams | null = null,\n eventTime?: number,\n configurationId: string | null = null\n ) {\n this.logEvent(\n {\n eventType,\n eventOriginator: EventOriginator.SWG_CLIENT,\n isFromUserAction,\n additionalParameters: eventParams,\n configurationId,\n },\n undefined,\n eventTime\n );\n }\n\n getReadyPromise(): Promise {\n return this.isReadyPromise_;\n }\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {EXPERIMENTS} from '../constants';\nimport {ErrorUtils} from '../utils/errors';\nimport {parseQueryString} from '../utils/url';\n\n/**\n * @fileoverview\n *\n * Client-side experiments in SwG.\n *\n * The experiments can be set in a few different ways:\n * 1. By gulp build rules using `--experiments=${experimentsString}` argument.\n * 2. By `#swg.experiments=${experimentsString}` parameter in the URL's\n * fragment.\n * 3. By `swg.configure({experiments: [array]})` call.\n *\n * The `${experimentsString}` is defined as following:\n * - experimentString = (experimentSpec,)*\n * - experimentSpec = experimentId | experimentId '=' num100 ('c')?\n *\n * Some examples:\n * - `A,B` - defines two experiments \"A\" and \"B\" that will be turned on.\n * - `A:100,B:100` - the same: \"A\" and \"B\" will be turned on.\n * - `A:0` - the experiment \"A\" will be disabled.\n * - `A:1` - enable the experiment \"A\" in 1% of impressions.\n * - `A:10c` - enable the experiment \"A\" in 10% of impressions with 10%\n * control. In this case, 20% of the impressions will be split into two\n * categories: experiment and control. Notice, a control can be requested\n * only for the fraction under 20%.\n *\n * Server-side experiments in SwG.\n *\n * These are only observed at runtime via the `#swg.experiments=${experimentsString}`\n * parameter in the URL's fragment.\n *\n * The `${experimentsString}` follows the convention of comma separated\n * experiment ID's, optionally prefixed with hyphen (`-`) indicating you want\n * the experiment to be disabled.\n *\n * An example would look like:\n * - `MyExperiment,-OtherExperiment` - indicates that you would like `MyExperiment`\n * to be enabled and `OtherExperiment` to be disabled.\n *\n * Due to restrictions, server flags can only be enabled following the\n * internal policy; otherwise they are ignored.\n */\n\nenum Selection {\n EXPERIMENT = 'e',\n CONTROL = 'c',\n}\n\n/**\n * A mutable copy of the comma-separated set of experiments.\n */\nlet experimentsString = EXPERIMENTS;\n\n/**\n * A parsed map of experiments.\n */\nlet experimentMap: {[key: string]: boolean} | null = null;\n\nexport function setExperimentsStringForTesting(s: string): void {\n experimentsString = s;\n experimentMap = null;\n}\n\n/**\n * Ensures that the experiments have been initialized and returns them.\n */\nfunction getExperiments(win: Window): {[key: string]: boolean} {\n if (!experimentMap) {\n experimentMap = {};\n let combinedExperimentString = experimentsString;\n try {\n const query = parseQueryString(win.location.hash);\n const experimentStringFromHash = query['swg.experiments'];\n if (experimentStringFromHash) {\n combinedExperimentString += ',' + experimentStringFromHash;\n }\n } catch (e) {\n // Ignore: experiment parsing cannot block runtime.\n ErrorUtils.throwAsync(e as Error);\n }\n\n // Format:\n // - experimentString = (experimentSpec,)*\n for (let experimentString of combinedExperimentString.split(',')) {\n experimentString = experimentString.trim();\n if (!experimentString) {\n continue;\n }\n try {\n parseSetExperiment(win, experimentMap, experimentString);\n } catch (e) {\n // Ignore: experiment parsing cannot block runtime.\n ErrorUtils.throwAsync(e as Error);\n }\n }\n }\n return experimentMap;\n}\n\nfunction parseSetExperiment(\n win: Window,\n experimentMap: {[key: string]: boolean},\n spec: string\n) {\n // Format:\n // - experimentSpec = experimentId | experimentId '=' num100 ('c')?\n let experimentId;\n let fraction;\n let control = false;\n const eq = spec.indexOf(':');\n if (eq == -1) {\n experimentId = spec;\n fraction = 100;\n control = false;\n } else {\n experimentId = spec.substring(0, eq).trim();\n spec = spec.substring(eq + 1);\n if (spec.substring(spec.length - 1) == Selection.CONTROL) {\n control = true;\n spec = spec.substring(0, spec.length - 1);\n }\n fraction = parseInt(spec, 10);\n }\n if (isNaN(fraction)) {\n throw new Error('invalid fraction');\n }\n\n // Calculate \"on\"/\"off\".\n let on;\n if (fraction > 99) {\n // Explicitly \"on\".\n on = true;\n } else if (fraction < 1) {\n // Explicitly \"off\".\n on = false;\n } else if (win.sessionStorage) {\n // Fractional and possibly with the control.\n // Note that:\n // a. We can't do persistent experiments if storage is not available.\n // b. We can't run control on more than 20%.\n control = control && fraction <= 20;\n try {\n // Set fraction in the experiment to make it unlaunchable.\n const storageKey =\n 'subscribe.google.com:e:' +\n experimentId +\n ':' +\n fraction +\n (control ? 'c' : '');\n let selection = parseSelection(win.sessionStorage.getItem(storageKey));\n if (!selection) {\n // Is experiment/control range?\n if (Math.random() * 100 <= fraction * (control ? 2 : 1)) {\n const inExperiment = control ? Math.random() <= 0.5 : true;\n selection = inExperiment ? Selection.EXPERIMENT : Selection.CONTROL;\n win.sessionStorage.setItem(storageKey, selection);\n }\n }\n on = !!selection;\n if (selection == Selection.CONTROL) {\n experimentId = 'c-' + experimentId;\n }\n } catch (e) {\n // Ignore: experiment parsing cannot block runtime.\n on = false;\n ErrorUtils.throwAsync(e as Error);\n }\n } else {\n on = false;\n }\n\n experimentMap[experimentId] = on;\n}\n\nfunction parseSelection(s: string | null): Selection | null {\n // Do a simple if-then to inline the whole Selection enum.\n return s == Selection.EXPERIMENT\n ? Selection.EXPERIMENT\n : s == Selection.CONTROL\n ? Selection.CONTROL\n : null;\n}\n\n/**\n * Whether the specified experiment is on or off.\n */\nexport function isExperimentOn(win: Window, experimentId: string): boolean {\n return getExperiments(win)[experimentId] || false;\n}\n\n/**\n * Toggles the experiment on or off. Returns the actual value of the experiment\n * after toggling is done.\n */\nexport function setExperiment(\n win: Window,\n experimentId: string,\n on: boolean\n): void {\n getExperiments(win)[experimentId] = on;\n}\n\nexport function getOnExperiments(win: Window): string[] {\n const experimentMap = getExperiments(win);\n const experiments = [];\n for (const experiment in experimentMap) {\n if (experimentMap[experiment]) {\n experiments.push(experiment);\n }\n }\n return experiments;\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {getRandomInts} from './random';\nimport {utf8EncodeSync} from './bytes';\nimport {warn} from './log';\n\nconst CHARS = '0123456789ABCDEF';\n\n/**\n * Ensures the passed value is safe to use for character 19 per rfc4122,\n * sec. 4.1.5. \"Sets the high bits of clock sequence\".\n */\nfunction getChar19(v: number): string {\n return CHARS[(v & 0x3) | 0x8];\n}\n\n/**\n * The returned identifier will always be an 8 digit valid hexidecimal number\n * and will be unique for each MS within a given month.\n */\nfunction getMonthlyTimeIdentifier(): string {\n const hexTime = Date.now().toString(16);\n return hexTime.substring(hexTime.length - 8).toUpperCase();\n}\n\n/**\n * Generates a RFC 4122 V4 UUID. Ex: \"92329D39-6F5C-4520-ABFC-AAB64544E172\"\n * The first 8 digits are unique for the millisecond of the month. The rest\n * are randomly generated.\n */\nexport function getUuid(): string {\n let uuid = getMonthlyTimeIdentifier() + '-';\n let rIndex = 0;\n const rands = getRandomInts(23, 16);\n for (let i = 9; i < 36; i++) {\n switch (i) {\n case 13:\n case 18:\n case 23:\n uuid += '-';\n break;\n case 14:\n uuid += '4';\n break;\n case 19:\n uuid += getChar19(rands[rIndex++]);\n break;\n default:\n uuid += CHARS[rands[rIndex++]];\n break;\n }\n }\n return uuid;\n}\n\nexport function getSwgTransactionId(): string {\n return getUuid() + '.swg';\n}\n\n/**\n * Returns a string whose length matches the length of format.\n */\nfunction padString(str: string, format: string): string {\n return (format + str).slice(-format.length);\n}\n\nconst PADDING = '00000000';\nfunction toHex(buffer: ArrayBuffer): string {\n const hexCodes = [];\n const view = new DataView(buffer);\n for (let i = 0; i < view.byteLength; i += 4) {\n // toString(16) will give the hex representation of the number without padding\n const stringValue = view.getUint32(i).toString(16);\n hexCodes.push(padString(stringValue, PADDING));\n }\n return hexCodes.join('');\n}\n\n/**\n * Returns a hexadecimal 128 character string that is the\n * SHA-512 hash of the passed string.\n * @param {string} stringToHash\n * @return {!Promise}\n */\nexport async function hash(stringToHash: string): Promise {\n const subtle = self.crypto?.subtle;\n\n if (!subtle) {\n const message = 'Swgjs only works on secure (HTTPS or localhost) pages.';\n warn(message);\n return Promise.reject(message);\n }\n\n return toHex(await subtle.digest('SHA-512', utf8EncodeSync(stringToHash)));\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Default styles to be set for top level friendly iframe.\n * Some attributes are not included such as height, left, margin-left; since\n * these attributes are updated by @media queries and having these values\n * defined here as !important does not work on IE/edge browsers.\n */\nexport const defaultStyles = {\n 'align-content': 'normal',\n 'animation': 'none',\n 'align-items': 'normal',\n 'align-self': 'auto',\n 'alignment-baseline': 'auto',\n 'backface-visibility': 'hidden',\n 'background-clip': 'border-box',\n 'background-image': 'none',\n 'baseline-shift': '0',\n 'block-size': 'auto',\n 'border': 'none',\n 'border-collapse': 'separate',\n 'bottom': '0',\n 'box-sizing': 'border-box',\n 'break-after': 'auto',\n 'break-before': 'auto',\n 'break-inside': 'auto',\n 'buffered-rendering': 'auto',\n 'caption-side': 'top',\n 'caret-color': 'rgb(51, 51, 51)',\n 'clear': 'none',\n 'color': 'rgb(51, 51, 51)',\n 'color-rendering': 'auto',\n 'column-count': 'auto',\n 'column-fill': 'balance',\n 'column-gap': 'normal',\n 'column-rule-color': 'rgb(51, 51, 51)',\n 'column-rule-style': 'none',\n 'column-rule-width': '0',\n 'column-span': 'none',\n 'column-width': 'auto',\n 'contain': 'none',\n 'counter-increment': 'none',\n 'counter-reset': 'none',\n 'cursor': 'auto',\n 'direction': 'inherit',\n 'display': 'block',\n 'empty-cells': 'show',\n 'filter': 'none',\n 'flex': 'none', // flex-grow, flex-shrink, and flex-basis.\n 'flex-flow': 'row nowrap', // flex-direction, flex-wrap.\n 'float': 'none',\n 'flood-color': 'rgb(0, 0, 0)',\n 'flood-opacity': '1',\n 'font': 'none',\n 'font-size': 'medium',\n 'font-family': '',\n 'height': 'auto',\n 'hyphens': 'manual',\n 'image-rendering': 'auto',\n 'inline-size': '', // Setting to 'auto' will not allow override.\n 'isolation': 'auto',\n 'justify-content': 'normal',\n 'justify-items': 'normal',\n 'justify-self': 'auto',\n 'letter-spacing': 'normal',\n 'lighting-color': 'rgb(255, 255, 255)',\n 'line-break': 'auto',\n 'line-height': 'normal',\n 'margin-bottom': '0',\n 'mask': 'none',\n 'max-block-size': 'none',\n 'max-height': 'none',\n 'max-inline-size': 'none',\n 'max-width': 'none',\n 'min-block-size': 'none',\n 'min-height': '0',\n 'min-inline-size': '0',\n 'min-width': '0',\n 'mix-blend-mode': 'normal',\n 'object-fit': 'fill', // Important for Safari browser.\n 'offset-distance': 'none', // Chrome only (Experimental).\n 'offset-path': 'none', // Chrome only (Experimental).\n 'offset-rotate': 'auto 0deg', // Chrome only (Experimental).\n 'opacity': '1',\n 'order': '0',\n 'orphans': '2',\n 'outline': 'none',\n 'overflow-anchor': 'auto',\n 'overflow-wrap': 'normal',\n 'overflow': 'visible',\n 'padding': '0',\n 'page': '',\n 'perspective': 'none',\n 'pointer-events': 'auto',\n 'position': 'static',\n 'quotes': '',\n 'resize': 'none',\n 'right': '0',\n 'scroll-behavior': 'auto',\n 'tab-size': '8', // Only Chrome, Safari (Experimental).\n 'table-layout': 'auto',\n 'text-align': 'start',\n 'text-align-last': 'auto',\n 'text-anchor': 'start',\n 'text-combine-upright': 'none',\n 'text-decoration': 'none',\n 'text-indent': '0',\n 'text-orientation': 'mixed',\n 'text-overflow': 'clip',\n 'text-rendering': 'auto',\n 'text-shadow': 'none',\n 'text-size-adjust': 'auto',\n 'text-transform': 'none',\n 'text-underline-position': 'auto',\n 'top': 'auto',\n 'touch-action': 'auto',\n 'transform': 'none',\n 'transition': 'none 0s ease 0s',\n 'unicode-bidi': 'normal',\n 'user-select': 'auto',\n 'vector-effect': 'none',\n 'vertical-align': 'baseline',\n 'visibility': 'visible',\n 'white-space': 'normal',\n 'widows': '2',\n 'word-break': 'normal',\n 'word-spacing': '0',\n 'word-wrap': 'normal',\n 'writing-mode': 'horizontal-tb',\n 'zoom': '1',\n 'z-index': 'auto',\n};\n\n/**\n * Sets the CSS styles of the specified element with !important. The styles\n * are specified as a map from CSS property names to their values.\n */\nexport function setImportantStyles(\n element: HTMLElement,\n styles: {[property: string]: string}\n) {\n for (const [property, value] of Object.entries(styles)) {\n element.style.setProperty(property, value.toString(), 'important');\n }\n}\n\n/**\n * Sets the CSS style of the specified element.\n */\nexport function setStyle(\n element: HTMLElement,\n property: string,\n value: string\n) {\n element.style.setProperty(property, value);\n}\n\n/**\n * Returns the value of the CSS style of the specified element.\n */\nexport function getStyle(element: HTMLElement, property: string): string {\n return element.style.getPropertyValue(property);\n}\n\n/**\n * Sets the CSS styles of the specified element. The styles\n * a specified as a map from CSS property names to their values.\n */\nexport function setStyles(\n element: HTMLElement,\n styles: {[property: string]: string}\n) {\n for (const [property, value] of Object.entries(styles)) {\n setStyle(element, property, value);\n }\n}\n\n/**\n * Resets styles that were set dynamically (i.e. inline)\n */\nexport function resetStyles(element: HTMLElement, properties: Array) {\n for (const property of properties) {\n setStyle(element, property, '');\n }\n}\n\n/**\n * Resets all the styles of an element to a given value. Defaults to null.\n * The valid values are 'inherit', 'initial', 'unset' or null.\n */\nexport function resetAllStyles(element: HTMLElement) {\n setImportantStyles(element, defaultStyles);\n}\n","/**\n * Copyright 2020 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {Duration, Timestamp} from '../proto/api_messages';\n\nexport function toTimestamp(millis: number): Timestamp {\n return new Timestamp(\n [Math.floor(millis / 1000), (millis % 1000) * 1000000],\n false\n );\n}\n\nexport function toDuration(millis: number): Duration {\n return new Duration(\n [Math.floor(millis / 1000), (millis % 1000) * 1000000],\n false\n );\n}\n\n/**\n * This function is used for convert the timestamp provided by publisher to\n * milliseconds. Although we required publishers to provide timestamp in\n * milliseconds, but there's a chance they may not follow the instruction.\n * So this function supports the conversion of seconds, milliseconds and\n * microseconds.\n * @param timestamp represented as seconds, milliseconds or microseconds\n */\nexport function convertPotentialTimestampToSeconds(timestamp: number): number {\n let timestampInSeconds;\n if (timestamp >= 1e14 || timestamp <= -1e14) {\n // Microseconds\n timestampInSeconds = Math.floor(timestamp / 1e6);\n } else if (timestamp >= 1e11 || timestamp <= -3e10) {\n // Milliseconds\n timestampInSeconds = Math.floor(timestamp / 1000);\n } else {\n // Seconds\n timestampInSeconds = timestamp;\n }\n return timestampInSeconds;\n}\n\n/**\n * @param timestamp represented as seconds, milliseconds or microseconds\n */\nexport function convertPotentialTimestampToMilliseconds(\n timestamp: number\n): number {\n let timestampInMilliseconds;\n if (timestamp >= 1e14 || timestamp <= -1e14) {\n // Microseconds\n timestampInMilliseconds = Math.floor(timestamp / 1000);\n } else if (timestamp >= 1e11 || timestamp <= -3e10) {\n // Milliseconds\n timestampInMilliseconds = timestamp;\n } else {\n // Seconds\n timestampInMilliseconds = Math.floor(timestamp * 1000);\n }\n return timestampInMilliseconds;\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {ActivityIframePort, ActivityPorts} from '../components/activities';\nimport {\n AnalyticsContext,\n AnalyticsEvent,\n AnalyticsEventMeta,\n AnalyticsRequest,\n EventOriginator,\n EventParams,\n FinishedLoggingResponse,\n Timestamp,\n} from '../proto/api_messages';\nimport {ClientEvent} from '../api/client-event-manager-api';\nimport {ClientEventManager} from './client-event-manager';\nimport {Deps} from './deps';\nimport {Doc} from '../model/doc';\nimport {INTERNAL_RUNTIME_VERSION} from '../constants';\nimport {createElement} from '../utils/dom';\nimport {feUrl} from './services';\nimport {getCanonicalTag, getCanonicalUrl} from '../utils/url';\nimport {getOnExperiments} from './experiments';\nimport {getSwgTransactionId} from '../utils/string';\nimport {log} from '../utils/log';\nimport {parseQueryString, parseUrl} from '../utils/url';\nimport {setImportantStyles} from '../utils/style';\nimport {toDuration, toTimestamp} from '../utils/date-utils';\n\nconst iframeStyles = {\n opacity: '0',\n position: 'absolute',\n top: '-10px',\n left: '-10px',\n height: '1px',\n width: '1px',\n};\n\n/**\n * The initial iframe load takes ~500 ms. We will wait at least that long\n * before a page redirect. Subsequent logs are much faster.\n */\nconst MAX_FIRST_WAIT = 500;\n\n/** We will wait at most 200 ms. */\nconst MAX_WAIT = 200;\n\n/**\n * If we logged and rapidly redirected, we will add a short delay in case\n * a message hasn't been transmitted yet.\n */\nconst TIMEOUT_ERROR = 'AnalyticsService timed out waiting for a response';\n\nfunction createErrorResponse(error: string): FinishedLoggingResponse {\n const response = new FinishedLoggingResponse();\n response.setComplete(false);\n response.setError(error);\n return response;\n}\n\nexport class AnalyticsService {\n private readonly activityPorts_: ActivityPorts;\n private readonly context_ = new AnalyticsContext();\n private readonly runtimeCreationTimestamp_: Timestamp;\n private readonly doc_: Doc;\n private readonly eventManager_: ClientEventManager;\n private readonly iframe_: HTMLIFrameElement;\n\n private everFinishedLog_ = false;\n /** If logging doesn't work don't force the user to wait. */\n private loggingBroken_ = false;\n private loggingResolver_: ((success: boolean) => void) | null = null;\n /** Stores log events while we wait to be ready for logging. */\n private logs_: ClientEvent[] = [];\n private portPromise_: Promise | null = null;\n private promiseToLog_: Promise | null = null;\n /** While false, we will buffer logs instead of sending them to the analytics service. */\n private readyForLogging_ = false;\n /**\n * If logging exceeds the timeouts (see const comments above) don't make\n * the user wait too long.\n */\n private timeout_: number | null = null;\n /**\n * This code creates a 'promise to log' that we can use to ensure all\n * logging is finished prior to redirecting the page.\n */\n private unfinishedLogs_ = 0;\n\n lastAction: Promise | null = null;\n\n constructor(private readonly deps_: Deps) {\n this.runtimeCreationTimestamp_ = toTimestamp(deps_.creationTimestamp());\n this.doc_ = deps_.doc();\n\n this.activityPorts_ = deps_.activities();\n\n this.iframe_ = createElement(this.doc_.getWin().document, 'iframe', {});\n setImportantStyles(this.iframe_, iframeStyles);\n this.doc_.getBody()?.appendChild(this.getElement());\n\n this.setStaticContext_();\n\n this.eventManager_ = deps_.eventManager();\n this.eventManager_.registerEventListener(\n this.handleClientEvent_.bind(this)\n );\n }\n\n /**\n * Sets ready for logging to true and logs all the client events that were previously buffered.\n */\n setReadyForLogging() {\n this.readyForLogging_ = true;\n for (const event of this.logs_) {\n this.handleClientEvent_(event);\n }\n }\n\n setTransactionId(transactionId: string): void {\n const oldTransactionId = this.context_.getTransactionId();\n this.context_.setTransactionId(transactionId);\n if (oldTransactionId != null && oldTransactionId != transactionId) {\n const eventType = AnalyticsEvent.EVENT_NEW_TX_ID;\n const eventParams = new EventParams();\n eventParams.setOldTransactionId(oldTransactionId);\n this.eventManager_.logSwgEvent(\n eventType,\n /* isFromUserAction= */ true,\n eventParams\n );\n }\n }\n\n getTransactionId(): string | null {\n return this.context_.getTransactionId();\n }\n\n getSku(): string | null {\n return this.context_.getSku();\n }\n\n setSku(sku: string): void {\n this.context_.setSku(sku);\n }\n\n setUrl(url: string): void {\n this.context_.setUrl(url);\n }\n\n addLabels(labels: string[]): void {\n if (labels && labels.length > 0) {\n const newLabels = ([] as string[]).concat(this.context_.getLabelList()!);\n for (const label of labels) {\n if (newLabels.indexOf(label) == -1) {\n newLabels.push(label);\n }\n }\n this.context_.setLabelList(newLabels);\n }\n }\n\n getElement(): HTMLIFrameElement {\n return this.iframe_;\n }\n\n private getQueryString_(): string {\n return this.doc_.getWin().location.search;\n }\n\n private getReferrer_(): string {\n return this.doc_.getWin().document.referrer;\n }\n\n private getLoadEventStartDelay_(): number {\n const performanceEntryList = this.getPerformanceEntryList_();\n if (!!performanceEntryList && !!performanceEntryList.length) {\n const timing = performanceEntryList[0] as PerformanceNavigationTiming;\n const eventStartDelay = timing.loadEventStart - timing.unloadEventEnd;\n if (eventStartDelay > 0) {\n return eventStartDelay;\n }\n }\n return 0;\n }\n\n private getPerformanceEntryList_(): PerformanceEntryList {\n return performance.getEntriesByType('navigation');\n }\n\n private setStaticContext_(): void {\n const context = this.context_;\n // These values should all be available during page load.\n context.setTransactionId(getSwgTransactionId());\n context.setReferringOrigin(parseUrl(this.getReferrer_()).origin);\n context.setClientVersion(`SwG ${INTERNAL_RUNTIME_VERSION}`);\n context.setUrl(getCanonicalUrl(this.doc_));\n context.setIsLockedContent(this.deps_.pageConfig().isLocked());\n\n // Default to empty, this is for investigative purposes only\n context.setUrlFromMarkup(getCanonicalTag(this.doc_) || '');\n\n const utmParams = parseQueryString(this.getQueryString_());\n const campaign = utmParams['utm_campaign'];\n const medium = utmParams['utm_medium'];\n const source = utmParams['utm_source'];\n if (campaign) {\n context.setUtmCampaign(campaign);\n }\n if (medium) {\n context.setUtmMedium(medium);\n }\n if (source) {\n context.setUtmSource(source);\n }\n }\n\n start(): Promise {\n // Only prepare port once.\n if (!this.portPromise_) {\n // Please note that currently openIframe reads the current analytics\n // context and that it may not contain experiments activated late during\n // the publisher's code lifecycle.\n this.addLabels(getOnExperiments(this.doc_.getWin()));\n this.portPromise_ = this.preparePort();\n }\n\n return this.portPromise_;\n }\n\n async preparePort(): Promise {\n // Open iframe.\n let port;\n try {\n port = await this.activityPorts_.openIframe(\n this.iframe_,\n feUrl('/serviceiframe'),\n null,\n true\n );\n } catch (message) {\n // If the port doesn't open register that logging is broken so\n // nothing is just waiting.\n this.loggingBroken_ = true;\n this.afterLogging_(\n createErrorResponse('Could not connect [' + message + ']')\n );\n return null;\n }\n\n // Register a listener for the logging to code indicate it is\n // finished logging.\n port.on(FinishedLoggingResponse, this.afterLogging_.bind(this));\n await port.whenReady();\n\n // The publisher should be done setting experiments but runtime\n // will forward them here if they aren't.\n this.addLabels(getOnExperiments(this.doc_.getWin()));\n\n return port;\n }\n\n setReadyToPay(isReadyToPay: boolean): void {\n this.context_.setReadyToPay(isReadyToPay);\n }\n\n close(): void {\n this.doc_.getBody()?.removeChild(this.getElement());\n }\n\n getContext(): AnalyticsContext {\n return this.context_;\n }\n\n private createLogRequest_(event: ClientEvent): AnalyticsRequest {\n const meta = new AnalyticsEventMeta();\n meta.setEventOriginator(event.eventOriginator);\n meta.setIsFromUserAction(!!event.isFromUserAction);\n if (!!event.configurationId) {\n meta.setConfigurationId(event.configurationId);\n }\n // Update the request's timestamp.\n this.context_.setClientTimestamp(toTimestamp(event.timestamp!));\n const loadEventStartDelay = this.getLoadEventStartDelay_();\n if (loadEventStartDelay > 0) {\n this.context_.setLoadEventStartDelay(toDuration(loadEventStartDelay));\n }\n this.context_.setRuntimeCreationTimestamp(this.runtimeCreationTimestamp_);\n const request = new AnalyticsRequest();\n request.setEvent(event.eventType!);\n request.setContext(this.context_);\n request.setMeta(meta);\n if (event.additionalParameters instanceof EventParams) {\n request.setParams(event.additionalParameters);\n } // Ignore event.additionalParameters. It may have data we shouldn't log.\n return request;\n }\n\n private shouldLogPublisherEvents_(): boolean {\n return this.deps_.config().enableSwgAnalytics === true;\n }\n\n /**\n * Listens for new events from the events manager and handles logging\n */\n private handleClientEvent_(event: ClientEvent): void {\n //this event is just used to communicate information internally. It should\n //not be reported to the SwG analytics service.\n if (event.eventType === AnalyticsEvent.EVENT_SUBSCRIPTION_STATE) {\n return;\n }\n\n // Permission should be asked from a privacy workgroup before this originator\n // can be submitted to the analytics service. It should most likely be treated\n // as another kind of publisher event here though.\n if (event.eventOriginator === EventOriginator.SHOWCASE_CLIENT) {\n return;\n }\n\n const blockedByPublisherConfig =\n ClientEventManager.isPublisherEvent(event) &&\n !this.shouldLogPublisherEvents_();\n if (blockedByPublisherConfig) {\n return;\n }\n\n if (!this.readyForLogging_) {\n // If we're not ready to log events yet,\n // store the event so we can log it later.\n this.logs_.push(event);\n return;\n }\n\n // Register we sent a log. The port will call this.afterLogging_ when done.\n this.unfinishedLogs_++;\n\n // Send log.\n this.lastAction = this.sendLog_(event);\n }\n\n async sendLog_(event: ClientEvent): Promise {\n const port = await this.start();\n const analyticsRequest = this.createLogRequest_(event);\n port?.execute(analyticsRequest);\n }\n\n /**\n * This function is called by the iframe after it sends the log to the server.\n */\n afterLogging_(message?: FinishedLoggingResponse): void {\n const success = message?.getComplete() || false;\n const error = message?.getError() || 'Unknown logging Error';\n const isTimeout = error === TIMEOUT_ERROR;\n\n if (!success) {\n log('Error when logging: ' + error);\n }\n\n this.unfinishedLogs_--;\n if (!isTimeout) {\n this.everFinishedLog_ = true;\n }\n\n // Nothing is waiting\n if (this.loggingResolver_ === null) {\n return;\n }\n\n if (this.unfinishedLogs_ === 0 || this.loggingBroken_ || isTimeout) {\n if (this.timeout_ !== null) {\n clearTimeout(this.timeout_);\n this.timeout_ = null;\n }\n this.loggingResolver_(success);\n this.promiseToLog_ = null;\n this.loggingResolver_ = null;\n }\n }\n\n /**\n * Please note that logs sent after getLoggingPromise is called are not\n * guaranteed to be finished when the promise is resolved. You should call\n * this function just prior to redirecting the page after SwG is finished\n * logging.\n */\n getLoggingPromise(): Promise {\n if (this.unfinishedLogs_ === 0 || this.loggingBroken_) {\n return Promise.resolve(true);\n }\n if (this.promiseToLog_ === null) {\n this.promiseToLog_ = new Promise((resolve) => {\n this.loggingResolver_ = resolve;\n });\n\n // The promise above should not wait forever if things go wrong. Let\n // the user proceed!\n this.timeout_ = self.setTimeout(\n () => {\n this.timeout_ = null;\n this.afterLogging_(createErrorResponse(TIMEOUT_ERROR));\n },\n this.everFinishedLog_ ? MAX_WAIT : MAX_FIRST_WAIT\n );\n }\n\n return this.promiseToLog_;\n }\n}\n","/**\n * Copyright 2019 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Returns an array of random values. The length of the array is numInts. Each\n * int will be >= 0 and < maxVal.\n */\nexport function getRandomInts(numInts: number, maxVal: number) {\n // Ensure array type is appropriate for the max value (performance)\n const arr =\n maxVal < 256\n ? new Uint8Array(numInts)\n : maxVal < 32768\n ? new Uint16Array(numInts)\n : new Uint32Array(numInts);\n\n self.crypto.getRandomValues(arr);\n for (let i = arr.length - 1; i > -1; i--) {\n arr[i] = arr[i] % maxVal;\n }\n\n return arr;\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Experiment flags.\n *\n * IMPORTANT: All flags should also be added to the e2e test configuration in\n * nightwatch.conf.js.\n */\nexport enum ExperimentFlags {\n /**\n * Experiment flag for enabling publication_id suffix to browser storage key.\n */\n ENABLE_PUBLICATION_ID_SUFFIX_FOR_STORAGE_KEY = 'enable-pub-id-suffix-for-storage-key',\n}\n\n/**\n * Experiment flags within article experiment config.\n */\nexport enum ArticleExperimentFlags {\n /**\n * Experiment flag to enable paywall background click behavior so that links\n * cannot be clicked through the darkened background and so that it closes\n * closable popups.\n */\n BACKGROUND_CLICK_BEHAVIOR_EXPERIMENT = 'background_click_behavior_experiment',\n\n /**\n * [FPA M0.5] Experiment flag to enable the new autoPromptManager flow to use\n * actionOrchestration from the article response as the source of the\n * targeted intervention funnel.\n */\n ACTION_ORCHESTRATION_EXPERIMENT = 'action_orchestration_experiment',\n\n /**\n * Experiment flag that filters out dismissible monetary CTAs if reader is\n * ineligible to purchase.\n */\n DISMISSIBILITY_CTA_FILTER_EXPERIMENT = 'dismissibility_cta_filter_experiment',\n}\n","/**\n * Copyright 2021 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Why is there a strings.ts and a swg-strings.ts, you ask? strings.ts is a\n// generated file, currently used for gaa builds. This file is used for swg and\n// swg-basic builds, and is currently manually updated.\n// TODO(stellachui): Figure out if they should be merged without a large impact\n// on binary size.\n\nexport const SWG_I18N_STRINGS = {\n 'SUBSCRIPTION_TITLE_LANG_MAP': {\n 'en': 'Subscribe with Google',\n 'ar': 'Google اشترك مع',\n 'de': 'Abonnieren mit Google',\n 'en-au': 'Subscribe with Google',\n 'en-ca': 'Subscribe with Google',\n 'en-gb': 'Subscribe with Google',\n 'en-us': 'Subscribe with Google',\n 'es': 'Suscríbete con Google',\n 'es-419': 'Suscríbete con Google',\n 'es-latam': 'Suscríbete con Google',\n 'es-latn': 'Suscríbete con Google',\n 'fr': \"S'abonner avec Google\",\n 'fr-ca': \"S'abonner avec Google\",\n 'hi': 'Google के ज़रिये सदस्यता',\n 'id': 'Berlangganan dengan Google',\n 'it': 'Abbonati con Google',\n 'ja': 'Google で購読',\n 'ko': 'Google 을 통한구독',\n 'ms': 'Langgan dengan Google',\n 'nl': 'Abonneren via Google',\n 'no': 'Abonner med Google',\n 'pl': 'Subskrybuj z Google',\n 'pt': 'Subscrever com o Google',\n 'pt-br': 'Assine com o Google',\n 'ru': 'Подпиcка через Google',\n 'sv': 'Prenumerera med Google',\n 'th': 'สมัครฟาน Google',\n 'tr': 'Google ile Abone Ol',\n 'uk': 'Підписатися через Google',\n 'zh-cn': '通过 Google 订阅',\n 'zh-hk': '透過 Google 訂閱',\n 'zh-tw': '透過 Google 訂閱',\n },\n 'CONTRIBUTION_TITLE_LANG_MAP': {\n 'en': 'Contribute with Google',\n 'ar': 'المساهمة باستخدام Google',\n 'de': 'Mit Google beitragen',\n 'en-au': 'Contribute with Google',\n 'en-ca': 'Contribute with Google',\n 'en-gb': 'Contribute with Google',\n 'en-us': 'Contribute with Google',\n 'es': '\tContribuye con Google',\n 'es-419': 'Contribuir con Google',\n 'es-latam': 'Contribuir con Google',\n 'es-latn': 'Contribuye con Google',\n 'fr': 'Contribuer avec Google',\n 'fr-ca': 'Contribuer avec Google',\n 'hi': 'Google खाते की मदद से योगदान करें',\n 'id': 'Berkontribusi dengan Google',\n 'it': 'Contribuisci con Google',\n 'ja': 'Google で寄付',\n 'ko': 'Google을 통해 참여하기',\n 'ms': 'Sumbangkan dengan Google',\n 'nl': 'Bijdragen met Google',\n 'no': 'Bidra med Google',\n 'pl': 'Wesprzyj publikację przez Google',\n 'pt': 'Contribuir utilizando o Google',\n 'pt-br': 'Contribua usando o Google',\n 'ru': 'Внести средства через Google',\n 'sv': 'Bidra med Google',\n 'th': 'มีส่วนร่วมผ่าน Google',\n 'tr': 'Google ile Katkıda Bulun',\n 'uk': 'Зробити внесок через Google',\n 'zh-cn': '通过 Google 捐赠',\n 'zh-hk': '透過 Google 提供內容',\n 'zh-tw': '透過 Google 捐款',\n },\n 'REGWALL_ALREADY_REGISTERED_LANG_MAP': {\n 'en': 'You have registered before.',\n 'ar': 'لقد سبق أن تسجّلت.',\n 'de': 'Du bist bereits registriert.',\n 'en-au': 'You have registered before.',\n 'en-ca': 'You have registered before.',\n 'en-gb': 'You have registered before.',\n 'en-us': 'You have registered before.',\n 'es': 'Ya te habías registrado anteriormente.',\n 'es-419': 'Ya te registraste antes.',\n 'fr': 'Vous vous êtes déjà inscrit.',\n 'fr-ca': 'Vous vous êtes inscrit auparavant.',\n 'hi': 'आपने पहले ही इसके लिए रजिस्टर कर लिया है.',\n 'id': 'Anda telah mendaftar sebelumnya.',\n 'it': 'Registrazione già effettuata in precedenza.',\n 'ja': 'すでに登録済みです。',\n 'ko': '이전에 등록한 사용자입니다.',\n 'ms': 'Anda telah mendaftar sebelum ini.',\n 'nl': 'Je hebt je al eerder geregistreerd.',\n 'no': 'Du er allerede registrert.',\n 'pl': 'Masz już wcześniejszą rejestrację.',\n 'pt': 'Já se registou anteriormente.',\n 'pt-br': 'Você já tem um cadastro.',\n 'ru': 'Вы уже зарегистрированы.',\n 'sv': 'Du har redan registrerat dig.',\n 'th': 'คุณเคยลงทะเบียนแล้ว',\n 'tr': 'Daha önce kaydolmuştunuz.',\n 'uk': 'Ви вже зареєструвалися раніше.',\n 'zh-cn': '您之前已注册。',\n 'zh-hk': '您之前已註冊。',\n 'zh-tw': '你已註冊這個出版品。',\n },\n 'NEWSLETTER_ALREADY_SIGNED_UP_LANG_MAP': {\n 'en': 'You have signed up before.',\n 'ar': 'سبق أن اشتركت في النشرة الإخبارية.',\n 'de': 'Du hast dich bereits angemeldet.',\n 'en-au': 'You have signed up before.',\n 'en-ca': 'You have signed up before.',\n 'en-gb': 'You have signed up before.',\n 'en-us': 'You have signed up before.',\n 'es': 'Ya te has registrado anteriormente.',\n 'es-419': 'Ya te registraste antes.',\n 'fr': 'Vous vous êtes déjà inscrit.',\n 'fr-ca': 'Vous vous êtes inscrit auparavant.',\n 'hi': 'न्यूज़लेटर के लिए पहले ही साइन अप किया जा चुका है.',\n 'id': 'Anda telah mendaftar sebelumnya.',\n 'it': \"Hai già effettuato l'iscrizione.\",\n 'ja': 'すでに登録されています。',\n 'ko': '이전에 가입한 사용자입니다.',\n 'ms': 'Anda sudah mendaftar sebelum ini.',\n 'nl': 'Je hebt je al eerder aangemeld.',\n 'no': 'Du er allerede registrert.',\n 'pl': 'Już wcześniej się zarejestrowałeś(-aś).',\n 'pt': 'Já se inscreveu anteriormente.',\n 'pt-br': 'Você se inscreveu anteriormente.',\n 'ru': 'Вы уже зарегистрированы.',\n 'sv': 'Du har redan registrerat dig.',\n 'th': 'คุณสมัครรับข้อมูลมาก่อนแล้ว',\n 'tr': 'Daha önce kaydolmuştunuz.',\n 'uk': 'Ви вже зареєструвалися.',\n 'zh-cn': '您之前已注册。',\n 'zh-hk': '您之前已訂閱。',\n 'zh-tw': '你已經訂閱了。',\n },\n 'REGWALL_REGISTER_FAILED_LANG_MAP': {\n 'en': 'Registration failed. Try registering again.',\n 'ar': 'تعذَّرت عملية التسجيل. يُرجى إعادة المحاولة.',\n 'de': 'Registrierung fehlgeschlagen. Versuche es noch einmal.',\n 'en-au': 'Registration failed. Try registering again.',\n 'en-ca': 'Registration failed. Try registering again.',\n 'en-gb': 'Registration failed. Try registering again.',\n 'en-us': 'Registration failed. Try registering again.',\n 'es': 'No se ha podido completar el registro. Prueba a registrarte de nuevo.',\n 'es-419': 'No se pudo completar el registro. Vuelve a intentarlo.',\n 'fr': \"Échec de l'enregistrement. Réessayez.\",\n 'fr-ca': \"Échec de l'inscription. Essayez de vous inscrire à nouveau.\",\n 'hi': 'रजिस्ट्रेशन नहीं हो सका. फिर से रजिस्टर करने की कोशिश करें.',\n 'id': 'Pendaftaran gagal. Coba daftar lagi.',\n 'it': 'Registrazione non riuscita. Prova a registrarti di nuovo.',\n 'ja': '登録できませんでした。もう一度お試しください。',\n 'ko': '등록에 실패했습니다. 다시 등록해 보세요.',\n 'ms': 'Pendaftaran gagal. Cuba mendaftar lagi.',\n 'nl': 'Registratie mislukt. Probeer opnieuw te registreren.',\n 'no': 'Registreringen mislyktes. Prøv å registrere deg på nytt.',\n 'pl': 'Rejestracja się nie udała. Spróbuj jeszcze raz się zarejestrować.',\n 'pt': 'Falha no registo. Tente registar-se novamente.',\n 'pt-br': 'Não foi possível fazer o registro. Tente novamente.',\n 'ru': 'Ошибка регистрации. Повторите попытку.',\n 'sv': 'Registreringen misslyckades. Försök att registrera dig igen.',\n 'th': 'ลงทะเบียนไม่สำเร็จ ลองลงทะเบียนอีกครั้ง',\n 'tr': 'Kayıt işlemi başarısız oldu. Tekrar kaydolmayı deneyin.',\n 'uk': 'Помилка реєстрації. Повторіть спробу.',\n 'zh-cn': '注册失败。请尝试重新注册。',\n 'zh-hk': '註冊失敗。請嘗試重新註冊。',\n 'zh-tw': '註冊失敗,請再試一次。',\n },\n 'NEWSLETTER_SIGN_UP_FAILED_LANG_MAP': {\n 'en': 'Signup failed. Try signing up again.',\n 'ar': 'تعذَّرت عملية الاشتراك. يُرجى إعادة المحاولة.',\n 'de': 'Anmeldung fehlgeschlagen. Versuche es noch einmal.',\n 'en-au': 'Sign-up failed. Try signing up again.',\n 'en-ca': 'Sign-up failed. Try signing up again.',\n 'en-gb': 'Sign-up failed. Try signing up again.',\n 'en-us': 'Sign-up failed. Try signing up again.',\n 'es': 'No se ha podido completar la suscripción. Prueba a suscribirte de nuevo.',\n 'es-419': 'Se produjo un error de registro. Vuelve a intentarlo.',\n 'fr': \"Échec de l'inscription. Réessayez.\",\n 'fr-ca': \"Échec de l'inscription. Essayez de vous inscrire à nouveau.\",\n 'hi': 'साइन अप नहीं किया जा सका. फिर से साइन अप करने की कोशिश करें.',\n 'id': 'Pendaftaran gagal. Coba daftar lagi.',\n 'it': 'Iscrizione non riuscita. Prova a iscriverti di nuovo.',\n 'ja': '登録できませんでした。もう一度お試しください。',\n 'ko': '가입에 실패했습니다. 다시 가입해 보세요.',\n 'ms': 'Daftar gagal. Cuba daftar lagi.',\n 'nl': 'Aanmelding mislukt. Probeer opnieuw aan te melden.',\n 'no': 'Registreringen mislyktes. Prøv å registrere deg på nytt.',\n 'pl': 'Rejestracja się nie udała. Spróbuj jeszcze raz się zarejestrować.',\n 'pt': 'Falha na inscrição. Tente inscrever-se novamente.',\n 'pt-br': 'Não foi possível se inscrever. Tente novamente.',\n 'ru': 'Не удалось зарегистрироваться. Повторите попытку.',\n 'sv': 'Registreringen misslyckades. Försök att registrera dig igen.',\n 'th': 'ลงชื่อสมัครใช้ไม่สำเร็จ ลองลงชื่อสมัครใช้อีกครั้ง',\n 'tr': 'Kaydolma işlemi başarısız oldu. Tekrar kaydolmayı deneyin.',\n 'uk': 'Помилка реєстрації. Повторіть спробу.',\n 'zh-cn': '注册失败。请尝试重新注册。',\n 'zh-hk': '申請失敗。請嘗試重新申請。',\n 'zh-tw': '訂閱失敗,請再試一次。',\n },\n 'REGWALL_ACCOUNT_CREATED_LANG_MAP': {\n 'en': 'Created an account with user@gmail.com%s',\n 'ar': 'تم إنشاء حساب باستخدام user@gmail.com%s.',\n 'de': 'Konto bei user@gmail.com%s wurde erstellt',\n 'en-au':\n 'Created an account with user@gmail.com%s',\n 'en-ca':\n 'Created an account with user@gmail.com%s',\n 'en-gb':\n 'Created an account with user@gmail.com%s',\n 'en-us':\n 'Created an account with user@gmail.com%s',\n 'es': 'Has creado una cuenta con user@gmail.com%s',\n 'es-419':\n 'Se creó una cuenta con user@gmail.com%s',\n 'fr': 'A créé un compte avec user@gmail.com%s',\n 'fr-ca':\n 'Un compte a été créé avec l\\'adresse user@gmail.com%s',\n 'hi': 'user@gmail.com%s का इस्तेमाल करके, एक खाता बनाया गया',\n 'id': 'Membuat akun dengan user@gmail.com%s',\n 'it': 'È stato creato un account con l\\'indirizzo user@gmail.com%s',\n 'ja': 'user@gmail.com%s でアカウントを作成しました',\n 'ko': 'user@gmail.com%s(으)로 계정을 만들었습니다.',\n 'ms': 'Membuat akaun dengan user@gmail.com%s',\n 'nl': 'Account gemaakt met user@gmail.com%s',\n 'no': 'Du har opprettet en konto med user@gmail.com%s',\n 'pl': 'Utworzono konto za pomocą adresu user@gmail.com%s',\n 'pt': 'Criou uma conta com user@gmail.com%s',\n 'pt-br':\n 'Conta criada com o e-mail user@gmail.com%s',\n 'ru': 'Вы зарегистрировали аккаунт на адрес user@gmail.com%s.',\n 'sv': 'Du skapade ett konto med user@gmail.com%s',\n 'th': 'สร้างบัญชีด้วย user@gmail.com%s',\n 'tr': 'user@gmail.com%s ile bir hesap oluşturun',\n 'uk': 'Обліковий запис створено за допомогою електронної адреси user@gmail.com%s',\n 'zh-cn': '已使用 user@gmail.com%s 创建帐号',\n 'zh-hk': '已使用 user@gmail.com%s 建立帳戶',\n 'zh-tw': '已使用 user@gmail.com%s 建立帳戶',\n },\n 'NEWSLETTER_SIGNED_UP_LANG_MAP': {\n 'en': 'Signed up with user@gmail.com%s for the newsletter',\n 'ar': 'تم الاشتراك في النشرة الإخبارية باستخدام user@gmail.com%s.',\n 'de': 'Du hast dich für den Newsletter von user@gmail.com%s angemeldet',\n 'en-au':\n 'Signed up with user@gmail.com%s for the newsletter',\n 'en-ca':\n 'Signed up with user@gmail.com%s for the newsletter',\n 'en-gb':\n 'Signed up with user@gmail.com%s for the newsletter',\n 'en-us':\n 'Signed up with user@gmail.com%s for the newsletter',\n 'es': 'Te has suscrito a la newsletter con user@gmail.com%s',\n 'es-419':\n 'Te registraste con user@gmail.com%s para recibir el boletín informativo',\n 'fr': 'S\\'est abonné à la newsletter avec user@gmail.com%s',\n 'fr-ca':\n 'Vous êtes inscrit au bulletin d\\'information avec l\\'adresse user@gmail.com%s',\n 'hi': 'न्यूज़लेटर पाने के लिए, user@gmail.com%s से साइन अप किया गया',\n 'id': 'Mendaftar dengan user@gmail.com%s untuk mendapatkan newsletter',\n 'it': 'Iscrizione alla newsletter con l\\'indirizzo user@gmail.com%s effettuata',\n 'ja': 'user@gmail.com%s でニュースレターを登録しました',\n 'ko': 'user@gmail.com%s(으)로 뉴스레터에 가입했습니다.',\n 'ms': 'Mendaftar dengan user@gmail.com%s untuk surat berita',\n 'nl': 'Aangemeld met user@gmail.com%s voor de nieuwsbrief',\n 'no': 'Du har registrert deg for nyhetsbrevet med user@gmail.com%s',\n 'pl': 'Zapisano się na newsletter za pomocą adresu user@gmail.com%s',\n 'pt': 'Inscreveu-se com user@gmail.com%s no boletim informativo',\n 'pt-br':\n 'Inscrição na newsletter feita com o e-mail user@gmail.com%s',\n 'ru': 'Вы подписались на новостную рассылку, используя аккаунт user@gmail.com%s.',\n 'sv': 'Du registrerade dig för nyhetsbrevet med user@gmail.com%s',\n 'th': 'ลงชื่อเข้าใช้ด้วย user@gmail.com%s สำหรับจดหมายข่าว',\n 'tr': 'Bülten için user@gmail.com%s ile kaydoldunuz',\n 'uk': 'Ви підписалися на інформаційні листи на електронну адресу user@gmail.com%s',\n 'zh-cn': '已使用 user@gmail.com%s 订阅简报',\n 'zh-hk': '已使用 user@gmail.com%s 註冊通訊',\n 'zh-tw':\n '已使用 user@gmail.com%s 訂閱電子報',\n },\n 'NO_MEMBERSHIP_FOUND_LANG_MAP': {\n 'en': 'No membership found',\n 'ar': 'لم يتم العثور على أي اشتراك.',\n 'de': 'Keine Mitgliedschaftsdaten gefunden',\n 'en-au': 'No membership found',\n 'en-ca': 'No membership found',\n 'en-gb': 'No membership found',\n 'en-us': 'No membership found',\n 'es': 'No se han encontrado suscripciones',\n 'es-419': 'No se encontró ninguna membresía',\n 'fr': 'Aucun abonnement trouvé',\n 'fr-ca': 'Aucun abonnement trouvé',\n 'hi': 'पैसे चुकाकर ली जाने वाली कोई सदस्यता नहीं मिली',\n 'id': 'Langganan tidak ditemukan',\n 'it': 'Nessun abbonamento trovato',\n 'ja': 'メンバーシップが見つかりません',\n 'ko': '멤버십 정보를 찾을 수 없습니다.',\n 'ms': 'Tiada keahlian ditemukan',\n 'nl': 'Geen lidmaatschap gevonden',\n 'no': 'Fant ingen abonnementer',\n 'pl': 'Nie znaleziono subskrypcji',\n 'pt': 'Nenhuma subscrição encontrada',\n 'pt-br': 'Nenhuma assinatura foi encontrada',\n 'ru': 'Подписка не найдена.',\n 'sv': 'Inget medlemskap hittades',\n 'th': 'ไม่พบการเป็นสมาชิก',\n 'tr': 'Üyelik bulunamadı',\n 'uk': 'Немає підписок',\n 'zh-cn': '未找到会员资料',\n 'zh-hk': '找不到會籍',\n 'zh-tw': '找不到會員資料',\n },\n 'CLOSE_BUTTON_DESCRIPTION': {\n 'en': 'Close dialog',\n 'ar': 'إغلاق مربّع الحوار',\n 'de': 'Dialogfeld schließen',\n 'en-au': 'Close dialog',\n 'en-ca': 'Close dialog',\n 'en-gb': 'Close dialog',\n 'en-us': 'Close dialog',\n 'es': 'Cerrar cuadro de diálogo',\n 'es-419': 'Cerrar diálogo',\n 'fr': 'Fermer la boîte de dialogue',\n 'fr-ca': 'Fermer la boîte de dialogue',\n 'hi': 'डायलॉग बॉक्स बंद करें',\n 'id': 'Tutup dialog',\n 'it': 'Chiudi la finestra di dialogo',\n 'ja': 'ダイアログを閉じる',\n 'ko': '대화상자 닫기',\n 'ms': 'Tutup dialog',\n 'nl': 'Dialoogvenster sluiten',\n 'no': 'Lukk dialogboksen',\n 'pl': 'Zamknij okno',\n 'pt': 'Fechar caixa de diálogo',\n 'pt-br': 'Fechar caixa de diálogo',\n 'ru': 'Закрыть диалоговое окно',\n 'sv': 'Stäng dialogrutan',\n 'th': 'ปิดกล่องโต้ตอบ',\n 'tr': 'İletişim kutusunu kapat',\n 'uk': 'Закрити вікно',\n 'zh-cn': '关闭对话框',\n 'zh-hk': '閂對話框',\n 'zh-tw': '關閉對話方塊',\n },\n 'CONTRIBUTE': {\n 'en': 'Contribute',\n 'ar': 'مساهمة',\n 'de': 'Beitragen',\n 'en-au': 'Contribute',\n 'en-ca': 'Contribute',\n 'en-gb': 'Contribute',\n 'en-us': 'Contribute',\n 'es': 'Contribuir',\n 'es-419': 'Contribuir',\n 'fr': 'Contribuer',\n 'fr-ca': 'Faire une contribution',\n 'hi': 'योगदान दें',\n 'id': 'Beri kontribusi',\n 'it': 'Contribuisci',\n 'ja': '寄付',\n 'ko': '후원',\n 'ms': 'Sumbang',\n 'nl': 'Bijdragen',\n 'no': 'Bidra',\n 'pl': 'Przekaż darowiznę',\n 'pt': 'Contribuir',\n 'pt-br': 'Contribuir',\n 'ru': 'Сделать взнос',\n 'sv': 'Bidra',\n 'th': 'สนับสนุน',\n 'tr': 'Katkıda bulun',\n 'uk': 'Зробити внесок',\n 'zh-cn': '捐赠',\n 'zh-hk': '資助',\n 'zh-tw': '捐款',\n },\n 'SUBSCRIBE': {\n 'en': 'Subscribe',\n 'ar': 'اشتراك',\n 'de': 'Abonnieren',\n 'en-au': 'Subscribe',\n 'en-ca': 'Subscribe',\n 'en-gb': 'Subscribe',\n 'en-us': 'Subscribe',\n 'es': 'Suscribirme',\n 'es-419': 'Suscribirse',\n 'fr': \"S'abonner\",\n 'fr-ca': \"S'abonner\",\n 'hi': 'सदस्यता लें',\n 'id': 'Langganan',\n 'it': 'Iscriviti',\n 'ja': '購読',\n 'ko': '구독',\n 'ms': 'Langgan',\n 'nl': 'Abonneren',\n 'no': 'Abonner',\n 'pl': 'Subskrybuj',\n 'pt': 'Subscrever',\n 'pt-br': 'Fazer inscrição',\n 'ru': 'Подписаться',\n 'sv': 'Prenumerera',\n 'th': 'สมัครสมาชิก',\n 'tr': 'Abone ol',\n 'uk': 'Підписатися',\n 'zh-cn': '订阅',\n 'zh-hk': '訂閱',\n 'zh-tw': '訂閱',\n },\n 'ALREADY_A_CONTRIBUTOR': {\n 'en': 'Already a contributor?',\n 'ar': 'هل أنت مساهم حالي؟',\n 'de': 'Du bist schon Beitragende/r?',\n 'en-au': 'Already a contributor?',\n 'en-ca': 'Already a contributor?',\n 'en-gb': 'Already a contributor?',\n 'en-us': 'Already a contributor?',\n 'es': '¿Ya has contribuido?',\n 'es-419': '¿Ya contribuyes?',\n 'fr': 'Déjà contributeur ?',\n 'fr-ca': 'Vous êtes déjà contributeur?',\n 'hi': 'क्या आपने पहले योगदान दिया है?',\n 'id': 'Sudah menjadi kontributor?',\n 'it': 'Hai già dato contributi?',\n 'ja': 'すでに寄付していますか?',\n 'ko': '이미 후원하고 계신가요?',\n 'ms': 'Sudah menjadi penyumbang?',\n 'nl': 'Ben je al een bijdrager?',\n 'no': 'Har du allerede bidratt?',\n 'pl': 'Już przekazujesz darowizny?',\n 'pt': 'Já contribui?',\n 'pt-br': 'Já faz contribuições?',\n 'ru': 'Уже делаете взносы?',\n 'sv': 'Är du redan en bidragsgivare?',\n 'th': 'หากเป็นผู้สนับสนุนอยู่แล้ว',\n 'tr': 'Halihazırda katkıda bulunuyor musunuz?',\n 'uk': 'Уже робите внески?',\n 'zh-cn': '已是捐赠者?',\n 'zh-hk': '已是資助者?',\n 'zh-tw': '已經是捐款者了嗎?',\n },\n 'ALREADY_A_SUBSCRIBER': {\n 'en': 'Already a subscriber?',\n 'ar': 'هل أنت مشترك حالي؟',\n 'de': 'Du bist bereits Abonnent/in?',\n 'en-au': 'Already a subscriber?',\n 'en-ca': 'Already a subscriber?',\n 'en-gb': 'Already a subscriber?',\n 'en-us': 'Already a subscriber?',\n 'es': '¿Ya te has suscrito?',\n 'es-419': '¿Ya te suscribiste?',\n 'fr': 'Déjà abonné ?',\n 'fr-ca': 'Vous êtes déjà abonné?',\n 'hi': 'क्या आपने पहले ही सदस्यता ले ली है?',\n 'id': 'Sudah berlangganan?',\n 'it': \"Hai già l'abbonamento?\",\n 'ja': '購読済みですか?',\n 'ko': '이미 구독 중이신가요?',\n 'ms': 'Sudah menjadi pelanggan?',\n 'nl': 'Ben je al abonnee?',\n 'no': 'Er du allerede abonnent?',\n 'pl': 'Już subskrybujesz?',\n 'pt': 'Já subscreve?',\n 'pt-br': 'Já é assinante?',\n 'ru': 'Уже подписаны?',\n 'sv': 'Prenumererar du redan?',\n 'th': 'หากเป็นสมาชิกอยู่แล้ว',\n 'tr': 'Halihazırda abone misiniz?',\n 'uk': 'Уже підписалися?',\n 'zh-cn': '已订阅?',\n 'zh-hk': '已經是訂閱者?',\n 'zh-tw': '已經是訂閱者了嗎?',\n },\n 'THANKS_FOR_VIEWING_THIS_AD': {\n 'en': 'Thanks for viewing this ad',\n 'ar': 'نشكرك على مشاهدة هذا الإعلان.',\n 'de': 'Vielen Dank, dass du dir diese Anzeige angesehen hast',\n 'en-au': 'Thanks for viewing this ad',\n 'en-ca': 'Thanks for viewing this ad',\n 'en-gb': 'Thanks for viewing this ad',\n 'en-us': 'Thanks for viewing this ad',\n 'es': 'Gracias por ver este anuncio',\n 'es-419': 'Gracias por ver este anuncio',\n 'fr': \"Merci d'avoir regardé cette annonce\",\n 'fr-ca': \"Merci d'avoir regardé cette annonce\",\n 'hi': 'यह विज्ञापन देखने के लिए धन्यवाद',\n 'id': 'Terima kasih telah melihat iklan ini',\n 'it': 'Grazie per aver visualizzato questo annuncio',\n 'ja': 'この広告をご覧いただきありがとうございます',\n 'ko': '이 광고를 시청해 주셔서 감사합니다.',\n 'ms': 'Terima kasih kerana menonton iklan ini',\n 'nl': 'Bedankt dat je deze advertentie hebt bekeken',\n 'no': 'Takk for at du så på denne annonsen',\n 'pl': 'Dziękujemy za obejrzenie tej reklamy',\n 'pt': 'Obrigado por ver este anúncio',\n 'pt-br': 'Agradecemos por assistir este anúncio',\n 'ru': 'Спасибо, что посмотрели эту рекламу!',\n 'sv': 'Tack för att du tittade på den här annonsen',\n 'th': 'ขอบคุณที่ดูโฆษณานี้',\n 'tr': 'Bu reklamı görüntülediğiniz için teşekkür ederiz',\n 'uk': 'Дякуємо, що переглянули це оголошення',\n 'zh-cn': '感谢观看此广告',\n 'zh-hk': '感謝觀看此廣告',\n 'zh-tw': '感謝觀看這則廣告',\n },\n 'VIEW_AN_AD': {\n 'en': 'View an ad',\n 'ar': 'عرض إعلان',\n 'de': 'Anzeige ansehen',\n 'en-au': 'View an ad',\n 'en-ca': 'View an ad',\n 'en-gb': 'View an ad',\n 'en-us': 'View an ad',\n 'es': 'Ver un anuncio',\n 'es-419': 'Ver un anuncio',\n 'fr': 'Visionner une annonce',\n 'fr-ca': 'Afficher une annonce',\n 'hi': 'कोई विज्ञापन देखें',\n 'id': 'Lihat iklan',\n 'it': 'Visualizza un annuncio',\n 'ja': '広告を表示',\n 'ko': '광고 보기',\n 'ms': 'Lihat iklan',\n 'nl': 'Een advertentie bekijken',\n 'no': 'Se en annonsv',\n 'pl': 'Obejrzyj reklamę',\n 'pt': 'Mostrar um anúncio',\n 'pt-br': 'Ver anúncio',\n 'ru': 'Посмотреть рекламу',\n 'sv': 'Se en annons',\n 'th': 'ดูโฆษณา',\n 'tr': 'Reklam izle',\n 'uk': 'Переглянути оголошення',\n 'zh-cn': '观看广告',\n 'zh-hk': '觀看廣告',\n 'zh-tw': '觀看廣告',\n },\n // Message ID: 7478828886861577969\n 'BACK_TO_HOMEPAGE': {\n 'en': 'Back to homepage',\n 'ar': 'الرجوع إلى الصفحة الرئيسية',\n 'de': 'Zurück zur Startseite',\n 'en-au': 'Back to homepage',\n 'en-ca': 'Back to homepage',\n 'en-gb': 'Back to homepage',\n 'en-us': 'Back to homepage',\n 'es': 'Volver a la página principal',\n 'es-419': 'Volver a la página principal',\n 'fr': \"Retourner à la page d'accueil\",\n 'fr-ca': \"Retour à la page d'accueil\",\n 'hi': 'होम पेज पर वापस जाएं',\n 'id': 'Kembali ke halaman beranda',\n 'it': 'Torna alla home page',\n 'ja': 'ホームページに戻る',\n 'ko': '홈페이지로 돌아가기',\n 'ms': 'Kembali kepada halaman utama',\n 'nl': 'Terug naar homepage',\n 'no': 'Tilbake til startsiden',\n 'pl': 'Powrót do strony głównej',\n 'pt': 'Volte à página inicial',\n 'pt-br': 'Voltar à página inicial',\n 'ru': 'Вернуться на главную страницу',\n 'sv': 'Tillbaka till startsidan',\n 'th': 'กลับไปที่หน้าแรก',\n 'tr': 'Ana sayfaya geri dön',\n 'uk': 'Назад на головну сторінку',\n 'zh-cn': '返回首页',\n 'zh-hk': '返回首頁',\n 'zh-tw': '返回首頁',\n },\n};\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {ActivityPorts} from '../components/activities';\nimport {Deps} from './deps';\nimport {SmartBoxMessage} from '../proto/api_messages';\nimport {SmartButtonOptions} from '../api/subscriptions';\nimport {createElement} from '../utils/dom';\nimport {feArgs, feUrl} from './services';\nimport {setImportantStyles} from '../utils/style';\n\nconst iframeAttributes = {\n 'frameborder': '0',\n 'scrolling': 'no',\n};\n\nexport enum Theme {\n LIGHT = 'light',\n DARK = 'dark',\n}\n\n/**\n * The class for Smart button Api.\n */\nexport class SmartSubscriptionButtonApi {\n private readonly activityPorts_: ActivityPorts;\n private readonly iframe_: HTMLIFrameElement;\n private readonly src_ = feUrl('/smartboxiframe');\n private readonly args_: {[key: string]: string};\n\n constructor(\n private readonly deps_: Deps,\n private readonly button_: Element,\n private readonly options_: SmartButtonOptions,\n private readonly callback_?: (event?: Event) => void\n ) {\n this.activityPorts_ = deps_.activities();\n\n this.iframe_ = createElement(\n deps_.win().document,\n 'iframe',\n iframeAttributes\n );\n\n const frontendArguments: {[key: string]: string} = {\n 'publicationId': this.deps_.pageConfig().getPublicationId(),\n 'theme': (this.options_ && this.options_.theme) || Theme.LIGHT,\n 'lang': (this.options_ && this.options_.lang) || 'en',\n };\n const messageTextColor = this.options_ && this.options_.messageTextColor;\n if (messageTextColor) {\n frontendArguments['messageTextColor'] = messageTextColor;\n }\n\n this.args_ = feArgs(frontendArguments);\n }\n\n handleSmartBoxClick_(smartBoxMessage: SmartBoxMessage) {\n if (smartBoxMessage && smartBoxMessage.getIsClicked()) {\n this.callback_?.();\n }\n }\n\n /**\n * Make a call to build button content and listens for the 'click' message.\n */\n start(): Element {\n setImportantStyles(this.iframe_, {\n 'opacity': '1',\n 'position': 'absolute',\n 'top': '0',\n 'bottom': '0',\n 'left': '0',\n 'height': '100%',\n 'right': '0',\n 'width': '100%',\n });\n this.button_.appendChild(this.iframe_);\n const args = this.activityPorts_.addDefaultArguments(this.args_);\n this.activityPorts_\n .openIframe(this.iframe_, this.src_, args)\n .then((port) => {\n port.on(SmartBoxMessage, this.handleSmartBoxClick_.bind(this));\n });\n return this.iframe_;\n }\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/** English is the default language. */\nconst DEFAULT_LANGUAGE_CODE = 'en';\n\n/**\n * Gets a message for a given language code, from a map of messages.\n */\nexport function msg(\n map: {[key: string]: string | undefined},\n languageCodeOrElement: string | HTMLElement\n): string | undefined {\n const defaultMsg = map[DEFAULT_LANGUAGE_CODE];\n\n // Verify params.\n if (typeof map !== 'object' || !languageCodeOrElement) {\n return defaultMsg;\n }\n\n // Get language code.\n let languageCode =\n typeof languageCodeOrElement === 'string'\n ? languageCodeOrElement\n : getLanguageCodeFromElement(languageCodeOrElement);\n\n // Normalize language code.\n languageCode = languageCode.toLowerCase();\n languageCode = languageCode.replace(/_/g, '-');\n\n // Search for a message matching the language code.\n // If a message can't be found, try again with a less specific language code.\n const languageCodeSegments = languageCode.split('-');\n while (languageCodeSegments.length) {\n const key = languageCodeSegments.join('-');\n if (key in map) {\n return map[key];\n }\n\n // Simplify language code.\n // Ex: \"en-US-SF\" => \"en-US\"\n languageCodeSegments.pop();\n }\n\n // There was an attempt.\n return defaultMsg;\n}\n\n/**\n * Gets a language code (ex: \"en-US\") from a given Element.\n */\nexport function getLanguageCodeFromElement(element: HTMLElement): string {\n if (element.lang) {\n // Get language from element itself.\n return element.lang;\n }\n\n if (element?.ownerDocument?.documentElement?.lang) {\n // Get language from element's document.\n return element.ownerDocument.documentElement.lang;\n }\n\n // There was an attempt.\n return DEFAULT_LANGUAGE_CODE;\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {ASSETS} from '../constants';\nimport {AnalyticsEvent} from '../proto/api_messages';\nimport {ButtonOptions, SmartButtonOptions} from '../api/subscriptions';\nimport {ConfiguredRuntime} from './runtime';\nimport {Deps} from './deps';\nimport {Doc} from '../model/doc';\nimport {SWG_I18N_STRINGS} from '../i18n/swg-strings';\nimport {SmartSubscriptionButtonApi, Theme} from './smart-button-api';\nimport {createElement} from '../utils/dom';\nimport {msg} from '../utils/i18n';\n\n/**\n * Properties:\n * - lang: Sets the button SVG and title. Default is \"en\".\n * - theme: \"light\" or \"dark\". Default is \"light\".\n */\nexport interface ButtonParams {\n options: SmartButtonOptions | ButtonOptions;\n clickFun: (event?: Event) => void;\n}\n\nexport enum ButtonAttributeValues {\n SUBSCRIPTION = 'subscription',\n CONTRIBUTION = 'contribution',\n}\n\nconst BUTTON_INNER_HTML = `
$textContent$`;\n\n/**\n * The button stylesheet can be found in the `/assets/swg-button.css`.\n * It's produced by the `gulp assets` task and deployed to\n * `https://news.google.com/swg/js/v1/swg-button.css`.\n */\nexport class ButtonApi {\n constructor(\n private readonly doc_: Doc,\n private readonly configuredRuntimePromise_: Promise\n ) {}\n\n /**\n */\n init() {\n const head = this.doc_.getHead();\n if (!head) {\n return;\n }\n\n const url = `${ASSETS}/swg-button.css`;\n const existing = head.querySelector(`link[href=\"${url}\"]`);\n if (existing) {\n return;\n }\n\n // \n head.appendChild(\n createElement(this.doc_.getWin().document, 'link', {\n 'rel': 'stylesheet',\n 'type': 'text/css',\n 'href': url,\n })\n );\n }\n\n create(\n optionsOrCallback: ButtonOptions | (() => void),\n callback?: () => void\n ): Element {\n const button = createElement(this.doc_.getWin().document, 'button', {});\n return this.attach(button, optionsOrCallback, callback);\n }\n\n /**\n * Attaches the Classic 'Subscribe With Google' button.\n */\n attach(\n button: HTMLElement,\n optionsOrCallback: ButtonOptions | (() => void),\n callback?: () => void\n ): Element {\n const options = this.setupButtonAndGetParams_(\n button,\n AnalyticsEvent.ACTION_SWG_BUTTON_CLICK,\n optionsOrCallback,\n callback\n ).options;\n\n const theme = options['theme'];\n button.classList.add(`swg-button-${theme}`);\n button.setAttribute('role', 'button');\n if (options['lang']) {\n button.setAttribute('lang', options['lang']);\n }\n button.setAttribute(\n 'title',\n msg(SWG_I18N_STRINGS.SUBSCRIPTION_TITLE_LANG_MAP, button)!\n );\n this.logSwgEvent_(AnalyticsEvent.IMPRESSION_SWG_BUTTON);\n\n return button;\n }\n\n /**\n * Attaches the new subscribe button, for subscription product types.\n */\n attachSubscribeButton(\n button: HTMLElement,\n optionsOrCallback: ButtonOptions | (() => void),\n callback?: () => void\n ): Element {\n const options: ButtonOptions = this.setupButtonAndGetParams_(\n button,\n AnalyticsEvent.ACTION_SWG_BUTTON_SHOW_OFFERS_CLICK,\n optionsOrCallback,\n callback\n ).options;\n\n const theme = options['theme'];\n button.classList.add(`swg-button-v2-${theme}`);\n button.setAttribute('role', 'button');\n if (options['lang']) {\n button.setAttribute('lang', options['lang']);\n }\n if (!options['enable']) {\n button.setAttribute('disabled', 'disabled');\n }\n button./*OK*/ innerHTML = BUTTON_INNER_HTML.replace(\n '$theme$',\n theme!\n ).replace(\n '$textContent$',\n msg(SWG_I18N_STRINGS.SUBSCRIPTION_TITLE_LANG_MAP, button)!\n );\n this.logSwgEvent_(AnalyticsEvent.IMPRESSION_SHOW_OFFERS_SWG_BUTTON);\n\n return button;\n }\n\n /**\n * Attaches the new contribute button, for contribution product types.\n */\n attachContributeButton(\n button: HTMLElement,\n optionsOrCallback: ButtonOptions | (() => void),\n callback?: () => void\n ): Element {\n const options: ButtonOptions = this.setupButtonAndGetParams_(\n button,\n AnalyticsEvent.ACTION_SWG_BUTTON_SHOW_CONTRIBUTIONS_CLICK,\n optionsOrCallback,\n callback\n ).options;\n\n const theme = options['theme'];\n button.classList.add(`swg-button-v2-${theme}`);\n button.setAttribute('role', 'button');\n if (options['lang']) {\n button.setAttribute('lang', options['lang']);\n }\n if (!options['enable']) {\n button.setAttribute('disabled', 'disabled');\n }\n button./*OK*/ innerHTML = BUTTON_INNER_HTML.replace(\n '$theme$',\n theme!\n ).replace(\n '$textContent$',\n msg(SWG_I18N_STRINGS.CONTRIBUTION_TITLE_LANG_MAP, button)!\n );\n this.logSwgEvent_(AnalyticsEvent.IMPRESSION_SHOW_CONTRIBUTIONS_SWG_BUTTON);\n\n return button;\n }\n\n /**\n * Attaches all buttons with the specified attribute set to any of the\n * attribute values.\n */\n attachButtonsWithAttribute(\n attribute: string,\n attributeValues: string[],\n options: ButtonOptions,\n attributeValueToCallback: {[key: string]: () => void}\n ) {\n for (const attributeValue of attributeValues) {\n const elements: HTMLElement[] = Array.from(\n this.doc_\n .getRootNode()\n .querySelectorAll(`[${attribute}=\"${attributeValue}\"]`)\n );\n for (const element of elements) {\n if (attributeValue === ButtonAttributeValues.SUBSCRIPTION) {\n this.attachSubscribeButton(\n element,\n options,\n attributeValueToCallback[attributeValue]\n );\n } else if (attributeValue === ButtonAttributeValues.CONTRIBUTION) {\n this.attachContributeButton(\n element,\n options,\n attributeValueToCallback[attributeValue]\n );\n }\n }\n }\n }\n\n async logSwgEvent_(eventType: AnalyticsEvent, isFromUserAction?: boolean) {\n const configuredRuntime = await this.configuredRuntimePromise_;\n configuredRuntime.eventManager().logSwgEvent(eventType, isFromUserAction);\n }\n\n private getOptions_(\n optionsOrCallback: ButtonOptions | SmartButtonOptions | (() => void)\n ): ButtonOptions | SmartButtonOptions {\n const options =\n optionsOrCallback && typeof optionsOrCallback != 'function'\n ? optionsOrCallback\n : {'theme': Theme.LIGHT};\n\n const theme = options['theme'];\n if (theme !== Theme.LIGHT && theme !== Theme.DARK) {\n options['theme'] = Theme.LIGHT;\n }\n return options;\n }\n\n private getCallback_(\n optionsOrCallback: SmartButtonOptions | ButtonOptions | (() => void),\n callback?: () => void\n ): (() => void) | ((event?: Event) => boolean) {\n return (\n (typeof optionsOrCallback === 'function' ? optionsOrCallback : null) ||\n callback!\n );\n }\n\n setupButtonAndGetParams_(\n button: Element,\n clickEvent: AnalyticsEvent,\n optionsOrCallback: SmartButtonOptions | ButtonOptions | (() => void),\n callbackFun?: () => void\n ): ButtonParams {\n const options = this.getOptions_(optionsOrCallback);\n const callback = this.getCallback_(optionsOrCallback, callbackFun);\n const clickFun = (event?: Event) => {\n this.logSwgEvent_(clickEvent, true);\n if (typeof callback === 'function') {\n callback(event);\n }\n };\n button.addEventListener('click', clickFun);\n return {options, clickFun};\n }\n\n attachSmartButton(\n deps: Deps,\n button: Element,\n optionsOrCallback: SmartButtonOptions | (() => void),\n callback?: () => void\n ): Element {\n const params = this.setupButtonAndGetParams_(\n button,\n AnalyticsEvent.ACTION_SWG_BUTTON_CLICK,\n optionsOrCallback,\n callback\n );\n // Add required CSS class, if missing.\n button.classList.add('swg-smart-button');\n return new SmartSubscriptionButtonApi(\n deps,\n button,\n params.options,\n params.clickFun\n ).start();\n }\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {ActivityIframeView} from '../ui/activity-iframe-view';\nimport {Entitlements} from '../api/entitlements';\nimport {LoginRequest} from '../api/subscriptions';\nimport {SubscribeResponse} from '../api/subscribe-response';\nimport {isCancelError} from '../utils/errors';\nimport {warn} from '../utils/log';\n\nenum CallbackId {\n ENTITLEMENTS = 1,\n SUBSCRIBE_REQUEST = 2,\n PAYMENT_RESPONSE = 3,\n LOGIN_REQUEST = 4,\n LINK_PROGRESS = 5,\n LINK_COMPLETE = 6,\n FLOW_STARTED = 7,\n FLOW_CANCELED = 8,\n PAY_CONFIRM_OPENED = 9,\n OFFERS_FLOW_REQUEST = 10,\n}\n\ntype Callback = (data: unknown) => void;\n\nexport class Callbacks {\n private readonly callbacks_: {\n [key in CallbackId]?: Callback;\n } = {};\n resultBuffer_: {\n [key in CallbackId]?: unknown;\n } = {};\n paymentResponsePromise_: Promise | null = null;\n\n setOnEntitlementsResponse(\n callback: (entitlementsPromise: Promise) => void\n ): void {\n this.setCallback_(CallbackId.ENTITLEMENTS, callback as Callback);\n }\n\n /**\n * @return Whether the callback has been found.\n */\n triggerEntitlementsResponse(promise: Promise): boolean {\n return this.trigger_(\n CallbackId.ENTITLEMENTS,\n promise.then((res) => res.clone())\n );\n }\n\n hasEntitlementsResponsePending(): boolean {\n return !!this.resultBuffer_[CallbackId.ENTITLEMENTS];\n }\n\n setOnLoginRequest(callback: (request: LoginRequest) => void): void {\n this.setCallback_(CallbackId.LOGIN_REQUEST, callback as Callback);\n }\n\n /**\n * @return Whether the callback has been found.\n */\n triggerLoginRequest(request: LoginRequest): boolean {\n return this.trigger_(CallbackId.LOGIN_REQUEST, request);\n }\n\n setOnLinkProgress(callback: () => void): void {\n this.setCallback_(CallbackId.LINK_PROGRESS, callback);\n }\n\n /**\n * @return Whether the callback has been found.\n */\n triggerLinkProgress(): boolean {\n return this.trigger_(CallbackId.LINK_PROGRESS, true);\n }\n\n resetLinkProgress(): void {\n this.resetCallback_(CallbackId.LINK_PROGRESS);\n }\n\n setOnLinkComplete(callback: () => void): void {\n this.setCallback_(CallbackId.LINK_COMPLETE, callback);\n }\n\n /**\n * @return Whether the callback has been found.\n */\n triggerLinkComplete(): boolean {\n return this.trigger_(CallbackId.LINK_COMPLETE, true);\n }\n\n hasLinkCompletePending(): boolean {\n return !!this.resultBuffer_[CallbackId.LINK_COMPLETE];\n }\n\n setOnPayConfirmOpened(\n callback: (activityIframeView: ActivityIframeView) => void\n ): void {\n this.setCallback_(CallbackId.PAY_CONFIRM_OPENED, callback as Callback);\n }\n\n /**\n * @return Whether the callback has been found.\n */\n triggerPayConfirmOpened(activityIframeView: ActivityIframeView): boolean {\n return this.trigger_(CallbackId.PAY_CONFIRM_OPENED, activityIframeView);\n }\n\n setOnSubscribeRequest(callback: () => void): void {\n this.setCallback_(CallbackId.SUBSCRIBE_REQUEST, callback);\n }\n\n /**\n * @return Whether the callback has been found.\n */\n triggerSubscribeRequest(): boolean {\n return this.trigger_(CallbackId.SUBSCRIBE_REQUEST, true);\n }\n\n hasSubscribeRequestCallback(): boolean {\n return !!this.callbacks_[CallbackId.SUBSCRIBE_REQUEST];\n }\n\n setOnOffersFlowRequest(callback: () => void): void {\n this.setCallback_(CallbackId.OFFERS_FLOW_REQUEST, callback);\n }\n\n /**\n * @return Whether the callback has been found.\n */\n triggerOffersFlowRequest(): boolean {\n return this.trigger_(CallbackId.OFFERS_FLOW_REQUEST, true);\n }\n\n hasOffersFlowRequestCallback(): boolean {\n return !!this.callbacks_[CallbackId.OFFERS_FLOW_REQUEST];\n }\n\n setOnSubscribeResponse(\n callback: (responsePromise: Promise) => void\n ): void {\n warn(\n `[swg.js:setOnSubscribeResponse]: This method has been deprecated, please switch usages to 'setOnPaymentResponse'`\n );\n this.setCallback_(CallbackId.PAYMENT_RESPONSE, callback as Callback);\n }\n\n setOnContributionResponse(\n callback: (responsePromise: Promise) => void\n ): void {\n warn(\n `[swg.js:setOnContributionResponse]: This method has been deprecated, please switch usages to 'setOnPaymentResponse'`\n );\n this.setCallback_(CallbackId.PAYMENT_RESPONSE, callback as Callback);\n }\n\n setOnPaymentResponse(\n callback: (responsePromise: Promise) => void\n ): void {\n this.setCallback_(CallbackId.PAYMENT_RESPONSE, callback as Callback);\n }\n\n /**\n * @return Whether the callback has been found.\n */\n triggerPaymentResponse(responsePromise: Promise): boolean {\n this.paymentResponsePromise_ = responsePromise.then(\n (res) => {\n this.trigger_(\n CallbackId.PAYMENT_RESPONSE,\n Promise.resolve(res.clone())\n );\n },\n (reason) => {\n if (isCancelError(reason)) {\n return;\n }\n throw reason;\n }\n );\n return !!this.callbacks_[CallbackId.PAYMENT_RESPONSE];\n }\n\n hasPaymentResponsePending(): boolean {\n return !!this.resultBuffer_[CallbackId.PAYMENT_RESPONSE];\n }\n\n setOnFlowStarted(\n callback: ({flow, data}: {flow: string; data: object}) => void\n ): void {\n this.setCallback_(CallbackId.FLOW_STARTED, callback as Callback);\n }\n\n /**\n * @return Whether the callback has been found.\n */\n triggerFlowStarted(flow: string, data = {}): boolean {\n return this.trigger_(CallbackId.FLOW_STARTED, {\n flow,\n data,\n });\n }\n\n setOnFlowCanceled(\n callback: (params: {flow: string; data: object}) => void\n ): void {\n this.setCallback_(CallbackId.FLOW_CANCELED, callback as Callback);\n }\n\n /**\n * @return Whether the callback has been found.\n */\n triggerFlowCanceled(flow: string, data = {}): boolean {\n return this.trigger_(CallbackId.FLOW_CANCELED, {\n flow,\n data,\n });\n }\n\n private setCallback_(id: CallbackId, callback: Callback): void {\n if (this.callbacks_[id]) {\n warn(\n `[swg.js]: You have registered multiple callbacks for the same response.`\n );\n }\n this.callbacks_[id] = callback;\n // If result already exist, execute the callback right away.\n if (id in this.resultBuffer_) {\n this.executeCallback_(id, callback, this.resultBuffer_[id]);\n }\n }\n\n private trigger_(id: CallbackId, data: unknown): boolean {\n this.resultBuffer_[id] = data;\n const callback = this.callbacks_[id];\n if (callback) {\n this.executeCallback_(id, callback, data);\n }\n return !!callback;\n }\n\n private resetCallback_(id: CallbackId): void {\n if (id in this.resultBuffer_) {\n delete this.resultBuffer_[id];\n }\n }\n\n private async executeCallback_(\n id: CallbackId,\n callback: Callback,\n data: unknown\n ): Promise {\n // Always execute callbacks in a microtask.\n await 0;\n\n callback(data);\n this.resetCallback_(id);\n }\n}\n","/**\n * Copyright 2021 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Container for attribution details for the publisher / creator.\n */\nexport class AttributionParams {\n constructor(\n public readonly displayName: string,\n public readonly avatarUrl: string\n ) {}\n}\n","/**\n * Copyright 2021 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\ninterface AutoPromptConfigParams {\n displayDelaySeconds?: number;\n numImpressionsBetweenPrompts?: number;\n dismissalBackOffSeconds?: number;\n maxDismissalsPerWeek?: number;\n maxDismissalsResultingHideSeconds?: number;\n impressionBackOffSeconds?: number;\n maxImpressions?: number;\n maxImpressionsResultingHideSeconds?: number;\n globalFrequencyCapDurationSeconds?: number;\n globalFrequencyCapDurationNano?: number;\n promptFrequencyCaps?: {\n audienceActionType?: string;\n frequencyCapDuration?: {\n seconds?: number;\n nano?: number;\n };\n }[];\n anyPromptFrequencyCapDurationSeconds?: number;\n anyPromptFrequencyCapDurationNano?: number;\n}\n\n/**\n * Container for the auto prompt configuation details.\n */\nexport class AutoPromptConfig {\n clientDisplayTrigger: ClientDisplayTrigger;\n explicitDismissalConfig: ExplicitDismissalConfig;\n impressionConfig: ImpressionConfig;\n frequencyCapConfig: FrequencyCapConfig;\n\n /**\n * @param {!AutoPromptConfigParams=} params\n */\n constructor({\n displayDelaySeconds,\n numImpressionsBetweenPrompts,\n dismissalBackOffSeconds,\n maxDismissalsPerWeek,\n maxDismissalsResultingHideSeconds,\n impressionBackOffSeconds,\n maxImpressions,\n maxImpressionsResultingHideSeconds,\n globalFrequencyCapDurationSeconds,\n globalFrequencyCapDurationNano,\n promptFrequencyCaps,\n anyPromptFrequencyCapDurationSeconds,\n anyPromptFrequencyCapDurationNano,\n }: AutoPromptConfigParams) {\n this.clientDisplayTrigger = new ClientDisplayTrigger(\n displayDelaySeconds,\n numImpressionsBetweenPrompts\n );\n this.explicitDismissalConfig = new ExplicitDismissalConfig(\n dismissalBackOffSeconds,\n maxDismissalsPerWeek,\n maxDismissalsResultingHideSeconds\n );\n this.impressionConfig = new ImpressionConfig(\n impressionBackOffSeconds,\n maxImpressions,\n maxImpressionsResultingHideSeconds\n );\n this.frequencyCapConfig = new FrequencyCapConfig(\n new GlobalFrequencyCap(\n new Duration(\n globalFrequencyCapDurationSeconds,\n globalFrequencyCapDurationNano\n )\n ),\n promptFrequencyCaps?.map(\n (promptFrequencyCap) =>\n new PromptFrequencyCap(\n promptFrequencyCap.audienceActionType,\n new Duration(\n promptFrequencyCap.frequencyCapDuration?.seconds,\n promptFrequencyCap.frequencyCapDuration?.nano\n )\n )\n ),\n new AnyPromptFrequencyCap(\n new Duration(\n anyPromptFrequencyCapDurationSeconds,\n anyPromptFrequencyCapDurationNano\n )\n )\n );\n }\n}\n\n/**\n * Client side conditions to trigger the display of the auto prompt.\n */\nexport class ClientDisplayTrigger {\n constructor(\n public readonly displayDelaySeconds?: number,\n public readonly numImpressionsBetweenPrompts?: number\n ) {}\n}\n\n/**\n * Configuration of explicit dismissal behavior and its effects.\n */\nexport class ExplicitDismissalConfig {\n constructor(\n public readonly backOffSeconds?: number,\n public readonly maxDismissalsPerWeek?: number,\n public readonly maxDismissalsResultingHideSeconds?: number\n ) {}\n}\n\n/**\n * Configuration of impression behavior and its effects.\n */\nexport class ImpressionConfig {\n constructor(\n public readonly backOffSeconds?: number,\n public readonly maxImpressions?: number,\n public readonly maxImpressionsResultingHideSeconds?: number\n ) {}\n}\n\n/**\n * Configuration of Prompt Frequency Capping.\n */\nexport class FrequencyCapConfig {\n constructor(\n public readonly globalFrequencyCap?: GlobalFrequencyCap,\n public readonly promptFrequencyCaps?: PromptFrequencyCap[],\n public readonly anyPromptFrequencyCap?: AnyPromptFrequencyCap\n ) {}\n}\n\nexport class GlobalFrequencyCap {\n constructor(public readonly frequencyCapDuration?: Duration) {}\n}\n\nexport class PromptFrequencyCap {\n constructor(\n public readonly audienceActionType?: string,\n public readonly frequencyCapDuration?: Duration\n ) {}\n}\n\nexport class AnyPromptFrequencyCap {\n constructor(public readonly frequencyCapDuration?: Duration) {}\n}\n\nexport class Duration {\n constructor(\n public readonly seconds?: number,\n public readonly nanos?: number\n ) {}\n}\n","/**\n * Copyright 2021 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {AttributionParams} from './attribution-params';\nimport {AutoPromptConfig} from './auto-prompt-config';\n\n/**\n * Client configuration options.\n */\ninterface ClientConfigOptions {\n attributionParams?: AttributionParams;\n autoPromptConfig?: AutoPromptConfig;\n paySwgVersion?: string;\n uiPredicates?: UiPredicates;\n useUpdatedOfferFlows?: boolean;\n skipAccountCreationScreen?: boolean;\n}\n\n/**\n * Container for the details relating to how the client should be configured.\n */\nexport class ClientConfig {\n public readonly attributionParams?: AttributionParams;\n public readonly autoPromptConfig?: AutoPromptConfig;\n public readonly paySwgVersion?: string;\n public readonly uiPredicates?: UiPredicates;\n public readonly useUpdatedOfferFlows?: boolean;\n public readonly skipAccountCreationScreen?: boolean;\n\n constructor({\n attributionParams,\n autoPromptConfig,\n paySwgVersion,\n uiPredicates,\n useUpdatedOfferFlows,\n skipAccountCreationScreen,\n }: ClientConfigOptions) {\n this.autoPromptConfig = autoPromptConfig;\n this.paySwgVersion = paySwgVersion;\n this.useUpdatedOfferFlows = useUpdatedOfferFlows || false;\n this.skipAccountCreationScreen = skipAccountCreationScreen || false;\n this.uiPredicates = uiPredicates;\n this.attributionParams = attributionParams;\n }\n}\n\n/**\n * Predicates to control UI elements.\n */\nexport class UiPredicates {\n constructor(\n public readonly canDisplayAutoPrompt?: boolean,\n public readonly canDisplayButton?: boolean,\n public readonly purchaseUnavailableRegion?: boolean\n ) {}\n}\n\n/**\n * The server sends this JSON representation of the client config.\n */\nexport interface ClientConfigJson {\n attributionParams?: {\n displayName: string;\n avatarUrl: string;\n };\n autoPromptConfig?: {\n clientDisplayTrigger?: {\n displayDelaySeconds?: number;\n numImpressionsBetweenPrompts?: number;\n };\n explicitDismissalConfig?: {\n backOffSeconds?: number;\n maxDismissalsPerWeek?: number;\n maxDismissalsResultingHideSeconds?: number;\n };\n impressionConfig?: {\n backOffSeconds?: number;\n maxImpressions?: number;\n maxImpressionsResultingHideSeconds?: number;\n };\n frequencyCapConfig?: {\n globalFrequencyCap?: {\n frequencyCapDuration?: {\n seconds?: number;\n nano?: number;\n };\n };\n promptFrequencyCaps?: Array<{\n audienceActionType?: string;\n frequencyCapDuration?: {\n seconds?: number;\n nano?: number;\n };\n }>;\n anyPromptFrequencyCap?: {\n frequencyCapDuration?: {\n seconds?: number;\n nano?: number;\n };\n };\n };\n };\n paySwgVersion?: string;\n uiPredicates?: {\n canDisplayAutoPrompt?: boolean;\n canDisplayButton?: boolean;\n purchaseUnavailableRegion?: boolean;\n };\n useUpdatedOfferFlows?: boolean;\n skipAccountCreationScreen?: boolean;\n}\n","/**\n * Copyright 2021 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {AttributionParams} from '../model/attribution-params';\nimport {AutoPromptConfig} from '../model/auto-prompt-config';\nimport {\n ClientConfig,\n ClientConfigJson,\n UiPredicates,\n} from '../model/client-config';\nimport {ClientOptions} from '../api/basic-subscriptions';\nimport {ClientTheme} from '../api/subscriptions';\nimport {Deps} from './deps';\nimport {Fetcher} from './fetcher';\nimport {serviceUrl} from './services';\n\n/**\n * Manager of how the client should be configured. Fetches and stores\n * configuration details from the server.\n */\nexport class ClientConfigManager {\n private responsePromise_: Promise | null = null;\n\n constructor(\n private readonly deps_: Deps,\n private readonly publicationId_: string,\n private readonly fetcher_: Fetcher,\n private readonly clientOptions_: ClientOptions = {}\n ) {}\n\n /**\n * Fetches the client config from the server.\n * @param readyPromise Optional promise to wait on before attempting to fetch the clientConfiguration.\n */\n fetchClientConfig(readyPromise?: Promise): Promise {\n if (!this.publicationId_) {\n throw new Error('fetchClientConfig requires publicationId');\n }\n if (!this.responsePromise_) {\n readyPromise = readyPromise || Promise.resolve();\n this.responsePromise_ = readyPromise.then(() => this.fetch_());\n }\n return this.responsePromise_;\n }\n\n /**\n * Gets the client config, if already requested. Otherwise returns a Promise\n * with a default ClientConfig.\n */\n getClientConfig(): Promise {\n return this.responsePromise_ || this.getDefaultConfig_();\n }\n\n /** Gets the default config. */\n private async getDefaultConfig_(): Promise {\n return new ClientConfig({\n paySwgVersion: this.deps_.config().paySwgVersion,\n skipAccountCreationScreen: this.clientOptions_.skipAccountCreationScreen,\n useUpdatedOfferFlows: !!this.deps_.config().paySwgVersion || undefined,\n });\n }\n\n /**\n * Convenience method for retrieving the auto prompt portion of the client\n * configuration.\n */\n async getAutoPromptConfig(): Promise {\n if (!this.responsePromise_) {\n this.fetchClientConfig();\n }\n const clientConfig = await this.responsePromise_;\n return clientConfig?.autoPromptConfig;\n }\n\n /**\n * Gets the language the UI should be displayed in. See\n * src/api/basic-subscriptions.ClientOptions.lang.\n */\n getLanguage(): string {\n return this.clientOptions_.lang || 'en';\n }\n\n /**\n * Gets the theme the UI should be displayed in. See\n * src/api/basic-subscriptions.ClientOptions.theme.\n */\n getTheme(): ClientTheme {\n const themeDefault = self.matchMedia(`(prefers-color-scheme: dark)`).matches\n ? ClientTheme.DARK\n : ClientTheme.LIGHT;\n return this.clientOptions_.theme || themeDefault;\n }\n\n /**\n * Returns whether scrolling on main page should be allowed when\n * subscription or contribution dialog is displayed.\n */\n shouldAllowScroll(): boolean {\n return !!this.clientOptions_.allowScroll;\n }\n\n /**\n * Returns whether iframes should also use the language specified in the\n * client options, rather than the default of letting the iframes decide the\n * display language. Note that this will return false if the lang option is\n * not set, even if forceLangInIframes was set.\n */\n shouldForceLangInIframes(): boolean {\n return (\n !!this.clientOptions_.forceLangInIframes && !!this.clientOptions_.lang\n );\n }\n\n /**\n * Determines whether a subscription or contribution button should be disabled.\n */\n async shouldEnableButton(): Promise {\n // Disable button if disableButton is set to be true in clientOptions.\n // If disableButton is set to be false or not set, then always enable button.\n // This is for testing purpose.\n if (this.clientOptions_.disableButton) {\n return Promise.resolve(false);\n }\n\n if (!this.responsePromise_) {\n this.fetchClientConfig();\n }\n\n // UI predicates decides whether to enable button.\n const clientConfig = await this.responsePromise_;\n return clientConfig?.uiPredicates?.canDisplayButton;\n }\n\n /**\n * Fetches the client config from the server.\n */\n async fetch_(): Promise {\n const article = await this.deps_.entitlementsManager().getArticle();\n\n if (article) {\n return this.parseClientConfig_(article['clientConfig']);\n }\n\n // If there was no article from the entitlement manager, we need\n // to fetch our own using the internal version.\n const url = serviceUrl(\n '/publication/' +\n encodeURIComponent(this.publicationId_) +\n '/clientconfiguration'\n );\n const json = await this.fetcher_.fetchCredentialedJson(url);\n return this.parseClientConfig_(json);\n }\n\n /**\n * Parses the fetched config into the ClientConfig container object.\n */\n parseClientConfig_(json: ClientConfigJson): ClientConfig {\n const autoPromptConfigJson = json['autoPromptConfig'];\n let autoPromptConfig = undefined;\n if (autoPromptConfigJson) {\n autoPromptConfig = new AutoPromptConfig({\n displayDelaySeconds:\n autoPromptConfigJson.clientDisplayTrigger?.displayDelaySeconds,\n dismissalBackOffSeconds:\n autoPromptConfigJson.explicitDismissalConfig?.backOffSeconds,\n maxDismissalsPerWeek:\n autoPromptConfigJson.explicitDismissalConfig?.maxDismissalsPerWeek,\n maxDismissalsResultingHideSeconds:\n autoPromptConfigJson.explicitDismissalConfig\n ?.maxDismissalsResultingHideSeconds,\n impressionBackOffSeconds:\n autoPromptConfigJson.impressionConfig?.backOffSeconds,\n maxImpressions: autoPromptConfigJson.impressionConfig?.maxImpressions,\n maxImpressionsResultingHideSeconds:\n autoPromptConfigJson.impressionConfig\n ?.maxImpressionsResultingHideSeconds,\n globalFrequencyCapDurationSeconds:\n autoPromptConfigJson.frequencyCapConfig?.globalFrequencyCap\n ?.frequencyCapDuration?.seconds,\n globalFrequencyCapDurationNano:\n autoPromptConfigJson.frequencyCapConfig?.globalFrequencyCap\n ?.frequencyCapDuration?.nano,\n promptFrequencyCaps:\n autoPromptConfigJson.frequencyCapConfig?.promptFrequencyCaps,\n anyPromptFrequencyCapDurationSeconds:\n autoPromptConfigJson.frequencyCapConfig?.anyPromptFrequencyCap\n ?.frequencyCapDuration?.seconds,\n anyPromptFrequencyCapDurationNano:\n autoPromptConfigJson.frequencyCapConfig?.anyPromptFrequencyCap\n ?.frequencyCapDuration?.nano,\n });\n }\n\n const uiPredicatesJson = json['uiPredicates'];\n let uiPredicates = undefined;\n if (uiPredicatesJson) {\n uiPredicates = new UiPredicates(\n uiPredicatesJson.canDisplayAutoPrompt,\n uiPredicatesJson.canDisplayButton,\n uiPredicatesJson.purchaseUnavailableRegion\n );\n }\n\n const attributionParamsJson = json['attributionParams'];\n let attributionParams;\n if (attributionParamsJson) {\n attributionParams = new AttributionParams(\n attributionParamsJson.displayName,\n attributionParamsJson.avatarUrl\n );\n }\n\n const paySwgVersion =\n this.deps_.config().paySwgVersion || json['paySwgVersion'];\n\n const useUpdatedOfferFlows =\n !!this.deps_.config().paySwgVersion || json['useUpdatedOfferFlows'];\n\n return new ClientConfig({\n autoPromptConfig,\n paySwgVersion,\n useUpdatedOfferFlows,\n skipAccountCreationScreen: this.clientOptions_.skipAccountCreationScreen,\n uiPredicates,\n attributionParams,\n });\n }\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {ActivityIframeView} from '../ui/activity-iframe-view';\nimport {ActivityPorts} from '../components/activities';\nimport {\n AlreadySubscribedResponse,\n EntitlementsResponse,\n SkuSelectedResponse,\n} from '../proto/api_messages';\nimport {ClientConfig} from '../model/client-config';\nimport {ClientConfigManager} from './client-config-manager';\nimport {Deps} from './deps';\nimport {DialogConfig} from '../components/dialog';\nimport {DialogManager} from '../components/dialog-manager';\nimport {\n OffersRequest,\n ProductType,\n SubscriptionFlows,\n SubscriptionRequest,\n} from '../api/subscriptions';\nimport {PageConfig} from '../model/page-config';\nimport {PayStartFlow} from './pay-flow';\nimport {feArgs, feUrl} from './services';\n\n/**\n * The class for Contributions flow.\n */\nexport class ContributionsFlow {\n private readonly win_: Window;\n private readonly clientConfigManager_: ClientConfigManager;\n private readonly activityPorts_: ActivityPorts;\n private readonly dialogManager_: DialogManager;\n private readonly activityIframeViewPromise_: Promise;\n private readonly shouldAnimateFade_: boolean;\n private isClosable_: boolean;\n\n constructor(\n private readonly deps_: Deps,\n private readonly options_?: OffersRequest\n ) {\n this.win_ = deps_.win();\n\n this.clientConfigManager_ = deps_.clientConfigManager();\n\n this.activityPorts_ = deps_.activities();\n\n this.dialogManager_ = deps_.dialogManager();\n\n // Default to showing close button.\n this.isClosable_ = this.options_?.isClosable ?? true;\n this.activityIframeViewPromise_ = this.getActivityIframeView_();\n\n this.shouldAnimateFade_ =\n this.options_?.shouldAnimateFade === undefined\n ? true\n : this.options_?.shouldAnimateFade;\n }\n\n private async getActivityIframeView_(): Promise {\n const clientConfig = await this.clientConfigManager_.getClientConfig();\n\n return new ActivityIframeView(\n this.win_,\n this.activityPorts_,\n this.getUrl_(clientConfig, this.deps_.pageConfig()),\n feArgs({\n 'productId': this.deps_.pageConfig().getProductId(),\n 'publicationId': this.deps_.pageConfig().getPublicationId(),\n 'productType': ProductType.UI_CONTRIBUTION,\n 'list': this.options_?.list || 'default',\n 'skus': this.options_?.skus || null,\n 'isClosable': this.isClosable_,\n 'supportsEventManager': true,\n }),\n /* shouldFadeBody */ true,\n /* hasLoadingIndicator_ */ false,\n /* shouldAnimateFade */ this.shouldAnimateFade_\n );\n }\n\n private handleLinkRequest_(response: AlreadySubscribedResponse): void {\n if (response.getSubscriberOrMember()) {\n this.deps_.callbacks().triggerLoginRequest({\n linkRequested: !!response.getLinkRequested(),\n });\n }\n }\n\n private startPayFlow_(response: SkuSelectedResponse): void {\n const sku = response.getSku();\n const isOneTime = response.getOneTime();\n if (sku) {\n const contributionRequest: SubscriptionRequest = {\n 'skuId': sku,\n };\n if (isOneTime) {\n contributionRequest['oneTime'] = isOneTime;\n }\n new PayStartFlow(\n this.deps_,\n contributionRequest,\n ProductType.UI_CONTRIBUTION\n ).start();\n }\n }\n\n /**\n * Starts the contributions flow or alreadyMember flow.\n */\n async start(): Promise {\n const activityIframeView = await this.activityIframeViewPromise_;\n\n // Start/cancel events.\n this.deps_\n .callbacks()\n .triggerFlowStarted(SubscriptionFlows.SHOW_CONTRIBUTION_OPTIONS);\n activityIframeView.onCancel(() => {\n this.deps_\n .callbacks()\n .triggerFlowCanceled(SubscriptionFlows.SHOW_CONTRIBUTION_OPTIONS);\n });\n activityIframeView.on(\n AlreadySubscribedResponse,\n this.handleLinkRequest_.bind(this)\n );\n activityIframeView.on(SkuSelectedResponse, this.startPayFlow_.bind(this));\n\n const clientConfig = await this.clientConfigManager_.getClientConfig();\n return this.dialogManager_.openView(\n activityIframeView,\n /* hidden */ false,\n this.getDialogConfig_(\n clientConfig,\n this.clientConfigManager_.shouldAllowScroll()\n )\n );\n }\n\n /**\n * Gets display configuration options for the opened dialog. Uses the\n * responsive desktop design properties if the updated offer flows UI (for\n * SwG Basic) is enabled. Permits override to allow scrolling.\n */\n private getDialogConfig_(\n clientConfig: ClientConfig,\n shouldAllowScroll: boolean\n ): DialogConfig {\n return clientConfig.useUpdatedOfferFlows && !shouldAllowScroll\n ? {\n shouldDisableBodyScrolling: true,\n closeOnBackgroundClick: this.isClosable_,\n }\n : {};\n }\n\n /**\n * Gets the complete URL that should be used for the activity iFrame view.\n */\n private getUrl_(clientConfig: ClientConfig, pageConfig: PageConfig): string {\n if (!clientConfig.useUpdatedOfferFlows) {\n return feUrl('/contributionsiframe');\n }\n\n if (this.clientConfigManager_.shouldForceLangInIframes()) {\n return feUrl('/contributionoffersiframe', {\n 'hl': this.clientConfigManager_.getLanguage(),\n 'publicationId': pageConfig.getPublicationId(),\n });\n }\n\n return feUrl('/contributionoffersiframe', {\n 'publicationId': pageConfig.getPublicationId(),\n });\n }\n\n /**\n * Shows \"no contribution found\" on activity iFrame view.\n */\n async showNoEntitlementFoundToast(): Promise {\n const activityIframeView = await this.activityIframeViewPromise_;\n activityIframeView!.execute(new EntitlementsResponse());\n }\n}\n","/**\n * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS-IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {ASSETS} from '../constants';\n\n/**\n * Template literal helper to enable syntax highlighting for our CSS below.\n */\nconst css = String.raw;\n\n/** @const {string} */\nexport const UI_CSS = css`\n body {\n padding: 0;\n margin: 0;\n }\n\n swg-container,\n swg-loading,\n swg-loading-animate,\n swg-loading-image {\n display: block;\n }\n\n swg-loading-container {\n width: 100% !important;\n display: flex !important;\n align-items: center !important;\n justify-content: center !important;\n min-height: 148px !important;\n height: 100% !important;\n bottom: 0 !important;\n margin-top: 5px !important;\n z-index: 2147483647 !important;\n }\n\n /**\n * Since the desktop view has the parent iframe (.swg-dialog) transparent,\n * this style adds the background with borders to the loading indicator.\n */\n @media (min-width: 630px), (min-height: 630px) {\n swg-loading-container {\n width: 560px !important;\n margin-left: auto !important;\n margin-right: auto !important;\n border-top-left-radius: 8px !important;\n border-top-right-radius: 8px !important;\n background-color: rgba(255, 255, 255, 1) !important;\n box-shadow: rgba(60, 64, 67, 0.3) 0 1px 1px,\n rgba(60, 64, 67, 0.15) 0 1px 4px 1px !important;\n }\n\n swg-loading-container.centered-on-desktop {\n height: 120px !important;\n min-height: 120px !important;\n border-radius: 8px !important;\n }\n }\n\n swg-loading {\n z-index: 2147483647 !important;\n width: 36px;\n height: 36px;\n overflow: hidden;\n animation: mspin-rotate 1568.63ms infinite linear;\n }\n\n swg-loading-animate {\n animation: mspin-revrot 5332ms infinite steps(4);\n }\n\n swg-loading-image {\n background-image: url('${ASSETS}/loader.svg');\n background-size: 100%;\n width: 11664px;\n height: 36px;\n animation: swg-loading-film 5332ms infinite steps(324);\n }\n\n @keyframes swg-loading-film {\n from {\n transform: translateX(0);\n }\n to {\n transform: translateX(-11664px);\n }\n }\n\n @keyframes mspin-rotate {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }\n\n @keyframes mspin-revrot {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(-360deg);\n }\n }\n`;\n\n/**\n * This contains common styles for the Dialog and Toast components.\n * Swgjs injects these styles as a \n
\n \n
`;\n\n// Error view for prompts that fail to init.\nconst ERROR_CSS = css`\n ${REWARDED_AD_PROMPT}\n`;\n\nexport const ERROR_HTML = html`\n \n
Something went wrong.
\n`;\n\nconst LOADING_CSS = css`\n swg-container,\n swg-loading,\n swg-loading-animate,\n swg-loading-image {\n display: block;\n }\n\n swg-loading-container {\n margin-left: auto !important;\n margin-right: auto !important;\n margin-top: auto !important;\n\n border-top-left-radius: 8px !important;\n border-top-right-radius: 8px !important;\n\n height: 148px !important;\n width: 100% !important;\n\n display: flex !important;\n align-items: center !important;\n justify-content: center !important;\n\n bottom: 0 !important;\n z-index: 2147483647 !important;\n\n background-color: rgba(255, 255, 255, 1) !important;\n box-shadow: rgba(60, 64, 67, 0.3) 0 1px 1px,\n rgba(60, 64, 67, 0.15) 0 1px 4px 1px !important;\n }\n\n swg-loading-container.centered-on-desktop {\n height: 120px !important;\n min-height: 120px !important;\n border-radius: 8px !important;\n }\n\n swg-loading {\n z-index: 2147483647 !important;\n width: 36px;\n height: 36px;\n overflow: hidden;\n animation: mspin-rotate 1568.63ms infinite linear;\n }\n\n swg-loading-animate {\n animation: mspin-revrot 5332ms infinite steps(4);\n }\n\n swg-loading-image {\n background-image: url('${ASSETS}/loader.svg');\n background-size: 100%;\n width: 11664px;\n height: 36px;\n animation: swg-loading-film 5332ms infinite steps(324);\n }\n\n @keyframes swg-loading-film {\n from {\n transform: translateX(0);\n }\n to {\n transform: translateX(-11664px);\n }\n }\n\n @keyframes mspin-rotate {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }\n\n @keyframes mspin-revrot {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(-360deg);\n }\n }\n\n @media (min-width: 450px) {\n swg-loading-container {\n width: 375px !important;\n }\n }\n`;\n\nexport const LOADING_HTML = html`\n \n \n \n \n \n \n \n \n`;\n\n// Rewarded ad wall prompt css and html.\nconst REWARDED_AD_CSS = css`\n ${DEFAULT_BUTTON}\n ${REWARDED_AD_CLOSE_BUTTON_CSS}\n ${REWARDED_AD_PROMPT}\n ${BACK_TO_HOME_CSS}\n ${EXIT_CSS}\n\n .rewarded-ad-container {\n margin: 0px;\n text-align: center;\n }\n\n .rewarded-ad-header {\n display: grid !important;\n grid-template-columns: 56px 1fr 56px;\n }\n\n .rewarded-ad-title {\n font-size: 28px; //1.75rem;\n line-height: 36px; // 2.25rem;\n font-weight: 400;\n letter-spacing: 0em;\n color: #202124;\n grid-column: 2;\n grid-row: 1;\n line-break: auto;\n }\n\n .rewarded-ad-message {\n margin-top: 8px;\n padding: 0px 11px 0px 11px;\n font-size: 16px; // 1rem;\n font-weight: 500;\n line-height: 24px; // 1.5rem;\n letter-spacing: 0.25px;\n color: #202124;\n }\n\n .rewarded-ad-cta {\n margin: 20px 14px 0px 14px;\n }\n\n .rewarded-ad-cta-button {\n padding: 6px; // 0.375rem;\n width: 100%;\n outline-offset: -2px; // 0.125rem;\n }\n\n .rewarded-ad-cta-button-inner {\n width: 100%;\n height: 36px; // 2.25rem;\n border-radius: 4px; // 0.25rem;\n font-size: 14px; // 0.875rem;\n font-weight: 500;\n letter-spacing: 0.25px;\n display: flex;\n justify-content: center;\n align-items: center;\n }\n\n .rewarded-ad-view-ad-button-inner {\n background-color: #1a73e8;\n color: white;\n }\n\n .rewarded-ad-cta-button:focus .rewarded-ad-view-ad-button-inner,\n .rewarded-ad-cta-button:hover .rewarded-ad-view-ad-button-inner {\n background-color: #145ab5;\n }\n\n .rewarded-ad-view-ad-button:disabled .rewarded-ad-view-ad-button-inner {\n background-color: darkgrey;\n color: lightgrey;\n }\n\n .rewarded-ad-support-button-inner {\n border: 1px solid #dadce0;\n border-radius: 4px; // 0.25rem;\n color: #1a73e8;\n }\n\n .rewarded-ad-cta-button:focus .rewarded-ad-support-button-inner,\n .rewarded-ad-cta-button:hover .rewarded-ad-support-button-inner {\n background-color: #e6e6e6;\n }\n\n .rewarded-ad-google-logo {\n float: left;\n height: 24px;\n margin: 20px 0px 20px 0px;\n }\n\n .rewarded-ad-sign-in-button {\n float: right;\n font-size: 14px; // 0.875rem;\n font-weight: 500;\n line-height: 20px; // 1.25rem;\n letter-spacing: 0.25px;\n text-align: right;\n color: #1a73e8;\n height: 48px;\n border-radius: 4px;\n margin: 8px 0px 8px 0px;\n padding: 0px 7px 0px 7px;\n }\n\n .rewarded-ad-sign-in-button:focus,\n .rewarded-ad-sign-in-button:hover {\n background-color: #f2f8ff;\n }\n\n .rewarded-ad-footer {\n margin-left: 20px;\n margin-right: 20px;\n }\n`;\n\nexport const REWARDED_AD_SUPPORT_HTML = html`\n
\n $SUPPORT_MESSAGE$\n
\n`;\n\nexport const REWARDED_AD_SIGN_IN_HTML = html`