Get payee details without shipping callback

This commit is contained in:
Philipp Stracker 2024-08-16 15:41:05 +02:00
parent 01d20b85ee
commit 00e2959700
No known key found for this signature in database
2 changed files with 257 additions and 141 deletions

View file

@ -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 = () => { export const payerData = () => {
const payer = PayPalCommerceGateway.payer; const payer = window.PayPalCommerceGateway?.payer;
if ( ! payer ) { if ( ! payer ) {
return null; return null;
} }
const phone = const getElementValue = ( selector ) =>
document.querySelector( '#billing_phone' ) || document.querySelector( selector )?.value;
typeof payer.phone !== 'undefined'
? { // Initialize data with existing payer values.
phone_type: 'HOME', const data = {
phone_number: { email_address: payer.email_address,
national_number: document.querySelector( phone: payer.phone,
'#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,
name: { name: {
surname: document.querySelector( '#billing_last_name' ) surname: payer.name?.surname,
? document.querySelector( '#billing_last_name' ).value given_name: payer.name?.given_name,
: payer.name.surname,
given_name: document.querySelector( '#billing_first_name' )
? document.querySelector( '#billing_first_name' ).value
: payer.name.given_name,
}, },
address: { address: {
country_code: document.querySelector( '#billing_country' ) country_code: payer.address?.country_code,
? document.querySelector( '#billing_country' ).value address_line_1: payer.address?.address_line_1,
: payer.address.country_code, address_line_2: payer.address?.address_line_2,
address_line_1: document.querySelector( '#billing_address_1' ) admin_area_1: payer.address?.admin_area_1,
? document.querySelector( '#billing_address_1' ).value admin_area_2: payer.address?.admin_area_2,
: payer.address.address_line_1, postal_code: payer.address?.postal_code,
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,
}, },
}; };
if ( phone ) { // Update data with DOM values where they exist.
payerData.phone = phone; 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 );
} );
}; };

View file

