summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/newmailaccount/content/accountProvisioner.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/newmailaccount/content/accountProvisioner.js')
-rw-r--r--comm/mail/components/newmailaccount/content/accountProvisioner.js892
1 files changed, 892 insertions, 0 deletions
diff --git a/comm/mail/components/newmailaccount/content/accountProvisioner.js b/comm/mail/components/newmailaccount/content/accountProvisioner.js
new file mode 100644
index 0000000000..14ba69c515
--- /dev/null
+++ b/comm/mail/components/newmailaccount/content/accountProvisioner.js
@@ -0,0 +1,892 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* globals MsgAccountManager, MozElements */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PluralForm: "resource://gre/modules/PluralForm.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AccountCreationUtils:
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm",
+
+ UIDensity: "resource:///modules/UIDensity.jsm",
+ UIFontSize: "resource:///modules/UIFontSize.jsm",
+});
+
+var { gAccountSetupLogger } = AccountCreationUtils;
+
+// AbortController to handle timeouts and abort the fetch requests.
+var gAbortController;
+var RETRY_TIMEOUT = 5000; // 5 seconds
+var CONNECTION_TIMEOUT = 15000; // 15 seconds
+var MAX_SMALL_ADDRESSES = 2;
+
+// Keep track of the prefers-reduce-motion media query for JS based animations.
+var gReducedMotion;
+
+// The main 3 Pane Window that we need to define on load in order to properly
+// update the UI when a new account is created.
+var gMainWindow;
+
+// Define window event listeners.
+window.addEventListener("load", () => {
+ gAccountProvisioner.onLoad();
+});
+window.addEventListener("unload", () => {
+ gAccountProvisioner.onUnload();
+});
+
+// Object to collect all the extra providers attributes to be used when
+// building the URL for the API call to purchase an item.
+var storedData = {};
+
+/**
+ * Helper method to split a value based on its first available blank space.
+ *
+ * @param {string} str - The string to split.
+ * @returns {Array} - An array with the generated first and last name.
+ */
+function splitName(str) {
+ let i = str.lastIndexOf(" ");
+ if (i >= 1) {
+ return [str.substring(0, i), str.substring(i + 1)];
+ }
+ return [str, ""];
+}
+
+/**
+ * Quick and simple HTML sanitization.
+ *
+ * @param {string} inputID - The ID of the currently used input field.
+ * @returns {string} - The HTML sanitized input value.
+ */
+function sanitizeName(inputID) {
+ let div = document.createElement("div");
+ div.textContent = document.getElementById(inputID).value;
+ return div.innerHTML.trim();
+}
+
+/**
+ * Replace occurrences of placeholder with the given node
+ *
+ * @param aTextContainer {Node} - DOM node containing the text child
+ * @param aTextNode {Node} - Text node containing the text, child of the aTextContainer
+ * @param aPlaceholder {String} - String to look for in aTextNode's textContent
+ * @param aReplacement {Node} - DOM node to insert instead of the found replacement
+ */
+function insertHTMLReplacement(
+ aTextContainer,
+ aTextNode,
+ aPlaceholder,
+ aReplacement
+) {
+ if (aTextNode.textContent.includes(aPlaceholder)) {
+ let placeIndex = aTextNode.textContent.indexOf(aPlaceholder);
+ let restNode = aTextNode.splitText(placeIndex + aPlaceholder.length);
+ aTextContainer.insertBefore(aReplacement, restNode);
+ let placeholderNode = aTextNode.splitText(placeIndex);
+ placeholderNode.remove();
+ }
+}
+
+/**
+ * This is our controller for the entire account provisioner setup process.
+ */
+var gAccountProvisioner = {
+ // If the setup wizard has already been initialized.
+ _isInited: false,
+ // If the data fetching of the providers is currently in progress.
+ _isLoadingProviders: false,
+ // If the providers have already been loaded.
+ _isLoadedProviders: false,
+ // Store a timeout retry in case fetching the providers fails.
+ _loadProviderRetryId: null,
+ // Array containing all fetched providers.
+ allProviders: [],
+ // Array containing all fetched provider names that only offer email.
+ mailProviders: [],
+ // Array containing all fetched provider names that also offer custom domain.
+ domainProviders: [],
+ // Handle a timeout to abort the fetch requests.
+ timeoutId: null,
+
+ /**
+ * Returns the URL for retrieving suggested names from the selected providers.
+ */
+ get suggestFromName() {
+ return Services.prefs.getCharPref("mail.provider.suggestFromName");
+ },
+
+ /**
+ * Initialize the main notification box for the account setup process.
+ */
+ get notificationBox() {
+ if (!this._notificationBox) {
+ this._notificationBox = new MozElements.NotificationBox(element => {
+ element.setAttribute("notificationside", "top");
+ document
+ .getElementById("accountProvisionerNotifications")
+ .append(element);
+ });
+ }
+ return this._notificationBox;
+ },
+
+ /**
+ * Clear currently running async fetches and reset important variables.
+ */
+ onUnload() {
+ this.clearAbortTimeout();
+ gAbortController.abort();
+ gAbortController = null;
+ },
+
+ async onLoad() {
+ // We can only init once, so bail out if we've been called again.
+ if (this._isInited) {
+ return;
+ }
+
+ gAccountSetupLogger.debug("Initializing provisioner wizard");
+ gReducedMotion = window.matchMedia(
+ "(prefers-reduced-motion: reduce)"
+ ).matches;
+
+ // Store the main window.
+ gMainWindow = Services.wm.getMostRecentWindow("mail:3pane");
+
+ // Initialize the fetch abort controller.
+ gAbortController = new AbortController();
+
+ // If we have a name stored, populate the search field with it.
+ if ("@mozilla.org/userinfo;1" in Cc) {
+ let userInfo = Cc["@mozilla.org/userinfo;1"].getService(Ci.nsIUserInfo);
+ // Assume that it's a genuine full name if it includes a space.
+ if (userInfo.fullname.includes(" ")) {
+ document.getElementById("mailName").value = userInfo.fullname;
+ document.getElementById("domainName").value = userInfo.fullname;
+ }
+ }
+
+ this.setupEventListeners();
+ await this.tryToFetchProviderList();
+
+ gAccountSetupLogger.debug("Provisioner wizard init complete.");
+
+ // Move the focus on the first available field.
+ document.getElementById("mailName").focus();
+ this._isInited = true;
+
+ Services.telemetry.scalarAdd("tb.account.opened_account_provisioner", 1);
+
+ UIDensity.registerWindow(window);
+ UIFontSize.registerWindow(window);
+ },
+
+ /**
+ * Set up the event listeners for the static elements in the page.
+ */
+ setupEventListeners() {
+ document.getElementById("cancelButton").onclick = () => {
+ window.close();
+ };
+
+ document.getElementById("existingButton").onclick = () => {
+ window.close();
+ gMainWindow.postMessage("open-account-setup-tab", "*");
+ };
+
+ document.getElementById("backButton").onclick = () => {
+ this.backToSetupView();
+ };
+ },
+
+ /**
+ * Return to the initial view without resetting any existing data.
+ */
+ backToSetupView() {
+ this.clearAbortTimeout();
+ this.clearNotifications();
+
+ // Clear search results.
+ let mailResultsArea = document.getElementById("mailResultsArea");
+ while (mailResultsArea.hasChildNodes()) {
+ mailResultsArea.lastChild.remove();
+ }
+ let domainResultsArea = document.getElementById("domainResultsArea");
+ while (domainResultsArea.hasChildNodes()) {
+ domainResultsArea.lastChild.remove();
+ }
+
+ // Update the UI to show the initial view.
+ document.getElementById("mailSearch").hidden = false;
+ document.getElementById("domainSearch").hidden = false;
+ document.getElementById("mailSearchResults").hidden = true;
+ document.getElementById("domainSearchResults").hidden = true;
+
+ // Update the buttons visibility.
+ document.getElementById("backButton").hidden = true;
+ document.getElementById("cancelButton").hidden = false;
+ document.getElementById("existingButton").hidden = false;
+
+ // Move the focus back on the first available field.
+ document.getElementById("mailName").focus();
+ },
+
+ /**
+ * Show a loading notification.
+ */
+ async startLoadingState(stringName) {
+ this.clearNotifications();
+
+ let notificationMessage = await document.l10n.formatValue(stringName);
+
+ gAccountSetupLogger.debug(`Status msg: ${notificationMessage}`);
+
+ let notification = this.notificationBox.appendNotification(
+ "accountSetupLoading",
+ {
+ label: notificationMessage,
+ priority: this.notificationBox.PRIORITY_INFO_LOW,
+ },
+ null
+ );
+ notification.setAttribute("align", "center");
+
+ // Hide the close button to prevent dismissing the notification.
+ notification.removeAttribute("dismissable");
+
+ this.ensureVisibleNotification();
+ },
+
+ /**
+ * Show an error notification in case something went wrong.
+ *
+ * @param {string} stringName - The name of the fluent string that needs to
+ * be attached to the notification.
+ * @param {boolean} isMsgError - True if the message comes from a server error
+ * response or try/catch.
+ */
+ async showErrorNotification(stringName, isMsgError) {
+ gAccountSetupLogger.debug(`Status error: ${stringName}`);
+
+ // Always remove any leftover notification before creating a new one.
+ this.clearNotifications();
+
+ // Fetch the fluent string only if this is not an error message coming from
+ // a previous method.
+ let notificationMessage = isMsgError
+ ? stringName
+ : await document.l10n.formatValue(stringName);
+
+ let notification = this.notificationBox.appendNotification(
+ "accountProvisionerError",
+ {
+ label: notificationMessage,
+ priority: this.notificationBox.PRIORITY_WARNING_MEDIUM,
+ },
+ null
+ );
+
+ // Hide the close button to prevent dismissing the notification.
+ notification.removeAttribute("dismissable");
+
+ this.ensureVisibleNotification();
+ },
+
+ async showSuccessNotification(stringName) {
+ // Always remove any leftover notification before creating a new one.
+ this.clearNotifications();
+
+ let notification = this.notificationBox.appendNotification(
+ "accountProvisionerSuccess",
+ {
+ label: await document.l10n.formatValue(stringName),
+ priority: this.notificationBox.PRIORITY_WARNING_MEDIUM,
+ },
+ null
+ );
+ notification.setAttribute("type", "success");
+ // Hide the close button to prevent dismissing the notification.
+ notification.removeAttribute("dismissable");
+
+ this.ensureVisibleNotification();
+ },
+
+ /**
+ * Clear all leftover notifications.
+ */
+ clearNotifications() {
+ this.notificationBox.removeAllNotifications();
+ },
+
+ /**
+ * Event handler for when the user selects an address by clicking on the price
+ * button for that address. This function spawns the content tab for the
+ * address order form, and then closes the Account Provisioner tab.
+ *
+ * @param {string} providerId - The ID of the chosen provider.
+ * @param {string} email - The chosen email address.
+ * @param {boolean} [isDomain=false] - If the fetched data comes from a domain
+ * search form.
+ */
+ onAddressSelected(providerId, email, isDomain = false) {
+ gAccountSetupLogger.debug("An address was selected by the user.");
+ let provider = this.allProviders.find(p => p.id == providerId);
+
+ let url = provider.api;
+ let inputID = isDomain ? "domainName" : "mailName";
+ let [firstName, lastName] = splitName(sanitizeName(inputID));
+ // Replace the variables in the API url.
+ url = url.replace("{firstname}", firstName);
+ url = url.replace("{lastname}", lastName);
+ url = url.replace("{email}", email);
+
+ // And add the extra data.
+ let data = storedData[providerId];
+ delete data.provider;
+ for (let name in data) {
+ url += `${!url.includes("?") ? "?" : "&"}${name}=${encodeURIComponent(
+ data[name]
+ )}`;
+ }
+
+ gAccountSetupLogger.debug("Opening up a contentTab with the order form.");
+ // Open the checkout content tab.
+ let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane");
+ let tabmail = mail3Pane.document.getElementById("tabmail");
+ tabmail.openTab("provisionerCheckoutTab", {
+ url,
+ realName: (firstName + " " + lastName).trim(),
+ email,
+ });
+
+ let providerHostname = new URL(url).hostname;
+ // Collect telemetry on which provider was selected for a new email account.
+ Services.telemetry.keyedScalarAdd(
+ "tb.account.selected_account_from_provisioner",
+ providerHostname,
+ 1
+ );
+
+ // The user has made a selection. Close the provisioner window and let the
+ // provider setup process take place in a dedicated tab.
+ window.close();
+ },
+
+ /**
+ * Attempt to fetch the provider list from the server.
+ */
+ async tryToFetchProviderList() {
+ // If we're already in the middle of getting the provider list, or we
+ // already got it before, bail out.
+ if (this._isLoadingProviders || this._isLoadedProviders) {
+ return;
+ }
+
+ this._isLoadingProviders = true;
+
+ // If there's a timeout ID for waking the account provisioner, clear it.
+ if (this._loadProviderRetryId) {
+ window.clearTimeout(this._loadProviderRetryId);
+ this._loadProviderRetryId = null;
+ }
+
+ await this.startLoadingState("account-provisioner-fetching-provisioners");
+
+ let providerListUrl = Services.prefs.getCharPref(
+ "mail.provider.providerList"
+ );
+
+ gAccountSetupLogger.debug(
+ `Trying to populate provider list from ${providerListUrl}…`
+ );
+
+ try {
+ let res = await fetch(providerListUrl, {
+ signal: gAbortController.signal,
+ });
+ this.startAbortTimeout();
+ let data = await res.json();
+ this.populateProvidersLists(data);
+ } catch (error) {
+ // Ugh, we couldn't get the JSON file. Maybe we're not online. Or maybe
+ // the server is down, or the file isn't being served. Regardless, if
+ // we get here, none of this stuff is going to work.
+ this._loadProviderRetryId = window.setTimeout(
+ () => this.tryToFetchProviderList(),
+ RETRY_TIMEOUT
+ );
+ this._isLoadingProviders = false;
+ this.showErrorNotification("account-provisioner-connection-issues");
+ gAccountSetupLogger.warn(`Failed to populate providers: ${error}`);
+ }
+ },
+
+ /**
+ * Validate a provider fetched during an API request to be sure we have all
+ * the necessary fields to complete a setup process.
+ *
+ * @param {object} provider - The fetched provider.
+ * @returns {boolean} - True if all the fields in the provider match the
+ * required fields.
+ */
+ providerHasCorrectFields(provider) {
+ let result = true;
+
+ let required = [
+ "id",
+ "label",
+ "paid",
+ "languages",
+ "api",
+ "tos_url",
+ "privacy_url",
+ "sells_domain",
+ ];
+
+ for (let field of required) {
+ let fieldExists = field in provider;
+ result &= fieldExists;
+
+ if (!fieldExists) {
+ gAccountSetupLogger.warn(
+ `A provider did not have the field ${field}, and will be skipped.`
+ );
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * Take the fetched providers, create checkboxes, icons and labels, and insert
+ * them below the corresponding search input.
+ *
+ * @param {?object} data - The object containing all fetched providers.
+ */
+ populateProvidersLists(data) {
+ gAccountSetupLogger.debug("Populating the provider list");
+ this.clearAbortTimeout();
+
+ if (!data || !data.length) {
+ gAccountSetupLogger.warn(
+ "The provider list we got back from the server was empty!"
+ );
+ this.showErrorNotification("account-provisioner-connection-issues");
+ return;
+ }
+
+ let mailProviderList = document.getElementById("mailProvidersList");
+ let domainProviderList = document.getElementById("domainProvidersList");
+
+ this.allProviders = data;
+ this.mailProviders = [];
+ this.domainProviders = [];
+
+ for (let provider of data) {
+ if (!this.providerHasCorrectFields(provider)) {
+ gAccountSetupLogger.warn(
+ "A provider had incorrect fields, and has been skipped"
+ );
+ continue;
+ }
+
+ let entry = document.createElement("li");
+ entry.setAttribute("id", provider.id);
+
+ if (provider.icon) {
+ let icon = document.createElement("img");
+ icon.setAttribute("src", provider.icon);
+ icon.setAttribute("alt", "");
+ entry.appendChild(icon);
+ }
+
+ let name = document.createElement("span");
+ name.textContent = provider.label;
+ entry.appendChild(name);
+
+ if (provider.sells_domain) {
+ domainProviderList.appendChild(entry);
+ this.domainProviders.push(provider.id);
+ } else {
+ mailProviderList.appendChild(entry);
+ this.mailProviders.push(provider.id);
+ }
+ }
+
+ this._isLoadedProviders = true;
+ this.clearNotifications();
+ },
+
+ /**
+ * Enable or disable the form fields when a fetch request starts or ends.
+ *
+ * @param {boolean} state - True if a fetch request is in progress.
+ */
+ updateSearchingState(state) {
+ for (let element of document.querySelectorAll(".disable-on-submit")) {
+ element.disabled = state;
+ }
+ },
+
+ /**
+ * Search for available email accounts.
+ *
+ * @param {DOMEvent} event - The form submit event.
+ */
+ async onMailFormSubmit(event) {
+ // Always prevent the actual form submission.
+ event.preventDefault();
+
+ // Quick HTML sanitization.
+ let name = sanitizeName("mailName");
+
+ // Bail out if the user didn't type anything.
+ if (!name) {
+ return;
+ }
+
+ let resultsArea = document.getElementById("mailSearchResults");
+ resultsArea.hidden = true;
+
+ this.startLoadingState("account-provisioner-searching-email");
+ let data = await this.submitFormRequest(name, this.mailProviders.join(","));
+ this.clearAbortTimeout();
+
+ let count = this.populateSearchResults(data);
+ if (!count) {
+ // Bail out if we didn't get any usable data.
+ gAccountSetupLogger.warn(
+ "We got nothing back from the server for search results!"
+ );
+ this.showErrorNotification("account-provisioner-searching-error");
+ return;
+ }
+
+ let resultsTitle = document.getElementById("mailResultsTitle");
+ let resultsString = await document.l10n.formatValue(
+ "account-provisioner-results-title",
+ { count }
+ );
+ // Attach the sanitized search terms to avoid HTML conversion in fluent.
+ resultsTitle.textContent = `${resultsString} "${name}"`;
+
+ // Hide the domain section.
+ document.getElementById("domainSearch").hidden = true;
+ // Show the results area.
+ resultsArea.hidden = false;
+ // Update the buttons visibility.
+ document.getElementById("cancelButton").hidden = true;
+ document.getElementById("existingButton").hidden = true;
+ // Show the back button.
+ document.getElementById("backButton").hidden = false;
+ },
+
+ /**
+ * Search for available domain names.
+ *
+ * @param {DOMEvent} event - The form submit event.
+ */
+ async onDomainFormSubmit(event) {
+ // Always prevent the actual form submission.
+ event.preventDefault();
+
+ // Quick HTML sanitization.
+ let name = sanitizeName("domainName");
+
+ // Bail out if the user didn't type anything.
+ if (!name) {
+ return;
+ }
+
+ let resultsArea = document.getElementById("domainSearchResults");
+ resultsArea.hidden = true;
+
+ this.startLoadingState("account-provisioner-searching-domain");
+ let data = await this.submitFormRequest(
+ name,
+ this.domainProviders.join(",")
+ );
+ this.clearAbortTimeout();
+
+ let count = this.populateSearchResults(data, true);
+ if (!count) {
+ // Bail out if we didn't get any usable data.
+ gAccountSetupLogger.warn(
+ "We got nothing back from the server for search results!"
+ );
+ this.showErrorNotification("account-provisioner-searching-error");
+ return;
+ }
+
+ let resultsTitle = document.getElementById("domainResultsTitle");
+ let resultsString = await document.l10n.formatValue(
+ "account-provisioner-results-title",
+ { count }
+ );
+ // Attach the sanitized search terms to avoid HTML conversion in fluent.
+ resultsTitle.textContent = `${resultsString} "${name}"`;
+
+ // Hide the mail section.
+ document.getElementById("mailSearch").hidden = true;
+ // Show the results area.
+ resultsArea.hidden = false;
+ // Update the buttons visibility.
+ document.getElementById("cancelButton").hidden = true;
+ document.getElementById("existingButton").hidden = true;
+ // Show the back button.
+ document.getElementById("backButton").hidden = false;
+ },
+
+ /**
+ * Update the UI to show the fetched address data.
+ *
+ * @param {object} data - The fetched data from an email or domain search.
+ * @param {boolean} [isDomain=false] - If the fetched data comes from a domain
+ * search form.
+ */
+ populateSearchResults(data, isDomain = false) {
+ if (!data || !data.length) {
+ return 0;
+ }
+
+ this.clearNotifications();
+
+ let resultsArea = isDomain
+ ? document.getElementById("domainResultsArea")
+ : document.getElementById("mailResultsArea");
+ // Clear previously generated content.
+ while (resultsArea.hasChildNodes()) {
+ resultsArea.lastChild.remove();
+ }
+
+ // Filter out possible errors or empty lists.
+ let validData = data.filter(
+ result => result.succeeded && result.addresses.length
+ );
+
+ if (!validData || !validData.length) {
+ return 0;
+ }
+
+ let providersList = isDomain ? this.domainProviders : this.mailProviders;
+
+ let count = 0;
+ for (let provider of validData) {
+ count += provider.addresses.length;
+
+ // Don't add a provider header if only 1 is currently available.
+ if (providersList.length > 1) {
+ let header = document.createElement("h5");
+ header.classList.add("result-list-header");
+ header.textContent = this.allProviders.find(
+ p => p.id == provider.provider
+ ).label;
+ resultsArea.appendChild(header);
+ }
+
+ let list = document.createElement("ul");
+
+ // Only show a chink of addresses if we got a long list.
+ let isLongList = provider.addresses.length > 5;
+ let addresses = isLongList
+ ? provider.addresses.slice(0, 4)
+ : provider.addresses;
+
+ for (let address of addresses) {
+ list.appendChild(this.createAddressRow(address, provider, isDomain));
+ }
+
+ resultsArea.appendChild(list);
+
+ // If we got more than 5 addresses, create an hidden bug expandable list
+ // with the rest of the data.
+ if (isLongList) {
+ let hiddenList = document.createElement("ul");
+ hiddenList.hidden = true;
+
+ for (let address of provider.addresses.slice(5)) {
+ hiddenList.appendChild(
+ this.createAddressRow(address, provider, isDomain)
+ );
+ }
+
+ let button = document.createElement("button");
+ button.setAttribute("type", "button");
+ button.classList.add("btn-link", "self-center");
+ document.l10n.setAttributes(
+ button,
+ "account-provisioner-all-results-button"
+ );
+ button.onclick = () => {
+ hiddenList.hidden = false;
+ button.hidden = true;
+ };
+
+ resultsArea.appendChild(button);
+ resultsArea.appendChild(hiddenList);
+ }
+ }
+
+ for (let provider of data) {
+ delete provider.succeeded;
+ delete provider.addresses;
+ delete provider.price;
+ storedData[provider.provider] = provider;
+ }
+
+ return count;
+ },
+
+ /**
+ * Create the list item to show the suggested address returned from a search.
+ *
+ * @param {object} address - The address returned from the provider search.
+ * @param {object} provider - The provider from which the address is
+ * @param {boolean} [isDomain=false] - If the fetched data comes from a domain
+ * search form.
+ * available.
+ * @returns {HTMLLIElement}
+ */
+ createAddressRow(address, provider, isDomain = false) {
+ let row = document.createElement("li");
+ row.classList.add("result-item");
+
+ let suggestedAddress = address.address || address;
+
+ let button = document.createElement("button");
+ button.setAttribute("type", "button");
+ button.onclick = () => {
+ this.onAddressSelected(provider.provider, suggestedAddress, isDomain);
+ };
+
+ let leftArea = document.createElement("span");
+ leftArea.classList.add("result-data");
+
+ let name = document.createElement("span");
+ name.classList.add("result-name");
+ name.textContent = suggestedAddress;
+ leftArea.appendChild(name);
+ row.setAttribute("data-label", suggestedAddress);
+
+ let price = document.createElement("span");
+ price.classList.add("result-price");
+
+ // Build the pricing text and handle possible free trials.
+ if (address.price) {
+ if (address.price != 0) {
+ // Some pricing is defined.
+ document.l10n.setAttributes(price, "account-provision-price-per-year", {
+ price: address.price,
+ });
+ } else if (address.price == 0) {
+ // Price is defined by it's zero.
+ document.l10n.setAttributes(price, "account-provisioner-free-account");
+ }
+ } else if (provider.price && provider.price != 0) {
+ // We don't have a price for the current result so let's try to use
+ // the general Provider's price.
+ document.l10n.setAttributes(price, "account-provision-price-per-year", {
+ price: provider.price,
+ });
+ } else {
+ // No price was specified, let's return "Free".
+ document.l10n.setAttributes(price, "account-provisioner-free-account");
+ }
+ leftArea.appendChild(price);
+
+ button.appendChild(leftArea);
+
+ let img = document.createElement("img");
+ document.l10n.setAttributes(img, "account-provisioner-open-in-tab-img");
+ img.setAttribute("alt", "");
+ img.setAttribute("src", "chrome://global/skin/icons/open-in-new.svg");
+ button.appendChild(img);
+
+ row.appendChild(button);
+
+ return row;
+ },
+
+ /**
+ * Fetches a list of suggested email addresses or domain names from a list of
+ * selected providers.
+ *
+ * @param {string} name - The search value typed by the user.
+ * @param {Array} providers - Array of providers to search for.
+ * @returns {object} - A list of available emails or domains.
+ */
+ async submitFormRequest(name, providers) {
+ // If the focused element is disabled by `updateSearchingState`, focus is
+ // lost. Save the focused element to restore it later.
+ let activeElement = document.activeElement;
+ this.updateSearchingState(true);
+
+ let [firstName, lastName] = splitName(name);
+ let url = `${this.suggestFromName}?first_name=${encodeURIComponent(
+ firstName
+ )}&last_name=${encodeURIComponent(lastName)}&providers=${encodeURIComponent(
+ providers
+ )}&version=2`;
+
+ let data;
+ try {
+ let res = await fetch(url, { signal: gAbortController.signal });
+ this.startAbortTimeout();
+ data = await res.json();
+ } catch (error) {
+ gAccountSetupLogger.warn(`Failed to fetch address data: ${error}`);
+ }
+
+ this.updateSearchingState(false);
+ // Restore focus.
+ activeElement.focus();
+ return data;
+ },
+
+ /**
+ * Start a timeout to abort a fetch request based on a time limit.
+ */
+ startAbortTimeout() {
+ this.timeoutId = setTimeout(() => {
+ gAbortController.abort();
+ this.showErrorNotification("account-provisioner-connection-timeout");
+ gAccountSetupLogger.warn("Connection timed out");
+ }, CONNECTION_TIMEOUT);
+ },
+
+ /**
+ * Clear any leftover timeout to prevent an unnecessary fetch abort.
+ */
+ clearAbortTimeout() {
+ if (this.timeoutId) {
+ window.clearTimeout(this.timeoutId);
+ this.timeoutId = null;
+ }
+ },
+
+ /**
+ * Always ensure the notification area is visible when a new notification is
+ * created.
+ */
+ ensureVisibleNotification() {
+ document.getElementById("accountProvisionerNotifications").scrollIntoView({
+ behavior: gReducedMotion ? "auto" : "smooth",
+ block: "start",
+ inline: "nearest",
+ });
+ },
+};