--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -555,79 +555,62 @@ var paymentDialogWrapper = {
paymentSrv.changeShippingOption(this.request.requestId, optionID);
},
onCloseDialogMessage() {
// The PR is complete(), just close the dialog
window.close();
},
- async onUpdateAutofillRecord(collectionName, record, guid, {
- errorStateChange,
- preserveOldProperties,
- selectedStateKey,
- successStateChange,
- }) {
- if (collectionName == "creditCards" && !guid && !record.isTemporary) {
- // We need to be logged in so we can encrypt the credit card number and
- // that's only supported when we're adding a new record.
- // TODO: "MasterPassword.ensureLoggedIn" can be removed after the storage
- // APIs are refactored to be async functions (bug 1399367).
- if (!await MasterPassword.ensureLoggedIn()) {
- Cu.reportError("User canceled master password entry");
- return;
+ async onUpdateAutofillRecord(collectionName, record, guid, messageID) {
+ let responseMessage = {
+ guid,
+ messageID,
+ stateChange: {},
+ };
+ try {
+ if (collectionName == "creditCards" && !guid && !record.isTemporary) {
+ // We need to be logged in so we can encrypt the credit card number and
+ // that's only supported when we're adding a new record.
+ // TODO: "MasterPassword.ensureLoggedIn" can be removed after the storage
+ // APIs are refactored to be async functions (bug 1399367).
+ if (!await MasterPassword.ensureLoggedIn()) {
+ throw new Error("User canceled master password entry");
+ }
}
- }
- let isTemporary = record.isTemporary;
- let collection = isTemporary ? this.temporaryStore[collectionName] :
- formAutofillStorage[collectionName];
- try {
+ let isTemporary = record.isTemporary;
+ let collection = isTemporary ? this.temporaryStore[collectionName] :
+ formAutofillStorage[collectionName];
+
if (guid) {
+ let preserveOldProperties = true;
await collection.update(guid, record, preserveOldProperties);
} else {
- guid = await collection.add(record);
+ responseMessage.guid = await collection.add(record);
}
if (isTemporary && collectionName == "addresses") {
// there will be no formautofill-storage-changed event to update state
// so add updated collection here
- Object.assign(successStateChange, {
+ Object.assign(responseMessage.stateChange, {
tempAddresses: this.temporaryStore.addresses.getAll(),
});
}
if (isTemporary && collectionName == "creditCards") {
// there will be no formautofill-storage-changed event to update state
// so add updated collection here
- Object.assign(successStateChange, {
+ Object.assign(responseMessage.stateChange, {
tempBasicCards: this.temporaryStore.creditCards.getAll(),
});
}
-
- // Select the new record
- if (selectedStateKey) {
- 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.sendMessageToContent("updateState", successStateChange);
} catch (ex) {
- this.sendMessageToContent("updateState", errorStateChange);
+ responseMessage.error = true;
+ } finally {
+ this.sendMessageToContent("updateAutofillRecord:Response", responseMessage);
}
},
/**
* @implement {nsIDOMEventListener}
* @param {Event} event
*/
handleEvent(event) {
@@ -690,22 +673,17 @@ var paymentDialogWrapper = {
this.onPaymentCancel();
break;
}
case "pay": {
this.onPay(data);
break;
}
case "updateAutofillRecord": {
- this.onUpdateAutofillRecord(data.collectionName, data.record, data.guid, {
- errorStateChange: data.errorStateChange,
- preserveOldProperties: data.preserveOldProperties,
- selectedStateKey: data.selectedStateKey,
- successStateChange: data.successStateChange,
- });
+ this.onUpdateAutofillRecord(data.collectionName, data.record, data.guid, data.messageID);
break;
}
}
},
};
if ("document" in this) {
// Running in a browser, not a unit test
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -238,64 +238,86 @@ export default class AddressForm extends
break;
}
default: {
throw new Error("Unexpected click target");
}
}
}
- saveRecord() {
+ async saveRecord() {
let record = this.formHandler.buildFormObject();
let currentState = this.requestStore.getState();
let {
page,
tempAddresses,
savedBasicCards,
"address-page": addressPage,
} = currentState;
let editing = !!addressPage.guid;
if (editing ? (addressPage.guid in tempAddresses) : !this.persistCheckbox.checked) {
record.isTemporary = true;
}
- let state = {
- errorStateChange: {
- page: {
- id: "address-page",
- onboardingWizard: page.onboardingWizard,
- error: this.dataset.errorGenericSave,
- },
- "address-page": addressPage,
- },
- preserveOldProperties: true,
- selectedStateKey: page.selectedStateKey,
- };
-
+ let successStateChange;
const previousId = page.previousId;
if (page.onboardingWizard && !Object.keys(savedBasicCards).length) {
- state.successStateChange = {
+ successStateChange = {
+ "basic-card-page": {
+ // 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: "address-page",
onboardingWizard: page.onboardingWizard,
},
};
} else {
- state.successStateChange = {
+ successStateChange = {
page: {
id: previousId || "payment-summary",
onboardingWizard: page.onboardingWizard,
},
};
}
if (previousId) {
- state.successStateChange[previousId] = Object.assign({}, currentState[previousId]);
- state.successStateChange[previousId].preserveFieldValues = true;
+ successStateChange[previousId] = Object.assign({}, currentState[previousId]);
+ successStateChange[previousId].preserveFieldValues = true;
}
- paymentRequest.updateAutofillRecord("addresses", record, addressPage.guid, state);
+ try {
+ let {guid} = await paymentRequest.updateAutofillRecord("addresses", record, addressPage.guid);
+ let selectedStateKey = addressPage.selectedStateKey;
+
+ 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: "address-page",
+ onboardingWizard: page.onboardingWizard,
+ error: this.dataset.errorGenericSave,
+ },
+ });
+ }
}
}
customElements.define("address-form", AddressForm);
--- a/browser/components/payments/res/containers/address-picker.js
+++ b/browser/components/payments/res/containers/address-picker.js
@@ -139,20 +139,20 @@ export default class AddressPicker exten
});
}
}
onClick({target}) {
let nextState = {
page: {
id: "address-page",
- selectedStateKey: [this.selectedStateKey],
},
"address-page": {
addressFields: this.getAttribute("address-fields"),
+ selectedStateKey: [this.selectedStateKey],
},
};
switch (target) {
case this.addLink: {
nextState["address-page"].guid = null;
nextState["address-page"].title = this.dataset.addAddressTitle;
break;
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -202,20 +202,20 @@ export default class BasicCardForm exten
case this.addressEditLink: {
let {
"basic-card-page": basicCardPage,
} = this.requestStore.getState();
let nextState = {
page: {
id: "address-page",
previousId: "basic-card-page",
- selectedStateKey: ["basic-card-page", "billingAddressGUID"],
},
"address-page": {
guid: null,
+ selectedStateKey: ["basic-card-page", "billingAddressGUID"],
title: this.dataset.billingAddressTitleAdd,
},
"basic-card-page": {
preserveFieldValues: true,
guid: basicCardPage.guid,
},
};
let billingAddressGUID = this.form.querySelector("#billingAddressGUID");
@@ -283,21 +283,20 @@ export default class BasicCardForm exten
onInvalid(event) {
this.saveButton.disabled = true;
}
updateSaveButtonState() {
this.saveButton.disabled = !this.form.checkValidity();
}
- saveRecord() {
+ async saveRecord() {
let record = this.formHandler.buildFormObject();
let currentState = this.requestStore.getState();
let {
- page,
tempBasicCards,
"basic-card-page": basicCardPage,
} = currentState;
let editing = !!basicCardPage.guid;
if (editing ? (basicCardPage.guid in tempBasicCards) : !this.persistCheckbox.checked) {
record.isTemporary = true;
}
@@ -307,34 +306,30 @@ export default class BasicCardForm exten
}
// Only save the card number if we're saving a new record, otherwise we'd
// overwrite the unmasked card number with the masked one.
if (!editing) {
record["cc-number"] = record["cc-number"] || "";
}
- let state = {
- errorStateChange: {
+ try {
+ let {guid} = await paymentRequest.updateAutofillRecord("creditCards", record,
+ basicCardPage.guid);
+ this.requestStore.setState({
+ page: {
+ id: "payment-summary",
+ },
+ selectedPaymentCard: guid,
+ });
+ } catch (ex) {
+ log.warn("saveRecord: error:", ex);
+ this.requestStore.setState({
page: {
id: "basic-card-page",
error: this.dataset.errorGenericSave,
},
- },
- preserveOldProperties: true,
- selectedStateKey: ["selectedPaymentCard"],
- successStateChange: {
- page: {
- id: "payment-summary",
- },
- },
- };
-
- const previousId = page.previousId;
- if (previousId) {
- state.successStateChange[previousId] = Object.assign({}, currentState[previousId]);
+ });
}
-
- paymentRequest.updateAutofillRecord("creditCards", record, basicCardPage.guid, state);
}
}
customElements.define("basic-card-form", BasicCardForm);
--- a/browser/components/payments/res/debugging.html
+++ b/browser/components/payments/res/debugging.html
@@ -54,16 +54,17 @@
<label class="block"><input type="radio" name="setCompleteStatus" value="timeout">Timeout</label>
</fieldset>
<label class="block"><input type="checkbox" id="setChangesPrevented">Prevent changes</label>
<section class="group">
<fieldset>
<legend>User Data Errors</legend>
+ <button id="saveVisibleForm" title="Bypasses field validation">Save Visible Form</button>
<button id="setShippingError">Shipping Error</button>
<button id="setAddressErrors">Address Errors</button>
</fieldset>
</section>
</section>
</div>
</body>
</html>
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -328,16 +328,21 @@ let buttonActions = {
refresh() {
window.parent.location.reload(true);
},
rerender() {
requestStore.setState({});
},
+ saveVisibleForm() {
+ // Bypasses field validation which is useful to test error handling.
+ paymentDialog.querySelector("#main-container > .page:not([hidden])").saveRecord();
+ },
+
setAddresses1() {
paymentDialog.setStateFromParent({savedAddresses: ADDRESSES_1});
},
setDupesAddresses() {
paymentDialog.setStateFromParent({savedAddresses: DUPED_ADDRESSES});
},
--- a/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
+++ b/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -15,26 +15,26 @@ export let requestStore = new PaymentsSt
changesPrevented: false,
orderDetailsShowing: false,
"basic-card-page": {
guid: null,
// preserveFieldValues: true,
},
"address-page": {
guid: null,
+ selectedStateKey: null,
title: "",
},
"payment-summary": {
},
page: {
id: "payment-summary",
previousId: null,
// onboardingWizard: true,
// error: "",
- // selectedStateKey: "",
},
request: {
completeStatus: "",
tabId: null,
topLevelPrincipal: {URI: {displayHost: null}},
requestId: null,
paymentMethods: [],
paymentDetails: {
--- a/browser/components/payments/res/paymentRequest.js
+++ b/browser/components/payments/res/paymentRequest.js
@@ -6,16 +6,17 @@
* Loaded in the unprivileged frame of each payment dialog.
*
* Communicates with privileged code via DOM Events.
*/
/* import-globals-from unprivileged-fallbacks.js */
var paymentRequest = {
+ _nextMessageID: 1,
domReadyPromise: null,
init() {
// listen to content
window.addEventListener("paymentChromeToContent", this);
window.addEventListener("keydown", this);
@@ -49,25 +50,33 @@ var paymentRequest = {
break;
}
default: {
throw new Error("Unexpected event type");
}
}
},
+ /**
+ * @param {string} messageType
+ * @param {[object]} detail
+ * @returns {number} message ID to be able to identify a reply (where applicable).
+ */
sendMessageToChrome(messageType, detail = {}) {
- log.debug("sendMessageToChrome:", messageType, detail);
+ let messageID = this._nextMessageID++;
+ log.debug("sendMessageToChrome:", messageType, messageID, detail);
let event = new CustomEvent("paymentContentToChrome", {
bubbles: true,
detail: Object.assign({
messageType,
+ messageID,
}, detail),
});
document.dispatchEvent(event);
+ return messageID;
},
toggleDebuggingConsole() {
let debuggingConsole = document.getElementById("debugging-console");
if (debuggingConsole.hidden && !debuggingConsole.src) {
debuggingConsole.src = "debugging.html";
}
debuggingConsole.hidden = !debuggingConsole.hidden;
@@ -138,24 +147,24 @@ var paymentRequest = {
state["address-page"] = {
addressFields: null,
guid: null,
};
if (shippingRequested) {
Object.assign(state["address-page"], {
+ selectedStateKey: ["selectedShippingAddress"],
title: paymentDialog.dataset.shippingAddressTitleAdd,
});
- state.page.selectedStateKey = ["selectedShippingAddress"];
} else {
Object.assign(state["address-page"], {
+ selectedStateKey: ["basic-card-page", "billingAddressGUID"],
title: paymentDialog.dataset.billingAddressTitleAdd,
});
- state.page.selectedStateKey = ["basic-card-page", "billingAddressGUID"];
}
} else if (!hasSavedCards) {
state.page = {
id: "basic-card-page",
onboardingWizard: true,
};
}
@@ -184,31 +193,40 @@ var paymentRequest = {
/**
* Add/update an autofill storage record.
*
* If the the `guid` argument is provided update the record; otherwise, add it.
* @param {string} collectionName The autofill collection that record belongs to.
* @param {object} record The autofill record to add/update
* @param {string} [guid] The guid of the autofill record to update
+ * @returns {Promise} when the update response is received
*/
- updateAutofillRecord(collectionName, record, guid, {
- errorStateChange,
- preserveOldProperties,
- selectedStateKey,
- successStateChange,
- }) {
- this.sendMessageToChrome("updateAutofillRecord", {
- collectionName,
- guid,
- record,
- errorStateChange,
- preserveOldProperties,
- selectedStateKey,
- successStateChange,
+ updateAutofillRecord(collectionName, record, guid) {
+ return new Promise((resolve, reject) => {
+ let messageID = this.sendMessageToChrome("updateAutofillRecord", {
+ collectionName,
+ guid,
+ record,
+ });
+
+ window.addEventListener("paymentChromeToContent", function onMsg({detail}) {
+ if (detail.messageType != "updateAutofillRecord:Response"
+ || detail.messageID != messageID) {
+ return;
+ }
+ log.debug("updateAutofillRecord: response:", detail);
+ window.removeEventListener("paymentChromeToContent", onMsg);
+ document.querySelector("payment-dialog").setStateFromParent(detail.stateChange);
+ if (detail.error) {
+ reject(detail);
+ } else {
+ resolve(detail);
+ }
+ });
});
},
/**
* @param {object} state object representing the UI state
* @param {string} methodID (GUID) uniquely identifying the selected payment method
* @returns {object?} the applicable modifier for the payment method
*/
--- a/browser/components/payments/test/browser/browser_payments_onboarding_wizard.js
+++ b/browser/components/payments/test/browser/browser_payments_onboarding_wizard.js
@@ -32,17 +32,17 @@ add_task(async function test_onboarding_
await spawnPaymentDialogTask(frame, async function() {
let {
PaymentTestUtils: PTU,
} = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "address-page" &&
- state.page.selectedStateKey[0] == "selectedShippingAddress";
+ state["address-page"].selectedStateKey[0] == "selectedShippingAddress";
}, "Address page is shown first during on-boarding if there are no saved addresses");
info("Checking if the address page has been rendered");
let addressSaveButton = content.document.querySelector("address-form .save-button");
ok(content.isVisible(addressSaveButton), "Address save button is rendered");
info("Check if the total header is visible on the address page during on-boarding");
let header = content.document.querySelector("header");
@@ -314,18 +314,18 @@ add_task(async function test_onboarding_
await spawnPaymentDialogTask(frame, async function() {
let {
PaymentTestUtils: PTU,
} = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "address-page" &&
- state.page.selectedStateKey[0] == "basic-card-page" &&
- state.page.selectedStateKey[1] == "billingAddressGUID";
+ state["address-page"].selectedStateKey[0] == "basic-card-page" &&
+ state["address-page"].selectedStateKey[1] == "billingAddressGUID";
// eslint-disable-next-line max-len
}, "Billing address page is shown first during on-boarding if requestShipping is turned off");
info("Checking if the billing address page has been rendered");
let addressSaveButton = content.document.querySelector("address-form .save-button");
ok(content.isVisible(addressSaveButton),
"Address save button is rendered");
@@ -433,18 +433,18 @@ add_task(async function test_back_button
await spawnPaymentDialogTask(frame, async function() {
let {
PaymentTestUtils: PTU,
} = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "address-page";
- }, "Address page is shown first if there are saved addresses during on boarding");
-
+ }, "Billing address page is shown first if there are no saved addresses " +
+ "and requestShipping is false during on boarding");
info("Checking if the address page has been rendered");
let addressSaveButton = content.document.querySelector("address-form .save-button");
ok(content.isVisible(addressSaveButton), "Address save button is rendered");
for (let [key, val] of Object.entries(PTU.Addresses.TimBL2)) {
let field = content.document.getElementById(key);
if (!field) {
ok(false, `${key} field not found`);
--- a/browser/components/payments/test/mochitest/test_address_form.html
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -135,52 +135,36 @@ add_task(async function test_saveButton(
sendString("+15555551212");
let messagePromise = promiseContentToChromeMessage("updateAutofillRecord");
is(form.saveButton.textContent, "Save", "Check label");
form.saveButton.scrollIntoView();
synthesizeMouseAtCenter(form.saveButton, {});
let details = await messagePromise;
+ ok(typeof(details.messageID) == "number" && details.messageID > 0, "Check messageID type");
+ delete details.messageID;
is(details.collectionName, "addresses", "Check collectionName");
isDeeply(details, {
collectionName: "addresses",
- errorStateChange: {
- page: {
- id: "address-page",
- error: "Generic error",
- onboardingWizard: undefined,
- },
- "address-page": {
- title: "Sample page title",
- },
- },
guid: undefined,
messageType: "updateAutofillRecord",
- preserveOldProperties: true,
record: {
"given-name": "Jaws",
"family-name": "Swaj",
"additional-name": "",
"organization": "Allizom",
"street-address": "404 Internet Super Highway",
"address-level2": "Firefoxity City",
"address-level1": "CA",
"postal-code": "00001",
"country": "US",
"email": "test@example.com",
"tel": "+15555551212",
},
- selectedStateKey: undefined,
- successStateChange: {
- page: {
- id: "payment-summary",
- onboardingWizard: undefined,
- },
- },
}, "Check event details for the message to chrome");
form.remove();
});
add_task(async function test_genericError() {
let form = new AddressForm();
await form.requestStore.setState({
page: {
--- a/browser/components/payments/test/mochitest/test_basic_card_form.html
+++ b/browser/components/payments/test/mochitest/test_basic_card_form.html
@@ -112,41 +112,30 @@ add_task(async function test_saveButton(
ok(!form.saveButton.disabled,
"Save button should be enabled since the required fields are filled");
let messagePromise = promiseContentToChromeMessage("updateAutofillRecord");
is(form.saveButton.textContent, "Save", "Check label");
synthesizeMouseAtCenter(form.saveButton, {});
let details = await messagePromise;
+ ok(typeof(details.messageID) == "number" && details.messageID > 0, "Check messageID type");
+ delete details.messageID;
is(details.collectionName, "creditCards", "Check collectionName");
isDeeply(details, {
collectionName: "creditCards",
- errorStateChange: {
- page: {
- id: "basic-card-page",
- error: "Generic error",
- },
- },
guid: undefined,
messageType: "updateAutofillRecord",
- preserveOldProperties: true,
record: {
"cc-exp-month": "11",
"cc-exp-year": year,
"cc-name": "J. Smith",
"cc-number": "4111 1111-1111 1111",
"billingAddressGUID": "",
},
- selectedStateKey: ["selectedPaymentCard"],
- successStateChange: {
- page: {
- id: "payment-summary",
- },
- },
}, "Check event details for the message to chrome");
form.remove();
});
add_task(async function test_genericError() {
let form = new BasicCardForm();
await form.requestStore.setState({
page: {