@ -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 widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData'; import UpdatePaymentData from './Helper/UpdatePaymentData';
import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState'; import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
import { setPayerData } from '../../../ppcp-button/resources/js/modules/Helper/PayerData';
/** /**
* Plugin-specific styling. * 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 * @see https://developers.google.com/pay/api/web/reference/client
* @typedef {Object} PaymentsClient * @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} createButton - The convenience method is used to
* @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. * generate a Google Pay payment button styled with the latest Google Pay branding for
* @property {Function} loadPaymentData - This method presents a Google Pay payment sheet that allows selection of a payment method and optionally configured parameters * insertion into a webpage.
* @property {Function} onPaymentAuthorized - This method is called when a payment is authorized in the payment sheet. * @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest)
* @property {Function} onPaymentDataChanged - This method handles payment data changes in the payment sheet such as shipping address and shipping options. * 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 * @typedef {Object} TransactionInfo
* @property {string} currencyCode - Required. The ISO 4217 alphabetic currency code. * @property {string} currencyCode - Required. The ISO 4217 alphabetic currency code.
* @property {string} countryCode - Optional. required for EEA countries, * @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} transactionId - Optional. A unique ID that identifies a facilitation
* @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price used: * attempt. Highly encouraged for troubleshooting.
* @property {string} totalPrice - Required. Total monetary value of the transaction with an optional decimal precision of two decimal places. * @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price
* @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet (e.g. subtotals, sales taxes, shipping charges, discounts etc.). * used:
* @property {string} totalPriceLabel - Optional. Custom label for the total price within the display items. * @property {string} totalPrice - Required. Total monetary value of the transaction with an
* @property {string} checkoutOption - Optional. Affects the submit button text displayed in the Google Pay payment sheet. * 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 { class GooglepayButton extends PaymentButton {
@ -78,7 +91,7 @@ class GooglepayButton extends PaymentButton {
#paymentsClient = null; #paymentsClient = null;
/** /**
* Details about the processed transaction. * Details about the processed transaction, provided to the Google SDK.
* *
* @type {?TransactionInfo} * @type {?TransactionInfo}
*/ */
@ -388,12 +401,14 @@ class GooglepayButton extends PaymentButton {
const initiatePaymentRequest = () => { const initiatePaymentRequest = () => {
window.ppcpFundingSource = 'googlepay'; window.ppcpFundingSource = 'googlepay';
const paymentDataRequest = this.paymentDataRequest(); const paymentDataRequest = this.paymentDataRequest();
this.log( this.log(
'onButtonClick: paymentDataRequest', 'onButtonClick: paymentDataRequest',
paymentDataRequest, paymentDataRequest,
this.context this.context
); );
this.paymentsClient.loadPaymentData( paymentDataRequest );
return this.paymentsClient.loadPaymentData( paymentDataRequest );
}; };
const validateForm = () => { const validateForm = () => {
@ -434,28 +449,24 @@ class GooglepayButton extends PaymentButton {
apiVersionMinor: 0, apiVersionMinor: 0,
}; };
const googlePayConfig = this.googlePayConfig; const useShippingCallback = this.requiresShipping;
const paymentDataRequest = Object.assign( {}, baseRequest ); const callbackIntents = [ 'PAYMENT_AUTHORIZATION' ];
paymentDataRequest.allowedPaymentMethods =
googlePayConfig.allowedPaymentMethods;
paymentDataRequest.transactionInfo = this.transactionInfo;
paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo;
if ( this.requiresShipping ) { if ( useShippingCallback ) {
paymentDataRequest.callbackIntents = [ callbackIntents.push( 'SHIPPING_ADDRESS', 'SHIPPING_OPTION' );
'SHIPPING_ADDRESS',
'SHIPPING_OPTION',
'PAYMENT_AUTHORIZATION',
];
paymentDataRequest.shippingAddressRequired = true;
paymentDataRequest.shippingAddressParameters =
this.shippingAddressParameters();
paymentDataRequest.shippingOptionRequired = true;
} else {
paymentDataRequest.callbackIntents = [ 'PAYMENT_AUTHORIZATION' ];
} }
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 ) { onPaymentAuthorized( paymentData ) {
this.log( 'onPaymentAuthorized' ); this.log( 'onPaymentAuthorized', paymentData );
return this.processPayment( paymentData ); return this.processPayment( paymentData );
} }
async processPayment( paymentData ) { async processPayment( paymentData ) {
this.log( 'processPayment' ); this.log( 'processPayment' );
return new Promise( async ( resolve, reject ) => { const paymentError = ( reason ) => {
try { this.error( reason );
const id = await this.contextHandler.createOrder();
this.log( 'processPayment: createOrder', id ); return this.processPaymentResponse(
'ERROR',
'PAYMENT_AUTHORIZATION',
reason
);
};
const confirmOrderResponse = await widgetBuilder.paypal const checkPayPalApproval = async ( orderId ) => {
.Googlepay() const confirmOrderResponse = await widgetBuilder.paypal
.confirmOrder( { .Googlepay()
orderId: id, .confirmOrder( {
paymentMethodData: paymentData.paymentMethodData, orderId,
} ); paymentMethodData: paymentData.paymentMethodData,
} );
this.log( this.log( 'confirmOrder', confirmOrderResponse );
'processPayment: confirmOrder',
confirmOrderResponse
);
/** Capture the Order on the Server */ return 'APPROVE' === confirmOrderResponse?.status;
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 );
} ),
},
}
);
if ( ! approveFailed ) { const approveOrderServerSide = async ( orderID ) => {
resolve( this.processPaymentResponse( 'SUCCESS' ) ); let isApproved = true;
} else {
resolve( this.log( 'approveOrder', orderID );
this.processPaymentResponse(
'ERROR', await this.contextHandler.approveOrder(
'PAYMENT_AUTHORIZATION', { orderID },
'FAILED TO APPROVE' {
) restart: () =>
); new Promise( ( resolve ) => {
} isApproved = false;
} else { resolve();
resolve( } ),
this.processPaymentResponse( order: {
'ERROR', get: () =>
'PAYMENT_AUTHORIZATION', new Promise( ( resolve ) => {
'TRANSACTION FAILED' 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 ) { } catch ( err ) {
resolve( resolve( paymentError( err.message ) );
this.processPaymentResponse(
'ERROR',
'PAYMENT_AUTHORIZATION',
err.message
)
);
} }
} ); } );
} }