From 00e29597002c7aed3add7b863a97b0e7e392ab69 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Fri, 16 Aug 2024 15:41:05 +0200
Subject: [PATCH 01/17] =?UTF-8?q?=E2=9C=A8=20Get=20payee=20details=20witho?=
=?UTF-8?q?ut=20shipping=20callback?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../resources/js/modules/Helper/PayerData.js | 168 +++++++++----
.../resources/js/GooglepayButton.js | 230 ++++++++++--------
2 files changed, 257 insertions(+), 141 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
index b9b84d14f..da7eca4d6 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
@@ -1,59 +1,135 @@
+/**
+ * Name details.
+ *
+ * @typedef {Object} NameDetails
+ * @property {?string} given_name - First name, e.g. "John".
+ * @property {?string} surname - Last name, e.g. "Doe".
+ */
+
+/**
+ * Postal address details.
+ *
+ * @typedef {Object} AddressDetails
+ * @property {?string} country_code - Country code (2-letter).
+ * @property {?string} address_line_1 - Address details, line 1 (street, house number).
+ * @property {?string} address_line_2 - Address details, line 2.
+ * @property {?string} admin_area_1 - State or region.
+ * @property {?string} admin_area_2 - State or region.
+ * @property {?string} postal_code - Zip code.
+ */
+
+/**
+ * Phone details.
+ *
+ * @typedef {Object} PhoneDetails
+ * @property {?string} phone_type - Type, usually 'HOME'
+ * @property {?{national_number: string}} phone_number - Phone number details.
+ */
+
+/**
+ * Payer details.
+ *
+ * @typedef {Object} PayerDetails
+ * @property {?string} email_address - Email address for billing communication.
+ * @property {?PhoneDetails} phone - Phone number for billing communication.
+ * @property {?NameDetails} name - Payer's name.
+ * @property {?AddressDetails} address - Postal billing address.
+ */
+
+// Map checkout fields to PayerData object properties.
+const FIELD_MAP = {
+ '#billing_email': [ 'email_address' ],
+ '#billing_last_name': [ 'name', 'surname' ],
+ '#billing_first_name': [ 'name', 'given_name' ],
+ '#billing_country': [ 'address', 'country_code' ],
+ '#billing_address_1': [ 'address', 'address_line_1' ],
+ '#billing_address_2': [ 'address', 'address_line_2' ],
+ '#billing_state': [ 'address', 'admin_area_1' ],
+ '#billing_city': [ 'address', 'admin_area_2' ],
+ '#billing_postcode': [ 'address', 'postal_code' ],
+ '#billing_phone': [ 'phone' ],
+};
+
+/**
+ * Returns billing details from the checkout form or global JS object.
+ *
+ * @return {?PayerDetails} Full billing details, or null on failure.
+ */
export const payerData = () => {
- const payer = PayPalCommerceGateway.payer;
+ const payer = window.PayPalCommerceGateway?.payer;
if ( ! payer ) {
return null;
}
- const phone =
- document.querySelector( '#billing_phone' ) ||
- typeof payer.phone !== 'undefined'
- ? {
- phone_type: 'HOME',
- phone_number: {
- national_number: document.querySelector(
- '#billing_phone'
- )
- ? document.querySelector( '#billing_phone' ).value
- : payer.phone.phone_number.national_number,
- },
- }
- : null;
- const payerData = {
- email_address: document.querySelector( '#billing_email' )
- ? document.querySelector( '#billing_email' ).value
- : payer.email_address,
+ const getElementValue = ( selector ) =>
+ document.querySelector( selector )?.value;
+
+ // Initialize data with existing payer values.
+ const data = {
+ email_address: payer.email_address,
+ phone: payer.phone,
name: {
- surname: document.querySelector( '#billing_last_name' )
- ? document.querySelector( '#billing_last_name' ).value
- : payer.name.surname,
- given_name: document.querySelector( '#billing_first_name' )
- ? document.querySelector( '#billing_first_name' ).value
- : payer.name.given_name,
+ surname: payer.name?.surname,
+ given_name: payer.name?.given_name,
},
address: {
- country_code: document.querySelector( '#billing_country' )
- ? document.querySelector( '#billing_country' ).value
- : payer.address.country_code,
- address_line_1: document.querySelector( '#billing_address_1' )
- ? document.querySelector( '#billing_address_1' ).value
- : payer.address.address_line_1,
- address_line_2: document.querySelector( '#billing_address_2' )
- ? document.querySelector( '#billing_address_2' ).value
- : payer.address.address_line_2,
- admin_area_1: document.querySelector( '#billing_state' )
- ? document.querySelector( '#billing_state' ).value
- : payer.address.admin_area_1,
- admin_area_2: document.querySelector( '#billing_city' )
- ? document.querySelector( '#billing_city' ).value
- : payer.address.admin_area_2,
- postal_code: document.querySelector( '#billing_postcode' )
- ? document.querySelector( '#billing_postcode' ).value
- : payer.address.postal_code,
+ country_code: payer.address?.country_code,
+ address_line_1: payer.address?.address_line_1,
+ address_line_2: payer.address?.address_line_2,
+ admin_area_1: payer.address?.admin_area_1,
+ admin_area_2: payer.address?.admin_area_2,
+ postal_code: payer.address?.postal_code,
},
};
- if ( phone ) {
- payerData.phone = phone;
+ // Update data with DOM values where they exist.
+ Object.entries( FIELD_MAP ).forEach( ( [ selector, path ] ) => {
+ const value = getElementValue( selector );
+ if ( value ) {
+ let current = data;
+ path.slice( 0, -1 ).forEach( ( key ) => {
+ current = current[ key ] = current[ key ] || {};
+ } );
+ current[ path[ path.length - 1 ] ] = value;
+ }
+ } );
+
+ // Handle phone separately due to its nested structure.
+ const phoneNumber = data.phone;
+ if ( phoneNumber && typeof phoneNumber === 'string' ) {
+ data.phone = {
+ phone_type: 'HOME',
+ phone_number: { national_number: phoneNumber },
+ };
}
- return payerData;
+
+ return data;
+};
+
+/**
+ * Updates the DOM with specific payer details.
+ *
+ * @param {PayerDetails} newData - New payer details.
+ */
+export const setPayerData = ( newData ) => {
+ const setValue = ( path, field, value ) => {
+ if ( null === value || undefined === value || ! field ) {
+ return;
+ }
+
+ if ( path[ 0 ] === 'phone' && typeof value === 'object' ) {
+ value = value.phone_number?.national_number;
+ }
+
+ if ( field.value !== value ) {
+ field.value = value;
+ }
+ };
+
+ Object.entries( FIELD_MAP ).forEach( ( [ selector, path ] ) => {
+ const value = path.reduce( ( obj, key ) => obj?.[ key ], newData );
+ const element = document.querySelector( selector );
+
+ setValue( path, element, value );
+ } );
};
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index 4b267c4e7..a9bfd693e 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -6,6 +6,7 @@ import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/Pa
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData';
import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
+import { setPayerData } from '../../../ppcp-button/resources/js/modules/Helper/PayerData';
/**
* Plugin-specific styling.
@@ -39,11 +40,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,12 +60,18 @@ 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.
*/
class GooglepayButton extends PaymentButton {
@@ -78,7 +91,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 +401,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 +449,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,
+ merchantInfo: this.googlePayConfig.merchantInfo,
+ callbackIntents,
+ emailRequired: true,
+ shippingAddressRequired: useShippingCallback,
+ shippingOptionRequired: useShippingCallback,
+ shippingAddressParameters: this.shippingAddressParameters(),
+ };
}
//------------------------
@@ -543,82 +554,111 @@ class GooglepayButton extends PaymentButton {
//------------------------
onPaymentAuthorized( paymentData ) {
- this.log( 'onPaymentAuthorized' );
+ this.log( 'onPaymentAuthorized', paymentData );
+
return this.processPayment( paymentData );
}
async processPayment( paymentData ) {
this.log( 'processPayment' );
- return new Promise( async ( resolve, reject ) => {
- try {
- const id = await this.contextHandler.createOrder();
+ const paymentError = ( reason ) => {
+ this.error( reason );
- this.log( 'processPayment: createOrder', id );
+ return this.processPaymentResponse(
+ 'ERROR',
+ 'PAYMENT_AUTHORIZATION',
+ reason
+ );
+ };
- const confirmOrderResponse = await widgetBuilder.paypal
- .Googlepay()
- .confirmOrder( {
- orderId: id,
- paymentMethodData: paymentData.paymentMethodData,
- } );
+ const checkPayPalApproval = async ( orderId ) => {
+ const confirmOrderResponse = await widgetBuilder.paypal
+ .Googlepay()
+ .confirmOrder( {
+ orderId,
+ paymentMethodData: paymentData.paymentMethodData,
+ } );
- this.log(
- 'processPayment: confirmOrder',
- confirmOrderResponse
- );
+ this.log( 'confirmOrder', confirmOrderResponse );
- /** 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 );
- } ),
- },
- }
- );
+ return 'APPROVE' === confirmOrderResponse?.status;
+ };
- 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'
- )
- );
+ const approveOrderServerSide = async ( orderID ) => {
+ let isApproved = true;
+
+ this.log( 'approveOrder', orderID );
+
+ await this.contextHandler.approveOrder(
+ { orderID },
+ {
+ restart: () =>
+ new Promise( ( resolve ) => {
+ isApproved = false;
+ resolve();
+ } ),
+ order: {
+ get: () =>
+ new Promise( ( resolve ) => {
+ resolve( null );
+ } ),
+ },
}
+ );
+
+ 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;
+ }
+
+ const success = await approveOrderServerSide( id );
+
+ if ( success ) {
+ resolve( this.processPaymentResponse( 'SUCCESS' ) );
+ } else {
+ resolve( paymentError( 'FAILED TO APPROVE' ) );
+ }
+ };
+
+ const propagatePayerDataToForm = () => {
+ const raw = paymentData?.paymentMethodData?.info?.billingAddress;
+
+ const payer = {
+ 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,
+ },
+ };
+
+ setPayerData( payer );
+ };
+
+ return new Promise( async ( resolve ) => {
+ try {
+ propagatePayerDataToForm();
+ await processPaymentPromise( resolve );
} catch ( err ) {
- resolve(
- this.processPaymentResponse(
- 'ERROR',
- 'PAYMENT_AUTHORIZATION',
- err.message
- )
- );
+ resolve( paymentError( err.message ) );
}
} );
}
From 63e9c8bf27729d0616f2f8249130d0c107adb274 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Fri, 16 Aug 2024 18:15:59 +0200
Subject: [PATCH 02/17] =?UTF-8?q?=E2=9C=A8=20New=20ConsoleLogger=20`group`?=
=?UTF-8?q?=20method?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Groups subsequent console.log output, for cleaner console output
---
.../js/modules/Renderer/PaymentButton.js | 9 ++++++
.../resources/js/GooglepayButton.js | 16 +++++-----
.../resources/js/helper/ConsoleLogger.js | 31 +++++++++++++++++++
3 files changed, 49 insertions(+), 7 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
index 11fe31f79..143265bfb 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/PaymentButton.js
@@ -599,6 +599,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-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index a9bfd693e..cfd5d4b07 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -560,7 +560,7 @@ class GooglepayButton extends PaymentButton {
}
async processPayment( paymentData ) {
- this.log( 'processPayment' );
+ this.logGroup( 'processPayment' );
const paymentError = ( reason ) => {
this.error( reason );
@@ -623,13 +623,13 @@ class GooglepayButton extends PaymentButton {
return;
}
- const success = await approveOrderServerSide( id );
+ const success = await approveOrderServerSide( id );
- if ( success ) {
- resolve( this.processPaymentResponse( 'SUCCESS' ) );
- } else {
- resolve( paymentError( 'FAILED TO APPROVE' ) );
- }
+ if ( success ) {
+ resolve( this.processPaymentResponse( 'SUCCESS' ) );
+ } else {
+ resolve( paymentError( 'FAILED TO APPROVE' ) );
+ }
};
const propagatePayerDataToForm = () => {
@@ -660,6 +660,8 @@ class GooglepayButton extends PaymentButton {
} catch ( err ) {
resolve( paymentError( err.message ) );
}
+
+ this.logGroup();
} );
}
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;
+ }
+ }
}
From c007d7909c2793b266eb733c565a7ec52b87af4b Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Fri, 16 Aug 2024 18:45:49 +0200
Subject: [PATCH 03/17] =?UTF-8?q?=E2=9C=A8=20Option=20to=20only=20set=20mi?=
=?UTF-8?q?ssing=20billing=20details?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../resources/js/modules/Helper/PayerData.js | 10 +++++++---
modules/ppcp-googlepay/resources/js/GooglepayButton.js | 2 +-
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
index da7eca4d6..1f97309c1 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
@@ -109,9 +109,13 @@ export const payerData = () => {
/**
* Updates the DOM with specific payer details.
*
- * @param {PayerDetails} newData - New payer details.
+ * @param {PayerDetails} newData - New payer details.
+ * @param {boolean} [overwriteExisting=false] - If set to true, all provided values will replace existing details. If false, or omitted, only undefined fields are updated.
*/
-export const setPayerData = ( newData ) => {
+export const setPayerData = ( newData, overwriteExisting = false ) => {
+ // TODO: Check if we can add some kind of "filter" to allow customization of the data.
+ // Or add JS flags like "onlyUpdateMissing".
+
const setValue = ( path, field, value ) => {
if ( null === value || undefined === value || ! field ) {
return;
@@ -121,7 +125,7 @@ export const setPayerData = ( newData ) => {
value = value.phone_number?.national_number;
}
- if ( field.value !== value ) {
+ if ( overwriteExisting || ! field.value ) {
field.value = value;
}
};
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index cfd5d4b07..d518c1850 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -634,8 +634,8 @@ class GooglepayButton extends PaymentButton {
const propagatePayerDataToForm = () => {
const raw = paymentData?.paymentMethodData?.info?.billingAddress;
-
const payer = {
+ email_address: paymentData?.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
From cf83d2ba6efa6e254ba508939f675121f4cf2401 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Fri, 16 Aug 2024 18:54:55 +0200
Subject: [PATCH 04/17] =?UTF-8?q?=F0=9F=90=9B=20Fix=20critical=20typo?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ppcp-googlepay/resources/js/GooglepayButton.js | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index d518c1850..d8a910f34 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -573,16 +573,18 @@ class GooglepayButton extends PaymentButton {
};
const checkPayPalApproval = async ( orderId ) => {
+ const confirmationData = {
+ orderId,
+ paymentMethodData: paymentData.paymentMethodData,
+ };
+
const confirmOrderResponse = await widgetBuilder.paypal
.Googlepay()
- .confirmOrder( {
- orderId,
- paymentMethodData: paymentData.paymentMethodData,
- } );
+ .confirmOrder( confirmationData );
this.log( 'confirmOrder', confirmOrderResponse );
- return 'APPROVE' === confirmOrderResponse?.status;
+ return 'APPROVED' === confirmOrderResponse?.status;
};
const approveOrderServerSide = async ( orderID ) => {
From 451dc842ed506fe6b48b10e7fad4e128fe050fef Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 20 Aug 2024 13:47:00 +0200
Subject: [PATCH 05/17] =?UTF-8?q?=E2=9C=A8=20New=20LocalStorage=20module?=
=?UTF-8?q?=20for=20Google=20Pay?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../js/modules/Helper/LocalStorage.js | 179 ++++++++++++++++++
.../resources/js/Helper/GooglePayStorage.js | 31 +++
2 files changed, 210 insertions(+)
create mode 100644 modules/ppcp-button/resources/js/modules/Helper/LocalStorage.js
create mode 100644 modules/ppcp-googlepay/resources/js/Helper/GooglePayStorage.js
diff --git a/modules/ppcp-button/resources/js/modules/Helper/LocalStorage.js b/modules/ppcp-button/resources/js/modules/Helper/LocalStorage.js
new file mode 100644
index 000000000..65494369e
--- /dev/null
+++ b/modules/ppcp-button/resources/js/modules/Helper/LocalStorage.js
@@ -0,0 +1,179 @@
+/* global localStorage */
+
+function checkLocalStorageAvailability() {
+ try {
+ const testKey = '__ppcp_test__';
+ localStorage.setItem( testKey, 'test' );
+ localStorage.removeItem( testKey );
+ return true;
+ } catch ( e ) {
+ return false;
+ }
+}
+
+function sanitizeKey( name ) {
+ return name
+ .toLowerCase()
+ .trim()
+ .replace( /[^a-z0-9_-]/g, '_' );
+}
+
+function deserializeEntry( serialized ) {
+ try {
+ const payload = JSON.parse( serialized );
+
+ return {
+ data: payload.data,
+ expires: payload.expires || 0,
+ };
+ } catch ( e ) {
+ return null;
+ }
+}
+
+function serializeEntry( data, timeToLive ) {
+ const payload = {
+ data,
+ expires: calculateExpiration( timeToLive ),
+ };
+
+ return JSON.stringify( payload );
+}
+
+function calculateExpiration( timeToLive ) {
+ return timeToLive ? Date.now() + timeToLive * 1000 : 0;
+}
+
+/**
+ * A reusable class for handling data storage in the browser's local storage,
+ * with optional expiration.
+ *
+ * Can be extended for module specific logic.
+ *
+ * @see GooglePaySession
+ */
+export class LocalStorage {
+ /**
+ * @type {string}
+ */
+ #group = '';
+
+ /**
+ * @type {null|boolean}
+ */
+ #canUseLocalStorage = null;
+
+ /**
+ * @param {string} group - Group name for all storage keys managed by this instance.
+ */
+ constructor( group ) {
+ this.#group = sanitizeKey( group ) + ':';
+ this.#removeExpired();
+ }
+
+ /**
+ * Removes all items in the current group that have reached the expiry date.
+ */
+ #removeExpired() {
+ if ( ! this.canUseLocalStorage ) {
+ return;
+ }
+
+ Object.keys( localStorage ).forEach( ( key ) => {
+ if ( ! key.startsWith( this.#group ) ) {
+ return;
+ }
+
+ const entry = deserializeEntry( localStorage.getItem( key ) );
+ if ( entry && entry.expires > 0 && entry.expires < Date.now() ) {
+ localStorage.removeItem( key );
+ }
+ } );
+ }
+
+ /**
+ * Sanitizes the given entry name and adds the group prefix.
+ *
+ * @throws {Error} If the name is empty after sanitization.
+ * @param {string} name - Entry name.
+ * @return {string} Prefixed and sanitized entry name.
+ */
+ #entryKey( name ) {
+ const sanitizedName = sanitizeKey( name );
+
+ if ( sanitizedName.length === 0 ) {
+ throw new Error( 'Name cannot be empty after sanitization' );
+ }
+
+ return `${ this.#group }${ sanitizedName }`;
+ }
+
+ /**
+ * Indicates, whether localStorage is available.
+ *
+ * @return {boolean} True means the localStorage API is available.
+ */
+ get canUseLocalStorage() {
+ if ( null === this.#canUseLocalStorage ) {
+ this.#canUseLocalStorage = checkLocalStorageAvailability();
+ }
+
+ return this.#canUseLocalStorage;
+ }
+
+ /**
+ * Stores data in the browser's local storage, with an optional timeout.
+ *
+ * @param {string} name - Name of the item in the storage.
+ * @param {any} data - The data to store.
+ * @param {number} [timeToLive=0] - Lifespan in seconds. 0 means the data won't expire.
+ * @throws {Error} If local storage is not available.
+ */
+ set( name, data, timeToLive = 0 ) {
+ if ( ! this.canUseLocalStorage ) {
+ throw new Error( 'Local storage is not available' );
+ }
+
+ const entry = serializeEntry( data, timeToLive );
+ const entryKey = this.#entryKey( name );
+
+ localStorage.setItem( entryKey, entry );
+ }
+
+ /**
+ * Retrieves previously stored data from the browser's local storage.
+ *
+ * @param {string} name - Name of the stored item.
+ * @return {any|null} The stored data, or null when no valid entry is found or it has expired.
+ * @throws {Error} If local storage is not available.
+ */
+ get( name ) {
+ if ( ! this.canUseLocalStorage ) {
+ throw new Error( 'Local storage is not available' );
+ }
+
+ const itemKey = this.#entryKey( name );
+ const entry = deserializeEntry( localStorage.getItem( itemKey ) );
+
+ if ( ! entry ) {
+ return null;
+ }
+
+ return entry.data;
+ }
+
+ /**
+ * Removes the specified entry from the browser's local storage.
+ *
+ * @param {string} name - Name of the stored item.
+ * @throws {Error} If local storage is not available.
+ */
+ clear( name ) {
+ if ( ! this.canUseLocalStorage ) {
+ throw new Error( 'Local storage is not available' );
+ }
+
+ const itemKey = this.#entryKey( name );
+ localStorage.removeItem( itemKey );
+ }
+}
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;
From 63b505590326c10174c0e0c54db12e82cb57b33d Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 20 Aug 2024 14:14:30 +0200
Subject: [PATCH 06/17] =?UTF-8?q?=F0=9F=92=A1=20Document=20payment=20workf?=
=?UTF-8?q?low?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../OnApproveHandler/onApproveForContinue.js | 16 ++++++++++++----
.../resources/js/GooglepayButton.js | 9 +++++++++
2 files changed, 21 insertions(+), 4 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js
index c60c163fd..0d699c170 100644
--- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js
+++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js
@@ -18,17 +18,25 @@ const onApprove = ( context, errorHandler ) => {
.then( ( res ) => {
return res.json();
} )
- .then( ( data ) => {
- if ( ! data.success ) {
+ .then( ( approveData ) => {
+ if ( ! approveData.success ) {
errorHandler.genericError();
return actions.restart().catch( ( err ) => {
errorHandler.genericError();
} );
}
- const orderReceivedUrl = data.data?.order_received_url;
+ const orderReceivedUrl = approveData.data?.order_received_url;
- location.href = orderReceivedUrl
+ /**
+ * Notice how this step initiates a redirect to a new page using a plain
+ * URL as new location. This process does not send any details about the
+ * approved order or billed customer.
+ * Also, due to the redirect starting _instantly_ there should be no other
+ * logic scheduled after calling `await onApprove()`;
+ */
+
+ window.location.href = orderReceivedUrl
? orderReceivedUrl
: context.config.redirect;
} );
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index d8a910f34..a68e22f4a 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -587,6 +587,14 @@ class GooglepayButton extends PaymentButton {
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;
@@ -625,6 +633,7 @@ class GooglepayButton extends PaymentButton {
return;
}
+ // This must be the last step in the process, as it initiates a redirect.
const success = await approveOrderServerSide( id );
if ( success ) {
From a51748f805416b51828c38f6600b38401d75dd7e Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 20 Aug 2024 14:18:30 +0200
Subject: [PATCH 07/17] =?UTF-8?q?=F0=9F=92=A1=20Improve=20comments=20in=20?=
=?UTF-8?q?PayerData=20module?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../resources/js/modules/Helper/PayerData.js | 49 +++++++++++--------
1 file changed, 29 insertions(+), 20 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
index 1f97309c1..59af8dc85 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
@@ -10,30 +10,30 @@
* Postal address details.
*
* @typedef {Object} AddressDetails
- * @property {?string} country_code - Country code (2-letter).
- * @property {?string} address_line_1 - Address details, line 1 (street, house number).
- * @property {?string} address_line_2 - Address details, line 2.
- * @property {?string} admin_area_1 - State or region.
- * @property {?string} admin_area_2 - State or region.
- * @property {?string} postal_code - Zip code.
+ * @property {undefined|string} country_code - Country code (2-letter).
+ * @property {undefined|string} address_line_1 - Address details, line 1 (street, house number).
+ * @property {undefined|string} address_line_2 - Address details, line 2.
+ * @property {undefined|string} admin_area_1 - State or region.
+ * @property {undefined|string} admin_area_2 - State or region.
+ * @property {undefined|string} postal_code - Zip code.
*/
/**
* Phone details.
*
* @typedef {Object} PhoneDetails
- * @property {?string} phone_type - Type, usually 'HOME'
- * @property {?{national_number: string}} phone_number - Phone number details.
+ * @property {undefined|string} phone_type - Type, usually 'HOME'
+ * @property {undefined|{national_number: string}} phone_number - Phone number details.
*/
/**
* Payer details.
*
* @typedef {Object} PayerDetails
- * @property {?string} email_address - Email address for billing communication.
- * @property {?PhoneDetails} phone - Phone number for billing communication.
- * @property {?NameDetails} name - Payer's name.
- * @property {?AddressDetails} address - Postal billing address.
+ * @property {undefined|string} email_address - Email address for billing communication.
+ * @property {undefined|PhoneDetails} phone - Phone number for billing communication.
+ * @property {undefined|NameDetails} name - Payer's name.
+ * @property {undefined|AddressDetails} address - Postal billing address.
*/
// Map checkout fields to PayerData object properties.
@@ -55,7 +55,14 @@ const FIELD_MAP = {
*
* @return {?PayerDetails} Full billing details, or null on failure.
*/
-export const payerData = () => {
+export function payerData() {
+ /**
+ * PayPalCommerceGateway.payer can be set from server-side or via JS:
+ * - Server-side: Set by PHP when a WC customer is known.
+ * - Dynamic JS: When a payment method provided billing data.
+ *
+ * @see {setPayerData}
+ */
const payer = window.PayPalCommerceGateway?.payer;
if ( ! payer ) {
return null;
@@ -104,18 +111,20 @@ export const payerData = () => {
}
return data;
-};
+}
/**
* Updates the DOM with specific payer details.
*
+ * Used by payment method callbacks that provide dedicated billing details, like Google Pay.
+ * Note: This code only works on classic checkout
+ *
* @param {PayerDetails} newData - New payer details.
- * @param {boolean} [overwriteExisting=false] - If set to true, all provided values will replace existing details. If false, or omitted, only undefined fields are updated.
+ * @param {boolean} [overwriteExisting=false] - If set to true, all provided values will
+ * replace existing details. If false, or omitted,
+ * only undefined fields are updated.
*/
-export const setPayerData = ( newData, overwriteExisting = false ) => {
- // TODO: Check if we can add some kind of "filter" to allow customization of the data.
- // Or add JS flags like "onlyUpdateMissing".
-
+export function setPayerData( newData, overwriteExisting = false ) {
const setValue = ( path, field, value ) => {
if ( null === value || undefined === value || ! field ) {
return;
@@ -136,4 +145,4 @@ export const setPayerData = ( newData, overwriteExisting = false ) => {
setValue( path, element, value );
} );
-};
+}
From c48c94e09d7030d86a4dfc6b3afc1fad25cd6596 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 20 Aug 2024 14:19:41 +0200
Subject: [PATCH 08/17] =?UTF-8?q?=E2=9C=A8=20New=20CheckoutBootstrap=20for?=
=?UTF-8?q?=20GooglePay?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This new module uses previously stored payer details to populate the checkout form on the classic checkout page.
---
.../js/ContextBootstrap/CheckoutBootstrap.js | 64 +++++++++++++++++++
.../resources/js/GooglepayButton.js | 43 +++++++------
modules/ppcp-googlepay/resources/js/boot.js | 18 ++++--
3 files changed, 101 insertions(+), 24 deletions(-)
create mode 100644 modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
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..b5e0c1a48
--- /dev/null
+++ b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
@@ -0,0 +1,64 @@
+import { GooglePayStorage } from '../Helper/GooglePayStorage';
+import { setPayerData } from '../../../../ppcp-button/resources/js/modules/Helper/PayerData';
+
+const CHECKOUT_FORM_SELECTOR = 'form.woocommerce-checkout';
+
+export class CheckoutBootstrap {
+ /**
+ * @type {GooglePayStorage}
+ */
+ #storage;
+
+ /**
+ * @type {null|HTMLFormElement}
+ */
+ #checkoutForm = null;
+
+ constructor( storage ) {
+ this.#storage = storage;
+
+ this.onFormSubmit = this.onFormSubmit.bind( this );
+ }
+
+ /**
+ * Returns the WooCommerce checkout form element.
+ *
+ * @return {HTMLFormElement|null} The form, or null if not a checkout page.
+ */
+ get checkoutForm() {
+ if ( null === this.#checkoutForm ) {
+ this.#checkoutForm = document.querySelector(
+ CHECKOUT_FORM_SELECTOR
+ );
+ }
+
+ return this.#checkoutForm;
+ }
+
+ /**
+ * Indicates, if the current page contains a checkout form.
+ *
+ * @return {boolean} True, if a checkout form is present.
+ */
+ get isPageWithCheckoutForm() {
+ return this.checkoutForm instanceof HTMLElement;
+ }
+
+ init() {
+ if ( ! this.isPageWithCheckoutForm ) {
+ return;
+ }
+
+ const billingData = this.#storage.getPayer();
+
+ if ( billingData ) {
+ setPayerData( billingData );
+
+ this.checkoutForm.addEventListener( 'submit', this.onFormSubmit );
+ }
+ }
+
+ onFormSubmit() {
+ this.#storage.clearPayer();
+ }
+}
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index a68e22f4a..26acd0139 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -7,6 +7,7 @@ import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/Wi
import UpdatePaymentData from './Helper/UpdatePaymentData';
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.
@@ -74,6 +75,26 @@ import { setPayerData } from '../../../ppcp-button/resources/js/modules/Helper/P
* 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
@@ -643,30 +664,16 @@ class GooglepayButton extends PaymentButton {
}
};
- const propagatePayerDataToForm = () => {
- const raw = paymentData?.paymentMethodData?.info?.billingAddress;
- const payer = {
- email_address: paymentData?.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,
- },
- };
+ const addBillingDataToSession = () => {
+ const payer = payerDataFromPaymentResponse( paymentData );
+ moduleStorage.setPayer( payer );
setPayerData( payer );
};
return new Promise( async ( resolve ) => {
try {
- propagatePayerDataToForm();
+ addBillingDataToSession();
await processPaymentPromise( resolve );
} catch ( err ) {
resolve( paymentError( err.message ) );
diff --git a/modules/ppcp-googlepay/resources/js/boot.js b/modules/ppcp-googlepay/resources/js/boot.js
index 99dd414f5..3071998a9 100644
--- a/modules/ppcp-googlepay/resources/js/boot.js
+++ b/modules/ppcp-googlepay/resources/js/boot.js
@@ -2,13 +2,23 @@ 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 } ) {
+ const context = ppcpConfig.context;
-( function ( { buttonConfig, ppcpConfig, jQuery } ) {
let manager;
const bootstrap = function () {
manager = new GooglepayManager( buttonConfig, ppcpConfig );
manager.init();
+
+ if ( 'continuation' === context || 'checkout' === context ) {
+ const checkoutBootstap = new CheckoutBootstrap( moduleStorage );
+
+ checkoutBootstap.init();
+ }
};
setupButtonEvents( function () {
@@ -18,10 +28,7 @@ import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Hel
} );
document.addEventListener( 'DOMContentLoaded', () => {
- if (
- typeof buttonConfig === 'undefined' ||
- typeof ppcpConfig === 'undefined'
- ) {
+ if ( ! buttonConfig || ! ppcpConfig ) {
// No PayPal buttons present on this page.
return;
}
@@ -52,5 +59,4 @@ import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Hel
} )( {
buttonConfig: window.wc_ppcp_googlepay,
ppcpConfig: window.PayPalCommerceGateway,
- jQuery: window.jQuery,
} );
From 97379628d441602c3ca3425b7f6df253765b09f8 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 20 Aug 2024 15:37:26 +0200
Subject: [PATCH 09/17] =?UTF-8?q?=E2=9C=A8=20Send=20payer=20details=20with?=
=?UTF-8?q?=20order=20approval=20request?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
First step to integrate payer details in the payment flow without final confirmation.
---
.../resources/js/modules/Helper/PayerData.js | 24 +++++++++++++++++++
.../OnApproveHandler/onApproveForContinue.js | 23 +++++++++++-------
.../resources/js/GooglepayButton.js | 6 ++---
3 files changed, 42 insertions(+), 11 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
index 59af8dc85..358b1587f 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
@@ -145,4 +145,28 @@ export function setPayerData( newData, overwriteExisting = false ) {
setValue( path, element, value );
} );
+
+ /*
+ * Persist the payer details to the global JS object, to make it available in other modules
+ * via tha `payerData()` accessor.
+ */
+ window.PayPalCommerceGateway.payer =
+ window.PayPalCommerceGateway.payer || {};
+ const currentPayerData = payerData();
+
+ if ( currentPayerData ) {
+ Object.entries( newData ).forEach( ( [ key, value ] ) => {
+ if (
+ overwriteExisting ||
+ null !== currentPayerData[ key ] ||
+ undefined !== currentPayerData[ key ]
+ ) {
+ currentPayerData[ key ] = value;
+ }
+ } );
+
+ window.PayPalCommerceGateway.payer = currentPayerData;
+ } else {
+ window.PayPalCommerceGateway.payer = newData;
+ }
}
diff --git a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js
index 0d699c170..d492802f1 100644
--- a/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js
+++ b/modules/ppcp-button/resources/js/modules/OnApproveHandler/onApproveForContinue.js
@@ -1,19 +1,26 @@
const onApprove = ( context, errorHandler ) => {
return ( data, actions ) => {
+ const canCreateOrder =
+ ! context.config.vaultingEnabled || data.paymentSource !== 'venmo';
+
+ const payload = {
+ nonce: context.config.ajax.approve_order.nonce,
+ order_id: data.orderID,
+ funding_source: window.ppcpFundingSource,
+ should_create_wc_order: canCreateOrder,
+ };
+
+ if ( canCreateOrder && data.payer ) {
+ payload.payer = data.payer;
+ }
+
return fetch( context.config.ajax.approve_order.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
- body: JSON.stringify( {
- nonce: context.config.ajax.approve_order.nonce,
- order_id: data.orderID,
- funding_source: window.ppcpFundingSource,
- should_create_wc_order:
- ! context.config.vaultingEnabled ||
- data.paymentSource !== 'venmo',
- } ),
+ body: JSON.stringify( payload ),
} )
.then( ( res ) => {
return res.json();
diff --git a/modules/ppcp-googlepay/resources/js/GooglepayButton.js b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
index 26acd0139..e9ca1bcc8 100644
--- a/modules/ppcp-googlepay/resources/js/GooglepayButton.js
+++ b/modules/ppcp-googlepay/resources/js/GooglepayButton.js
@@ -583,6 +583,8 @@ class GooglepayButton extends PaymentButton {
async processPayment( paymentData ) {
this.logGroup( 'processPayment' );
+ const payer = payerDataFromPaymentResponse( paymentData );
+
const paymentError = ( reason ) => {
this.error( reason );
@@ -622,7 +624,7 @@ class GooglepayButton extends PaymentButton {
this.log( 'approveOrder', orderID );
await this.contextHandler.approveOrder(
- { orderID },
+ { orderID, payer },
{
restart: () =>
new Promise( ( resolve ) => {
@@ -665,8 +667,6 @@ class GooglepayButton extends PaymentButton {
};
const addBillingDataToSession = () => {
- const payer = payerDataFromPaymentResponse( paymentData );
-
moduleStorage.setPayer( payer );
setPayerData( payer );
};
From 915754799371e7e919289e4a547be8196377df55 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Wed, 21 Aug 2024 13:51:40 +0200
Subject: [PATCH 10/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Improve=20PayerData?=
=?UTF-8?q?=20module?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../resources/js/modules/Helper/PayerData.js | 212 ++++++++++--------
1 file changed, 118 insertions(+), 94 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
index 358b1587f..6b899bc9e 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
@@ -2,38 +2,38 @@
* Name details.
*
* @typedef {Object} NameDetails
- * @property {?string} given_name - First name, e.g. "John".
- * @property {?string} surname - Last name, e.g. "Doe".
+ * @property {string} [given_name] - First name, e.g. "John".
+ * @property {string} [surname] - Last name, e.g. "Doe".
*/
/**
* Postal address details.
*
* @typedef {Object} AddressDetails
- * @property {undefined|string} country_code - Country code (2-letter).
- * @property {undefined|string} address_line_1 - Address details, line 1 (street, house number).
- * @property {undefined|string} address_line_2 - Address details, line 2.
- * @property {undefined|string} admin_area_1 - State or region.
- * @property {undefined|string} admin_area_2 - State or region.
- * @property {undefined|string} postal_code - Zip code.
+ * @property {string} [country_code] - Country code (2-letter).
+ * @property {string} [address_line_1] - Address details, line 1 (street, house number).
+ * @property {string} [address_line_2] - Address details, line 2.
+ * @property {string} [admin_area_1] - State or region.
+ * @property {string} [admin_area_2] - State or region.
+ * @property {string} [postal_code] - Zip code.
*/
/**
* Phone details.
*
* @typedef {Object} PhoneDetails
- * @property {undefined|string} phone_type - Type, usually 'HOME'
- * @property {undefined|{national_number: string}} phone_number - Phone number details.
+ * @property {string} [phone_type] - Type, usually 'HOME'
+ * @property {{national_number: string}} [phone_number] - Phone number details.
*/
/**
* Payer details.
*
* @typedef {Object} PayerDetails
- * @property {undefined|string} email_address - Email address for billing communication.
- * @property {undefined|PhoneDetails} phone - Phone number for billing communication.
- * @property {undefined|NameDetails} name - Payer's name.
- * @property {undefined|AddressDetails} address - Postal billing address.
+ * @property {string} [email_address] - Email address for billing communication.
+ * @property {PhoneDetails} [phone] - Phone number for billing communication.
+ * @property {NameDetails} [name] - Payer's name.
+ * @property {AddressDetails} [address] - Postal billing address.
*/
// Map checkout fields to PayerData object properties.
@@ -50,123 +50,147 @@ const FIELD_MAP = {
'#billing_phone': [ 'phone' ],
};
-/**
- * Returns billing details from the checkout form or global JS object.
- *
- * @return {?PayerDetails} Full billing details, or null on failure.
- */
-export function payerData() {
- /**
- * PayPalCommerceGateway.payer can be set from server-side or via JS:
- * - Server-side: Set by PHP when a WC customer is known.
- * - Dynamic JS: When a payment method provided billing data.
- *
- * @see {setPayerData}
- */
- const payer = window.PayPalCommerceGateway?.payer;
- if ( ! payer ) {
- return null;
- }
+function normalizePayerDetails( details ) {
+ return {
+ email_address: details.email_address,
+ phone: details.phone,
+ name: {
+ surname: details.name?.surname,
+ given_name: details.name?.given_name,
+ },
+ address: {
+ country_code: details.address?.country_code,
+ address_line_1: details.address?.address_line_1,
+ address_line_2: details.address?.address_line_2,
+ admin_area_1: details.address?.admin_area_1,
+ admin_area_2: details.address?.admin_area_2,
+ postal_code: details.address?.postal_code,
+ },
+ };
+}
+function mergePayerDetails( firstPayer, secondPayer ) {
+ const mergeNestedObjects = ( target, source ) => {
+ for ( const [ key, value ] of Object.entries( source ) ) {
+ if ( null !== value && undefined !== value ) {
+ if ( 'object' === typeof value ) {
+ target[ key ] = mergeNestedObjects(
+ target[ key ] || {},
+ value
+ );
+ } else {
+ target[ key ] = value;
+ }
+ }
+ }
+ return target;
+ };
+
+ return mergeNestedObjects(
+ normalizePayerDetails( firstPayer ),
+ normalizePayerDetails( secondPayer )
+ );
+}
+
+function getCheckoutBillingDetails() {
const getElementValue = ( selector ) =>
document.querySelector( selector )?.value;
- // Initialize data with existing payer values.
- const data = {
- email_address: payer.email_address,
- phone: payer.phone,
- name: {
- surname: payer.name?.surname,
- given_name: payer.name?.given_name,
- },
- address: {
- country_code: payer.address?.country_code,
- address_line_1: payer.address?.address_line_1,
- address_line_2: payer.address?.address_line_2,
- admin_area_1: payer.address?.admin_area_1,
- admin_area_2: payer.address?.admin_area_2,
- postal_code: payer.address?.postal_code,
- },
+ const setNestedValue = ( obj, path, value ) => {
+ let current = obj;
+ for ( let i = 0; i < path.length - 1; i++ ) {
+ current = current[ path[ i ] ] = current[ path[ i ] ] || {};
+ }
+ current[ path[ path.length - 1 ] ] = value;
};
- // Update data with DOM values where they exist.
+ const data = {};
+
Object.entries( FIELD_MAP ).forEach( ( [ selector, path ] ) => {
const value = getElementValue( selector );
if ( value ) {
- let current = data;
- path.slice( 0, -1 ).forEach( ( key ) => {
- current = current[ key ] = current[ key ] || {};
- } );
- current[ path[ path.length - 1 ] ] = value;
+ setNestedValue( data, path, value );
}
} );
- // Handle phone separately due to its nested structure.
- const phoneNumber = data.phone;
- if ( phoneNumber && typeof phoneNumber === 'string' ) {
+ if ( data.phone && 'string' === typeof data.phone ) {
data.phone = {
phone_type: 'HOME',
- phone_number: { national_number: phoneNumber },
+ phone_number: { national_number: data.phone },
};
}
return data;
}
-/**
- * Updates the DOM with specific payer details.
- *
- * Used by payment method callbacks that provide dedicated billing details, like Google Pay.
- * Note: This code only works on classic checkout
- *
- * @param {PayerDetails} newData - New payer details.
- * @param {boolean} [overwriteExisting=false] - If set to true, all provided values will
- * replace existing details. If false, or omitted,
- * only undefined fields are updated.
- */
-export function setPayerData( newData, overwriteExisting = false ) {
+function setCheckoutBillingDetails( payer ) {
const setValue = ( path, field, value ) => {
if ( null === value || undefined === value || ! field ) {
return;
}
- if ( path[ 0 ] === 'phone' && typeof value === 'object' ) {
+ if ( 'phone' === path[ 0 ] && 'object' === typeof value ) {
value = value.phone_number?.national_number;
}
- if ( overwriteExisting || ! field.value ) {
- field.value = value;
- }
+ field.value = value;
};
+ const getNestedValue = ( obj, path ) =>
+ path.reduce( ( current, key ) => current?.[ key ], obj );
+
Object.entries( FIELD_MAP ).forEach( ( [ selector, path ] ) => {
- const value = path.reduce( ( obj, key ) => obj?.[ key ], newData );
+ const value = getNestedValue( payer, path );
const element = document.querySelector( selector );
setValue( path, element, value );
} );
+}
- /*
- * Persist the payer details to the global JS object, to make it available in other modules
- * via tha `payerData()` accessor.
- */
- window.PayPalCommerceGateway.payer =
- window.PayPalCommerceGateway.payer || {};
- const currentPayerData = payerData();
+export function getWooCommerceCustomerDetails() {
+ // Populated on server-side with details about the current WooCommerce customer.
+ return window.PayPalCommerceGateway?.payer ?? null;
+}
- if ( currentPayerData ) {
- Object.entries( newData ).forEach( ( [ key, value ] ) => {
- if (
- overwriteExisting ||
- null !== currentPayerData[ key ] ||
- undefined !== currentPayerData[ key ]
- ) {
- currentPayerData[ key ] = value;
- }
- } );
+export function getSessionCustomerDetails() {
+ // Populated by JS via `setSessionCustomerDetails()`
+ return window.PayPalCommerceGateway?.sessionPayer ?? null;
+}
- window.PayPalCommerceGateway.payer = currentPayerData;
- } else {
- window.PayPalCommerceGateway.payer = newData;
+/**
+ * Stores customer details in the current JS context for use in the same request.
+ * Details that are set are not persisted during navigation.
+ *
+ * @param {unknown} details - New payer details
+ */
+export function setSessionCustomerDetails( details ) {
+ if ( details && 'object' === typeof details ) {
+ window.PayPalCommerceGateway.sessionPayer =
+ normalizePayerDetails( details );
+ }
+}
+
+export function payerData() {
+ const payer =
+ getWooCommerceCustomerDetails() ?? getSessionCustomerDetails();
+
+ if ( ! payer ) {
+ return null;
+ }
+
+ const formData = getCheckoutBillingDetails();
+
+ if ( formData ) {
+ return mergePayerDetails( payer, formData );
+ }
+
+ return normalizePayerDetails( payer );
+}
+
+export function setPayerData( newData, updateCheckout = false ) {
+ setSessionCustomerDetails( newData );
+
+ if ( updateCheckout ) {
+ setCheckoutBillingDetails( newData );
}
}
From eba92e6b81c48b1745d4ffbf68623ea878f976e1 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Wed, 21 Aug 2024 13:52:11 +0200
Subject: [PATCH 11/17] =?UTF-8?q?=E2=9C=A8=20Detect=20logged=20in=20custom?=
=?UTF-8?q?er=20in=20checkout=20module?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../js/ContextBootstrap/CheckoutBootstrap.js | 22 ++++++++++++++++---
1 file changed, 19 insertions(+), 3 deletions(-)
diff --git a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
index b5e0c1a48..d913b0b3f 100644
--- a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
+++ b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
@@ -1,5 +1,8 @@
import { GooglePayStorage } from '../Helper/GooglePayStorage';
-import { setPayerData } from '../../../../ppcp-button/resources/js/modules/Helper/PayerData';
+import {
+ getWooCommerceCustomerDetails,
+ setPayerData,
+} from '../../../../ppcp-button/resources/js/modules/Helper/PayerData';
const CHECKOUT_FORM_SELECTOR = 'form.woocommerce-checkout';
@@ -49,16 +52,29 @@ export class CheckoutBootstrap {
return;
}
+ this.#populateCheckoutFields();
+ }
+
+ #populateCheckoutFields() {
+ const loggedInData = getWooCommerceCustomerDetails();
+
+ // If customer is logged in, we use the details from the customer profile.
+ if ( loggedInData ) {
+ return;
+ }
+
const billingData = this.#storage.getPayer();
if ( billingData ) {
setPayerData( billingData );
- this.checkoutForm.addEventListener( 'submit', this.onFormSubmit );
+ this.checkoutForm.addEventListener( 'submit', () =>
+ this.#onFormSubmit()
+ );
}
}
- onFormSubmit() {
+ #onFormSubmit() {
this.#storage.clearPayer();
}
}
From 734951adcb2d5250d5c768d29d6eb34936193350 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Wed, 21 Aug 2024 14:21:45 +0200
Subject: [PATCH 12/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Minor=20cleanup?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../resources/js/modules/Helper/PayerData.js | 28 +++++++++----------
.../js/ContextBootstrap/CheckoutBootstrap.js | 17 ++++++-----
2 files changed, 22 insertions(+), 23 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
index 6b899bc9e..df13ef92f 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
@@ -149,12 +149,12 @@ function setCheckoutBillingDetails( payer ) {
export function getWooCommerceCustomerDetails() {
// Populated on server-side with details about the current WooCommerce customer.
- return window.PayPalCommerceGateway?.payer ?? null;
+ return window.PayPalCommerceGateway?.payer;
}
-export function getSessionCustomerDetails() {
- // Populated by JS via `setSessionCustomerDetails()`
- return window.PayPalCommerceGateway?.sessionPayer ?? null;
+export function getSessionBillingDetails() {
+ // Populated by JS via `setSessionBillingDetails()`
+ return window.PayPalCommerceGateway?.tempPayer;
}
/**
@@ -163,16 +163,16 @@ export function getSessionCustomerDetails() {
*
* @param {unknown} details - New payer details
*/
-export function setSessionCustomerDetails( details ) {
- if ( details && 'object' === typeof details ) {
- window.PayPalCommerceGateway.sessionPayer =
- normalizePayerDetails( details );
+export function setSessionBillingDetails( details ) {
+ if ( ! details || 'object' !== typeof details ) {
+ return;
}
+
+ window.PayPalCommerceGateway.tempPayer = normalizePayerDetails( details );
}
export function payerData() {
- const payer =
- getWooCommerceCustomerDetails() ?? getSessionCustomerDetails();
+ const payer = getWooCommerceCustomerDetails() ?? getSessionBillingDetails();
if ( ! payer ) {
return null;
@@ -187,10 +187,10 @@ export function payerData() {
return normalizePayerDetails( payer );
}
-export function setPayerData( newData, updateCheckout = false ) {
- setSessionCustomerDetails( newData );
+export function setPayerData( payerDetails, updateCheckoutForm = false ) {
+ setSessionBillingDetails( payerDetails );
- if ( updateCheckout ) {
- setCheckoutBillingDetails( newData );
+ if ( updateCheckoutForm ) {
+ setCheckoutBillingDetails( payerDetails );
}
}
diff --git a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
index d913b0b3f..9e1c30e9f 100644
--- a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
+++ b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
@@ -19,8 +19,6 @@ export class CheckoutBootstrap {
constructor( storage ) {
this.#storage = storage;
-
- this.onFormSubmit = this.onFormSubmit.bind( this );
}
/**
@@ -44,7 +42,7 @@ export class CheckoutBootstrap {
* @return {boolean} True, if a checkout form is present.
*/
get isPageWithCheckoutForm() {
- return this.checkoutForm instanceof HTMLElement;
+ return null !== this.checkoutForm;
}
init() {
@@ -65,13 +63,14 @@ export class CheckoutBootstrap {
const billingData = this.#storage.getPayer();
- if ( billingData ) {
- setPayerData( billingData );
-
- this.checkoutForm.addEventListener( 'submit', () =>
- this.#onFormSubmit()
- );
+ if ( ! billingData ) {
+ return;
}
+
+ setPayerData( billingData, true );
+ this.checkoutForm.addEventListener( 'submit', () =>
+ this.#onFormSubmit()
+ );
}
#onFormSubmit() {
From 3e0a44ca1f57b7a954ed1702cfc8a03bd0a0c411 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Mon, 26 Aug 2024 18:22:42 +0200
Subject: [PATCH 13/17] =?UTF-8?q?=F0=9F=9A=A7=20Enqueue=20missing=20script?=
=?UTF-8?q?=20for=20billing=20data=20logic?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
When PayLater is disabled and we're in "continuation" context, then the new billing data logic is not working: The relevant JS script is not enqueued.
---
modules/ppcp-googlepay/src/GooglepayModule.php | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php
index b7feedc07..9079a4b9a 100644
--- a/modules/ppcp-googlepay/src/GooglepayModule.php
+++ b/modules/ppcp-googlepay/src/GooglepayModule.php
@@ -93,6 +93,12 @@ class GooglepayModule implements ModuleInterface {
static function () use ( $c, $button ) {
$smart_button = $c->get( 'button.smart-button' );
assert( $smart_button instanceof SmartButtonInterface );
+
+ /*
+ * TODO: When PayLater is disabled and we're in "continuation" context, then no JS is enqueued.
+ * Find a solution to enqueue the CheckoutBootstrap module in that situation.
+ */
+
if ( $smart_button->should_load_ppcp_script() ) {
$button->enqueue();
return;
From 15a09d9722f8f9d1e1a862d110bed61d768373fb Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 27 Aug 2024 12:32:00 +0200
Subject: [PATCH 14/17] =?UTF-8?q?=E2=9C=A8=20Decouple=20PayerData=20from?=
=?UTF-8?q?=20global=20PPCP=20object?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ppcp-button/resources/js/modules/Helper/PayerData.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
index df13ef92f..5695facb0 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/PayerData.js
@@ -149,12 +149,12 @@ function setCheckoutBillingDetails( payer ) {
export function getWooCommerceCustomerDetails() {
// Populated on server-side with details about the current WooCommerce customer.
- return window.PayPalCommerceGateway?.payer;
+ return window?.PayPalCommerceGateway?.payer;
}
export function getSessionBillingDetails() {
// Populated by JS via `setSessionBillingDetails()`
- return window.PayPalCommerceGateway?.tempPayer;
+ return window._PpcpPayerSessionDetails;
}
/**
@@ -168,7 +168,7 @@ export function setSessionBillingDetails( details ) {
return;
}
- window.PayPalCommerceGateway.tempPayer = normalizePayerDetails( details );
+ window._PpcpPayerSessionDetails = normalizePayerDetails( details );
}
export function payerData() {
From 07c73985e3ece321284e271138822c7b0c9fdb40 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 27 Aug 2024 12:32:53 +0200
Subject: [PATCH 15/17] =?UTF-8?q?=E2=9C=A8=20Always=20load=20GooglePay=20s?=
=?UTF-8?q?cripts=20on=20checkout=20page?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
modules/ppcp-googlepay/src/GooglepayModule.php | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/modules/ppcp-googlepay/src/GooglepayModule.php b/modules/ppcp-googlepay/src/GooglepayModule.php
index 9079a4b9a..114fe7f3d 100644
--- a/modules/ppcp-googlepay/src/GooglepayModule.php
+++ b/modules/ppcp-googlepay/src/GooglepayModule.php
@@ -94,16 +94,20 @@ class GooglepayModule implements ModuleInterface {
$smart_button = $c->get( 'button.smart-button' );
assert( $smart_button instanceof SmartButtonInterface );
- /*
- * TODO: When PayLater is disabled and we're in "continuation" context, then no JS is enqueued.
- * Find a solution to enqueue the CheckoutBootstrap module in that situation.
- */
-
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.
From 5b054581036fab0486bf08ac9cf00f84d23671ae Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 27 Aug 2024 12:34:50 +0200
Subject: [PATCH 16/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Decouple=20init=20lo?=
=?UTF-8?q?gic=20from=20global=20PPCP=20config?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Allow Google Pay logic to initialize on pages that do not provide a global PayPalCommerceGateway object. Required to use CheckoutBootstrap to popuplate billing fields in continuation mode.
---
.../js/ContextBootstrap/CheckoutBootstrap.js | 61 +++++++++++++------
modules/ppcp-googlepay/resources/js/boot.js | 56 ++++++++++++-----
2 files changed, 83 insertions(+), 34 deletions(-)
diff --git a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
index 9e1c30e9f..1e1933a10 100644
--- a/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
+++ b/modules/ppcp-googlepay/resources/js/ContextBootstrap/CheckoutBootstrap.js
@@ -13,12 +13,34 @@ export class CheckoutBootstrap {
#storage;
/**
- * @type {null|HTMLFormElement}
+ * @type {HTMLFormElement|null}
*/
- #checkoutForm = 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 );
}
/**
@@ -27,37 +49,32 @@ export class CheckoutBootstrap {
* @return {HTMLFormElement|null} The form, or null if not a checkout page.
*/
get checkoutForm() {
- if ( null === this.#checkoutForm ) {
- this.#checkoutForm = document.querySelector(
- CHECKOUT_FORM_SELECTOR
- );
- }
-
return this.#checkoutForm;
}
/**
- * Indicates, if the current page contains a checkout form.
+ * Initializes the checkout process.
*
- * @return {boolean} True, if a checkout form is present.
+ * @throws {Error} If called on a page without a checkout form.
*/
- get isPageWithCheckoutForm() {
- return null !== this.checkoutForm;
- }
-
init() {
- if ( ! this.isPageWithCheckoutForm ) {
- return;
+ 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 customer is logged in, we use the details from the customer profile.
if ( loggedInData ) {
+ // If customer is logged in, we use the details from the customer profile.
return;
}
@@ -68,11 +85,17 @@ export class CheckoutBootstrap {
}
setPayerData( billingData, true );
- this.checkoutForm.addEventListener( 'submit', () =>
- this.#onFormSubmit()
+ 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/boot.js b/modules/ppcp-googlepay/resources/js/boot.js
index 3071998a9..666286ed4 100644
--- a/modules/ppcp-googlepay/resources/js/boot.js
+++ b/modules/ppcp-googlepay/resources/js/boot.js
@@ -1,3 +1,12 @@
+/**
+ * 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';
@@ -5,31 +14,48 @@ import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Hel
import { CheckoutBootstrap } from './ContextBootstrap/CheckoutBootstrap';
import moduleStorage from './Helper/GooglePayStorage';
-( function ( { buttonConfig, ppcpConfig } ) {
+( function ( { buttonConfig, ppcpConfig = {} } ) {
const context = ppcpConfig.context;
- let manager;
+ function bootstrapPayButton() {
+ if ( ! buttonConfig || ! ppcpConfig ) {
+ return;
+ }
- const bootstrap = function () {
- manager = new GooglepayManager( buttonConfig, ppcpConfig );
+ const manager = new GooglepayManager( buttonConfig, ppcpConfig );
manager.init();
- if ( 'continuation' === context || 'checkout' === context ) {
- const checkoutBootstap = new CheckoutBootstrap( moduleStorage );
-
- checkoutBootstap.init();
- }
- };
-
- setupButtonEvents( function () {
- if ( manager ) {
+ 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 ( ! buttonConfig || ! ppcpConfig ) {
- // No PayPal buttons present on this page.
+ /*
+ * No PayPal buttons present on this page, but maybe a bootstrap module needs to be
+ * initialized. Run bootstrap without trying to load an SDK or payment configuration.
+ */
+ bootstrap();
+
return;
}
From 813f24da1cc717f09bf4a7437bb0c4b42d46a1e8 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Tue, 27 Aug 2024 12:46:15 +0200
Subject: [PATCH 17/17] =?UTF-8?q?=F0=9F=92=A1=20Update=20comment?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
modules/ppcp-googlepay/resources/js/boot.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/modules/ppcp-googlepay/resources/js/boot.js b/modules/ppcp-googlepay/resources/js/boot.js
index 666286ed4..fb9e8e313 100644
--- a/modules/ppcp-googlepay/resources/js/boot.js
+++ b/modules/ppcp-googlepay/resources/js/boot.js
@@ -52,7 +52,8 @@ import moduleStorage from './Helper/GooglePayStorage';
if ( ! buttonConfig || ! ppcpConfig ) {
/*
* No PayPal buttons present on this page, but maybe a bootstrap module needs to be
- * initialized. Run bootstrap without trying to load an SDK or payment configuration.
+ * initialized. Skip loading the SDK or gateway configuration, and directly initialize
+ * the module.
*/
bootstrap();