` );
+ wrapper.setAttribute( 'id', previewId );
+ wrapper.setAttribute( 'class', previewClass );
+ return wrapper;
}
/**
@@ -109,10 +117,12 @@ class PreviewButton {
console.error( 'Skip render, button is not configured yet' );
return;
}
+
this.domWrapper = this.createNewWrapper();
- this.domWrapper.insertAfter( this.wrapper );
+ this._insertWrapper();
} else {
- this.domWrapper.empty().show();
+ this._emptyWrapper();
+ this._showWrapper();
}
this.isVisible = true;
@@ -151,16 +161,38 @@ class PreviewButton {
* Using a timeout here will make the button visible again at the end of the current
* event queue.
*/
- setTimeout( () => this.domWrapper.show() );
+ setTimeout( () => this._showWrapper() );
}
remove() {
this.isVisible = false;
if ( this.domWrapper ) {
- this.domWrapper.hide().empty();
+ this._hideWrapper();
+ this._emptyWrapper();
}
}
+
+ _showWrapper() {
+ this.domWrapper.style.display = '';
+ }
+
+ _hideWrapper() {
+ this.domWrapper.style.display = 'none';
+ }
+
+ _emptyWrapper() {
+ this.domWrapper.innerHTML = '';
+ }
+
+ _insertWrapper() {
+ const wrapperElement = document.querySelector( this.wrapper );
+
+ wrapperElement.parentNode.insertBefore(
+ this.domWrapper,
+ wrapperElement.nextSibling
+ );
+ }
}
export default PreviewButton;
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js b/modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js
similarity index 91%
rename from modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js
rename to modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js
index 1ea2f04bc..fbc958a0c 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/PreviewButtonManager.js
+++ b/modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js
@@ -1,7 +1,8 @@
import { loadCustomScript } from '@paypal/paypal-js';
-import widgetBuilder from './WidgetBuilder';
+import widgetBuilder from '../Renderer/WidgetBuilder';
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger';
+import DummyPreviewButton from './DummyPreviewButton';
/**
* Manages all PreviewButton instances of a certain payment method on the page.
@@ -26,6 +27,13 @@ class PreviewButtonManager {
*/
#onInit;
+ /**
+ * Initialize the new PreviewButtonManager.
+ *
+ * @param {string} methodName - Name of the payment method, e.g. "Google Pay"
+ * @param {Object} buttonConfig
+ * @param {Object} defaultAttributes
+ */
constructor( { methodName, buttonConfig, defaultAttributes } ) {
// Define the payment method name in the derived class.
this.methodName = methodName;
@@ -102,27 +110,15 @@ class PreviewButtonManager {
*
* This dummy is only visible on the admin side, and not rendered on the front-end.
*
- * @todo Consider refactoring this into a new class that extends the PreviewButton class.
* @param {string} wrapperId
* @return {any}
*/
- createDummy( wrapperId ) {
- const elButton = document.createElement( 'div' );
- elButton.classList.add( 'ppcp-button-apm', 'ppcp-button-dummy' );
- elButton.innerHTML = `
${
- this.apiError ?? 'Not Available'
- } `;
-
- document.querySelector( wrapperId ).appendChild( elButton );
-
- const instDummy = {
- setDynamic: () => instDummy,
- setPpcpConfig: () => instDummy,
- render: () => {},
- remove: () => {},
- };
-
- return instDummy;
+ createDummyButtonInstance( wrapperId ) {
+ return new DummyPreviewButton( {
+ selector: wrapperId,
+ label: this.apiError,
+ methodName: this.methodName,
+ } );
}
registerEventListeners() {
@@ -319,6 +315,17 @@ class PreviewButtonManager {
this.log( 'configureAllButtons', ppcpConfig );
Object.entries( this.buttons ).forEach( ( [ id, button ] ) => {
+ const limitWrapper = ppcpConfig.button?.wrapper;
+
+ /**
+ * When the ppcpConfig object specifies a button wrapper, then ensure to limit preview
+ * changes to this individual wrapper. If no button wrapper is defined, the
+ * configuration is relevant for all buttons on the page.
+ */
+ if ( limitWrapper && button.wrapper !== limitWrapper ) {
+ return;
+ }
+
this._configureButton( id, {
...ppcpConfig,
button: {
@@ -349,12 +356,11 @@ class PreviewButtonManager {
let newInst;
if ( this.apiConfig && 'object' === typeof this.apiConfig ) {
- newInst = this.createButtonInstance( id ).setButtonConfig(
- this.buttonConfig
- );
+ newInst = this.createButtonInstance( id );
} else {
- newInst = this.createDummy( id );
+ newInst = this.createDummyButtonInstance( id );
}
+ newInst.setButtonConfig( this.buttonConfig );
this.buttons[ id ] = newInst;
}
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
index 8da7b6e24..3ce35c9b5 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
@@ -625,6 +625,15 @@ export default class PaymentButton {
this.#logger.error( ...args );
}
+ /**
+ * Open or close a log-group
+ *
+ * @param {?string} [label=null] Group label.
+ */
+ logGroup( label = null ) {
+ this.#logger.group( label );
+ }
+
/**
* Determines if the current button instance has valid and complete configuration details.
* Used during initialization to decide if the button can be initialized or should be skipped.
diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php
index 069355da6..891d4c0a9 100644
--- a/modules/ppcp-button/services.php
+++ b/modules/ppcp-button/services.php
@@ -320,6 +320,7 @@ return array(
},
'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply {
return new MessagesApply(
+ $container->get( 'api.paylater-countries' ),
$container->get( 'api.shop.country' )
);
},
diff --git a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
index 36069e463..a96302e65 100644
--- a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
+++ b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
@@ -85,7 +85,8 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
$this->add_products( $products );
$this->cart->calculate_totals();
- $total = (float) $this->cart->get_total( 'numeric' );
+ $total = (float) $this->cart->get_total( 'numeric' );
+ $shipping_fee = (float) $this->cart->get_shipping_total();
$this->restore_real_cart();
@@ -113,7 +114,7 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
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,
'funding' => array(
diff --git a/modules/ppcp-button/src/Helper/ContextTrait.php b/modules/ppcp-button/src/Helper/ContextTrait.php
index 0c1bb4201..9f58cbcdf 100644
--- a/modules/ppcp-button/src/Helper/ContextTrait.php
+++ b/modules/ppcp-button/src/Helper/ContextTrait.php
@@ -243,4 +243,20 @@ trait ContextTrait {
$screen = get_current_screen();
return $screen && $screen->is_block_editor();
}
+
+ /**
+ * Checks if is WooCommerce Settings Payments tab screen (/wp-admin/admin.php?page=wc-settings&tab=checkout).
+ *
+ * @return bool
+ */
+ protected function is_wc_settings_payments_tab(): bool {
+ if ( ! is_admin() || isset( $_GET['section'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
+ return false;
+ }
+
+ $page = wc_clean( wp_unslash( $_GET['page'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification
+ $tab = wc_clean( wp_unslash( $_GET['tab'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification
+
+ return $page === 'wc-settings' && $tab === 'checkout';
+ }
}
diff --git a/modules/ppcp-button/src/Helper/MessagesApply.php b/modules/ppcp-button/src/Helper/MessagesApply.php
index c0d0b1d6b..4f4a3d9f6 100644
--- a/modules/ppcp-button/src/Helper/MessagesApply.php
+++ b/modules/ppcp-button/src/Helper/MessagesApply.php
@@ -18,17 +18,9 @@ class MessagesApply {
/**
* In which countries credit messaging is available.
*
- * @var array
+ * @var string[]
*/
- private $countries = array(
- 'US',
- 'DE',
- 'GB',
- 'FR',
- 'AU',
- 'IT',
- 'ES',
- );
+ private $allowed_countries;
/**
* 2-letter country code of the shop.
@@ -40,10 +32,12 @@ class MessagesApply {
/**
* MessagesApply constructor.
*
- * @param string $country 2-letter country code of the shop.
+ * @param string[] $allowed_countries In which countries credit messaging is available.
+ * @param string $country 2-letter country code of the shop.
*/
- public function __construct( string $country ) {
- $this->country = $country;
+ public function __construct( array $allowed_countries, string $country ) {
+ $this->allowed_countries = $allowed_countries;
+ $this->country = $country;
}
/**
@@ -52,6 +46,6 @@ class MessagesApply {
* @return bool
*/
public function for_country(): bool {
- return in_array( $this->country, $this->countries, true );
+ return in_array( $this->country, $this->allowed_countries, true );
}
}
diff --git a/modules/ppcp-card-fields/resources/js/Render.js b/modules/ppcp-card-fields/resources/js/Render.js
index 9a35ff449..146396288 100644
--- a/modules/ppcp-card-fields/resources/js/Render.js
+++ b/modules/ppcp-card-fields/resources/js/Render.js
@@ -1,47 +1,35 @@
import { cardFieldStyles } from './CardFieldsHelper';
+import { hide } from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
+
+function renderField( cardField, inputField ) {
+ if ( ! inputField || inputField.hidden || ! cardField ) {
+ return;
+ }
+
+ // Insert the PayPal card field after the original input field.
+ const styles = cardFieldStyles( inputField );
+ cardField( { style: { input: styles } } ).render( inputField.parentNode );
+
+ // Hide the original input field.
+ hide( inputField, true );
+ inputField.hidden = true;
+}
export function renderFields( cardFields ) {
- const nameField = document.getElementById(
- 'ppcp-credit-card-gateway-card-name'
+ renderField(
+ cardFields.NameField,
+ document.getElementById( 'ppcp-credit-card-gateway-card-name' )
);
- if ( nameField && nameField.hidden !== true ) {
- const styles = cardFieldStyles( nameField );
- cardFields
- .NameField( { style: { input: styles } } )
- .render( nameField.parentNode );
- nameField.hidden = true;
- }
-
- const numberField = document.getElementById(
- 'ppcp-credit-card-gateway-card-number'
+ renderField(
+ cardFields.NumberField,
+ document.getElementById( 'ppcp-credit-card-gateway-card-number' )
);
- if ( numberField && numberField.hidden !== true ) {
- const styles = cardFieldStyles( numberField );
- cardFields
- .NumberField( { style: { input: styles } } )
- .render( numberField.parentNode );
- numberField.hidden = true;
- }
-
- const expiryField = document.getElementById(
- 'ppcp-credit-card-gateway-card-expiry'
+ renderField(
+ cardFields.ExpiryField,
+ document.getElementById( 'ppcp-credit-card-gateway-card-expiry' )
);
- if ( expiryField && expiryField.hidden !== true ) {
- const styles = cardFieldStyles( expiryField );
- cardFields
- .ExpiryField( { style: { input: styles } } )
- .render( expiryField.parentNode );
- expiryField.hidden = true;
- }
-
- const cvvField = document.getElementById(
- 'ppcp-credit-card-gateway-card-cvc'
+ renderField(
+ cardFields.CVVField,
+ document.getElementById( 'ppcp-credit-card-gateway-card-cvc' )
);
- if ( cvvField && cvvField.hidden !== true ) {
- const styles = cardFieldStyles( cvvField );
- cardFields
- .CVVField( { style: { input: styles } } )
- .render( cvvField.parentNode );
- cvvField.hidden = true;
- }
}
diff --git a/modules/ppcp-googlepay/assets/images/googlepay.png b/modules/ppcp-googlepay/assets/images/googlepay.png
deleted file mode 100644
index b264fd0ee..000000000
Binary files a/modules/ppcp-googlepay/assets/images/googlepay.png and /dev/null differ
diff --git a/modules/ppcp-googlepay/assets/images/googlepay.svg b/modules/ppcp-googlepay/assets/images/googlepay.svg
new file mode 100644
index 000000000..0abef7bce
--- /dev/null
+++ b/modules/ppcp-googlepay/assets/images/googlepay.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/ppcp-googlepay/extensions.php b/modules/ppcp-googlepay/extensions.php
index f49376736..20b19510c 100644
--- a/modules/ppcp-googlepay/extensions.php
+++ b/modules/ppcp-googlepay/extensions.php
@@ -72,7 +72,7 @@ return array(
'googlepay_button_enabled' => array(
'title' => __( 'Google Pay Button', 'woocommerce-paypal-payments' ),
'title_html' => sprintf(
- '
',
+ '
',
$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(
- '
',
+ '
',
$module_url,
__( 'Google Pay', 'woocommerce-paypal-payments' )
),
diff --git a/modules/ppcp-googlepay/resources/css/styles.scss b/modules/ppcp-googlepay/resources/css/styles.scss
index 93cb05f47..3c6fe8912 100644
--- a/modules/ppcp-googlepay/resources/css/styles.scss
+++ b/modules/ppcp-googlepay/resources/css/styles.scss
@@ -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 {
diff --git a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js
index d61b674a7..d49bee615 100644
--- a/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js
+++ b/modules/ppcp-googlepay/resources/js/Context/BaseHandler.js
@@ -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 );
} );
} );
}
diff --git a/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js b/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js
index 79de6b39d..81d60b078 100644
--- a/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js
+++ b/modules/ppcp-googlepay/resources/js/Context/PayNowHandler.js
@@ -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 );
} );
}
diff --git a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js
index a8aa6e8bd..670b9a7c0 100644
--- a/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js
+++ b/modules/ppcp-googlepay/resources/js/Context/SingleProductHandler.js
@@ -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 );
} );
}
diff --git a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
new file mode 100644
index 000000000..1e1933a10
--- /dev/null
+++ b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
@@ -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();
+ }
+}
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index 4b267c4e7..d4d9df55f 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -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;
diff --git a/modules/ppcp-googlepay/resources/js/Helper/GooglePayStorage.js b/modules/ppcp-googlepay/resources/js/Helper/GooglePayStorage.js
new file mode 100644
index 000000000..faa2520e5
--- /dev/null
+++ b/modules/ppcp-googlepay/resources/js/Helper/GooglePayStorage.js
@@ -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;
diff --git a/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js b/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js
new file mode 100644
index 000000000..9216ad7c9
--- /dev/null
+++ b/modules/ppcp-googlepay/resources/js/Helper/TransactionInfo.js
@@ -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;
+ }
+ }
+}
diff --git a/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js
new file mode 100644
index 000000000..e6b27ee55
--- /dev/null
+++ b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButton.js
@@ -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 );
+ }
+ }
+}
diff --git a/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js
new file mode 100644
index 000000000..a3e9f66af
--- /dev/null
+++ b/modules/ppcp-googlepay/resources/js/Preview/GooglePayPreviewButtonManager.js
@@ -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,
+ } );
+ }
+}
diff --git a/modules/ppcp-googlepay/resources/js/boot-admin.js b/modules/ppcp-googlepay/resources/js/boot-admin.js
index 7b5342078..953a6088e 100644
--- a/modules/ppcp-googlepay/resources/js/boot-admin.js
+++ b/modules/ppcp-googlepay/resources/js/boot-admin.js
@@ -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();
diff --git a/modules/ppcp-googlepay/resources/js/boot.js b/modules/ppcp-googlepay/resources/js/boot.js
index 99dd414f5..fb9e8e313 100644
--- a/modules/ppcp-googlepay/resources/js/boot.js
+++ b/modules/ppcp-googlepay/resources/js/boot.js
@@ -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,
} );
diff --git a/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php b/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php
index 27da5ef48..e489bc771 100644
--- a/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php
+++ b/modules/ppcp-googlepay/src/Endpoint/UpdatePaymentDataEndpoint.php
@@ -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(),
);
}
diff --git a/modules/ppcp-googlepay/src/GooglePayGateway.php b/modules/ppcp-googlepay/src/GooglePayGateway.php
index cef3916d9..16fe5f690 100644
--- a/modules/ppcp-googlepay/src/GooglePayGateway.php
+++ b/modules/ppcp-googlepay/src/GooglePayGateway.php
@@ -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();
diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php
index 0738dfec4..1b57a816a 100644
--- a/modules/ppcp-googlepay/src/GooglepayModule.php
+++ b/modules/ppcp-googlepay/src/GooglepayModule.php
@@ -100,11 +100,21 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
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.
diff --git a/modules/ppcp-onboarding/resources/js/onboarding.js b/modules/ppcp-onboarding/resources/js/onboarding.js
index 905c66c65..5a6ab333a 100644
--- a/modules/ppcp-onboarding/resources/js/onboarding.js
+++ b/modules/ppcp-onboarding/resources/js/onboarding.js
@@ -326,7 +326,9 @@ window.ppcp_onboarding_productionCallback = function ( ...args ) {
isDisconnecting = true;
- document.querySelector( '.woocommerce-save-button' ).click();
+ const saveButton = document.querySelector( '.woocommerce-save-button' );
+ saveButton.removeAttribute( 'disabled' );
+ saveButton.click();
};
// Prevent the message about unsaved checkbox/radiobutton when reloading the page.
diff --git a/modules/ppcp-status-report/src/StatusReportModule.php b/modules/ppcp-status-report/src/StatusReportModule.php
index 8aaf8fc6c..da015d586 100644
--- a/modules/ppcp-status-report/src/StatusReportModule.php
+++ b/modules/ppcp-status-report/src/StatusReportModule.php
@@ -181,6 +181,38 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo
$subscriptions_mode_settings
),
),
+ array(
+ 'label' => esc_html__( 'PayPal Shipping Callback', 'woocommerce-paypal-payments' ),
+ 'exported_label' => 'PayPal Shipping Callback',
+ 'description' => esc_html__( 'Whether the "Require final confirmation on checkout" setting is disabled.', 'woocommerce-paypal-payments' ),
+ 'value' => $this->bool_to_html(
+ $settings->has( 'blocks_final_review_enabled' ) && ! $settings->get( 'blocks_final_review_enabled' )
+ ),
+ ),
+ array(
+ 'label' => esc_html__( 'Apple Pay', 'woocommerce-paypal-payments' ),
+ 'exported_label' => 'Apple Pay',
+ 'description' => esc_html__( 'Whether Apple Pay is enabled.', 'woocommerce-paypal-payments' ),
+ 'value' => $this->bool_to_html(
+ $settings->has( 'applepay_button_enabled' ) && $settings->get( 'applepay_button_enabled' )
+ ),
+ ),
+ array(
+ 'label' => esc_html__( 'Google Pay', 'woocommerce-paypal-payments' ),
+ 'exported_label' => 'Google Pay',
+ 'description' => esc_html__( 'Whether Google Pay is enabled.', 'woocommerce-paypal-payments' ),
+ 'value' => $this->bool_to_html(
+ $settings->has( 'googlepay_button_enabled' ) && $settings->get( 'googlepay_button_enabled' )
+ ),
+ ),
+ array(
+ 'label' => esc_html__( 'Fastlane', 'woocommerce-paypal-payments' ),
+ 'exported_label' => 'Fastlane',
+ 'description' => esc_html__( 'Whether Fastlane is enabled.', 'woocommerce-paypal-payments' ),
+ 'value' => $this->bool_to_html(
+ $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' )
+ ),
+ ),
);
echo wp_kses_post(
diff --git a/modules/ppcp-wc-gateway/resources/css/common.scss b/modules/ppcp-wc-gateway/resources/css/common.scss
index ac4bcab32..595f3eba7 100644
--- a/modules/ppcp-wc-gateway/resources/css/common.scss
+++ b/modules/ppcp-wc-gateway/resources/css/common.scss
@@ -31,9 +31,25 @@ $background-ident-color: #fbfbfb;
&.ppcp-button-dummy {
display: flex;
+ min-height: 25px;
align-items: center;
justify-content: center;
background: #0001;
+ position: relative;
+
+ &:before {
+ content: '';
+ position: absolute;
+ left: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 42px;
+ height: 24px;
+ background-image: var(--apm-button-dummy-background, none);
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center left;
+ }
}
}
@@ -82,6 +98,14 @@ $background-ident-color: #fbfbfb;
}
}
+.ppcp-notice-success {
+ border-left-color: #00a32a;
+
+ .highlight {
+ color: #00a32a;
+ }
+}
+
.ppcp-notice-warning {
border-left-color: #dba617;
diff --git a/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js b/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js
index c76aa8960..a7b61b32a 100644
--- a/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js
+++ b/modules/ppcp-wc-gateway/resources/js/helper/ConsoleLogger.js
@@ -18,6 +18,13 @@ export default class ConsoleLogger {
*/
#enabled = false;
+ /**
+ * Tracks the current log-group that was started using `this.group()`
+ *
+ * @type {?string}
+ */
+ #openGroup = null;
+
constructor( ...prefixes ) {
if ( prefixes.length ) {
this.#prefix = `[${ prefixes.join( ' | ' ) }]`;
@@ -55,4 +62,28 @@ export default class ConsoleLogger {
error( ...args ) {
console.error( this.#prefix, ...args );
}
+
+ /**
+ * Starts or ends a group in the browser console.
+ *
+ * @param {string} [label=null] - The group label. Omit to end the current group.
+ */
+ group( label = null ) {
+ if ( ! this.#enabled ) {
+ return;
+ }
+
+ if ( ! label || this.#openGroup ) {
+ // eslint-disable-next-line
+ console.groupEnd();
+ this.#openGroup = null;
+ }
+
+ if ( label ) {
+ // eslint-disable-next-line
+ console.group( label );
+
+ this.#openGroup = label;
+ }
+ }
}
diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php
index 9cb8dc0c0..6ca2ecafe 100644
--- a/modules/ppcp-wc-gateway/services.php
+++ b/modules/ppcp-wc-gateway/services.php
@@ -27,6 +27,7 @@ use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderReauthorizeAction;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\RefreshFeatureStatusEndpoint;
+use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
use WooCommerce\PayPalCommerce\WcGateway\Helper\FeesUpdater;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
@@ -303,6 +304,37 @@ return array(
static function ( ContainerInterface $container ): AuthorizeOrderActionNotice {
return new AuthorizeOrderActionNotice();
},
+ 'wcgateway.notice.checkout-blocks' =>
+ static function ( ContainerInterface $container ): string {
+ $settings = $container->get( 'wcgateway.settings' );
+ assert( $settings instanceof Settings );
+
+ $axo_available = $container->has( 'axo.available' ) && $container->get( 'axo.available' );
+ $axo_enabled = $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' );
+
+ if ( $axo_available && $axo_enabled ) {
+ return '';
+ }
+
+ if ( CartCheckoutDetector::has_block_checkout() ) {
+ return '';
+ }
+
+ $checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' );
+ $instructions_link = 'https://woocommerce.com/document/cart-checkout-blocks-status/#using-the-cart-and-checkout-blocks';
+
+ $notice_content = sprintf(
+ /* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the WooCommerce Checkout instructions. */
+ __(
+ '
Info: The
Checkout page of your store currently uses a classic checkout layout or a custom checkout widget. Advanced Card Processing supports the new
Checkout
block which improves conversion rates. See
this page for instructions on how to upgrade to the new Checkout layout.',
+ 'woocommerce-paypal-payments'
+ ),
+ esc_url( $checkout_page_link ),
+ esc_url( $instructions_link )
+ );
+
+ return '
';
+ },
'wcgateway.settings.sections-renderer' => static function ( ContainerInterface $container ): SectionsRenderer {
return new SectionsRenderer(
$container->get( 'wcgateway.current-ppcp-settings-page-id' ),
@@ -545,6 +577,17 @@ return array(
'requirements' => array(),
'gateway' => 'paypal',
),
+ 'dcc_block_checkout_notice' => array(
+ 'heading' => '',
+ 'html' => $container->get( 'wcgateway.notice.checkout-blocks' ),
+ 'type' => 'ppcp-html',
+ 'classes' => array(),
+ 'screens' => array(
+ State::STATE_ONBOARDED,
+ ),
+ 'requirements' => array( 'dcc' ),
+ 'gateway' => 'dcc',
+ ),
'dcc_enabled' => array(
'title' => __( 'Enable/Disable', 'woocommerce-paypal-payments' ),
'desc_tip' => true,
diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/card-button-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/card-button-fields.php
index 1b8d13d9c..1bb6c24b8 100644
--- a/modules/ppcp-wc-gateway/src/Settings/Fields/card-button-fields.php
+++ b/modules/ppcp-wc-gateway/src/Settings/Fields/card-button-fields.php
@@ -32,7 +32,7 @@ return function ( ContainerInterface $container, array $fields ): array {
),
'
',
' ',
- ' br>'
+ '
'
),
'type' => 'ppcp-heading',
'screens' => array(
diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php
index 06b4f51ce..0090b6632 100644
--- a/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php
+++ b/modules/ppcp-wc-gateway/src/Settings/Fields/pay-later-tab-fields.php
@@ -847,7 +847,7 @@ return function ( ContainerInterface $container, array $fields ): array {
__( 'When enabled, a %1$sPay Later button%2$s is displayed for eligible customers.%3$sPayPal buttons must be enabled to display the Pay Later button.', 'woocommerce-paypal-payments' ),
'
',
' ',
- ' br>'
+ '
'
),
),
'pay_later_button_enabled' => array(
diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php
index 3a2513e98..011cfd192 100644
--- a/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php
+++ b/modules/ppcp-wc-gateway/src/Settings/Fields/paypal-smart-button-fields.php
@@ -233,7 +233,7 @@ return function ( ContainerInterface $container, array $fields ): array {
),
'
',
' ',
- ' br>'
+ '
'
),
'type' => 'ppcp-heading',
'screens' => array(
diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php
index 241de3af3..978aaf017 100644
--- a/woocommerce-paypal-payments.php
+++ b/woocommerce-paypal-payments.php
@@ -223,22 +223,6 @@ define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' );
}
);
- add_action(
- 'in_plugin_update_message-woocommerce-paypal-payments/woocommerce-paypal-payments.php',
- static function( array $plugin_data, \stdClass $new_data ) {
- if ( version_compare( $plugin_data['Version'], '3.0.0', '<' ) &&
- version_compare( $new_data->new_version, '3.0.0', '>=' ) ) {
- printf(
- '
%s : %s',
- esc_html__( 'Warning', 'woocommerce-paypal-payments' ),
- esc_html__( 'WooCommerce PayPal Payments version 3.0.0 contains significant changes that may impact your website. We strongly recommend reviewing the changes and testing the update on a staging site before updating it on your production environment.', 'woocommerce-paypal-payments' )
- );
- }
- },
- 10,
- 2
- );
-
/**
* Check if WooCommerce is active.
*