Merge branch 'trunk' into update/add-title-description-and-gateway

This commit is contained in:
Emili Castells Guasch 2024-09-20 15:26:48 +02:00
commit b8a7a533c2
190 changed files with 6070 additions and 1680 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" width="92px" height="50px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" viewBox="160 160 774 422" enable-background="new 0 0 1094 742" xml:space="preserve">
<path id="Base_1_" fill="#FFFFFF" d="M722.7,170h-352c-110,0-200,90-200,200l0,0c0,110,90,200,200,200h352c110,0,200-90,200-200l0,0
C922.7,260,832.7,170,722.7,170z"/>
<path id="Outline" fill="#3C4043" d="M722.7,186.2c24.7,0,48.7,4.9,71.3,14.5c21.9,9.3,41.5,22.6,58.5,39.5
c16.9,16.9,30.2,36.6,39.5,58.5c9.6,22.6,14.5,46.6,14.5,71.3s-4.9,48.7-14.5,71.3c-9.3,21.9-22.6,41.5-39.5,58.5
c-16.9,16.9-36.6,30.2-58.5,39.5c-22.6,9.6-46.6,14.5-71.3,14.5h-352c-24.7,0-48.7-4.9-71.3-14.5c-21.9-9.3-41.5-22.6-58.5-39.5
c-16.9-16.9-30.2-36.6-39.5-58.5c-9.6-22.6-14.5-46.6-14.5-71.3s4.9-48.7,14.5-71.3c9.3-21.9,22.6-41.5,39.5-58.5
c16.9-16.9,36.6-30.2,58.5-39.5c22.6-9.6,46.6-14.5,71.3-14.5L722.7,186.2 M722.7,170h-352c-110,0-200,90-200,200l0,0
c0,110,90,200,200,200h352c110,0,200-90,200-200l0,0C922.7,260,832.7,170,722.7,170L722.7,170z"/>
<g id="G_Pay_Lockup_1_">
<g id="Pay_Typeface_3_">
<path id="Letter_p_3_" fill="#3C4043" d="M529.3,384.2v60.5h-19.2V295.3H561c12.9,0,23.9,4.3,32.9,12.9
c9.2,8.6,13.8,19.1,13.8,31.5c0,12.7-4.6,23.2-13.8,31.7c-8.9,8.5-19.9,12.7-32.9,12.7h-31.7V384.2z M529.3,313.7v52.1h32.1
c7.6,0,14-2.6,19-7.7c5.1-5.1,7.7-11.3,7.7-18.3c0-6.9-2.6-13-7.7-18.1c-5-5.3-11.3-7.9-19-7.9h-32.1V313.7z"/>
<path id="Letter_a_3_" fill="#3C4043" d="M657.9,339.1c14.2,0,25.4,3.8,33.6,11.4c8.2,7.6,12.3,18,12.3,31.2v63h-18.3v-14.2h-0.8
c-7.9,11.7-18.5,17.5-31.7,17.5c-11.3,0-20.7-3.3-28.3-10s-11.4-15-11.4-25c0-10.6,4-19,12-25.2c8-6.3,18.7-9.4,32-9.4
c11.4,0,20.8,2.1,28.1,6.3v-4.4c0-6.7-2.6-12.3-7.9-17c-5.3-4.7-11.5-7-18.6-7c-10.7,0-19.2,4.5-25.4,13.6l-16.9-10.6
C625.9,345.8,639.7,339.1,657.9,339.1z M633.1,413.3c0,5,2.1,9.2,6.4,12.5c4.2,3.3,9.2,5,14.9,5c8.1,0,15.3-3,21.6-9
s9.5-13,9.5-21.1c-6-4.7-14.3-7.1-25-7.1c-7.8,0-14.3,1.9-19.5,5.6C635.7,403.1,633.1,407.8,633.1,413.3z"/>
<path id="Letter_y_3_" fill="#3C4043" d="M808.2,342.4l-64,147.2h-19.8l23.8-51.5L706,342.4h20.9l30.4,73.4h0.4l29.6-73.4H808.2z"
/>
</g>
<g id="G_Mark_1_">
<path id="Blue_500" fill="#4285F4" d="M452.93,372c0-6.26-0.56-12.25-1.6-18.01h-80.48v33L417.2,387
c-1.88,10.98-7.93,20.34-17.2,26.58v21.41h27.59C443.7,420.08,452.93,398.04,452.93,372z"/>
<path id="Green_500_1_" fill="#34A853" d="M400.01,413.58c-7.68,5.18-17.57,8.21-29.14,8.21c-22.35,0-41.31-15.06-48.1-35.36
h-28.46v22.08c14.1,27.98,43.08,47.18,76.56,47.18c23.14,0,42.58-7.61,56.73-20.71L400.01,413.58z"/>
<path id="Yellow_500_1_" fill="#FABB05" d="M320.09,370.05c0-5.7,0.95-11.21,2.68-16.39v-22.08h-28.46
c-5.83,11.57-9.11,24.63-9.11,38.47s3.29,26.9,9.11,38.47l28.46-22.08C321.04,381.26,320.09,375.75,320.09,370.05z"/>
<path id="Red_500" fill="#E94235" d="M370.87,318.3c12.63,0,23.94,4.35,32.87,12.85l24.45-24.43
c-14.85-13.83-34.21-22.32-57.32-22.32c-33.47,0-62.46,19.2-76.56,47.18l28.46,22.08C329.56,333.36,348.52,318.3,370.87,318.3z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -4,7 +4,7 @@
"description": "Googlepay module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -18,7 +18,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
'wcgateway.settings.fields' => function ( array $fields, ContainerInterface $container ): array {
// Used in various places to mark fields for the preview button.
$apm_name = 'GooglePay';
@ -72,7 +72,7 @@ return array(
'googlepay_button_enabled' => array(
'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ),
'title_html' => sprintf(
'<img src="%sassets/images/googlepay.png" alt="%s" style="max-width: 150px; max-height: 45px;" />',
'<img src="%sassets/images/googlepay.svg" alt="%s" style="max-width: 150px; max-height: 45px;" />',
$module_url,
__( 'Google Pay', 'woocommerce-paypal-payments' )
),
@ -117,7 +117,7 @@ return array(
'googlepay_button_enabled' => array(
'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ),
'title_html' => sprintf(
'<img src="%sassets/images/googlepay.png" alt="%s" style="max-width: 150px; max-height: 45px;" />',
'<img src="%sassets/images/googlepay.svg" alt="%s" style="max-width: 150px; max-height: 45px;" />',
$module_url,
__( 'Google Pay', 'woocommerce-paypal-payments' )
),

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Googlepay;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): GooglepayModule {
return new GooglepayModule();
};

View file

@ -13,6 +13,11 @@
outline-offset: -1px;
border-radius: var(--apm-button-border-radius);
}
&.ppcp-preview-button.ppcp-button-dummy {
/* URL must specify the correct module-folder! */
--apm-button-dummy-background: url(../../../ppcp-googlepay/assets/images/googlepay.png);
}
}
.wp-block-woocommerce-checkout, .wp-block-woocommerce-cart {

View file

@ -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 );
} );
} );
}

