mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-05 08:59:14 +08:00
Merge pull request #2513 from woocommerce/PCP-3527-google-pay-shipping-callback-not-calculating-totals-correctly-on-single-product-page
Google Pay: Shipping callback not calculating totals correctly on Single Product page (3527)
This commit is contained in:
commit
c3d53853de
7 changed files with 274 additions and 43 deletions
|
@ -1,5 +1,6 @@
|
|||
import ErrorHandler from '../../../../ppcp-button/resources/js/modules/ErrorHandler';
|
||||
import CartActionHandler from '../../../../ppcp-button/resources/js/modules/ActionHandler/CartActionHandler';
|
||||
import TransactionInfo from '../Helper/TransactionInfo';
|
||||
|
||||
class BaseHandler {
|
||||
constructor( buttonConfig, ppcpConfig, externalHandler ) {
|
||||
|
@ -34,13 +35,14 @@ class BaseHandler {
|
|||
|
||||
// handle script reload
|
||||
const data = result.data;
|
||||
const transaction = new TransactionInfo(
|
||||
data.total,
|
||||
data.shipping_fee,
|
||||
data.currency_code,
|
||||
data.country_code
|
||||
);
|
||||
|
||||
resolve( {
|
||||
countryCode: data.country_code,
|
||||
currencyCode: data.currency_code,
|
||||
totalPriceStatus: 'FINAL',
|
||||
totalPrice: data.total_str,
|
||||
} );
|
||||
resolve( transaction );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Spinner from '../../../../ppcp-button/resources/js/modules/Helper/Spinner';
|
||||
import BaseHandler from './BaseHandler';
|
||||
import CheckoutActionHandler from '../../../../ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler';
|
||||
import TransactionInfo from '../Helper/TransactionInfo';
|
||||
|
||||
class PayNowHandler extends BaseHandler {
|
||||
validateContext() {
|
||||
|
@ -14,12 +15,14 @@ class PayNowHandler extends BaseHandler {
|
|||
return new Promise( async ( resolve, reject ) => {
|
||||
const data = this.ppcpConfig.pay_now;
|
||||
|
||||
resolve( {
|
||||
countryCode: data.country_code,
|
||||
currencyCode: data.currency_code,
|
||||
totalPriceStatus: 'FINAL',
|
||||
totalPrice: data.total_str,
|
||||
} );
|
||||
const transaction = new TransactionInfo(
|
||||
data.total,
|
||||
data.shipping_fee,
|
||||
data.currency_code,
|
||||
data.country_code
|
||||
);
|
||||
|
||||
resolve( transaction );
|
||||
} );
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import SimulateCart from '../../../../ppcp-button/resources/js/modules/Helper/Si
|
|||
import ErrorHandler from '../../../../ppcp-button/resources/js/modules/ErrorHandler';
|
||||
import UpdateCart from '../../../../ppcp-button/resources/js/modules/Helper/UpdateCart';
|
||||
import BaseHandler from './BaseHandler';
|
||||
import TransactionInfo from '../Helper/TransactionInfo';
|
||||
|
||||
class SingleProductHandler extends BaseHandler {
|
||||
validateContext() {
|
||||
|
@ -42,12 +43,14 @@ class SingleProductHandler extends BaseHandler {
|
|||
this.ppcpConfig.ajax.simulate_cart.endpoint,
|
||||
this.ppcpConfig.ajax.simulate_cart.nonce
|
||||
).simulate( ( data ) => {
|
||||
resolve( {
|
||||
countryCode: data.country_code,
|
||||
currencyCode: data.currency_code,
|
||||
totalPriceStatus: 'FINAL',
|
||||
totalPrice: data.total_str,
|
||||
} );
|
||||
const transaction = new TransactionInfo(
|
||||
data.total,
|
||||
data.shipping_fee,
|
||||
data.currency_code,
|
||||
data.country_code
|
||||
);
|
||||
|
||||
resolve( transaction );
|
||||
}, products );
|
||||
} );
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/PaymentButton';
|
||||
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
|
||||
import UpdatePaymentData from './Helper/UpdatePaymentData';
|
||||
import TransactionInfo from './Helper/TransactionInfo';
|
||||
import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
|
||||
import { setPayerData } from '../../../ppcp-button/resources/js/modules/Helper/PayerData';
|
||||
import moduleStorage from './Helper/GooglePayStorage';
|
||||
|
@ -480,7 +481,7 @@ class GooglepayButton extends PaymentButton {
|
|||
return {
|
||||
...baseRequest,
|
||||
allowedPaymentMethods: this.googlePayConfig.allowedPaymentMethods,
|
||||
transactionInfo: this.transactionInfo,
|
||||
transactionInfo: this.transactionInfo.finalObject,
|
||||
merchantInfo: this.googlePayConfig.merchantInfo,
|
||||
callbackIntents,
|
||||
emailRequired: true,
|
||||
|
@ -513,6 +514,16 @@ class GooglepayButton extends PaymentButton {
|
|||
).update( paymentData );
|
||||
const transactionInfo = this.transactionInfo;
|
||||
|
||||
// Check, if the current context uses the WC cart.
|
||||
const hasRealCart = [
|
||||
'checkout-block',
|
||||
'checkout',
|
||||
'cart-block',
|
||||
'cart',
|
||||
'mini-cart',
|
||||
'pay-now',
|
||||
].includes( this.context );
|
||||
|
||||
this.log( 'onPaymentDataChanged:updatedData', updatedData );
|
||||
this.log(
|
||||
'onPaymentDataChanged:transactionInfo',
|
||||
|
@ -521,7 +532,6 @@ class GooglepayButton extends PaymentButton {
|
|||
|
||||
updatedData.country_code = transactionInfo.countryCode;
|
||||
updatedData.currency_code = transactionInfo.currencyCode;
|
||||
updatedData.total_str = transactionInfo.totalPrice;
|
||||
|
||||
// Handle unserviceable address.
|
||||
if ( ! updatedData.shipping_options?.shippingOptions?.length ) {
|
||||
|
@ -531,20 +541,37 @@ class GooglepayButton extends PaymentButton {
|
|||
return;
|
||||
}
|
||||
|
||||
switch ( paymentData.callbackTrigger ) {
|
||||
case 'INITIALIZE':
|
||||
case 'SHIPPING_ADDRESS':
|
||||
paymentDataRequestUpdate.newShippingOptionParameters =
|
||||
updatedData.shipping_options;
|
||||
paymentDataRequestUpdate.newTransactionInfo =
|
||||
this.calculateNewTransactionInfo( updatedData );
|
||||
break;
|
||||
case 'SHIPPING_OPTION':
|
||||
paymentDataRequestUpdate.newTransactionInfo =
|
||||
this.calculateNewTransactionInfo( updatedData );
|
||||
break;
|
||||
if (
|
||||
[ 'INITIALIZE', 'SHIPPING_ADDRESS' ].includes(
|
||||
paymentData.callbackTrigger
|
||||
)
|
||||
) {
|
||||
paymentDataRequestUpdate.newShippingOptionParameters =
|
||||
this.sanitizeShippingOptions(
|
||||
updatedData.shipping_options
|
||||
);
|
||||
}
|
||||
|
||||
if ( updatedData.total && hasRealCart ) {
|
||||
transactionInfo.setTotal(
|
||||
updatedData.total,
|
||||
updatedData.shipping_fee
|
||||
);
|
||||
|
||||
// This page contains a real cart and potentially a form for shipping options.
|
||||
this.syncShippingOptionWithForm(
|
||||
paymentData?.shippingOptionData?.id
|
||||
);
|
||||
} else {
|
||||
transactionInfo.shippingFee = this.getShippingCosts(
|
||||
paymentData?.shippingOptionData?.id,
|
||||
updatedData.shipping_options
|
||||
);
|
||||
}
|
||||
|
||||
paymentDataRequestUpdate.newTransactionInfo =
|
||||
this.calculateNewTransactionInfo( transactionInfo );
|
||||
|
||||
resolve( paymentDataRequestUpdate );
|
||||
} catch ( error ) {
|
||||
this.error( 'Error during onPaymentDataChanged:', error );
|
||||
|
@ -553,6 +580,76 @@ class GooglepayButton extends PaymentButton {
|
|||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Pay throws an error, when the shippingOptions entries contain
|
||||
* custom properties. This function strips unsupported properties from the
|
||||
* provided ajax response.
|
||||
*
|
||||
* @param {Object} responseData Data returned from the ajax endpoint.
|
||||
* @return {Object} Sanitized object.
|
||||
*/
|
||||
sanitizeShippingOptions( responseData ) {
|
||||
// Sanitize the shipping options.
|
||||
const cleanOptions = responseData.shippingOptions.map( ( item ) => ( {
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
description: item.description,
|
||||
} ) );
|
||||
|
||||
// Ensure that the default option is valid.
|
||||
let defaultOptionId = responseData.defaultSelectedOptionId;
|
||||
if ( ! cleanOptions.some( ( item ) => item.id === defaultOptionId ) ) {
|
||||
defaultOptionId = cleanOptions[ 0 ].id;
|
||||
}
|
||||
|
||||
return {
|
||||
defaultSelectedOptionId: defaultOptionId,
|
||||
shippingOptions: cleanOptions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shipping costs as numeric value.
|
||||
*
|
||||
* TODO - Move this to the PaymentButton base class
|
||||
*
|
||||
* @param {string} shippingId - The shipping method ID.
|
||||
* @param {Object} shippingData - The PaymentDataRequest object that
|
||||
* contains shipping options.
|
||||
* @param {Array} shippingData.shippingOptions
|
||||
* @param {string} shippingData.defaultSelectedOptionId
|
||||
*
|
||||
* @return {number} The shipping costs.
|
||||
*/
|
||||
getShippingCosts(
|
||||
shippingId,
|
||||
{ shippingOptions = [], defaultSelectedOptionId = '' } = {}
|
||||
) {
|
||||
if ( ! shippingOptions?.length ) {
|
||||
this.log( 'Cannot calculate shipping cost: No Shipping Options' );
|
||||
return 0;
|
||||
}
|
||||
|
||||
const findOptionById = ( id ) =>
|
||||
shippingOptions.find( ( option ) => option.id === id );
|
||||
|
||||
const getValidShippingId = () => {
|
||||
if (
|
||||
'shipping_option_unselected' === shippingId ||
|
||||
! findOptionById( shippingId )
|
||||
) {
|
||||
// Entered on initial call, and when changing the shipping country.
|
||||
return defaultSelectedOptionId;
|
||||
}
|
||||
|
||||
return shippingId;
|
||||
};
|
||||
|
||||
const currentOption = findOptionById( getValidShippingId() );
|
||||
|
||||
return Number( currentOption?.cost ) || 0;
|
||||
}
|
||||
|
||||
unserviceableShippingAddressError() {
|
||||
return {
|
||||
reason: 'SHIPPING_ADDRESS_UNSERVICEABLE',
|
||||
|
@ -561,13 +658,14 @@ class GooglepayButton extends PaymentButton {
|
|||
};
|
||||
}
|
||||
|
||||
calculateNewTransactionInfo( updatedData ) {
|
||||
return {
|
||||
countryCode: updatedData.country_code,
|
||||
currencyCode: updatedData.currency_code,
|
||||
totalPriceStatus: 'FINAL',
|
||||
totalPrice: updatedData.total_str,
|
||||
};
|
||||
/**
|
||||
* Recalculates and returns the plain transaction info object.
|
||||
*
|
||||
* @param {TransactionInfo} transactionInfo - Internal transactionInfo instance.
|
||||
* @return {{totalPrice: string, countryCode: string, totalPriceStatus: string, currencyCode: string}} Updated details.
|
||||
*/
|
||||
calculateNewTransactionInfo( transactionInfo ) {
|
||||
return transactionInfo.finalObject;
|
||||
}
|
||||
|
||||
//------------------------
|
||||
|
@ -699,6 +797,55 @@ class GooglepayButton extends PaymentButton {
|
|||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the shipping option in the checkout form, if a form with shipping options is
|
||||
* detected.
|
||||
*
|
||||
* @param {string} shippingOption - The shipping option ID, e.g. "flat_rate:4".
|
||||
* @return {boolean} - True if a shipping option was found and selected, false otherwise.
|
||||
*/
|
||||
syncShippingOptionWithForm( shippingOption ) {
|
||||
const wrappers = [
|
||||
// Classic checkout, Classic cart.
|
||||
'.woocommerce-shipping-methods',
|
||||
// Block checkout.
|
||||
'.wc-block-components-shipping-rates-control',
|
||||
// Block cart.
|
||||
'.wc-block-components-totals-shipping',
|
||||
];
|
||||
|
||||
const sanitizedShippingOption = shippingOption.replace( /"/g, '' );
|
||||
|
||||
// Check for radio buttons with shipping options.
|
||||
for ( const wrapper of wrappers ) {
|
||||
const selector = `${ wrapper } input[type="radio"][value="${ sanitizedShippingOption }"]`;
|
||||
const radioInput = document.querySelector( selector );
|
||||
|
||||
if ( radioInput ) {
|
||||
radioInput.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for select list with shipping options.
|
||||
for ( const wrapper of wrappers ) {
|
||||
const selector = `${ wrapper } select option[value="${ sanitizedShippingOption }"]`;
|
||||
const selectOption = document.querySelector( selector );
|
||||
|
||||
if ( selectOption ) {
|
||||
const selectElement = selectOption.closest( 'select' );
|
||||
|
||||
if ( selectElement ) {
|
||||
selectElement.value = sanitizedShippingOption;
|
||||
selectElement.dispatchEvent( new Event( 'change' ) );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default GooglepayButton;
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
export default class TransactionInfo {
|
||||
#country = '';
|
||||
#currency = '';
|
||||
#amount = 0;
|
||||
#shippingFee = 0;
|
||||
|
||||
constructor( total, shippingFee, currency, country ) {
|
||||
this.#country = country;
|
||||
this.#currency = currency;
|
||||
|
||||
this.shippingFee = shippingFee;
|
||||
this.amount = total - shippingFee;
|
||||
}
|
||||
|
||||
set amount( newAmount ) {
|
||||
this.#amount = this.toAmount( newAmount );
|
||||
}
|
||||
|
||||
get amount() {
|
||||
return this.#amount;
|
||||
}
|
||||
|
||||
set shippingFee( newCost ) {
|
||||
this.#shippingFee = this.toAmount( newCost );
|
||||
}
|
||||
|
||||
get shippingFee() {
|
||||
return this.#shippingFee;
|
||||
}
|
||||
|
||||
get currencyCode() {
|
||||
return this.#currency;
|
||||
}
|
||||
|
||||
get countryCode() {
|
||||
return this.#country;
|
||||
}
|
||||
|
||||
get totalPrice() {
|
||||
const total = this.#amount + this.#shippingFee;
|
||||
|
||||
return total.toFixed( 2 );
|
||||
}
|
||||
|
||||
get finalObject() {
|
||||
return {
|
||||
countryCode: this.countryCode,
|
||||
currencyCode: this.currencyCode,
|
||||
totalPriceStatus: 'FINAL',
|
||||
totalPrice: this.totalPrice,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the value to a number and rounds to a precision of 2 digits.
|
||||
*
|
||||
* @param {any} value - The value to sanitize.
|
||||
* @return {number} Numeric value.
|
||||
*/
|
||||
toAmount( value ) {
|
||||
value = Number( value ) || 0;
|
||||
return Math.round( value * 100 ) / 100;
|
||||
}
|
||||
|
||||
setTotal( totalPrice, shippingFee ) {
|
||||
totalPrice = this.toAmount( totalPrice );
|
||||
|
||||
if ( totalPrice ) {
|
||||
this.shippingFee = shippingFee;
|
||||
this.amount = totalPrice - this.shippingFee;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -90,7 +90,8 @@ class UpdatePaymentDataEndpoint {
|
|||
WC()->cart->calculate_fees();
|
||||
WC()->cart->calculate_totals();
|
||||
|
||||
$total = (float) WC()->cart->get_total( 'numeric' );
|
||||
$total = (float) WC()->cart->get_total( 'numeric' );
|
||||
$shipping_fee = (float) WC()->cart->get_shipping_total();
|
||||
|
||||
// Shop settings.
|
||||
$base_location = wc_get_base_location();
|
||||
|
@ -100,7 +101,7 @@ class UpdatePaymentDataEndpoint {
|
|||
wp_send_json_success(
|
||||
array(
|
||||
'total' => $total,
|
||||
'total_str' => ( new Money( $total, $currency_code ) )->value_str(),
|
||||
'shipping_fee' => $shipping_fee,
|
||||
'currency_code' => $currency_code,
|
||||
'country_code' => $shop_country_code,
|
||||
'shipping_options' => $this->get_shipping_options(),
|
||||
|
@ -146,6 +147,7 @@ class UpdatePaymentDataEndpoint {
|
|||
wc_price( (float) $rate->get_cost(), array( 'currency' => get_woocommerce_currency() ) )
|
||||
)
|
||||
),
|
||||
'cost' => $rate->get_cost(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue