--- a/toolkit/components/payments/content/paymentDialogWrapper.js
+++ b/toolkit/components/payments/content/paymentDialogWrapper.js
@@ -94,43 +94,46 @@ var paymentDialogWrapper = {
});
return address;
},
/**
* @param {string} guid The GUID of the basic card record from storage.
* @param {string} cardSecurityCode The associated card security code (CVV/CCV/etc.)
+ * @param {string} billingAddressGUID The GUID of the address to be used for billing.
* @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) {
+ async _convertProfileBasicCardToPaymentMethodData(guid, cardSecurityCode, billingAddressGUID) {
let cardData = 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;
}
// User canceled master password entry
return null;
}
+ let billingAddress = await this._convertProfileAddressToPaymentAddress(billingAddressGUID);
let methodData = this.createBasicCardResponseData({
cardholderName: cardData["cc-name"],
cardNumber,
expiryMonth: cardData["cc-exp-month"].toString().padStart(2, "0"),
expiryYear: cardData["cc-exp-year"].toString(),
cardSecurityCode,
+ billingAddress,
});
return methodData;
},
init(requestId, frame) {
if (!requestId || typeof(requestId) != "string") {
throw new Error("Invalid PaymentRequest ID");
@@ -400,19 +403,21 @@ var paymentDialogWrapper = {
paymentSrv.respondPayment(showResponse);
window.close();
},
async onPay({
selectedPayerAddressGUID: payerGUID,
selectedPaymentCardGUID: paymentCardGUID,
selectedPaymentCardSecurityCode: cardSecurityCode,
+ selectedBillingAddressGUID: billingGUID,
}) {
let methodData = await this._convertProfileBasicCardToPaymentMethodData(paymentCardGUID,
- cardSecurityCode);
+ cardSecurityCode,
+ billingGUID);
if (!methodData) {
// TODO (Bug 1429265/Bug 1429205): Handle when a user hits cancel on the
// Master Password dialog.
Cu.reportError("Bug 1429265/Bug 1429205: User canceled master password entry");
return;
}
--- a/toolkit/components/payments/res/components/address-option.css
+++ b/toolkit/components/payments/res/components/address-option.css
@@ -41,18 +41,18 @@ address-option > .tel {
address-option > .name,
address-option > .street-address,
address-option > .email,
address-option > .tel {
white-space: nowrap;
}
-address-picker.shipping-related address-option > .email,
-address-picker.shipping-related address-option.rich-select-selected-clone > .tel {
+address-picker:-moz-any(.billing-related, .shipping-related) address-option > .email,
+address-picker:-moz-any(.billing-related, .shipping-related) address-option.rich-select-selected-clone > .tel {
display: none;
}
/* for payer contact details:
* display fields selectively based on the contents of the address-fields attribute
*/
address-picker.payer-related address-option > .name,
address-picker.payer-related address-option > .street-address,
--- a/toolkit/components/payments/res/components/rich-select.css
+++ b/toolkit/components/payments/res/components/rich-select.css
@@ -15,16 +15,21 @@ rich-select[open] {
}
rich-select[open] > .rich-select-popup-box {
box-shadow: 0 0 5px black;
position: absolute;
z-index: 1;
}
+rich-select[disabled] {
+ pointer-events: none;
+ color: GrayText;
+}
+
.rich-select-popup-box > .rich-option[selected] {
background-color: #ffa;
}
.rich-option {
display: grid;
border-bottom: 1px solid #ddd;
background: #fff; /* TODO: system colors */
--- a/toolkit/components/payments/res/containers/address-picker.js
+++ b/toolkit/components/payments/res/containers/address-picker.js
@@ -124,16 +124,29 @@ class AddressPicker extends PaymentState
`does not exist in options`);
}
}
get selectedStateKey() {
return this.getAttribute("selected-state-key");
}
+ get disabled() {
+ return this.dropdown.getAttribute("disabled") == "true";
+ }
+
+ set disabled(val) {
+ if (val) {
+ this.dropdown.setAttribute("disabled", "true");
+ return true;
+ }
+ this.dropdown.removeAttribute("disabled");
+ return false;
+ }
+
handleEvent(event) {
switch (event.type) {
case "change": {
this.onChange(event);
break;
}
}
}
--- a/toolkit/components/payments/res/containers/payment-dialog.js
+++ b/toolkit/components/payments/res/containers/payment-dialog.js
@@ -22,16 +22,20 @@ class PaymentDialog extends PaymentState
this._hostNameEl = contents.querySelector("#host-name");
this._cancelButton = contents.querySelector("#cancel");
this._cancelButton.addEventListener("click", this.cancelRequest);
this._payButton = contents.querySelector("#pay");
this._payButton.addEventListener("click", this);
+ this._useSameAsShipping = contents.querySelector("#useSameAsShipping");
+ this._useSameAsShipping.addEventListener("click", this);
+ this._billingAddressPicker = contents.querySelector("#billingAddressPicker");
+
this._viewAllButton = contents.querySelector("#view-all");
this._viewAllButton.addEventListener("click", this);
this._mainContainer = contents.getElementById("main-container");
this._orderDetailsOverlay = contents.querySelector("#order-details-overlay");
this._shippingTypeLabel = contents.querySelector("#shipping-type-label");
this._shippingRelatedEls = contents.querySelectorAll(".shipping-related");
@@ -59,42 +63,70 @@ class PaymentDialog extends PaymentState
switch (event.target) {
case this._viewAllButton:
let orderDetailsShowing = !this.requestStore.getState().orderDetailsShowing;
this.requestStore.setState({ orderDetailsShowing });
break;
case this._payButton:
this.pay();
break;
+ case this._useSameAsShipping:
+ this.useSameAsShipping();
+ break;
}
}
}
cancelRequest() {
paymentRequest.cancel();
}
pay() {
let {
selectedPayerAddress,
selectedPaymentCard,
selectedPaymentCardSecurityCode,
+ selectedBillingAddress,
} = this.requestStore.getState();
paymentRequest.pay({
selectedPayerAddressGUID: selectedPayerAddress,
selectedPaymentCardGUID: selectedPaymentCard,
selectedPaymentCardSecurityCode,
+ selectedBillingAddressGUID: selectedBillingAddress,
+ });
+ }
+
+ useSameAsShipping() {
+ let useShippingAddressAsPaymentAddress = this._useSameAsShipping.checked;
+ this._billingAddressPicker.disabled = useShippingAddressAsPaymentAddress;
+ let selectedBillingAddress = null;
+ if (useShippingAddressAsPaymentAddress) {
+ selectedBillingAddress = this.requestStore.getState().selectedShippingAddress;
+ }
+ this.requestStore.setState({
+ useShippingAddressAsPaymentAddress,
+ selectedBillingAddress,
});
}
changeShippingAddress(shippingAddressGUID) {
paymentRequest.changeShippingAddress({
shippingAddressGUID,
});
+
+ let {
+ useShippingAddressAsPaymentAddress,
+ selectedShippingAddress,
+ } = this.requestStore.getState();
+ if (useShippingAddressAsPaymentAddress) {
+ this.requestStore.setState({
+ selectedBillingAddress: selectedShippingAddress,
+ });
+ }
}
changeShippingOption(optionID) {
paymentRequest.changeShippingOption({
optionID,
});
}
--- a/toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
+++ b/toolkit/components/payments/res/mixins/PaymentStateSubscriberMixin.js
@@ -12,16 +12,17 @@
/**
* State of the payment request dialog.
*/
let requestStore = new PaymentsStore({
changesPrevented: false,
completionState: "initial",
orderDetailsShowing: false,
+ useShippingAddressAsPaymentAddress: false,
page: {
id: "payment-summary",
},
request: {
tabId: null,
topLevelPrincipal: {URI: {displayHost: null}},
requestId: null,
paymentMethods: [],
@@ -36,16 +37,17 @@ let requestStore = new PaymentsStore({
paymentOptions: {
requestPayerName: false,
requestPayerEmail: false,
requestPayerPhone: false,
requestShipping: false,
shippingType: "shipping",
},
},
+ selectedBillingAddress: null,
selectedPayerAddress: null,
selectedPaymentCard: null,
selectedPaymentCardSecurityCode: null,
selectedShippingAddress: null,
selectedShippingOption: null,
savedAddresses: {},
savedBasicCards: {},
});
--- a/toolkit/components/payments/res/paymentRequest.xhtml
+++ b/toolkit/components/payments/res/paymentRequest.xhtml
@@ -19,16 +19,17 @@
<!ENTITY cancelPaymentButton.label "Cancel">
<!ENTITY approvePaymentButton.label "Pay">
<!ENTITY processingPaymentButton.label "Processing">
<!ENTITY successPaymentButton.label "Done">
<!ENTITY failPaymentButton.label "Fail">
<!ENTITY unknownPaymentButton.label "Unknown">
<!ENTITY orderDetailsLabel "Order Details">
<!ENTITY orderTotalLabel "Total">
+ <!ENTITY useSameAsShipping.label "Use shipping address as payment address">
<!ENTITY basicCardPage.error.genericSave "There was an error saving the payment card.">
<!ENTITY basicCardPage.backButton.label "Back">
<!ENTITY basicCardPage.saveButton.label "Save">
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>&paymentSummaryTitle;</title>
@@ -99,16 +100,21 @@
<div class="shipping-related"><label>&shippingOptionsLabel;</label></div>
<shipping-option-picker class="shipping-related"></shipping-option-picker>
<div><label>&paymentMethodsLabel;</label></div>
<payment-method-picker selected-state-key="selectedPaymentCard"
data-add-link-label="&basicCard.addLink.label;"
data-edit-link-label="&basicCard.editLink.label;">
</payment-method-picker>
+ <address-picker id="billingAddressPicker"
+ class="billing-related"
+ selected-state-key="selectedBillingAddress"></address-picker>
+ <input type="checkbox" id="useSameAsShipping"/>
+ <label for="useSameAsShipping">&useSameAsShipping.label;</label>
<div class="payer-related"><label>&payerLabel;</label></div>
<address-picker class="payer-related"
selected-state-key="selectedPayerAddress"></address-picker>
<div id="error-text"></div>
</section>
<footer id="controls-container">
--- a/toolkit/components/payments/test/PaymentTestUtils.jsm
+++ b/toolkit/components/payments/test/PaymentTestUtils.jsm
@@ -102,16 +102,47 @@ var PaymentTestUtils = {
},
getElementTextContent: selector => {
let doc = content.document;
let element = doc.querySelector(selector);
return element.textContent;
},
+ getShippingAddressDetails: () => {
+ let doc = content.document;
+ let select = doc.querySelector("address-picker.shipping-related > rich-select");
+ let popupBox = Cu.waiveXrays(select).popupBox;
+ let selectedOptionIndex = Array.from(popupBox.children)
+ .findIndex(item => item.hasAttribute("selected"));
+ let selectedOption = popupBox.children[selectedOptionIndex];
+ return {
+ optionCount: popupBox.children.length,
+ selectedOptionIndex,
+ selectedOptionValue: selectedOption && selectedOption.getAttribute("value"),
+ };
+ },
+
+ getBillingDetails: () => {
+ let doc = content.document;
+ let checkbox = doc.querySelector("#useSameAsShipping");
+ let select = doc.querySelector("#billingAddressPicker > rich-select");
+ let popupBox = Cu.waiveXrays(select).popupBox;
+ let selectedOptionIndex = Array.from(popupBox.children)
+ .findIndex(item => item.hasAttribute("selected"));
+ let selectedOption = popupBox.children[selectedOptionIndex];
+ return {
+ checkboxChecked: checkbox.checked,
+ pickerDisabled: select.getAttribute("disabled") == "true",
+ optionCount: popupBox.children.length,
+ selectedOptionIndex,
+ selectedOptionValue: selectedOption && selectedOption.getAttribute("value"),
+ };
+ },
+
getShippingOptions: () => {
let select = content.document.querySelector("shipping-option-picker > rich-select");
let popupBox = Cu.waiveXrays(select).popupBox;
let selectedOptionIndex = Array.from(popupBox.children)
.findIndex(item => item.hasAttribute("selected"));
let selectedOption = popupBox.children[selectedOptionIndex];
let currencyAmount = selectedOption.querySelector("currency-amount");
return {
@@ -139,16 +170,35 @@ var PaymentTestUtils = {
let optionPicker =
doc.querySelector("shipping-option-picker");
let select = optionPicker.querySelector("rich-select");
let option = select.querySelector(`[value="${value}"]`);
select.click();
option.click();
},
+ selectBillingAddressByCountry: country => {
+ let doc = content.document;
+ let addressPicker =
+ doc.querySelector("address-picker[selected-state-key='selectedBillingAddress']");
+ let select = addressPicker.querySelector("rich-select");
+ let option = select.querySelector(`[country="${country}"]`);
+ select.click();
+ option.click();
+ },
+
+ setUseShippingAddressAsBillingAddress: value => {
+ let doc = content.document;
+ let checkbox = doc.querySelector("#useSameAsShipping");
+ if (checkbox.checked == value) {
+ throw new Error("checkbox already has checked value of " + value);
+ }
+ checkbox.click();
+ },
+
/**
* Click the cancel button
*
* Don't await on this task since the cancel can close the dialog before
* ContentTask can resolve the promise.
*
* @returns {undefined}
*/
--- a/toolkit/components/payments/test/browser/browser.ini
+++ b/toolkit/components/payments/test/browser/browser.ini
@@ -13,8 +13,9 @@ support-files =
[browser_request_serialization.js]
[browser_request_shipping.js]
[browser_request_summary.js]
uses-unsafe-cpows = true
[browser_shippingaddresschange_error.js]
[browser_show_dialog.js]
skip-if = os == 'win' && debug # bug 1418385
[browser_total.js]
+[browser_use_same_as_shipping.js]
--- a/toolkit/components/payments/test/browser/browser_change_shipping.js
+++ b/toolkit/components/payments/test/browser/browser_change_shipping.js
@@ -62,16 +62,19 @@ add_task(async function test_change_ship
}, PTU.ContentTasks.awaitPaymentRequestEventPromise);
info("got shippingaddresschange event");
shippingOptions =
await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getShippingOptions);
todo_is(shippingOptions.selectedOptionCurrency, "EUR",
"Shipping options should be in EUR when shippingaddresschange is implemented");
+ await spawnPaymentDialogTask(frame,
+ PTU.DialogContentTasks.selectBillingAddressByCountry,
+ "US");
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");
@@ -114,16 +117,19 @@ add_task(async function test_no_shipping
}
);
ContentTask.spawn(browser, {
eventName: "shippingaddresschange",
}, PTU.ContentTasks.ensureNoPaymentRequestEvent);
info("added shipping change handler");
+ await spawnPaymentDialogTask(frame,
+ PTU.DialogContentTasks.selectBillingAddressByCountry,
+ "US");
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");
--- a/toolkit/components/payments/test/browser/browser_show_dialog.js
+++ b/toolkit/components/payments/test/browser/browser_show_dialog.js
@@ -83,16 +83,19 @@ add_task(async function test_show_comple
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
}
);
info("entering CSC");
await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.setSecurityCode, {
securityCode: "999",
});
+ await spawnPaymentDialogTask(frame,
+ PTU.DialogContentTasks.selectBillingAddressByCountry,
+ "US");
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);
let addressLines = address["street-address"].split("\n");
@@ -144,16 +147,19 @@ add_task(async function test_show_comple
info("changing shipping option to '1' from default selected option of '2'");
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.selectShippingOptionById, "1");
await ContentTask.spawn(browser, {
eventName: "shippingoptionchange",
}, PTU.ContentTasks.awaitPaymentRequestEventPromise);
info("got shippingoptionchange event");
+ await spawnPaymentDialogTask(frame,
+ PTU.DialogContentTasks.selectBillingAddressByCountry,
+ "US");
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.shippingOption, "1", "Check shipping option");
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/browser/browser_use_same_as_shipping.js
@@ -0,0 +1,132 @@
+"use strict";
+
+let TIM_BL_GUID;
+let TIM_BL2_GUID;
+
+add_task(async function setup_profiles() {
+ let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+ (subject, data) => data == "add");
+ TIM_BL_GUID = formAutofillStorage.addresses.add(PTU.Addresses.TimBL);
+ await onChanged;
+
+ onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+ (subject, data) => data == "add");
+ TIM_BL2_GUID = formAutofillStorage.addresses.add(PTU.Addresses.TimBL2);
+ await onChanged;
+
+ onChanged = TestUtils.topicObserved("formautofill-storage-changed",
+ (subject, data) => data == "add");
+
+ formAutofillStorage.creditCards.add(PTU.BasicCards.JohnDoe);
+ await onChanged;
+});
+
+add_task(async function test_use_same_as_shipping() {
+ await BrowserTestUtils.withNewTab({
+ gBrowser,
+ url: BLANK_PAGE_URL,
+ }, async browser => {
+ let {win, frame} =
+ await setupPaymentDialog(browser, {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.twoShippingOptions,
+ options: PTU.Options.requestShippingOption,
+ merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
+ }
+ );
+
+ let shippingAddresses =
+ await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getShippingAddressDetails);
+ is(shippingAddresses.optionCount, 2, "there should be two shipping addresses");
+ is(shippingAddresses.selectedOptionValue, TIM_BL_GUID, "TimBL should be selected by default");
+
+ let paymentOptions =
+ await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getBillingDetails);
+ is(paymentOptions.checkboxChecked, false, "Checkbox should be unchecked by default");
+ is(paymentOptions.pickerDisabled, false, "Picker should be enabled by default");
+ is(paymentOptions.selectedOptionValue, null,
+ "Billing address picker should not have an option selected by default");
+
+ await spawnPaymentDialogTask(frame,
+ PTU.DialogContentTasks.setUseShippingAddressAsBillingAddress,
+ true);
+
+ paymentOptions =
+ await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getBillingDetails);
+ is(paymentOptions.checkboxChecked, true, "Checkbox should now be checked");
+ is(paymentOptions.pickerDisabled, true, "Picker should now be disabled");
+ is(paymentOptions.selectedOptionValue, shippingAddresses.selectedOptionValue,
+ "Billing address picker should match the shipping address picker");
+
+ await spawnPaymentDialogTask(frame,
+ PTU.DialogContentTasks.selectShippingAddressByCountry,
+ "DE");
+
+ shippingAddresses =
+ await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getShippingAddressDetails);
+ is(shippingAddresses.selectedOptionValue, TIM_BL2_GUID, "TimBL2 should now be selected");
+ paymentOptions =
+ await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getBillingDetails);
+ is(paymentOptions.selectedOptionValue, shippingAddresses.selectedOptionValue,
+ "Billing address picker should update to match shipping address picker");
+
+ await spawnPaymentDialogTask(frame,
+ PTU.DialogContentTasks.setUseShippingAddressAsBillingAddress,
+ false);
+
+ paymentOptions =
+ await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getBillingDetails);
+ is(paymentOptions.checkboxChecked, false, "Checkbox should now be unchecked");
+ is(paymentOptions.pickerDisabled, false, "Picker should now be enabled");
+ is(paymentOptions.selectedOptionValue, null,
+ "Billing address picker should reset to null");
+
+ await spawnPaymentDialogTask(frame,
+ PTU.DialogContentTasks.selectBillingAddressByCountry,
+ "US");
+
+ paymentOptions =
+ await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.getBillingDetails);
+ is(paymentOptions.selectedOptionValue, TIM_BL_GUID,
+ "Billing address picker should have TimBL's US address selected");
+
+ 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");
+
+ let timBL2addressLine = PTU.Addresses.TimBL2["street-address"].split("\n");
+ let actualShippingAddress = result.response.shippingAddress;
+ let expectedAddress = PTU.Addresses.TimBL2;
+ is(actualShippingAddress.addressLine[0], timBL2addressLine[0], "Address line 1 should match");
+ is(actualShippingAddress.addressLine[1], timBL2addressLine[1], "Address line 2 should match");
+ is(actualShippingAddress.country, expectedAddress.country, "Country should match");
+ is(actualShippingAddress.region, expectedAddress["address-level1"], "Region should match");
+ is(actualShippingAddress.city, expectedAddress["address-level2"], "City should match");
+ is(actualShippingAddress.postalCode, expectedAddress["postal-code"], "Zip code should match");
+ is(actualShippingAddress.organization, expectedAddress.organization, "Org should match");
+ is(actualShippingAddress.recipient,
+ `${expectedAddress["given-name"]} ${expectedAddress["additional-name"]} ` +
+ `${expectedAddress["family-name"]}`,
+ "Recipient should match");
+ is(actualShippingAddress.phone, expectedAddress.tel, "Phone should match");
+
+ let methodDetails = result.methodDetails;
+ is(methodDetails.cardholderName, "John Doe", "Check cardholderName");
+ is(methodDetails.cardNumber, "999999999999", "Check cardNumber");
+ is(methodDetails.expiryMonth, "01", "Check expiryMonth");
+ is(methodDetails.expiryYear, "9999", "Check expiryYear");
+ is(methodDetails.billingAddress.country, "US",
+ "Billing address should be in US as was just selected");
+ let timBLaddressLine = PTU.Addresses.TimBL["street-address"].split("\n");
+ is(methodDetails.billingAddress.addressLine[0], timBLaddressLine[0],
+ "Billing address line 1 should match TimBL's US address");
+ is(methodDetails.billingAddress.addressLine[1], timBLaddressLine[1],
+ "Billing address line 2 should match TimBL's US address");
+
+ await BrowserTestUtils.waitForCondition(() => win.closed, "dialog should be closed");
+ });
+});