View file

@ -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 );
} );
}

View file

@ -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 );
} );
}

View file

@ -0,0 +1,102 @@
import { GooglePayStorage } from '../Helper/GooglePayStorage';
import {
getWooCommerceCustomerDetails,
setPayerData,
} from '../../../../ppcp-button/resources/js/modules/Helper/PayerData';
const CHECKOUT_FORM_SELECTOR = 'form.woocommerce-checkout';
export class CheckoutBootstrap {
/**
* @type {GooglePayStorage}
*/
#storage;
/**
* @type {HTMLFormElement|null}
*/
#checkoutForm;
/**
* @param {GooglePayStorage} storage
*/
constructor( storage ) {
this.#storage = storage;
this.#checkoutForm = CheckoutBootstrap.getCheckoutForm();
}
/**
* Indicates if the current page contains a checkout form.
*
* @return {boolean} True if a checkout form is present.
*/
static isPageWithCheckoutForm() {
return null !== CheckoutBootstrap.getCheckoutForm();
}
/**
* Retrieves the WooCommerce checkout form element.
*
* @return {HTMLFormElement|null} The form, or null if not a checkout page.
*/
static getCheckoutForm() {
return document.querySelector( CHECKOUT_FORM_SELECTOR );
}
/**
* Returns the WooCommerce checkout form element.
*
* @return {HTMLFormElement|null} The form, or null if not a checkout page.
*/
get checkoutForm() {
return this.#checkoutForm;
}
/**
* Initializes the checkout process.
*
* @throws {Error} If called on a page without a checkout form.
*/
init() {
if ( ! this.#checkoutForm ) {
throw new Error(
'Checkout form not found. Cannot initialize CheckoutBootstrap.'
);
}
this.#populateCheckoutFields();
}
/**
* Populates checkout fields with stored or customer data.
*/
#populateCheckoutFields() {
const loggedInData = getWooCommerceCustomerDetails();
if ( loggedInData ) {
// If customer is logged in, we use the details from the customer profile.
return;
}
const billingData = this.#storage.getPayer();
if ( ! billingData ) {
return;
}
setPayerData( billingData, true );
this.checkoutForm.addEventListener(
'submit',
this.#onFormSubmit.bind( this )
);
}
/**
* Clean-up when checkout form is submitted.
*
* Immediately removes the payer details from the localStorage.
*/
#onFormSubmit() {
this.#storage.clearPayer();
}
}

