From 00e29597002c7aed3add7b863a97b0e7e392ab69 Mon Sep 17 00:00:00 2001
From: Philipp Stracker
Date: Fri, 16 Aug 2024 15:41:05 +0200
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Get=20payee=20details=20without=20s?=
=?UTF-8?q?hipping=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 ) );
}
} );
}