summaryrefslogtreecommitdiffstats
path: root/browser/components/payments/res/containers/address-form.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/payments/res/containers/address-form.js')
-rw-r--r--browser/components/payments/res/containers/address-form.js447
1 files changed, 447 insertions, 0 deletions
diff --git a/browser/components/payments/res/containers/address-form.js b/browser/components/payments/res/containers/address-form.js
new file mode 100644
index 0000000000..287251ea7d
--- /dev/null
+++ b/browser/components/payments/res/containers/address-form.js
@@ -0,0 +1,447 @@
+/* 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/. */
+
+/* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
+import LabelledCheckbox from "../components/labelled-checkbox.js";
+import PaymentRequestPage from "../components/payment-request-page.js";
+import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
+import paymentRequest from "../paymentRequest.js";
+import HandleEventMixin from "../mixins/HandleEventMixin.js";
+/* import-globals-from ../unprivileged-fallbacks.js */
+
+/**
+ * <address-form></address-form>
+ *
+ * Don't use document.getElementById or document.querySelector* to access form
+ * elements, use querySelector on `this` or `this.form` instead so that elements
+ * can be found before the element is connected.
+ *
+ * XXX: Bug 1446164 - This form isn't localized when used via this custom element
+ * as it will be much easier to share the logic once we switch to Fluent.
+ */
+
+export default class AddressForm extends HandleEventMixin(
+ PaymentStateSubscriberMixin(PaymentRequestPage)
+) {
+ constructor() {
+ super();
+
+ this.genericErrorText = document.createElement("div");
+ this.genericErrorText.setAttribute("aria-live", "polite");
+ this.genericErrorText.classList.add("page-error");
+
+ this.cancelButton = document.createElement("button");
+ this.cancelButton.className = "cancel-button";
+ this.cancelButton.addEventListener("click", this);
+
+ this.backButton = document.createElement("button");
+ this.backButton.className = "back-button";
+ this.backButton.addEventListener("click", this);
+
+ this.saveButton = document.createElement("button");
+ this.saveButton.className = "save-button primary";
+ this.saveButton.addEventListener("click", this);
+
+ this.persistCheckbox = new LabelledCheckbox();
+ this.persistCheckbox.className = "persist-checkbox";
+
+ // Combination of AddressErrors and PayerErrors as keys
+ this._errorFieldMap = {
+ addressLine: "#street-address",
+ city: "#address-level2",
+ country: "#country",
+ dependentLocality: "#address-level3",
+ email: "#email",
+ // Bug 1472283 is on file to support
+ // additional-name and family-name.
+ // XXX: For now payer name errors go on the family-name and address-errors
+ // go on the given-name so they don't overwrite each other.
+ name: "#family-name",
+ organization: "#organization",
+ phone: "#tel",
+ postalCode: "#postal-code",
+ // Bug 1472283 is on file to support
+ // additional-name and family-name.
+ recipient: "#given-name",
+ region: "#address-level1",
+ // Bug 1474905 is on file to properly support regionCode. See
+ // full note in paymentDialogWrapper.js
+ regionCode: "#address-level1",
+ };
+
+ // The markup is shared with form autofill preferences.
+ let url = "formautofill/editAddress.xhtml";
+ this.promiseReady = this._fetchMarkup(url).then(doc => {
+ this.form = doc.getElementById("form");
+ return this.form;
+ });
+ }
+
+ _fetchMarkup(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.responseType = "document";
+ xhr.addEventListener("error", reject);
+ xhr.addEventListener("load", evt => {
+ resolve(xhr.response);
+ });
+ xhr.open("GET", url);
+ xhr.send();
+ });
+ }
+
+ connectedCallback() {
+ this.promiseReady.then(form => {
+ this.body.appendChild(form);
+
+ let record = undefined;
+ this.formHandler = new EditAddress(
+ {
+ form,
+ },
+ record,
+ {
+ DEFAULT_REGION: PaymentDialogUtils.DEFAULT_REGION,
+ getFormFormat: PaymentDialogUtils.getFormFormat,
+ findAddressSelectOption: PaymentDialogUtils.findAddressSelectOption,
+ countries: PaymentDialogUtils.countries,
+ }
+ );
+
+ // The EditAddress constructor adds `input` event listeners on the same element,
+ // which update field validity. By adding our event listeners after this constructor,
+ // validity will be updated before our handlers get the event
+ this.form.addEventListener("input", this);
+ this.form.addEventListener("invalid", this);
+ this.form.addEventListener("change", this);
+
+ // The "invalid" event does not bubble and needs to be listened for on each
+ // form element.
+ for (let field of this.form.elements) {
+ field.addEventListener("invalid", this);
+ }
+
+ this.body.appendChild(this.persistCheckbox);
+ this.body.appendChild(this.genericErrorText);
+
+ this.footer.appendChild(this.cancelButton);
+ this.footer.appendChild(this.backButton);
+ this.footer.appendChild(this.saveButton);
+ // Only call the connected super callback(s) once our markup is fully
+ // connected, including the shared form fetched asynchronously.
+ super.connectedCallback();
+ });
+ }
+
+ render(state) {
+ if (!this.id) {
+ throw new Error("AddressForm without an id");
+ }
+ let record;
+ let { page, [this.id]: addressPage } = state;
+
+ if (this.id && page && page.id !== this.id) {
+ log.debug(`${this.id}: no need to further render inactive page`);
+ return;
+ }
+
+ let editing = !!addressPage.guid;
+ this.cancelButton.textContent = this.dataset.cancelButtonLabel;
+ this.backButton.textContent = this.dataset.backButtonLabel;
+ if (editing) {
+ this.saveButton.textContent = this.dataset.updateButtonLabel;
+ } else {
+ this.saveButton.textContent = this.dataset.nextButtonLabel;
+ }
+
+ this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
+ this.persistCheckbox.infoTooltip = this.dataset.persistCheckboxInfoTooltip;
+
+ this.backButton.hidden = page.onboardingWizard;
+ this.cancelButton.hidden = !page.onboardingWizard;
+
+ this.pageTitleHeading.textContent = editing
+ ? this.dataset.titleEdit
+ : this.dataset.titleAdd;
+ this.genericErrorText.textContent = page.error;
+
+ let addresses = paymentRequest.getAddresses(state);
+
+ // If an address is selected we want to edit it.
+ if (editing) {
+ record = addresses[addressPage.guid];
+ if (!record) {
+ throw new Error(
+ "Trying to edit a non-existing address: " + addressPage.guid
+ );
+ }
+ // When editing an existing record, prevent changes to persistence
+ this.persistCheckbox.hidden = true;
+ } else {
+ let {
+ saveAddressDefaultChecked,
+ } = PaymentDialogUtils.getDefaultPreferences();
+ if (typeof saveAddressDefaultChecked != "boolean") {
+ throw new Error(`Unexpected non-boolean value for saveAddressDefaultChecked from
+ PaymentDialogUtils.getDefaultPreferences(): ${typeof saveAddressDefaultChecked}`);
+ }
+ // Adding a new record: default persistence to the pref value when in a not-private session
+ this.persistCheckbox.hidden = false;
+ this.persistCheckbox.checked = state.isPrivate
+ ? false
+ : saveAddressDefaultChecked;
+ }
+
+ let selectedStateKey = this.getAttribute("selected-state-key").split("|");
+ log.debug(`${this.id}#render got selectedStateKey: ${selectedStateKey}`);
+
+ if (addressPage.addressFields) {
+ this.form.dataset.addressFields = addressPage.addressFields;
+ } else {
+ this.form.dataset.addressFields = "mailing-address tel";
+ }
+ this.formHandler.loadRecord(record);
+
+ // Add validation to some address fields
+ this.updateRequiredState();
+
+ // Show merchant errors for the appropriate address form.
+ let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(
+ state,
+ selectedStateKey
+ );
+ for (let [errorName, errorSelector] of Object.entries(
+ this._errorFieldMap
+ )) {
+ let errorText = "";
+ // Never show errors on an 'add' screen as they would be for a different address.
+ if (editing && merchantFieldErrors) {
+ if (errorName == "region" || errorName == "regionCode") {
+ errorText =
+ merchantFieldErrors.regionCode || merchantFieldErrors.region || "";
+ } else {
+ errorText = merchantFieldErrors[errorName] || "";
+ }
+ }
+ let container = this.form.querySelector(errorSelector + "-container");
+ let field = this.form.querySelector(errorSelector);
+ field.setCustomValidity(errorText);
+ let span = paymentRequest.maybeCreateFieldErrorElement(container);
+ span.textContent = errorText;
+ }
+
+ this.updateSaveButtonState();
+ }
+
+ onChange(event) {
+ if (event.target.id == "country") {
+ this.updateRequiredState();
+ }
+ this.updateSaveButtonState();
+ }
+
+ onInvalid(event) {
+ if (event.target instanceof HTMLFormElement) {
+ this.onInvalidForm(event);
+ } else {
+ this.onInvalidField(event);
+ }
+ }
+
+ onClick(evt) {
+ switch (evt.target) {
+ case this.cancelButton: {
+ paymentRequest.cancel();
+ break;
+ }
+ case this.backButton: {
+ let currentState = this.requestStore.getState();
+ const previousId = currentState.page.previousId;
+ let state = {
+ page: {
+ id: previousId || "payment-summary",
+ },
+ };
+ if (previousId) {
+ state[previousId] = Object.assign({}, currentState[previousId], {
+ preserveFieldValues: true,
+ });
+ }
+ this.requestStore.setState(state);
+ break;
+ }
+ case this.saveButton: {
+ if (this.form.checkValidity()) {
+ this.saveRecord();
+ }
+ break;
+ }
+ default: {
+ throw new Error("Unexpected click target");
+ }
+ }
+ }
+
+ onInput(event) {
+ event.target.setCustomValidity("");
+ this.updateSaveButtonState();
+ }
+
+ /**
+ * @param {Event} event - "invalid" event
+ * Note: Keep this in-sync with the equivalent version in basic-card-form.js
+ */
+ onInvalidField(event) {
+ let field = event.target;
+ let container = field.closest(`#${field.id}-container`);
+ let errorTextSpan = paymentRequest.maybeCreateFieldErrorElement(container);
+ errorTextSpan.textContent = field.validationMessage;
+ }
+
+ onInvalidForm() {
+ this.saveButton.disabled = true;
+ }
+
+ updateRequiredState() {
+ for (let field of this.form.elements) {
+ let container = field.closest(`#${field.id}-container`);
+ if (field.localName == "button" || !container) {
+ continue;
+ }
+ let span = container.querySelector(".label-text");
+ span.setAttribute(
+ "fieldRequiredSymbol",
+ this.dataset.fieldRequiredSymbol
+ );
+ container.toggleAttribute("required", field.required && !field.disabled);
+ }
+ }
+
+ updateSaveButtonState() {
+ this.saveButton.disabled = !this.form.checkValidity();
+ }
+
+ async saveRecord() {
+ let record = this.formHandler.buildFormObject();
+ let currentState = this.requestStore.getState();
+ let {
+ page,
+ tempAddresses,
+ savedBasicCards,
+ [this.id]: addressPage,
+ } = currentState;
+ let editing = !!addressPage.guid;
+
+ if (
+ editing
+ ? addressPage.guid in tempAddresses
+ : !this.persistCheckbox.checked
+ ) {
+ record.isTemporary = true;
+ }
+
+ let successStateChange;
+ const previousId = page.previousId;
+ if (page.onboardingWizard && !Object.keys(savedBasicCards).length) {
+ successStateChange = {
+ "basic-card-page": {
+ selectedStateKey: "selectedPaymentCard",
+ // Preserve field values as the user may have already edited the card
+ // page and went back to the address page to make a correction.
+ preserveFieldValues: true,
+ },
+ page: {
+ id: "basic-card-page",
+ previousId: this.id,
+ onboardingWizard: page.onboardingWizard,
+ },
+ };
+ } else {
+ successStateChange = {
+ page: {
+ id: previousId || "payment-summary",
+ onboardingWizard: page.onboardingWizard,
+ },
+ };
+ }
+
+ if (previousId) {
+ successStateChange[previousId] = Object.assign(
+ {},
+ currentState[previousId]
+ );
+ successStateChange[previousId].preserveFieldValues = true;
+ }
+
+ try {
+ let { guid } = await paymentRequest.updateAutofillRecord(
+ "addresses",
+ record,
+ addressPage.guid
+ );
+ let selectedStateKey = this.getAttribute("selected-state-key").split("|");
+
+ if (selectedStateKey.length == 1) {
+ Object.assign(successStateChange, {
+ [selectedStateKey[0]]: guid,
+ });
+ } else if (selectedStateKey.length == 2) {
+ // Need to keep properties like preserveFieldValues from getting removed.
+ let subObj = Object.assign({}, successStateChange[selectedStateKey[0]]);
+ subObj[selectedStateKey[1]] = guid;
+ Object.assign(successStateChange, {
+ [selectedStateKey[0]]: subObj,
+ });
+ } else {
+ throw new Error(
+ `selectedStateKey not supported: '${selectedStateKey}'`
+ );
+ }
+
+ this.requestStore.setState(successStateChange);
+ } catch (ex) {
+ log.warn("saveRecord: error:", ex);
+ this.requestStore.setState({
+ page: {
+ id: this.id,
+ onboardingWizard: page.onboardingWizard,
+ error: this.dataset.errorGenericSave,
+ },
+ });
+ }
+ }
+
+ /**
+ * Get the dictionary of field-specific merchant errors relevant to the
+ * specific form identified by the state key.
+ * @param {object} state The application state
+ * @param {string[]} stateKey The key in state to return address errors for.
+ * @returns {object} with keys as PaymentRequest field names and values of
+ * merchant-provided error strings.
+ */
+ static merchantFieldErrorsForForm(state, stateKey) {
+ let { paymentDetails } = state.request;
+ switch (stateKey.join("|")) {
+ case "selectedShippingAddress": {
+ return paymentDetails.shippingAddressErrors;
+ }
+ case "selectedPayerAddress": {
+ return paymentDetails.payerErrors;
+ }
+ case "basic-card-page|billingAddressGUID": {
+ // `paymentMethod` can be null.
+ return (
+ (paymentDetails.paymentMethodErrors &&
+ paymentDetails.paymentMethodErrors.billingAddress) ||
+ {}
+ );
+ }
+ default: {
+ throw new Error("Unknown selectedStateKey");
+ }
+ }
+ }
+}
+
+customElements.define("address-form", AddressForm);