View file

@ -5,7 +5,10 @@ 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';
/**
* Plugin-specific styling.
@ -39,11 +42,17 @@ import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper
*
* @see https://developers.google.com/pay/api/web/reference/client
* @typedef {Object} PaymentsClient
* @property {Function} createButton - The convenience method is used to generate a Google Pay payment button styled with the latest Google Pay branding for insertion into a webpage.
* @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) method to determine a user's ability to return a form of payment from the Google Pay API.
* @property {Function} loadPaymentData - This method presents a Google Pay payment sheet that allows selection of a payment method and optionally configured parameters
* @property {Function} onPaymentAuthorized - This method is called when a payment is authorized in the payment sheet.
* @property {Function} onPaymentDataChanged - This method handles payment data changes in the payment sheet such as shipping address and shipping options.
* @property {Function} createButton - The convenience method is used to
* generate a Google Pay payment button styled with the latest Google Pay branding for
* insertion into a webpage.
* @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest)
* method to determine a user's ability to return a form of payment from the Google Pay API.
* @property {(Object) => Promise} loadPaymentData - This method presents a Google Pay payment
* sheet that allows selection of a payment method and optionally configured parameters
* @property {Function} onPaymentAuthorized - This method is called when a payment is
* authorized in the payment sheet.
* @property {Function} onPaymentDataChanged - This method handles payment data changes
* in the payment sheet such as shipping address and shipping options.
*/
/**
@ -53,14 +62,40 @@ import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper
* @typedef {Object} TransactionInfo
* @property {string} currencyCode - Required. The ISO 4217 alphabetic currency code.
* @property {string} countryCode - Optional. required for EEA countries,
* @property {string} transactionId - Optional. A unique ID that identifies a facilitation attempt. Highly encouraged for troubleshooting.
* @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price used:
* @property {string} totalPrice - Required. Total monetary value of the transaction with an optional decimal precision of two decimal places.
* @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet (e.g. subtotals, sales taxes, shipping charges, discounts etc.).
* @property {string} totalPriceLabel - Optional. Custom label for the total price within the display items.
* @property {string} checkoutOption - Optional. Affects the submit button text displayed in the Google Pay payment sheet.
* @property {string} transactionId - Optional. A unique ID that identifies a facilitation
* attempt. Highly encouraged for troubleshooting.
* @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price
* used:
* @property {string} totalPrice - Required. Total monetary value of the transaction with an
* optional decimal precision of two decimal places.
* @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet
* (e.g. subtotals, sales taxes, shipping charges, discounts etc.).
* @property {string} totalPriceLabel - Optional. Custom label for the total price within the
* display items.
* @property {string} checkoutOption - Optional. Affects the submit button text displayed in the
* Google Pay payment sheet.
*/
function payerDataFromPaymentResponse( response ) {
const raw = response?.paymentMethodData?.info?.billingAddress;
return {
email_address: response?.email,
name: {
given_name: raw.name.split( ' ' )[ 0 ], // Assuming first name is the first part
surname: raw.name.split( ' ' ).slice( 1 ).join( ' ' ), // Assuming last name is the rest
},
address: {
country_code: raw.countryCode,
address_line_1: raw.address1,
address_line_2: raw.address2,
admin_area_1: raw.administrativeArea,
admin_area_2: raw.locality,
postal_code: raw.postalCode,
},
};
}
class GooglepayButton extends PaymentButton {
/**
* @inheritDoc
@ -78,7 +113,7 @@ class GooglepayButton extends PaymentButton {
#paymentsClient = null;
/**
* Details about the processed transaction.
* Details about the processed transaction, provided to the Google SDK.
*
* @type {?TransactionInfo}
*/
@ -388,12 +423,14 @@ class GooglepayButton extends PaymentButton {
const initiatePaymentRequest = () => {
window.ppcpFundingSource = 'googlepay';
const paymentDataRequest = this.paymentDataRequest();
this.log(
'onButtonClick: paymentDataRequest',
paymentDataRequest,
this.context
);
this.paymentsClient.loadPaymentData( paymentDataRequest );
return this.paymentsClient.loadPaymentData( paymentDataRequest );
};
const validateForm = () => {
@ -434,28 +471,24 @@ class GooglepayButton extends PaymentButton {
apiVersionMinor: 0,
};
const googlePayConfig = this.googlePayConfig;
const paymentDataRequest = Object.assign( {}, baseRequest );
paymentDataRequest.allowedPaymentMethods =
googlePayConfig.allowedPaymentMethods;
paymentDataRequest.transactionInfo = this.transactionInfo;
paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo;
const useShippingCallback = this.requiresShipping;
const callbackIntents = [ 'PAYMENT_AUTHORIZATION' ];
if ( this.requiresShipping ) {
paymentDataRequest.callbackIntents = [
'SHIPPING_ADDRESS',
'SHIPPING_OPTION',
'PAYMENT_AUTHORIZATION',
];
paymentDataRequest.shippingAddressRequired = true;
paymentDataRequest.shippingAddressParameters =
this.shippingAddressParameters();
paymentDataRequest.shippingOptionRequired = true;
} else {
paymentDataRequest.callbackIntents = [ 'PAYMENT_AUTHORIZATION' ];
if ( useShippingCallback ) {
callbackIntents.push( 'SHIPPING_ADDRESS', 'SHIPPING_OPTION' );
}
return paymentDataRequest;
return {
...baseRequest,
allowedPaymentMethods: this.googlePayConfig.allowedPaymentMethods,
transactionInfo: this.transactionInfo.finalObject,
merchantInfo: this.googlePayConfig.merchantInfo,
callbackIntents,
emailRequired: true,
shippingAddressRequired: useShippingCallback,
shippingOptionRequired: useShippingCallback,
shippingAddressParameters: this.shippingAddressParameters(),
};
}
//------------------------
@ -481,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',
@ -489,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 ) {
@ -499,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 );
@ -521,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',
@ -529,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;
}
//------------------------
@ -543,83 +673,111 @@ class GooglepayButton extends PaymentButton {
//------------------------
onPaymentAuthorized( paymentData ) {
this.log( 'onPaymentAuthorized' );
this.log( 'onPaymentAuthorized', paymentData );
return this.processPayment( paymentData );
}
async processPayment( paymentData ) {
this.log( 'processPayment' );
this.logGroup( 'processPayment' );
return new Promise( async ( resolve, reject ) => {
try {
const id = await this.contextHandler.createOrder();
const payer = payerDataFromPaymentResponse( paymentData );
this.log( 'processPayment: createOrder', id );
const paymentError = ( reason ) => {
this.error( reason );
const confirmOrderResponse = await widgetBuilder.paypal
.Googlepay()
.confirmOrder( {
orderId: id,
paymentMethodData: paymentData.paymentMethodData,
} );
return this.processPaymentResponse(
'ERROR',
'PAYMENT_AUTHORIZATION',
reason
);
};
this.log(
'processPayment: confirmOrder',
confirmOrderResponse
);
const checkPayPalApproval = async ( orderId ) => {
const confirmationData = {
orderId,
paymentMethodData: paymentData.paymentMethodData,
};
/** Capture the Order on the Server */
if ( confirmOrderResponse.status === 'APPROVED' ) {
let approveFailed = false;
await this.contextHandler.approveOrder(
{
orderID: id,
},
{
// actions mock object.
restart: () =>
new Promise( ( resolve, reject ) => {
approveFailed = true;
resolve();
} ),
order: {
get: () =>
new Promise( ( resolve, reject ) => {
resolve( null );
} ),
},
}
);
const confirmOrderResponse = await widgetBuilder.paypal
.Googlepay()
.confirmOrder( confirmationData );
if ( ! approveFailed ) {
resolve( this.processPaymentResponse( 'SUCCESS' ) );
} else {
resolve(
this.processPaymentResponse(
'ERROR',
'PAYMENT_AUTHORIZATION',
'FAILED TO APPROVE'
)
);
}
} else {
resolve(
this.processPaymentResponse(
'ERROR',
'PAYMENT_AUTHORIZATION',
'TRANSACTION FAILED'
)
);
this.log( 'confirmOrder', confirmOrderResponse );
return 'APPROVED' === confirmOrderResponse?.status;
};
/**
* This approval mainly confirms that the orderID is valid.
*
* It's still needed because this handler redirects to the checkout page if the server-side
* approval was successful.
*
* @param {string} orderID
*/
const approveOrderServerSide = async ( orderID ) => {
let isApproved = true;
this.log( 'approveOrder', orderID );
await this.contextHandler.approveOrder(
{ orderID, payer },
{
restart: () =>
new Promise( ( resolve ) => {
isApproved = false;
resolve();
} ),
order: {
get: () =>
new Promise( ( resolve ) => {
resolve( null );
} ),
},
}
} catch ( err ) {
resolve(
this.processPaymentResponse(
'ERROR',
'PAYMENT_AUTHORIZATION',
err.message
)
);
);
return isApproved;
};
const processPaymentPromise = async ( resolve ) => {
const id = await this.contextHandler.createOrder();
this.log( 'createOrder', id );
const isApprovedByPayPal = await checkPayPalApproval( id );
if ( ! isApprovedByPayPal ) {
resolve( paymentError( 'TRANSACTION FAILED' ) );
return;
}
// This must be the last step in the process, as it initiates a redirect.
const success = await approveOrderServerSide( id );
if ( success ) {
resolve( this.processPaymentResponse( 'SUCCESS' ) );
} else {
resolve( paymentError( 'FAILED TO APPROVE' ) );
}
};
const addBillingDataToSession = () => {
moduleStorage.setPayer( payer );
setPayerData( payer );
};
return new Promise( async ( resolve ) => {
try {
addBillingDataToSession();
await processPaymentPromise( resolve );
} catch ( err ) {
resolve( paymentError( err.message ) );
}
this.logGroup();
} );
}
@ -639,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;

View file

@ -0,0 +1,31 @@
import { LocalStorage } from '../../../../ppcp-button/resources/js/modules/Helper/LocalStorage';
export class GooglePayStorage extends LocalStorage {
static PAYER = 'payer';
static PAYER_TTL = 900; // 15 minutes in seconds
constructor() {
super( 'ppcp-googlepay' );
}
getPayer() {
return this.get( GooglePayStorage.PAYER );
}
setPayer( data ) {
/*
* The payer details are deleted on successful checkout, or after the TTL is reached.
* This helps to remove stale data from the browser, in case the customer chooses to
* use a different method to complete the purchase.
*/
this.set( GooglePayStorage.PAYER, data, GooglePayStorage.PAYER_TTL );
}
clearPayer() {
this.clear( GooglePayStorage.PAYER );
}
}
const moduleStorage = new GooglePayStorage();
export default moduleStorage;

View file

@ -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;
}
}
}

View file

@ -0,0 +1,46 @@
import GooglepayButton from '../GooglepayButton';
import PreviewButton from '../../../../ppcp-button/resources/js/modules/Preview/PreviewButton';
/**
* A single GooglePay preview button instance.
*/
export default class GooglePayPreviewButton extends PreviewButton {
constructor( args ) {
super( args );
this.selector = `${ args.selector }GooglePay`;
this.defaultAttributes = {
button: {
style: {
type: 'pay',
color: 'black',
language: 'en',
},
},
};
}
createButton( buttonConfig ) {
const button = new GooglepayButton(
'preview',
null,
buttonConfig,
this.ppcpConfig
);
button.init( this.apiConfig );
}
/**
* Merge form details into the config object for preview.
* Mutates the previewConfig object; no return value.
* @param buttonConfig
* @param ppcpConfig
*/
dynamicPreviewConfig( buttonConfig, ppcpConfig ) {
// Merge the current form-values into the preview-button configuration.
if ( ppcpConfig.button && buttonConfig.button ) {
Object.assign( buttonConfig.button.style, ppcpConfig.button.style );
}
}
}

View file

@ -0,0 +1,57 @@
import PreviewButtonManager from '../../../../ppcp-button/resources/js/modules/Preview/PreviewButtonManager';
import GooglePayPreviewButton from './GooglePayPreviewButton';
/**
* Manages all GooglePay preview buttons on this page.
*/
export default class GooglePayPreviewButtonManager extends PreviewButtonManager {
constructor() {
const args = {
methodName: 'GooglePay',
buttonConfig: window.wc_ppcp_googlepay_admin,
};
super( args );
}
/**
* Responsible for fetching and returning the PayPal configuration object for this payment
* method.
*
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
* @return {Promise<{}>}
*/
async fetchConfig( payPal ) {
const apiMethod = payPal?.Googlepay()?.config;
if ( ! apiMethod ) {
this.error(
'configuration object cannot be retrieved from PayPal'
);
return {};
}
try {
return await apiMethod();
} catch ( error ) {
if ( error.message.includes( 'Not Eligible' ) ) {
this.apiError = 'Not Eligible';
}
return null;
}
}
/**
* This method is responsible for creating a new PreviewButton instance and returning it.
*
* @param {string} wrapperId - CSS ID of the wrapper element.
* @return {GooglePayPreviewButton}
*/
createButtonInstance( wrapperId ) {
return new GooglePayPreviewButton( {
selector: wrapperId,
apiConfig: this.apiConfig,
methodName: this.methodName,
} );
}
}

View file

@ -1,5 +1,4 @@
import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager';
import GooglePayPreviewButton from './GooglepayPreviewButton';
import GooglePayPreviewButtonManager from './Preview/GooglePayPreviewButtonManager';
/**
* Accessor that creates and returns a single PreviewButtonManager instance.
@ -13,59 +12,5 @@ const buttonManager = () => {
return GooglePayPreviewButtonManager.instance;
};
/**
* Manages all GooglePay preview buttons on this page.
*/
class GooglePayPreviewButtonManager extends PreviewButtonManager {
constructor() {
const args = {
methodName: 'GooglePay',
buttonConfig: window.wc_ppcp_googlepay_admin,
};
super( args );
}
/**
* Responsible for fetching and returning the PayPal configuration object for this payment
* method.
*
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
* @return {Promise<{}>} Promise that resolves when API configuration is available.
*/
async fetchConfig( payPal ) {
const apiMethod = payPal?.Googlepay()?.config;
if ( ! apiMethod ) {
this.error(
'configuration object cannot be retrieved from PayPal'
);
return {};
}
try {
return await apiMethod();
} catch ( error ) {
if ( error.message.includes( 'Not Eligible' ) ) {
this.apiError = 'Not Eligible';
}
return null;
}
}
/**
* This method is responsible for creating a new PreviewButton instance and returning it.
*
* @param {string} wrapperId - CSS ID of the wrapper element.
* @return {GooglePayPreviewButton} The new preview button instance.
*/
createButtonInstance( wrapperId ) {
return new GooglePayPreviewButton( {
selector: wrapperId,
apiConfig: this.apiConfig,
} );
}
}
// Initialize the preview button manager.
buttonManager();

View file

@ -1,28 +1,62 @@
/**
* Initialize the GooglePay module in the front end.
* In some cases, this module is loaded when the `window.PayPalCommerceGateway` object is not
* present. In that case, the page does not contain a Google Pay button, but some other logic
* that is related to Google Pay (e.g., the CheckoutBootstrap module)
*
* @file
*/
import { loadCustomScript } from '@paypal/paypal-js';
import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading';
import GooglepayManager from './GooglepayManager';
import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper';
import { CheckoutBootstrap } from './ContextBootstrap/CheckoutBootstrap';
import moduleStorage from './Helper/GooglePayStorage';
( function ( { buttonConfig, ppcpConfig, jQuery } ) {
let manager;
( function ( { buttonConfig, ppcpConfig = {} } ) {
const context = ppcpConfig.context;
const bootstrap = function () {
manager = new GooglepayManager( buttonConfig, ppcpConfig );
manager.init();
};
setupButtonEvents( function () {
if ( manager ) {
manager.reinit();
function bootstrapPayButton() {
if ( ! buttonConfig || ! ppcpConfig ) {
return;
}
} );
const manager = new GooglepayManager( buttonConfig, ppcpConfig );
manager.init();
setupButtonEvents( function () {
manager.reinit();
} );
}
function bootstrapCheckout() {
if ( context && ! [ 'continuation', 'checkout' ].includes( context ) ) {
// Context must be missing/empty, or "continuation"/"checkout" to proceed.
return;
}
if ( ! CheckoutBootstrap.isPageWithCheckoutForm() ) {
return;
}
const checkoutBootstrap = new CheckoutBootstrap( moduleStorage );
checkoutBootstrap.init();
}
function bootstrap() {
bootstrapPayButton();
bootstrapCheckout();
}
document.addEventListener( 'DOMContentLoaded', () => {
if (
typeof buttonConfig === 'undefined' ||
typeof ppcpConfig === 'undefined'
) {
// No PayPal buttons present on this page.
if ( ! buttonConfig || ! ppcpConfig ) {
/*
* No PayPal buttons present on this page, but maybe a bootstrap module needs to be
* initialized. Skip loading the SDK or gateway configuration, and directly initialize
* the module.
*/
bootstrap();
return;
}
@ -52,5 +86,4 @@ import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Hel
} )( {
buttonConfig: window.wc_ppcp_googlepay,
ppcpConfig: window.PayPalCommerceGateway,
jQuery: window.jQuery,
} );

View file

@ -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(),
);
}

View file

@ -114,7 +114,7 @@ class GooglePayGateway extends WC_Payment_Gateway {
$this->description = $this->get_option( 'description', '' );
$this->module_url = $module_url;
$this->icon = esc_url( $this->module_url ) . 'assets/images/googlepay.png';
$this->icon = esc_url( $this->module_url ) . 'assets/images/googlepay.svg';
$this->init_form_fields();
$this->init_settings();

View file

@ -16,30 +16,37 @@ use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint;
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus;
use WooCommerce\PayPalCommerce\Googlepay\Helper\AvailabilityNotice;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class GooglepayModule
*/
class GooglepayModule implements ModuleInterface {
class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
// Clears product status when appropriate.
add_action(
@ -93,11 +100,21 @@ class GooglepayModule implements ModuleInterface {
static function () use ( $c, $button ) {
$smart_button = $c->get( 'button.smart-button' );
assert( $smart_button instanceof SmartButtonInterface );
if ( $smart_button->should_load_ppcp_script() ) {
$button->enqueue();
return;
}
/*
* Checkout page, but no PPCP scripts were loaded. Most likely in continuation mode.
* Need to enqueue some Google Pay scripts to populate the billing form with details
* provided by Google Pay.
*/
if ( is_checkout() ) {
$button->enqueue();
}
if ( has_block( 'woocommerce/checkout' ) || has_block( 'woocommerce/cart' ) ) {
/**
* Should add this to the ButtonInterface.
@ -200,13 +217,7 @@ class GooglepayModule implements ModuleInterface {
echo '<div id="ppc-button-' . esc_attr( GooglePayGateway::ID ) . '"></div>';
}
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
return true;
}
}