Uniformize ApplePay javascript coding style.

Add support for checkout form data to ApplePay.
Add checkout shipping mode option in ApplePay settings.
This commit is contained in:
Pedro Silva 2023-11-09 17:50:57 +00:00
parent 3a39bccc54
commit 2f5f51e518
No known key found for this signature in database
GPG key ID: E2EE20C0669D24B3
9 changed files with 292 additions and 248 deletions

View file

@ -162,7 +162,7 @@ return array(
),
),
),
'applepay_button_domain_registration' => array(
'applepay_button_domain_registration' => array(
'title' => __( 'Domain Registration', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-text',
'text' =>
@ -177,13 +177,13 @@ return array(
'Registering the website domain on the PayPal site is mandated by Apple. Payments will fail if the Apple Pay button is used on an unregistered domain.',
'woocommerce-paypal-payments'
),
'classes' => array('ppcp-field-indent'),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'requirements' => array(),
),
'applepay_button_domain_validation' => array(
'applepay_button_domain_validation' => array(
'title' => __( 'Domain Validation', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-text',
'text' => $domain_validation_text
@ -200,13 +200,13 @@ return array(
'Apple requires the website domain to be registered and validated. PayPal Payments automatically presents your domain association file for Apple to validate the manually registered domain.',
'woocommerce-paypal-payments'
),
'classes' => array('ppcp-field-indent'),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'requirements' => array(),
),
'applepay_button_device_eligibility' => array(
'applepay_button_device_eligibility' => array(
'title' => __( 'Device Eligibility', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-text',
'text' => $device_eligibility_text
@ -218,13 +218,13 @@ return array(
'Apple Pay demands certain Apple devices for secure payment execution. This helps determine if your current device is compliant.',
'woocommerce-paypal-payments'
),
'classes' => array('ppcp-field-indent'),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => 'paypal',
'requirements' => array(),
),
'applepay_button_type' => array(
'applepay_button_type' => array(
'title' => __( 'Button Label', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
@ -232,7 +232,7 @@ return array(
'This controls the label of the Apple Pay button.',
'woocommerce-paypal-payments'
),
'classes' => array('ppcp-field-indent'),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'pay',
@ -241,7 +241,7 @@ return array(
'gateway' => 'paypal',
'requirements' => array(),
),
'applepay_button_color' => array(
'applepay_button_color' => array(
'title' => __( 'Button Color', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
@ -251,7 +251,7 @@ return array(
),
'label' => '',
'input_class' => array( 'wc-enhanced-select' ),
'classes' => array('ppcp-field-indent'),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'default' => 'black',
'options' => PropertiesDictionary::button_colors(),
@ -259,7 +259,7 @@ return array(
'gateway' => 'paypal',
'requirements' => array(),
),
'applepay_button_language' => array(
'applepay_button_language' => array(
'title' => __( 'Button Language', 'woocommerce-paypal-payments' ),
'type' => 'select',
'desc_tip' => true,
@ -267,7 +267,7 @@ return array(
'The language and region used for the displayed Apple Pay button. The default value is the current language and region setting in a browser.',
'woocommerce-paypal-payments'
),
'classes' => array('ppcp-field-indent'),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'en',
@ -276,10 +276,10 @@ return array(
'gateway' => 'paypal',
'requirements' => array(),
),
'applepay_checkout_data_mode' => array(
'applepay_checkout_data_mode' => array(
'title' => __( 'Send checkout billing and shipping data to Apple Pay', 'woocommerce-paypal-payments' ),
'type' => 'select',
'classes' => array('ppcp-field-indent'),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'desc_tip' => true,

View file

@ -16,7 +16,7 @@ class ApplepayButton {
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.paymentsClient = null;
this.form_saved = false;
this.formData = null;
this.contextHandler = ContextHandlerFactory.create(
this.context,
@ -24,10 +24,13 @@ class ApplepayButton {
this.ppcpConfig
);
this.updated_contact_info = []
this.updatedContactInfo = []
this.selectedShippingMethod = []
this.nonce = document.getElementById('woocommerce-process-checkout-nonce')?.value || buttonConfig.nonce
// Stores initialization data sent to the button.
this.initialPaymentRequest = null;
this.log = function() {
if ( this.buttonConfig.is_debug ) {
console.log('[ApplePayButton]', ...arguments);
@ -35,6 +38,13 @@ class ApplepayButton {
}
this.refreshContextData();
// Debug helpers
jQuery(document).on('ppcp-applepay-debug', () => {
console.log('ApplePayButton', this.context, this);
});
document.ppcpApplepayButtons = document.ppcpApplepayButtons || {};
document.ppcpApplepayButtons[this.context] = this;
}
init(config) {
@ -47,6 +57,7 @@ class ApplepayButton {
this.isInitialized = true;
this.applePayConfig = config;
const isEligible = this.applePayConfig.isEligible;
if (isEligible) {
this.fetchTransactionInfo().then(() => {
const isSubscriptionProduct = this.ppcpConfig?.data_client_id?.has_subscriptions === true;
@ -84,6 +95,7 @@ class ApplepayButton {
async fetchTransactionInfo() {
this.transactionInfo = await this.contextHandler.transactionInfo();
}
/**
* Returns configurations relative to this button context.
*/
@ -108,6 +120,7 @@ class ApplepayButton {
return config;
}
initEventHandlers() {
const { wrapper, ppcpButtonWrapper } = this.contextConfig();
const wrapper_id = '#' + wrapper;
@ -127,22 +140,25 @@ class ApplepayButton {
syncButtonVisibility();
}
/**
* Starts an ApplePay session.
*/
applePaySession(paymentRequest) {
this.log('applePaySession', paymentRequest);
const session = new ApplePaySession(4, paymentRequest)
session.begin()
const session = new ApplePaySession(4, paymentRequest);
session.begin();
if (this.shouldAllowShippingInButton()) {
session.onshippingmethodselected = this.onshippingmethodselected(session)
session.onshippingcontactselected = this.onshippingcontactselected(session)
if (this.shouldRequireShippingInButton()) {
session.onshippingmethodselected = this.onShippingMethodSelected(session);
session.onshippingcontactselected = this.onShippingContactSelected(session);
}
session.onvalidatemerchant = this.onvalidatemerchant(session);
session.onpaymentauthorized = this.onpaymentauthorized(session);
session.onvalidatemerchant = this.onValidateMerchant(session);
session.onpaymentauthorized = this.onPaymentAuthorized(session);
return session;
}
/**
* Add a Apple Pay purchase button
* Adds an Apple Pay purchase button.
*/
addButton() {
this.log('addButton', this.context);
@ -179,8 +195,9 @@ class ApplepayButton {
async onButtonClick() {
this.log('onButtonClick', this.context);
const paymentDataRequest = this.paymentDataRequest();
// trigger woocommerce validation if we are in the checkout page
const paymentRequest = this.paymentRequest();
// Trigger woocommerce validation if we are in the checkout page.
if (this.context === 'checkout') {
const checkoutFormSelector = 'form.woocommerce-checkout';
const errorHandler = new ErrorHandler(
@ -189,16 +206,16 @@ class ApplepayButton {
);
try {
const formData = new FormData(document.querySelector(checkoutFormSelector));
this.form_saved = Object.fromEntries(formData.entries());
// This line should be reviewed, the widgetBuilder.paypal.Applepay().confirmOrder fails if we add it.
this.update_request_data_with_form(paymentDataRequest);
this.formData = Object.fromEntries(formData.entries());
this.updateRequestDataWithForm(paymentRequest);
} catch (error) {
console.error(error);
}
this.log('=== paymentDataRequest', paymentDataRequest);
this.log('=== paymentRequest', paymentRequest);
const session = this.applePaySession(paymentDataRequest);
const session = this.applePaySession(paymentRequest);
const formValidator = PayPalCommerceGateway.early_checkout_validation_enabled ?
new FormValidator(
PayPalCommerceGateway.ajax.validate_checkout.endpoint,
@ -219,7 +236,9 @@ class ApplepayButton {
}
return;
}
this.applePaySession(paymentDataRequest)
// Default session initialization.
this.applePaySession(paymentRequest);
}
/**
@ -227,10 +246,10 @@ class ApplepayButton {
*
* @returns {false|*}
*/
shouldAllowShippingInButton() {
shouldRequireShippingInButton() {
return this.contextHandler.shippingAllowed()
&& this.buttonConfig.product.needShipping
&& (this.context !== 'checkout' || this.shouldAllowShippingInButton());
&& (this.context !== 'checkout' || this.shouldUpdateButtonWithFormData());
}
/**
@ -239,41 +258,70 @@ class ApplepayButton {
* @returns {boolean}
*/
shouldUpdateButtonWithFormData() {
return this.ppcpConfig?.preferences?.checkout_data_mode === 'use_applepay';
if (this.context !== 'checkout') {
return false;
}
return this.buttonConfig?.preferences?.checkout_data_mode === 'use_applepay';
}
update_request_data_with_form(paymentDataRequest) {
/**
* Indicates how payment completion should be handled if with the context handler default actions.
* Or with ApplePay module specific completion.
*
* @returns {boolean}
*/
shouldCompletePaymentWithContextHandler() {
// Data already handled, ex: PayNow
if (!this.contextHandler.shippingAllowed()) {
return true;
}
// Use WC form data mode in Checkout.
if (this.context === 'checkout' && !this.shouldUpdateButtonWithFormData()) {
return true;
}
return false;
}
/**
* Updates ApplePay paymentRequest with form data.
*/
updateRequestDataWithForm(paymentRequest) {
if (!this.shouldUpdateButtonWithFormData()) {
return;
}
// Add billing address.
paymentDataRequest.billingContact = this.fill_billing_contact(this.form_saved);
paymentRequest.billingContact = this.fillBillingContact(this.formData);
// Add custom data.
// "applicationData" is originating a "PayPalApplePayError: An internal server error has occurred" on paypal.Applepay().confirmOrder().
// paymentDataRequest.applicationData = this.fill_application_data(this.form_saved);
// paymentRequest.applicationData = this.fillApplicationData(this.formData);
if (!this.shouldAllowShippingInButton()) {
if (!this.shouldRequireShippingInButton()) {
return;
}
// Add shipping address.
paymentDataRequest.shippingContact = this.fill_shipping_contact(this.form_saved);
paymentRequest.shippingContact = this.fillShippingContact(this.formData);
// Get shipping methods.
const rate = this.transactionInfo.chosenShippingMethods[0];
paymentDataRequest.shippingMethods = [];
paymentRequest.shippingMethods = [];
// Add selected shipping method.
for (const shippingPackage of this.transactionInfo.shippingPackages) {
if (rate === shippingPackage.id) {
paymentDataRequest.shippingMethods.push({
const shippingMethod = {
'label' : shippingPackage.label,
'detail' : '',
'amount' : shippingPackage.cost_str,
'identifier' : shippingPackage.id,
});
};
// Remember this shipping method as the selected one.
this.selectedShippingMethod = shippingMethod;
paymentRequest.shippingMethods.push(shippingMethod);
break;
}
}
@ -281,7 +329,7 @@ class ApplepayButton {
// Add other shipping methods.
for (const shippingPackage of this.transactionInfo.shippingPackages) {
if (rate !== shippingPackage.id) {
paymentDataRequest.shippingMethods.push({
paymentRequest.shippingMethods.push({
'label' : shippingPackage.label,
'detail' : '',
'amount' : shippingPackage.cost_str,
@ -290,10 +338,13 @@ class ApplepayButton {
}
}
this.log('=== paymentDataRequest.shippingMethods', paymentDataRequest.shippingMethods);
// Store for reuse in case this data is not provided by ApplePay on authorization.
this.initialPaymentRequest = paymentRequest;
this.log('=== paymentRequest.shippingMethods', paymentRequest.shippingMethods);
}
paymentDataRequest() {
paymentRequest() {
const applepayConfig = this.applePayConfig
const buttonConfig = this.buttonConfig
let baseRequest = {
@ -301,22 +352,28 @@ class ApplepayButton {
merchantCapabilities: applepayConfig.merchantCapabilities,
supportedNetworks: applepayConfig.supportedNetworks,
requiredShippingContactFields: ["postalAddress", "email", "phone"],
requiredBillingContactFields: ["postalAddress"],
requiredBillingContactFields: ["postalAddress"], // ApplePay does not implement billing email and phone fields.
}
if (!this.shouldAllowShippingInButton()) {
baseRequest.requiredShippingContactFields = ["email", "phone"];
if (!this.shouldRequireShippingInButton()) {
if (this.shouldCompletePaymentWithContextHandler()) {
// Data needs handled externally.
baseRequest.requiredShippingContactFields = [];
} else {
// Minimum data required for order creation.
baseRequest.requiredShippingContactFields = ["email", "phone"];
}
}
const paymentDataRequest = Object.assign({}, baseRequest);
paymentDataRequest.currencyCode = buttonConfig.shop.currencyCode;
paymentDataRequest.total = {
const paymentRequest = Object.assign({}, baseRequest);
paymentRequest.currencyCode = buttonConfig.shop.currencyCode;
paymentRequest.total = {
label: buttonConfig.shop.totalLabel,
type: "final",
amount: this.transactionInfo.totalPrice,
}
return paymentDataRequest
return paymentRequest;
}
refreshContextData() {
@ -334,7 +391,7 @@ class ApplepayButton {
// Payment process
//------------------------
onvalidatemerchant(session) {
onValidateMerchant(session) {
this.log('onvalidatemerchant', this.buttonConfig.ajax_url);
return (applePayValidateMerchantEvent) => {
this.log('onvalidatemerchant call');
@ -368,83 +425,90 @@ class ApplepayButton {
validation: false,
'woocommerce-process-checkout-nonce': this.nonce,
}
})
});
this.log('onvalidatemerchant session abort');
session.abort();
});
};
}
onshippingmethodselected(session) {
onShippingMethodSelected(session) {
this.log('onshippingmethodselected', this.buttonConfig.ajax_url);
const ajax_url = this.buttonConfig.ajax_url
const ajax_url = this.buttonConfig.ajax_url;
return (event) => {
this.log('onshippingmethodselected call');
const data = this.getShippingMethodData(event);
jQuery.ajax({
url: ajax_url,
method: 'POST',
data: data,
success: (applePayShippingMethodUpdate, textStatus, jqXHR) => {
this.log('onshippingmethodselected ok');
let response = applePayShippingMethodUpdate.data
let response = applePayShippingMethodUpdate.data;
if (applePayShippingMethodUpdate.success === false) {
response.errors = createAppleErrors(response.errors)
response.errors = createAppleErrors(response.errors);
}
this.selectedShippingMethod = event.shippingMethod
//order the response shipping methods, so that the selected shipping method is the first one
let orderedShippingMethods = response.newShippingMethods.sort((a, b) => {
this.selectedShippingMethod = event.shippingMethod;
// Sort the response shipping methods, so that the selected shipping method is the first one.
response.newShippingMethods = response.newShippingMethods.sort((a, b) => {
if (a.label === this.selectedShippingMethod.label) {
return -1
return -1;
}
return 1
})
//update the response.newShippingMethods with the ordered shipping methods
response.newShippingMethods = orderedShippingMethods
return 1;
});
if (applePayShippingMethodUpdate.success === false) {
response.errors = createAppleErrors(response.errors)
response.errors = createAppleErrors(response.errors);
}
session.completeShippingMethodSelection(response)
session.completeShippingMethodSelection(response);
},
error: (jqXHR, textStatus, errorThrown) => {
this.log('onshippingmethodselected error', textStatus);
console.warn(textStatus, errorThrown)
session.abort()
console.warn(textStatus, errorThrown);
session.abort();
},
})
});
};
}
onshippingcontactselected(session) {
onShippingContactSelected(session) {
this.log('onshippingcontactselected', this.buttonConfig.ajax_url);
const ajax_url = this.buttonConfig.ajax_url
const ajax_url = this.buttonConfig.ajax_url;
return (event) => {
this.log('onshippingcontactselected call');
const data = this.getShippingContactData(event);
jQuery.ajax({
url: ajax_url,
method: 'POST',
data: data,
success: (applePayShippingContactUpdate, textStatus, jqXHR) => {
this.log('onshippingcontactselected ok');
let response = applePayShippingContactUpdate.data
this.updated_contact_info = event.shippingContact
let response = applePayShippingContactUpdate.data;
this.updatedContactInfo = event.shippingContact;
if (applePayShippingContactUpdate.success === false) {
response.errors = createAppleErrors(response.errors)
response.errors = createAppleErrors(response.errors);
}
if (response.newShippingMethods) {
this.selectedShippingMethod = response.newShippingMethods[0]
this.selectedShippingMethod = response.newShippingMethods[0];
}
session.completeShippingContactSelection(response)
session.completeShippingContactSelection(response);
},
error: (jqXHR, textStatus, errorThrown) => {
this.log('onshippingcontactselected error', textStatus);
console.warn(textStatus, errorThrown)
session.abort()
console.warn(textStatus, errorThrown);
session.abort();
},
})
});
};
}
getShippingContactData(event) {
const product_id = this.buttonConfig.product.id;
@ -459,7 +523,7 @@ class ApplepayButton {
caller_page: 'productDetail',
product_quantity: this.productQuantity,
simplified_contact: event.shippingContact,
need_shipping: this.shouldAllowShippingInButton(),
need_shipping: this.shouldRequireShippingInButton(),
'woocommerce-process-checkout-nonce': this.nonce,
};
case 'cart':
@ -471,11 +535,12 @@ class ApplepayButton {
action: 'ppcp_update_shipping_contact',
simplified_contact: event.shippingContact,
caller_page: 'cart',
need_shipping: this.shouldAllowShippingInButton(),
need_shipping: this.shouldRequireShippingInButton(),
'woocommerce-process-checkout-nonce': this.nonce,
};
}
}
getShippingMethodData(event) {
const product_id = this.buttonConfig.product.id;
@ -485,11 +550,11 @@ class ApplepayButton {
case 'product': return {
action: 'ppcp_update_shipping_method',
shipping_method: event.shippingMethod,
simplified_contact: this.updatedContactInfo || this.initialPaymentRequest.shippingContact || this.initialPaymentRequest.billingContact,
product_id: product_id,
products: JSON.stringify(this.products),
caller_page: 'productDetail',
product_quantity: this.productQuantity,
simplified_contact: this.updated_contact_info,
'woocommerce-process-checkout-nonce': this.nonce,
}
case 'cart':
@ -500,14 +565,14 @@ class ApplepayButton {
return {
action: 'ppcp_update_shipping_method',
shipping_method: event.shippingMethod,
simplified_contact: this.updatedContactInfo || this.initialPaymentRequest.shippingContact || this.initialPaymentRequest.billingContact,
caller_page: 'cart',
simplified_contact: this.updated_contact_info,
'woocommerce-process-checkout-nonce': this.nonce,
}
}
}
onpaymentauthorized(session) {
onPaymentAuthorized(session) {
this.log('onpaymentauthorized');
return async (event) => {
this.log('onpaymentauthorized call');
@ -518,8 +583,10 @@ class ApplepayButton {
const processInWooAndCapture = async (data) => {
return new Promise((resolve, reject) => {
try {
const billingContact = data.billing_contact
const shippingContact = data.shipping_contact
const billingContact = data.billing_contact || this.initialPaymentRequest.billingContact;
const shippingContact = data.shipping_contact || this.initialPaymentRequest.shippingContact;
const shippingMethod = this.selectedShippingMethod || (this.initialPaymentRequest.shippingMethods || [])[0];
let request_data = {
action: 'ppcp_create_order',
'caller_page': this.context,
@ -529,7 +596,7 @@ class ApplepayButton {
'shipping_contact': shippingContact,
'billing_contact': billingContact,
'token': event.payment.token,
'shipping_method': this.selectedShippingMethod,
'shipping_method': shippingMethod,
'woocommerce-process-checkout-nonce': this.nonce,
'funding_source': 'applepay',
'_wp_http_referer': '/?wc-ajax=update_order_review',
@ -547,16 +614,16 @@ class ApplepayButton {
},
success: (authorizationResult, textStatus, jqXHR) => {
this.log('onpaymentauthorized ok');
resolve(authorizationResult)
resolve(authorizationResult);
},
error: (jqXHR, textStatus, errorThrown) => {
this.log('onpaymentauthorized error', textStatus);
reject(new Error(errorThrown));
},
})
});
} catch (error) {
this.log('onpaymentauthorized catch', error);
console.log(error) // handle error
console.log(error); // handle error
}
});
}
@ -578,8 +645,8 @@ class ApplepayButton {
if (confirmOrderResponse.approveApplePayPayment.status === "APPROVED") {
try {
if (!this.shouldAllowShippingInButton()) {
// No shipping, expect immediate capture, ex: PayNow.
if (this.shouldCompletePaymentWithContextHandler()) {
// No shipping, expect immediate capture, ex: PayNow, Checkout with form data.
let approveFailed = false;
await this.contextHandler.approveOrder({
@ -602,7 +669,7 @@ class ApplepayButton {
} else {
this.log('onpaymentauthorized approveOrder FAIL');
session.completePayment(ApplePaySession.STATUS_FAILURE);
session.abort()
session.abort();
console.error(error);
}
@ -616,17 +683,17 @@ class ApplepayButton {
};
let authorizationResult = await processInWooAndCapture(data);
if (authorizationResult.result === "success") {
session.completePayment(ApplePaySession.STATUS_SUCCESS)
window.location.href = authorizationResult.redirect
session.completePayment(ApplePaySession.STATUS_SUCCESS);
window.location.href = authorizationResult.redirect;
} else {
session.completePayment(ApplePaySession.STATUS_FAILURE)
session.completePayment(ApplePaySession.STATUS_FAILURE);
}
}
} catch (error) {
session.completePayment(ApplePaySession.STATUS_FAILURE);
session.abort()
session.abort();
console.error(error);
}
} else {
@ -640,43 +707,44 @@ class ApplepayButton {
} catch (error) {
console.error('Error confirming order with applepay token', error);
session.completePayment(ApplePaySession.STATUS_FAILURE);
session.abort()
session.abort();
}
};
}
fill_billing_contact(form_saved) {
fillBillingContact(data) {
return {
givenName: form_saved.billing_first_name ?? '',
familyName: form_saved.billing_last_name ?? '',
emailAddress: form_saved.billing_email ?? '',
phoneNumber: form_saved.billing_phone ?? '',
addressLines: [form_saved.billing_address_1, form_saved.billing_address_2],
locality: form_saved.billing_city ?? '',
postalCode: form_saved.billing_postcode ?? '',
countryCode: form_saved.billing_country ?? '',
administrativeArea: form_saved.billing_state ?? '',
}
}
fill_shipping_contact(form_saved) {
if (form_saved.shipping_first_name === "") {
return this.fill_billing_contact(form_saved)
}
return {
givenName: (form_saved?.shipping_first_name && form_saved.shipping_first_name !== "") ? form_saved.shipping_first_name : form_saved?.billing_first_name,
familyName: (form_saved?.shipping_last_name && form_saved.shipping_last_name !== "") ? form_saved.shipping_last_name : form_saved?.billing_last_name,
emailAddress: (form_saved?.shipping_email && form_saved.shipping_email !== "") ? form_saved.shipping_email : form_saved?.billing_email,
phoneNumber: (form_saved?.shipping_phone && form_saved.shipping_phone !== "") ? form_saved.shipping_phone : form_saved?.billing_phone,
addressLines: [form_saved.shipping_address_1 ?? '', form_saved.shipping_address_2 ?? ''],
locality: (form_saved?.shipping_city && form_saved.shipping_city !== "") ? form_saved.shipping_city : form_saved?.billing_city,
postalCode: (form_saved?.shipping_postcode && form_saved.shipping_postcode !== "") ? form_saved.shipping_postcode : form_saved?.billing_postcode,
countryCode: (form_saved?.shipping_country && form_saved.shipping_country !== "") ? form_saved.shipping_country : form_saved?.billing_country,
administrativeArea: (form_saved?.shipping_state && form_saved.shipping_state !== "") ? form_saved.shipping_state : form_saved?.billing_state,
givenName: data.billing_first_name ?? '',
familyName: data.billing_last_name ?? '',
emailAddress: data.billing_email ?? '',
phoneNumber: data.billing_phone ?? '',
addressLines: [data.billing_address_1, data.billing_address_2],
locality: data.billing_city ?? '',
postalCode: data.billing_postcode ?? '',
countryCode: data.billing_country ?? '',
administrativeArea: data.billing_state ?? '',
}
}
fill_application_data(form_saved) {
const jsonString = JSON.stringify(form_saved);
fillShippingContact(data) {
if (data.shipping_first_name === "") {
return this.fillBillingContact(data);
}
return {
givenName: (data?.shipping_first_name && data.shipping_first_name !== "") ? data.shipping_first_name : data?.billing_first_name,
familyName: (data?.shipping_last_name && data.shipping_last_name !== "") ? data.shipping_last_name : data?.billing_last_name,
emailAddress: (data?.shipping_email && data.shipping_email !== "") ? data.shipping_email : data?.billing_email,
phoneNumber: (data?.shipping_phone && data.shipping_phone !== "") ? data.shipping_phone : data?.billing_phone,
addressLines: [data.shipping_address_1 ?? '', data.shipping_address_2 ?? ''],
locality: (data?.shipping_city && data.shipping_city !== "") ? data.shipping_city : data?.billing_city,
postalCode: (data?.shipping_postcode && data.shipping_postcode !== "") ? data.shipping_postcode : data?.billing_postcode,
countryCode: (data?.shipping_country && data.shipping_country !== "") ? data.shipping_country : data?.billing_country,
administrativeArea: (data?.shipping_state && data.shipping_state !== "") ? data.shipping_state : data?.billing_state,
}
}
fillApplicationData(data) {
const jsonString = JSON.stringify(data);
let utf8Str = encodeURIComponent(jsonString).replace(/%([0-9A-F]{2})/g, (match, p1) => {
return String.fromCharCode('0x' + p1);
});

View file

@ -15,12 +15,14 @@ class BaseHandler {
transactionInfo() {
return new Promise((resolve, reject) => {
const endpoint = this.ppcpConfig.ajax.cart_script_params.endpoint;
const separator = (endpoint.indexOf('?') !== -1) ? '&' : '?';
fetch(
this.ppcpConfig.ajax.cart_script_params.endpoint,
endpoint + separator + 'shipping=1',
{
method: 'GET',
credentials: 'same-origin',
credentials: 'same-origin'
}
)
.then(result => result.json())

View file

@ -1,28 +1,17 @@
import Spinner from "../../../../ppcp-button/resources/js/modules/Helper/Spinner";
import CheckoutActionHandler
from "../../../../ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler";
import ErrorHandler from "../../../../ppcp-button/resources/js/modules/ErrorHandler";
import BaseHandler from "./BaseHandler";
class CheckoutHandler extends BaseHandler {
createOrder() {
const errorHandler = new ErrorHandler(
this.ppcpConfig.labels.error.generic,
document.querySelector('.woocommerce-notices-wrapper')
);
const spinner = new Spinner();
const actionHandler = new CheckoutActionHandler(
actionHandler() {
return new CheckoutActionHandler(
this.ppcpConfig,
errorHandler,
spinner
this.errorHandler(),
new Spinner()
);
return actionHandler.configuration().createOrder(null, null);
}
}
export default CheckoutHandler;

View file

@ -312,7 +312,6 @@ class ApplePayButton implements ButtonInterface {
$user_country,
$allowed_shipping_countries
);
$product_need_shipping = $applepay_request_data_object->need_shipping();
if ( ! $is_allowed_selling_country ) {
$this->response_templates->response_with_data_errors(
@ -320,7 +319,7 @@ class ApplePayButton implements ButtonInterface {
);
return;
}
if ( $product_need_shipping && ! $is_allowed_shipping_country ) {
if ( $applepay_request_data_object->need_shipping() && ! $is_allowed_shipping_country ) {
$this->response_templates->response_with_data_errors(
array( array( 'errorCode' => 'addressUnserviceable' ) )
);
@ -406,9 +405,6 @@ class ApplePayButton implements ButtonInterface {
$applepay_request_data_object = $this->applepay_data_object_http();
//phpcs:disable WordPress.Security.NonceVerification
$this->logger->info('== $_POST ==');
$this->logger->info(print_r($_POST, true));
$context = wc_clean( wp_unslash( $_POST['caller_page'] ?? '' ) );
if ( ! is_string( $context ) ) {
$this->response_templates->response_with_data_errors(
@ -423,9 +419,6 @@ class ApplePayButton implements ButtonInterface {
}
$applepay_request_data_object->order_data( $context );
$this->logger->info('== $applepay_request_data_object ==');
$this->logger->info(print_r($applepay_request_data_object, true));
$this->update_posted_data( $applepay_request_data_object );
if ( $context === 'product' ) {
$cart_item_key = $this->prepare_cart( $applepay_request_data_object );
@ -459,7 +452,7 @@ class ApplePayButton implements ButtonInterface {
}
);
}
$this->add_addresses_to_order( $applepay_request_data_object );
WC()->checkout()->process_checkout();
}
@ -773,33 +766,6 @@ class ApplePayButton implements ButtonInterface {
return $results;
}
/**
* Add address billing and shipping data to order
*
* @param ApplePayDataObjectHttp $applepay_request_data_object ApplePayDataObjectHttp.
*/
protected function add_addresses_to_order(
ApplePayDataObjectHttp $applepay_request_data_object
): void {
add_action(
'woocommerce_checkout_create_order',
static function ( WC_Order $order, array $data ) use ( $applepay_request_data_object ) {
$this->logger->info('== HOOK woocommerce_checkout_create_order');
if ( ! empty( $applepay_request_data_object->shipping_method() ) ) {
$billing_address = $applepay_request_data_object->billing_address();
$shipping_address = $applepay_request_data_object->shipping_address();
// apple puts email in shipping_address while we get it from WC's billing_address.
$billing_address['email'] = $shipping_address['email'];
$billing_address['phone'] = $shipping_address['phone'];
$this->logger->info(print_r($billing_address, true));
$order->set_address( $billing_address, 'billing' );
$order->set_address( $shipping_address, 'shipping' );
}
},
10,
2
);
}
/**
* Empty the cart to use for calculations
* while saving its contents in a field
@ -881,8 +847,6 @@ $this->logger->info(print_r($billing_address, true));
add_filter(
'woocommerce_checkout_posted_data',
function ( array $data ) use ( $applepay_request_data_object ): array {
$this->logger->info('== HOOK woocommerce_checkout_posted_data');
$data['payment_method'] = 'ppcp-gateway';
$data['shipping_method'] = $applepay_request_data_object->shipping_method();
$data['billing_first_name'] = $applepay_request_data_object->billing_address()['first_name'] ?? '';
@ -894,10 +858,18 @@ $this->logger->info('== HOOK woocommerce_checkout_posted_data');
$data['billing_city'] = $applepay_request_data_object->billing_address()['city'] ?? '';
$data['billing_state'] = $applepay_request_data_object->billing_address()['state'] ?? '';
$data['billing_postcode'] = $applepay_request_data_object->billing_address()['postcode'] ?? '';
$data['billing_email'] = $applepay_request_data_object->billing_address()['email'] ?? '';
$data['billing_phone'] = $applepay_request_data_object->billing_address()['phone'] ?? '';
if ( ! empty( $applepay_request_data_object->need_shipping() ) ) {
$data['billing_email'] = $applepay_request_data_object->shipping_address()['email'] ?? '';
$data['billing_phone'] = $applepay_request_data_object->shipping_address()['phone'] ?? '';
// ApplePay doesn't send us a billing email or phone, use the shipping contacts instead.
if ( ! ( $data['billing_email'] ?? false ) ) {
$data['billing_email'] = $applepay_request_data_object->shipping_address()['email'] ?? '';
}
if ( ! ( $data['billing_phone'] ?? false ) ) {
$data['billing_phone'] = $applepay_request_data_object->shipping_address()['phone'] ?? '';
}
if ( ! empty( $applepay_request_data_object->shipping_method() ) ) {
$data['shipping_first_name'] = $applepay_request_data_object->shipping_address()['first_name'] ?? '';
$data['shipping_last_name'] = $applepay_request_data_object->shipping_address()['last_name'] ?? '';
$data['shipping_company'] = $applepay_request_data_object->shipping_address()['company'] ?? '';
@ -910,7 +882,6 @@ $this->logger->info('== HOOK woocommerce_checkout_posted_data');
$data['shipping_email'] = $applepay_request_data_object->shipping_address()['email'] ?? '';
$data['shipping_phone'] = $applepay_request_data_object->shipping_address()['phone'] ?? '';
}
$this->logger->info(print_r($data, true));
return $data;
}

View file

@ -202,29 +202,29 @@ class DataToAppleButtonScripts {
$checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT;
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),
'button' => array(
'button' => array(
'wrapper' => 'applepay-container',
'mini_cart_wrapper' => 'applepay-container-minicart',
'type' => $type,
'color' => $color,
'lang' => $lang,
),
'product' => array(
'product' => array(
'needShipping' => $cart->needs_shipping(),
'subtotal' => $cart->get_subtotal(),
),
'shop' => array(
'shop' => array(
'countryCode' => $shop_country_code,
'currencyCode' => $currency_code,
'totalLabel' => $total_label,
),
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'woocommerce-process_checkout' ),
'ajax_url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'woocommerce-process_checkout' ),
);
}
@ -250,28 +250,28 @@ class DataToAppleButtonScripts {
$checkout_data_mode = $this->settings->has( 'applepay_checkout_data_mode' ) ? $this->settings->get( 'applepay_checkout_data_mode' ) : PropertiesDictionary::BILLING_DATA_MODE_DEFAULT;
return array(
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'sdk_url' => $this->sdk_url,
'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false,
'preferences' => array(
'checkout_data_mode' => $checkout_data_mode,
),
'button' => array(
'button' => array(
'wrapper' => 'applepay-container',
'mini_cart_wrapper' => 'applepay-container-minicart',
'type' => $type,
'color' => $color,
'lang' => $lang,
),
'product' => array(
'product' => array(
'needShipping' => false,
'subtotal' => 0,
),
'shop' => array(
'shop' => array(
'countryCode' => $shop_country_code,
'currencyCode' => $currency_code,
'totalLabel' => $total_label,
),
'ajax_url' => admin_url( 'admin-ajax.php' ),
'ajax_url' => admin_url( 'admin-ajax.php' ),
);
}
}

View file

@ -198,8 +198,8 @@ class PropertiesDictionary {
*/
public static function billing_data_modes(): array {
return array(
PropertiesDictionary::BILLING_DATA_MODE_USE_WC => __( 'Use WC checkout form data (do not show shipping address fields)', 'woocommerce-paypal-payments' ),
PropertiesDictionary::BILLING_DATA_MODE_USE_APPLEPAY => __( 'Do not use WC checkout form data (request billing and shipping addresses on Apple Pay)', 'woocommerce-paypal-payments' ),
self::BILLING_DATA_MODE_USE_WC => __( 'Use WC checkout form data (do not show shipping address fields)', 'woocommerce-paypal-payments' ),
self::BILLING_DATA_MODE_USE_APPLEPAY => __( 'Do not use WC checkout form data (request billing and shipping addresses on Apple Pay)', 'woocommerce-paypal-payments' ),
);
}
}

View file

@ -70,6 +70,8 @@ class CartScriptParamsEndpoint implements EndpointInterface {
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
}
$include_shipping = (bool) wc_clean( wp_unslash( $_GET['shipping'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$script_data = $this->smart_button->script_data();
$total = (float) WC()->cart->get_total( 'numeric' );
@ -79,52 +81,23 @@ class CartScriptParamsEndpoint implements EndpointInterface {
$shop_country_code = $base_location['country'] ?? '';
$currency_code = get_woocommerce_currency();
$response = array(
'url_params' => $script_data['url_params'],
'button' => $script_data['button'],
'messages' => $script_data['messages'],
'amount' => WC()->cart->get_total( 'raw' ),
$calculated_packages = WC()->shipping->calculate_shipping(
WC()->cart->get_shipping_packages()
'total' => $total,
'total_str' => ( new Money( $total, $currency_code ) )->value_str(),
'currency_code' => $currency_code,
'country_code' => $shop_country_code,
);
$shipping_packages = array();
foreach ( $calculated_packages[0]['rates'] as $rate ) {
$rate_cost = $rate->get_cost();
/**
* The shipping rate.
*
* @var \WC_Shipping_Rate $rate
*/
$shipping_packages[] = array(
'id' => $rate->get_id(),
'label' => $rate->get_label(),
'cost' => (float) $rate_cost,
'cost_str' => ( new Money( (float) $rate_cost, $currency_code ) )->value_str(),
'description' => html_entity_decode(
wp_strip_all_tags(
wc_price( (float) $rate->get_cost(), array( 'currency' => get_woocommerce_currency() ) )
)
),
);
if ( $include_shipping ) {
$response = $this->append_shipping_data( $response, $currency_code );
}
wp_send_json_success(
array(
'url_params' => $script_data['url_params'],
'button' => $script_data['button'],
'messages' => $script_data['messages'],
'amount' => WC()->cart->get_total( 'raw' ),
'total' => $total,
'total_str' => ( new Money( $total, $currency_code ) )->value_str(),
'currency_code' => $currency_code,
'country_code' => $shop_country_code,
'chosen_shipping_methods' => WC()->session->get( 'chosen_shipping_methods' ),
'shipping_packages' => $shipping_packages
)
);
wp_send_json_success( $response );
return true;
} catch ( Throwable $error ) {
$this->logger->error( "CartScriptParamsEndpoint execution failed. {$error->getMessage()} {$error->getFile()}:{$error->getLine()}" );
@ -133,4 +106,45 @@ class CartScriptParamsEndpoint implements EndpointInterface {
return false;
}
}
/**
* Appends shipping data to response.
*
* @param array $response The response array.
* @param string $currency_code The currency code.
* @return array
*/
private function append_shipping_data( array $response, string $currency_code ): array {
$calculated_packages = WC()->shipping->calculate_shipping(
WC()->cart->get_shipping_packages()
);
$shipping_packages = array();
foreach ( $calculated_packages[0]['rates'] as $rate ) {
$rate_cost = $rate->get_cost();
/**
* The shipping rate.
*
* @var \WC_Shipping_Rate $rate
*/
$shipping_packages[] = array(
'id' => $rate->get_id(),
'label' => $rate->get_label(),
'cost' => (float) $rate_cost,
'cost_str' => ( new Money( (float) $rate_cost, $currency_code ) )->value_str(),
'description' => html_entity_decode(
wp_strip_all_tags(
wc_price( (float) $rate->get_cost(), array( 'currency' => get_woocommerce_currency() ) )
)
),
);
}
$response['chosen_shipping_methods'] = WC()->session->get( 'chosen_shipping_methods' );
$response['shipping_packages'] = $shipping_packages;
return $response;
}
}

View file

@ -142,7 +142,7 @@ return array(
'This controls the label of the Google Pay button.',
'woocommerce-paypal-payments'
),
'classes' => array('ppcp-field-indent'),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'pay',
@ -161,7 +161,7 @@ return array(
),
'label' => '',
'input_class' => array( 'wc-enhanced-select' ),
'classes' => array('ppcp-field-indent'),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'default' => 'black',
'options' => PropertiesDictionary::button_colors(),
@ -177,7 +177,7 @@ return array(
'The language and region used for the displayed Google Pay button. The default value is the current language and region setting in a browser.',
'woocommerce-paypal-payments'
),
'classes' => array('ppcp-field-indent'),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'en',
@ -194,7 +194,7 @@ return array(
'Synchronizes your available shipping options with Google Pay. Enabling this may impact the buyer experience.',
'woocommerce-paypal-payments'
),
'classes' => array('ppcp-field-indent'),
'classes' => array( 'ppcp-field-indent' ),
'label' => __( 'Enable Google Pay shipping callback', 'woocommerce-paypal-payments' ),
'default' => 'no',
'screens' => array( State::STATE_ONBOARDED ),