media/js/base/fxa-form.es6.js (258 lines of code) (raw):

/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ const FxaForm = {}; let formElem; let entrypointInput; let entrypointExp; let entrypointVar; let utmCampaign; let utmContent; let utmSource; let utmTerm; const utms = [ 'utm_source', 'utm_campaign', 'utm_content', 'utm_term', 'utm_medium' ]; const fxaParams = [ 'device_id', 'flow_id', 'flow_begin_time', 'entrypoint_experiment', 'entrypoint_variation' ]; const acceptedParams = utms.concat(fxaParams); /** * Flag to skip non-essential attribution data. * Default to true. */ FxaForm.skipAttribution = true; /** * Creates a hidden form input. * @param name {String} input name. * @param value (String) input value. * @returns HTML element. */ FxaForm.createInput = function (name, value) { const input = document.createElement('input'); input.type = 'hidden'; input.name = name; input.value = value; return input; }; /** * Fetch and validate accepted params from the page URL for FxA referral. * https://mozilla.github.io/ecosystem-platform/docs/relying-parties/metrics-for-relying-parties#metrics-related-query-parameters * @returns {Object} if params are valid, else {null}. */ FxaForm.getAttributionData = function (params) { const allowedChars = /^[\w/.%-]+$/; const finalParams = {}; for (let i = 0; i < acceptedParams.length; i++) { const acceptedParam = acceptedParams[i]; if (Object.prototype.hasOwnProperty.call(params, acceptedParam)) { try { const foundParam = decodeURIComponent(params[acceptedParam]); if (allowedChars.test(foundParam)) { finalParams[acceptedParam] = foundParam; } } catch (e) { // silently drop malformed parameter values (issue #10897) } } } // Both utm_source and utm_campaign are considered required, so only pass through referral data if they exist. // Alternatively, pass through entrypoint_experiment and entrypoint_variation independently. if ( (Object.prototype.hasOwnProperty.call(finalParams, 'utm_source') && Object.prototype.hasOwnProperty.call( finalParams, 'utm_campaign' )) || (Object.prototype.hasOwnProperty.call( finalParams, 'entrypoint_experiment' ) && Object.prototype.hasOwnProperty.call( finalParams, 'entrypoint_variation' )) ) { return finalParams; } return null; }; /** * Pass through utm_params from URL if present, * to attribute external marketing campaigns. * * @returns {Object} of utm parameters */ FxaForm.getUTMParams = function () { const urlParams = new window._SearchParams().utmParams(); return FxaForm.getAttributionData(urlParams) || {}; }; /** * Get tokens from FxA for analytics purposes. * This is non-critical to the user flow. */ FxaForm.fetchTokens = function () { let destURL = formElem.getAttribute('action') + 'metrics-flow'; // add required params to the token fetch request destURL += '?form_type=email'; destURL += '&entrypoint=' + entrypointInput.value; destURL += '&utm_source=' + utmSource.value; if (utmContent) { destURL += '&utm_content=' + utmContent.value; } if (utmCampaign) { destURL += '&utm_campaign=' + utmCampaign.value; } if (utmTerm) { destURL += '&utm_term=' + utmTerm.value; } if (entrypointExp) { destURL += '&entrypoint_experiment=' + entrypointExp.value; } if (entrypointVar) { destURL += '&entrypoint_variation=' + entrypointVar.value; } return fetch(destURL) .then((resp) => { return resp.json(); }) .then((r) => { formElem.querySelector('[name="device_id"]').value = r.deviceId; formElem.querySelector('[name="flow_id"]').value = r.flowId; formElem.querySelector('[name="flow_begin_time"]').value = r.flowBeginTime; }) .catch(() => { // silently fail, leaving flow_id and flow_begin_time as default empty value }); }; /** * Builds extraURLParams object for passing to UITour.showFirefoxAccounts(). * @returns {Object} extraURLParams */ FxaForm.getExtraURLParams = function () { const utmSource = document.getElementById('fxa-email-form-utm-source'); const utmCampaign = document.getElementById('fxa-email-form-utm-campaign'); const entrypointExp = document.getElementById( 'fxa-email-form-entrypoint-experiment' ); const entrypointVar = document.getElementById( 'fxa-email-form-entrypoint-variation' ); // Only include basic page source/campaign if attribution is skipped. if (FxaForm.skipAttribution) { return utmSource.value && utmCampaign.value ? { utm_source: utmSource.value, utm_campaign: utmCampaign.value } : null; } const extraURLParams = FxaForm.getUTMParams(); if (entrypointExp && entrypointExp.value) { extraURLParams['entrypoint_experiment'] = entrypointExp.value; } if (entrypointVar && entrypointVar.value) { extraURLParams['entrypoint_variation'] = entrypointVar.value; } const formElem = document.getElementById('fxa-email-form'); if (formElem) { const deviceId = formElem.querySelector('[name="device_id"]'); const flowId = formElem.querySelector('[name="flow_id"]'); const flowBeginTime = formElem.querySelector( '[name="flow_begin_time"]' ); if (deviceId && deviceId.value) { extraURLParams['device_id'] = deviceId.value; } if (flowId && flowId.value) { extraURLParams['flow_id'] = flowId.value; } if (flowBeginTime && flowBeginTime.value) { extraURLParams['flow_begin_time'] = parseInt( flowBeginTime.value, 10 ); } } return extraURLParams; }; /** * Intercept event handler for FxA forms, lets the browser drive the FxA Flow using * the `showFirefoxAccounts` UITour API. Attaches several UTM parameters from the current page * that will be forwarded to the browser and later on to FxA services. * @param event {Event} */ FxaForm.interceptFxANavigation = function (event) { event.preventDefault(); const extraURLParams = FxaForm.getExtraURLParams(); const entrypointInput = document.getElementById( 'fxa-email-form-entrypoint' ); let email = document.getElementById('fxa-email-field'); let entrypoint = null; if (entrypointInput && entrypointInput.value) { entrypoint = entrypointInput.value; } if (email) { email = email.value; } return Mozilla.UITour.showFirefoxAccounts( extraURLParams, entrypoint, email ); }; /** * Sets the service context parameters for Sync on Firefox desktop. */ FxaForm.setServiceContext = function () { const form = document.getElementById('fxa-email-form'); // If the form is not present, do nothing. if (!form) { return; } const contextField = form.querySelector('[name="context"]'); const userVer = parseFloat(Mozilla.Client._getFirefoxVersion()); const useUITourForFxA = userVer >= 80 && typeof Mozilla.UITour !== 'undefined'; if (useUITourForFxA) { // context is required for all Firefox desktop clients. if (!contextField) { const context = FxaForm.createInput('context', 'fx_desktop_v3'); form.appendChild(context); } Mozilla.UITour.ping(() => { // intercept the flow and submit the form using the UITour API. form.addEventListener('submit', FxaForm.interceptFxANavigation); }); } }; /** * Configures Sync for Firefox browsers. */ FxaForm.configureSync = function () { // Configure Sync for Firefox desktop browsers. if (Mozilla.Client._isFirefoxDesktop()) { FxaForm.setServiceContext(); } }; FxaForm.isSupported = function () { return 'Promise' in window && 'fetch' in window; }; /** * Initializes FxA form. Responsible for configuring Sync on Firefox desktop, * as well as passing attribution params from the page URL through the form * to FxA. * @param {Boolean} skipAttr - skips attribution, configuring Sync only. * @returns {Promise} */ FxaForm.init = function (skipAttr) { if (!FxaForm.isSupported()) { return false; } FxaForm.skipAttribution = typeof skipAttr === 'boolean' ? skipAttr : true; formElem = document.getElementById('fxa-email-form'); entrypointInput = document.getElementById('fxa-email-form-entrypoint'); entrypointExp = document.getElementById( 'fxa-email-form-entrypoint-experiment' ); entrypointVar = document.getElementById( 'fxa-email-form-entrypoint-variation' ); utmCampaign = document.getElementById('fxa-email-form-utm-campaign'); utmContent = document.getElementById('fxa-email-form-utm-content'); utmSource = document.getElementById('fxa-email-form-utm-source'); utmTerm = document.getElementById('fxa-email-form-utm-term'); return new window.Promise((resolve, reject) => { if (formElem) { if (!FxaForm.skipAttribution) { // Pass through UTM params from the URL to the form. const utms = FxaForm.getUTMParams(); Object.keys(utms).forEach((i) => { // check if input is available if (formElem.querySelector('[name="' + i + '"]')) { formElem.querySelector('[name="' + i + '"]').value = utms[i]; } else { // create input if one is not present const input = FxaForm.createInput(i, utms[i]); formElem.appendChild(input); } }); FxaForm.fetchTokens().then(() => { resolve(); }); } else { resolve(); } } else { reject(); } }); }; export default FxaForm;