--- a/browser/components/payments/content/paymentDialogWrapper.js
+++ b/browser/components/payments/content/paymentDialogWrapper.js
@@ -30,35 +30,62 @@ XPCOMUtils.defineLazyGetter(this, "formA
} catch (ex) {
storage = null;
Cu.reportError(ex);
}
return storage;
});
+class TempCollection {
+ constructor(data = {}) {
+ this._data = data;
+ }
+ get(guid) {
+ return this._data[guid];
+ }
+ update(guid, record, preserveOldProperties) {
+ if (preserveOldProperties) {
+ Object.assign(this._data[guid], record);
+ } else {
+ this._data[guid] = record;
+ }
+ return this._data[guid];
+ }
+ add(record) {
+ let guid = "temp-" + Math.abs(Math.random() * 0xffffffff|0);
+ this._data[guid] = record;
+ return guid;
+ }
+ getAll() {
+ return this._data;
+ }
+}
+
var paymentDialogWrapper = {
componentsLoaded: new Map(),
frame: null,
mm: null,
request: null,
+ temporaryStore: null,
QueryInterface: ChromeUtils.generateQI([
Ci.nsIObserver,
Ci.nsISupportsWeakReference,
]),
/**
* Note: This method is async because formAutofillStorage plans to become async.
*
* @param {string} guid
* @returns {object} containing only the requested payer values.
*/
async _convertProfileAddressToPayerData(guid) {
- let addressData = formAutofillStorage.addresses.get(guid);
+ let addressData = this.temporaryStore.addresses.get(guid) ||
+ formAutofillStorage.addresses.get(guid);
if (!addressData) {
throw new Error(`Payer address not found: ${guid}`);
}
let {
requestPayerName,
requestPayerEmail,
requestPayerPhone,
@@ -75,17 +102,18 @@ var paymentDialogWrapper = {
/**
* Note: This method is async because formAutofillStorage plans to become async.
*
* @param {string} guid
* @returns {nsIPaymentAddress}
*/
async _convertProfileAddressToPaymentAddress(guid) {
- let addressData = formAutofillStorage.addresses.get(guid);
+ let addressData = this.temporaryStore.addresses.get(guid) ||
+ formAutofillStorage.addresses.get(guid);
if (!addressData) {
throw new Error(`Shipping address not found: ${guid}`);
}
let address = this.createPaymentAddress({
country: addressData.country,
addressLines: addressData["street-address"].split("\n"),
region: addressData["address-level1"],
@@ -102,30 +130,35 @@ var paymentDialogWrapper = {
/**
* @param {string} guid The GUID of the basic card record from storage.
* @param {string} cardSecurityCode The associated card security code (CVV/CCV/etc.)
* @throws if the user cancels entering their master password or an error decrypting
* @returns {nsIBasicCardResponseData?} returns response data or null (if the
* master password dialog was cancelled);
*/
async _convertProfileBasicCardToPaymentMethodData(guid, cardSecurityCode) {
- let cardData = formAutofillStorage.creditCards.get(guid);
+ let cardData = this.temporaryStore.creditCards.get(guid) ||
+ formAutofillStorage.creditCards.get(guid);
if (!cardData) {
throw new Error(`Basic card not found in storage: ${guid}`);
}
let cardNumber;
- try {
- cardNumber = await MasterPassword.decrypt(cardData["cc-number-encrypted"], true);
- } catch (ex) {
- if (ex.result != Cr.NS_ERROR_ABORT) {
- throw ex;
+ if (cardData.isTemporary) {
+ cardNumber = cardData["cc-number"];
+ } else {
+ try {
+ cardNumber = await MasterPassword.decrypt(cardData["cc-number-encrypted"], true);
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_ABORT) {
+ throw ex;
+ }
+ // User canceled master password entry
+ return null;
}
- // User canceled master password entry
- return null;
}
let billingAddressGUID = cardData.billingAddressGUID;
let billingAddress;
try {
billingAddress = await this._convertProfileAddressToPaymentAddress(billingAddressGUID);
} catch (ex) {
// The referenced address may not exist if it was deleted or hasn't yet synced to this profile
@@ -159,16 +192,21 @@ var paymentDialogWrapper = {
this.frame = frame;
this.mm = frame.frameLoader.messageManager;
this.mm.addMessageListener("paymentContentToChrome", this);
this.mm.loadFrameScript("chrome://payments/content/paymentDialogFrameScript.js", true);
if (AppConstants.platform == "win") {
this.frame.setAttribute("selectmenulist", "ContentSelectDropdown-windows");
}
this.frame.loadURI("resource://payments/paymentRequest.xhtml");
+
+ this.temporaryStore = {
+ addresses: new TempCollection(),
+ creditCards: new TempCollection(),
+ };
},
createShowResponse({
acceptStatus,
methodName = "",
methodData = null,
payerName = "",
payerEmail = "",
@@ -390,17 +428,19 @@ var paymentDialogWrapper = {
initializeFrame() {
let requestSerialized = this._serializeRequest(this.request);
let chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
let isPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWindow);
this.sendMessageToContent("showPaymentRequest", {
request: requestSerialized,
savedAddresses: this.fetchSavedAddresses(),
+ tempAddresses: this.temporaryStore.addresses.getAll(),
savedBasicCards: this.fetchSavedPaymentCards(),
+ tempBasicCards: this.temporaryStore.creditCards.getAll(),
isPrivate,
});
Services.obs.addObserver(this, "formautofill-storage-changed", true);
},
debugFrame() {
// To avoid self-XSS-type attacks, ensure that Browser Chrome debugging is enabled.
@@ -493,32 +533,51 @@ var paymentDialogWrapper = {
},
async onUpdateAutofillRecord(collectionName, record, guid, {
errorStateChange,
preserveOldProperties,
selectedStateKey,
successStateChange,
}) {
- if (collectionName == "creditCards" && !guid) {
+ 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];
+
try {
if (guid) {
- await formAutofillStorage[collectionName].update(guid, record, preserveOldProperties);
+ await collection.update(guid, record, preserveOldProperties);
} else {
- guid = await formAutofillStorage[collectionName].add(record);
+ 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, {
+ 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, {
+ tempBasicCards: this.temporaryStore.creditCards.getAll(),
+ });
}
// Select the new record
if (selectedStateKey) {
Object.assign(successStateChange, {
[selectedStateKey]: guid,
});
}
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -1,13 +1,14 @@
/* 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 PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
import paymentRequest from "../paymentRequest.js";
/* import-globals-from ../unprivileged-fallbacks.js */
/**
* <address-form></address-form>
*
* XXX: Bug 1446164 - This form isn't localized when used via this custom element
@@ -28,16 +29,18 @@ export default class AddressForm extends
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";
this.saveButton.addEventListener("click", this);
+ this.persistCheckbox = new LabelledCheckbox();
+
// 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;
});
}
@@ -63,57 +66,70 @@ export default class AddressForm extends
this.formHandler = new EditAddress({
form,
}, record, {
DEFAULT_REGION: PaymentDialogUtils.DEFAULT_REGION,
getFormFormat: PaymentDialogUtils.getFormFormat,
supportedCountries: PaymentDialogUtils.supportedCountries,
});
+ this.appendChild(this.persistCheckbox);
this.appendChild(this.genericErrorText);
this.appendChild(this.cancelButton);
this.appendChild(this.backButton);
this.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) {
+ let record = {};
+ let {
+ page,
+ } = state;
+
+ if (this.id && page && page.id !== this.id) {
+ log.debug(`AddressForm: no need to further render inactive page: ${page.id}`);
+ return;
+ }
+
this.cancelButton.textContent = this.dataset.cancelButtonLabel;
this.backButton.textContent = this.dataset.backButtonLabel;
this.saveButton.textContent = this.dataset.saveButtonLabel;
-
- let record = {};
- let {
- page,
- savedAddresses,
- } = state;
+ this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
this.backButton.hidden = page.onboardingWizard;
this.cancelButton.hidden = !page.onboardingWizard;
if (page.addressFields) {
this.setAttribute("address-fields", page.addressFields);
} else {
this.removeAttribute("address-fields");
}
this.pageTitle.textContent = page.title;
this.genericErrorText.textContent = page.error;
let editing = !!page.guid;
+ let addresses = paymentRequest.getAddresses(state);
// If an address is selected we want to edit it.
if (editing) {
- record = savedAddresses[page.guid];
+ record = addresses[page.guid];
if (!record) {
throw new Error("Trying to edit a non-existing address: " + page.guid);
}
+ // When editing an existing record, prevent changes to persistence
+ this.persistCheckbox.hidden = true;
+ } else {
+ // Adding a new record: default persistence to checked when in a not-private session
+ this.persistCheckbox.hidden = false;
+ this.persistCheckbox.checked = !state.isPrivate;
}
this.formHandler.loadRecord(record);
}
handleEvent(event) {
switch (event.type) {
case "click": {
@@ -146,18 +162,24 @@ export default class AddressForm extends
}
}
}
saveRecord() {
let record = this.formHandler.buildFormObject();
let {
page,
+ tempAddresses,
savedBasicCards,
} = this.requestStore.getState();
+ let editing = !!page.guid;
+
+ if (editing ? (page.guid in tempAddresses) : !this.persistCheckbox.checked) {
+ record.isTemporary = true;
+ }
let state = {
errorStateChange: {
page: {
id: "address-page",
onboardingWizard: page.onboardingWizard,
error: this.dataset.errorGenericSave,
},
--- a/browser/components/payments/res/containers/address-picker.js
+++ b/browser/components/payments/res/containers/address-picker.js
@@ -1,20 +1,21 @@
/* 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 AddressOption from "../components/address-option.js";
import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
import RichSelect from "../components/rich-select.js";
+import paymentRequest from "../paymentRequest.js";
/**
* <address-picker></address-picker>
* Container around add/edit links and <rich-select> with
- * <address-option> listening to savedAddresses.
+ * <address-option> listening to savedAddresses & tempAddresses.
*/
export default class AddressPicker extends PaymentStateSubscriberMixin(HTMLElement) {
static get observedAttributes() {
return ["address-fields"];
}
constructor() {
@@ -83,26 +84,26 @@ export default class AddressPicker exten
result[guid] = address;
}
}
}
return result;
}
render(state) {
- let {savedAddresses} = state;
+ let addresses = paymentRequest.getAddresses(state);
let desiredOptions = [];
let fieldNames;
if (this.hasAttribute("address-fields")) {
let names = this.getAttribute("address-fields").split(/\s+/);
if (names.length) {
fieldNames = names;
}
}
- let filteredAddresses = this.filterAddresses(savedAddresses, fieldNames);
+ let filteredAddresses = this.filterAddresses(addresses, fieldNames);
for (let [guid, address] of Object.entries(filteredAddresses)) {
let optionEl = this.dropdown.getOptionByValue(guid);
if (!optionEl) {
optionEl = new AddressOption();
optionEl.value = guid;
}
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -81,31 +81,36 @@ export default class BasicCardForm exten
// connected, including the shared form fetched asynchronously.
super.connectedCallback();
});
}
render(state) {
let {
page,
- savedAddresses,
selectedShippingAddress,
} = state;
+ if (this.id && page && page.id !== this.id) {
+ log.debug(`BasicCardForm: no need to further render inactive page: ${page.id}`);
+ return;
+ }
+
this.cancelButton.textContent = this.dataset.cancelButtonLabel;
this.backButton.textContent = this.dataset.backButtonLabel;
this.saveButton.textContent = this.dataset.saveButtonLabel;
this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
// The back button is temporarily hidden(See Bug 1462461).
this.backButton.hidden = !!page.onboardingWizard;
this.cancelButton.hidden = !page.onboardingWizard;
let record = {};
let basicCards = paymentRequest.getBasicCards(state);
+ let addresses = paymentRequest.getAddresses(state);
this.genericErrorText.textContent = page.error;
let editing = !!page.guid;
this.form.querySelector("#cc-number").disabled = editing;
// If a card is selected we want to edit it.
if (editing) {
@@ -113,25 +118,26 @@ export default class BasicCardForm exten
record = basicCards[page.guid];
if (!record) {
throw new Error("Trying to edit a non-existing card: " + page.guid);
}
// When editing an existing record, prevent changes to persistence
this.persistCheckbox.hidden = true;
} else {
this.pageTitle.textContent = this.dataset.addBasicCardTitle;
+ // Use a currently selected shipping address as the default billing address
if (selectedShippingAddress) {
record.billingAddressGUID = selectedShippingAddress;
}
// Adding a new record: default persistence to checked when in a not-private session
this.persistCheckbox.hidden = false;
this.persistCheckbox.checked = !state.isPrivate;
}
- this.formHandler.loadRecord(record, savedAddresses);
+ this.formHandler.loadRecord(record, addresses);
}
handleEvent(event) {
switch (event.type) {
case "click": {
this.onClick(event);
break;
}
@@ -164,58 +170,42 @@ export default class BasicCardForm exten
saveRecord() {
let record = this.formHandler.buildFormObject();
let {
page,
tempBasicCards,
} = this.requestStore.getState();
let editing = !!page.guid;
- let tempRecord = editing && tempBasicCards[page.guid];
+
+ if (editing ? (page.guid in tempBasicCards) : !this.persistCheckbox.checked) {
+ record.isTemporary = true;
+ }
for (let editableFieldName of ["cc-name", "cc-exp-month", "cc-exp-year"]) {
record[editableFieldName] = record[editableFieldName] || "";
}
// 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"] || "";
}
- if (!tempRecord && this.persistCheckbox.checked) {
- log.debug(`BasicCardForm: persisting creditCard record: ${page.guid || "(new)"}`);
- paymentRequest.updateAutofillRecord("creditCards", record, page.guid, {
- errorStateChange: {
- page: {
- id: "basic-card-page",
- error: this.dataset.errorGenericSave,
- },
+ paymentRequest.updateAutofillRecord("creditCards", record, page.guid, {
+ errorStateChange: {
+ page: {
+ id: "basic-card-page",
+ error: this.dataset.errorGenericSave,
},
- preserveOldProperties: true,
- selectedStateKey: "selectedPaymentCard",
- successStateChange: {
- page: {
- id: "payment-summary",
- },
- },
- });
- } else {
- // This record will never get inserted into the store
- // so we generate a faux-guid for a new record
- record.guid = page.guid || "temp-" + Math.abs(Math.random() * 0xffffffff|0);
-
- log.debug(`BasicCardForm: saving temporary record: ${record.guid}`);
- this.requestStore.setState({
+ },
+ preserveOldProperties: true,
+ selectedStateKey: "selectedPaymentCard",
+ successStateChange: {
page: {
id: "payment-summary",
},
- selectedPaymentCard: record.guid,
- tempBasicCards: Object.assign({}, tempBasicCards, {
- // Mix-in any previous values - equivalent to the store's preserveOldProperties: true,
- [record.guid]: Object.assign({}, tempRecord, record),
- }),
- });
- }
+ },
+ });
}
}
customElements.define("basic-card-form", BasicCardForm);
--- a/browser/components/payments/res/containers/payment-dialog.js
+++ b/browser/components/payments/res/containers/payment-dialog.js
@@ -124,32 +124,32 @@ export default class PaymentDialog exten
/**
* Set some state from the privileged parent process.
* Other elements that need to set state should use their own `this.requestStore.setState`
* method provided by the `PaymentStateSubscriberMixin`.
*
* @param {object} state - See `PaymentsStore.setState`
*/
setStateFromParent(state) {
- let oldSavedAddresses = this.requestStore.getState().savedAddresses;
+ let oldAddresses = paymentRequest.getAddresses(this.requestStore.getState());
this.requestStore.setState(state);
// Check if any foreign-key constraints were invalidated.
state = this.requestStore.getState();
let {
- savedAddresses,
selectedPayerAddress,
selectedPaymentCard,
selectedShippingAddress,
selectedShippingOption,
} = state;
+ let addresses = paymentRequest.getAddresses(state);
let shippingOptions = state.request.paymentDetails.shippingOptions;
- let shippingAddress = selectedShippingAddress && savedAddresses[selectedShippingAddress];
+ let shippingAddress = selectedShippingAddress && addresses[selectedShippingAddress];
let oldShippingAddress = selectedShippingAddress &&
- oldSavedAddresses[selectedShippingAddress];
+ oldAddresses[selectedShippingAddress];
// Ensure `selectedShippingAddress` never refers to a deleted address.
// We also compare address timestamps to notify about changes
// made outside the payments UI.
if (shippingAddress) {
// invalidate the cached value if the address was modified
if (oldShippingAddress &&
shippingAddress.guid == oldShippingAddress.guid &&
@@ -190,19 +190,19 @@ export default class PaymentDialog exten
this._cachedState.selectedShippingOption = selectedShippingOption;
this.requestStore.setState({
selectedShippingOption,
});
}
// Ensure `selectedPayerAddress` never refers to a deleted address and refers
// to an address if one exists.
- if (!savedAddresses[selectedPayerAddress]) {
+ if (!addresses[selectedPayerAddress]) {
this.requestStore.setState({
- selectedPayerAddress: Object.keys(savedAddresses)[0] || null,
+ selectedPayerAddress: Object.keys(addresses)[0] || null,
});
}
}
_renderPayButton(state) {
this._payButton.disabled = state.changesPrevented;
switch (state.completionState) {
case "initial":
--- a/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
+++ b/browser/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -44,16 +44,17 @@ export let requestStore = new PaymentsSt
},
selectedPayerAddress: null,
selectedPaymentCard: null,
selectedPaymentCardSecurityCode: null,
selectedShippingAddress: null,
selectedShippingOption: null,
savedAddresses: {},
savedBasicCards: {},
+ tempAddresses: {},
tempBasicCards: {},
});
/**
* A mixin to render UI based upon the requestStore and get updated when that store changes.
*
* Attaches `requestStore` to the element to give access to the store.
--- a/browser/components/payments/res/paymentRequest.js
+++ b/browser/components/payments/res/paymentRequest.js
@@ -220,16 +220,21 @@ var paymentRequest = {
return state.request.paymentDetails.totalItem;
},
onPaymentRequestUnload() {
// remove listeners that may be used multiple times here
window.removeEventListener("paymentChromeToContent", this);
},
+ getAddresses(state) {
+ let addresses = Object.assign({}, state.savedAddresses, state.tempAddresses);
+ return addresses;
+ },
+
getBasicCards(state) {
let cards = Object.assign({}, state.savedBasicCards, state.tempBasicCards);
return cards;
},
};
paymentRequest.init();
--- a/browser/components/payments/res/paymentRequest.xhtml
+++ b/browser/components/payments/res/paymentRequest.xhtml
@@ -41,16 +41,17 @@
<!ENTITY basicCardPage.error.genericSave "There was an error saving the payment card.">
<!ENTITY basicCardPage.backButton.label "Back">
<!ENTITY basicCardPage.saveButton.label "Save">
<!ENTITY basicCardPage.persistCheckbox.label "Save credit card to Firefox (Security code will not be saved)">
<!ENTITY addressPage.error.genericSave "There was an error saving the address.">
<!ENTITY addressPage.cancelButton.label "Cancel">
<!ENTITY addressPage.backButton.label "Back">
<!ENTITY addressPage.saveButton.label "Save">
+ <!ENTITY addressPage.persistCheckbox.label "Save address to Firefox">
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>&paymentSummaryTitle;</title>
<!-- chrome: is needed for global.dtd -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' chrome:"/>
@@ -88,16 +89,17 @@
<section>
<div id="error-text"></div>
<div class="shipping-related"
id="shipping-type-label"
data-shipping-address-label="&shippingAddressLabel;"
data-delivery-address-label="&deliveryAddressLabel;"
+ data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
data-pickup-address-label="&pickupAddressLabel;"><label></label></div>
<address-picker class="shipping-related"
data-add-link-label="&address.addLink.label;"
data-edit-link-label="&address.editLink.label;"
selected-state-key="selectedShippingAddress"></address-picker>
<div class="shipping-related"><label>&shippingOptionsLabel;</label></div>
<shipping-option-picker class="shipping-related"></shipping-option-picker>
@@ -143,16 +145,17 @@
hidden="hidden"></basic-card-form>
<address-form id="address-page"
class="page"
data-error-generic-save="&addressPage.error.genericSave;"
data-cancel-button-label="&addressPage.cancelButton.label;"
data-back-button-label="&addressPage.backButton.label;"
data-save-button-label="&addressPage.saveButton.label;"
+ data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
hidden="hidden"></address-form>
</div>
<div id="disabled-overlay" hidden="hidden">
<!-- overlay to prevent changes while waiting for a response from the merchant -->
</div>
</template>
--- a/browser/components/payments/test/PaymentTestUtils.jsm
+++ b/browser/components/payments/test/PaymentTestUtils.jsm
@@ -359,16 +359,29 @@ var PaymentTestUtils = {
"street-address": "1 Pommes Frittes Place",
"address-level2": "Berlin",
"address-level1": "BE",
"postal-code": "02138",
country: "DE",
tel: "+16172535702",
email: "timbl@example.org",
},
+ /* Used as a temporary (not persisted in autofill storage) address in tests */
+ Temp: {
+ "given-name": "Temp",
+ "family-name": "McTempFace",
+ "organization": "Temps Inc.",
+ "street-address": "1a Temporary Ave.",
+ "address-level2": "Temp Town",
+ "address-level1": "CA",
+ "postal-code": "31337",
+ "country": "US",
+ "tel": "+15032541000",
+ "email": "tempie@example.com",
+ },
},
BasicCards: {
JohnDoe: {
"cc-exp-month": 1,
"cc-exp-year": 9999,
"cc-name": "John Doe",
"cc-number": "999999999999",
--- a/browser/components/payments/test/browser/browser_address_edit.js
+++ b/browser/components/payments/test/browser/browser_address_edit.js
@@ -1,12 +1,18 @@
/* eslint-disable no-shadow */
"use strict";
+async function setup() {
+ await setupFormAutofillStorage();
+ // adds 2 addresses and one card
+ await addSampleAddressesAndBasicCard();
+}
+
add_task(async function test_add_link() {
await BrowserTestUtils.withNewTab({
gBrowser,
url: BLANK_PAGE_URL,
}, async browser => {
let {win, frame} =
await setupPaymentDialog(browser, {
methodData: [PTU.MethodData.basicCard],
@@ -49,16 +55,20 @@ add_task(async function test_add_link()
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "address-page" && !state.page.guid;
}, "Check add page state");
let title = content.document.querySelector("address-form h1");
is(title.textContent, "Add Shipping Address", "Page title should be set");
+ let persistInput = content.document.querySelector("address-form labelled-checkbox");
+ ok(!persistInput.hidden, "checkbox should be visible when adding a new address");
+ ok(Cu.waiveXrays(persistInput).checked, "persist checkbox should be checked by default");
+
info("filling fields");
for (let [key, val] of Object.entries(address)) {
let field = content.document.getElementById(key);
if (!field) {
ok(false, `${key} field not found`);
}
field.value = val;
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
@@ -136,31 +146,37 @@ add_task(async function test_edit_link()
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "address-page" && !!state.page.guid;
}, "Check edit page state");
let title = content.document.querySelector("address-form h1");
is(title.textContent, "Edit Shipping Address", "Page title should be set");
+ let persistInput = content.document.querySelector("address-form labelled-checkbox");
+ ok(persistInput.hidden, "checkbox should be hidden when editing an address");
+
info("overwriting field values");
for (let [key, val] of Object.entries(address)) {
let field = content.document.getElementById(key);
field.value = val;
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
}
content.document.querySelector("address-form button:last-of-type").click();
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
let addresses = Object.entries(state.savedAddresses);
return addresses.length == 1 &&
addresses[0][1]["given-name"] == address["given-name"];
}, "Check address was edited");
+ // check nothing went into tempAddresses
+ is(Object.keys(state.tempAddresses).length, 0, "tempAddresses collection was untouched");
+
let addressGUIDs = Object.keys(state.savedAddresses);
is(addressGUIDs.length, 1, "Check there is still one address");
let savedAddress = state.savedAddresses[addressGUIDs[0]];
for (let [key, val] of Object.entries(address)) {
is(savedAddress[key], val, "Check updated " + key);
}
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
@@ -215,16 +231,20 @@ add_task(async function test_add_payer_c
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
return state.page.id == "address-page" && !state.page.guid;
}, "Check add page state");
let title = content.document.querySelector("address-form h1");
is(title.textContent, "Add Payer Contact", "Page title should be set");
+ let persistInput = content.document.querySelector("address-form labelled-checkbox");
+ ok(!persistInput.hidden, "checkbox should be visible when adding a new address");
+ ok(Cu.waiveXrays(persistInput).checked, "persist checkbox should be checked by default");
+
info("filling fields");
for (let [key, val] of Object.entries(address)) {
let field = content.document.getElementById(key);
if (!field) {
ok(false, `${key} field not found`);
}
field.value = val;
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
@@ -299,16 +319,19 @@ add_task(async function test_edit_payer_
state = await PTU.DialogContentUtils.waitForState(content, (state) => {
info("state.page.id: " + state.page.id + "; state.page.guid: " + state.page.guid);
return state.page.id == "address-page" && !!state.page.guid;
}, "Check edit page state");
let title = content.document.querySelector("address-form h1");
is(title.textContent, "Edit Payer Contact", "Page title should be set");
+ let persistInput = content.document.querySelector("address-form labelled-checkbox");
+ ok(persistInput.hidden, "checkbox should be hidden when editing an address");
+
info("overwriting field values");
for (let [key, val] of Object.entries(address)) {
let field = content.document.getElementById(key);
field.value = val + "1";
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
}
info("check that non-payer requested fields are hidden");
@@ -345,8 +368,115 @@ add_task(async function test_edit_payer_
}, EXPECTED_ADDRESS);
info("clicking cancel");
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
});
});
+
+add_task(async function test_private_persist_addresses() {
+ await formAutofillStorage.addresses._nukeAllRecords();
+ await setup();
+
+ is((await formAutofillStorage.addresses.getAll()).length, 2,
+ "Setup results in 2 stored addresses at start of test");
+
+ await withNewTabInPrivateWindow({
+ url: BLANK_PAGE_URL,
+ }, async browser => {
+ info("in new tab w. private window");
+ let {frame} =
+ // setupPaymentDialog from a private window.
+ await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.twoShippingOptions,
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ }
+ );
+ info("/setupPaymentDialog");
+
+ await spawnPaymentDialogTask(frame, async () => {
+ let {
+ PaymentTestUtils: PTU,
+ } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+ info("adding link");
+ // click through to add/edit address page
+ let addLink = content.document.querySelector("address-picker a.add-link");
+ addLink.click();
+ info("link clicked");
+
+ let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+ return state.page.id == "address-page" && !state.page.guid;
+ }, "Check add page state");
+
+ info("on address-page and state.isPrivate: " + state.isPrivate);
+ ok(state.isPrivate,
+ "isPrivate flag is set when paymentrequest is shown in a private session");
+ ok(typeof state.isPrivate,
+ "isPrivate is a flag");
+ });
+
+ info("wait for initialAddresses");
+ let initialAddresses =
+ await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getShippingAddresses);
+ is(initialAddresses.options.length, 2, "Got expected number of pre-filled shipping addresses");
+
+ // // listen for shippingaddresschange event in merchant (private) window
+ info("listen for shippingaddresschange");
+ let shippingAddressChangePromise = ContentTask.spawn(browser, {
+ eventName: "shippingaddresschange",
+ }, PTU.ContentTasks.awaitPaymentRequestEventPromise);
+
+ // add an address
+ // (return to summary view)
+ info("add an address");
+ await spawnPaymentDialogTask(frame, async () => {
+ let {
+ PaymentTestUtils: PTU,
+ } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {});
+
+ let persistInput = content.document.querySelector("address-form labelled-checkbox");
+ ok(!persistInput.hidden, "checkbox should be visible when adding a new address");
+ ok(!Cu.waiveXrays(persistInput).checked, "persist checkbox should be unchecked by default");
+
+ info("add the temp address");
+ let addressToAdd = PTU.Addresses.Temp;
+ for (let [key, val] of Object.entries(addressToAdd)) {
+ let field = content.document.getElementById(key);
+ field.value = val;
+ }
+ content.document.querySelector("address-form button:last-of-type").click();
+
+ info("wait until we return to the summary page");
+ let state = await PTU.DialogContentUtils.waitForState(content, (state) => {
+ return state.page.id == "payment-summary";
+ }, "Return to summary page");
+
+ is(Object.keys(state.tempAddresses).length, 1, "Address added to temporary collection");
+ });
+
+ await shippingAddressChangePromise;
+ info("got shippingaddresschange event");
+
+ // verify address picker has more addresses and new selected
+ info("check shipping addresses");
+ let addresses =
+ await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getShippingAddresses);
+
+ is(addresses.options.length, 3, "Got expected number of shipping addresses");
+
+ info("clicking pay");
+ spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
+
+ // Add a handler to complete the payment above.
+ info("acknowledging the completion from the merchant page");
+ let result = await ContentTask.spawn(browser, {}, PTU.ContentTasks.addCompletionHandler);
+ is(result.response.methodName, "basic-card", "Check methodName");
+ });
+ // verify formautofill store doesnt have the new temp addresses
+ is((await formAutofillStorage.addresses.getAll()).length, 2,
+ "Original 2 stored addresses at end of test");
+});
+
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -112,16 +112,25 @@ function withNewDialogFrame(requestId, t
let args = {
gBrowser,
url: `chrome://payments/content/paymentDialogWrapper.xul?requestId=${requestId}`,
};
return BrowserTestUtils.withNewTab(args, dialogTabTask);
}
+async function withNewTabInPrivateWindow(args = {}, taskFn) {
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({private: true});
+ let tabArgs = Object.assign(args, {
+ browser: privateWin.gBrowser,
+ });
+ await withMerchantTab(tabArgs, taskFn);
+ await BrowserTestUtils.closeWindow(privateWin);
+}
+
/**
* Spawn a content task inside the inner unprivileged frame of a privileged Payment Request dialog.
*
* @param {string} requestId
* @param {Function} contentTaskFn
* @param {object?} [args = null] for the content task
* @returns {Promise}
*/
--- a/browser/components/payments/test/mochitest/test_address_picker.html
+++ b/browser/components/payments/test/mochitest/test_address_picker.html
@@ -6,16 +6,17 @@ Test the address-picker component
<head>
<meta charset="utf-8">
<title>Test the address-picker component</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<script src="payments_common.js"></script>
<script src="../../res/vendor/custom-elements.min.js"></script>
+ <script src="../../res/unprivileged-fallbacks.js"></script>
<link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/>
<link rel="stylesheet" type="text/css" href="../../res/components/address-option.css"/>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<p id="display">
<address-picker id="picker1"