mirror of
https://github.com/Feodor2/Mypal68.git
synced 2025-06-18 14:55:44 -04:00
448 lines
14 KiB
JavaScript
448 lines
14 KiB
JavaScript
/* 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);
|