Bug 1476204 - Handle autofill record update state changes in the unpriv. PR forms. r=jaws

This is easier to understand as we don't have to round-trip the whole success and error states to the privileged wrapper which could potentially lead to stale state changes.
This is also much simpler for the basic-card-form as it doesn't need a lot of the complexity of the previous implementation.

* Move selectedStateKey from page to address-page since it doesn't apply to basic-card-page

MozReview-Commit-ID: B4kiZNWElGI

--HG--
extra : rebase_source : bcca13bbdc506961834e2e3cc078dad7d6ee7ca7
This commit is contained in:
Matthew Noorenberghe 2018-07-25 12:37:51 -07:00
parent 97013686ad
commit b0560084b3
11 changed files with 136 additions and 144 deletions

View File

@ -560,69 +560,52 @@ var paymentDialogWrapper = {
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;
}
}
let isTemporary = record.isTemporary;
let collection = isTemporary ? this.temporaryStore[collectionName] :
formAutofillStorage[collectionName];
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];
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);
}
},
@ -695,12 +678,7 @@ var paymentDialogWrapper = {
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;
}
}

View File

@ -243,7 +243,7 @@ export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequ
}
}
saveRecord() {
async saveRecord() {
let record = this.formHandler.buildFormObject();
let currentState = this.requestStore.getState();
let {
@ -258,22 +258,15 @@ export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequ
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",
@ -281,7 +274,7 @@ export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequ
},
};
} else {
state.successStateChange = {
successStateChange = {
page: {
id: previousId || "payment-summary",
onboardingWizard: page.onboardingWizard,
@ -290,11 +283,40 @@ export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequ
}
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,
},
});
}
}
}

View File

@ -144,10 +144,10 @@ export default class AddressPicker extends RichPicker {
let nextState = {
page: {
id: "address-page",
selectedStateKey: [this.selectedStateKey],
},
"address-page": {
addressFields: this.getAttribute("address-fields"),
selectedStateKey: [this.selectedStateKey],
},
};

View File

@ -207,10 +207,10 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
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": {
@ -288,11 +288,10 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
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;
@ -312,28 +311,24 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
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);
}
}

View File

@ -59,6 +59,7 @@
<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>

View File

@ -333,6 +333,11 @@ let buttonActions = {
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});
},

View File

@ -20,6 +20,7 @@ export let requestStore = new PaymentsStore({
},
"address-page": {
guid: null,
selectedStateKey: null,
title: "",
},
"payment-summary": {
@ -29,7 +30,6 @@ export let requestStore = new PaymentsStore({
previousId: null,
// onboardingWizard: true,
// error: "",
// selectedStateKey: "",
},
request: {
completeStatus: "",

View File

@ -11,6 +11,7 @@
/* import-globals-from unprivileged-fallbacks.js */
var paymentRequest = {
_nextMessageID: 1,
domReadyPromise: null,
init() {
@ -54,15 +55,23 @@ var paymentRequest = {
}
},
/**
* @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() {
@ -143,14 +152,14 @@ var paymentRequest = {
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 = {
@ -189,21 +198,30 @@ var paymentRequest = {
* @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);
}
});
});
},

View File

@ -37,7 +37,7 @@ add_task(async function test_onboarding_wizard_without_saved_addresses_and_saved
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");
@ -319,8 +319,8 @@ add_task(async function test_onboarding_wizard_with_requestShipping_turned_off()
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");
@ -438,8 +438,8 @@ add_task(async function test_back_button_on_basic_card_page_during_onboarding()
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");

View File

@ -140,22 +140,13 @@ add_task(async function test_saveButton() {
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",
@ -169,13 +160,6 @@ add_task(async function test_saveButton() {
"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();
});

View File

@ -117,18 +117,13 @@ add_task(async function test_saveButton() {
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,
@ -136,12 +131,6 @@ add_task(async function test_saveButton() {
"cc-number": "4111 1111-1111 1111",
"billingAddressGUID": "",
},
selectedStateKey: ["selectedPaymentCard"],
successStateChange: {
page: {
id: "payment-summary",
},
},
}, "Check event details for the message to chrome");
form.remove();
});