mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-04 08:47:23 +08:00
🔀 Merge branch 'trunk'
# Conflicts: # modules/ppcp-applepay/resources/js/ApplepayButton.js # modules/ppcp-applepay/resources/js/boot-admin.js # modules/ppcp-button/resources/js/modules/Preview/PreviewButtonManager.js # modules/ppcp-googlepay/resources/js/GooglepayButton.js # modules/ppcp-googlepay/resources/js/boot-admin.js
This commit is contained in:
commit
a128228c86
303 changed files with 21512 additions and 5378 deletions
|
@ -7,6 +7,7 @@ import Renderer from './modules/Renderer/Renderer';
|
|||
import ErrorHandler from './modules/ErrorHandler';
|
||||
import HostedFieldsRenderer from './modules/Renderer/HostedFieldsRenderer';
|
||||
import CardFieldsRenderer from './modules/Renderer/CardFieldsRenderer';
|
||||
import CardFieldsFreeTrialRenderer from './modules/Renderer/CardFieldsFreeTrialRenderer';
|
||||
import MessageRenderer from './modules/Renderer/MessageRenderer';
|
||||
import Spinner from './modules/Helper/Spinner';
|
||||
import {
|
||||
|
@ -215,12 +216,23 @@ const bootstrap = () => {
|
|||
spinner
|
||||
);
|
||||
if ( typeof paypal.CardFields !== 'undefined' ) {
|
||||
creditCardRenderer = new CardFieldsRenderer(
|
||||
PayPalCommerceGateway,
|
||||
errorHandler,
|
||||
spinner,
|
||||
onCardFieldsBeforeSubmit
|
||||
);
|
||||
if (
|
||||
PayPalCommerceGateway.is_free_trial_cart &&
|
||||
PayPalCommerceGateway.user?.has_wc_card_payment_tokens !== true
|
||||
) {
|
||||
creditCardRenderer = new CardFieldsFreeTrialRenderer(
|
||||
PayPalCommerceGateway,
|
||||
errorHandler,
|
||||
spinner
|
||||
);
|
||||
} else {
|
||||
creditCardRenderer = new CardFieldsRenderer(
|
||||
PayPalCommerceGateway,
|
||||
errorHandler,
|
||||
spinner,
|
||||
onCardFieldsBeforeSubmit
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const renderer = new Renderer(
|
||||
|
|
|
@ -173,61 +173,6 @@ class CheckoutActionHandler {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
addPaymentMethodConfiguration() {
|
||||
return {
|
||||
createVaultSetupToken: async () => {
|
||||
const response = await fetch(
|
||||
this.config.ajax.create_setup_token.endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
nonce: this.config.ajax.create_setup_token.nonce,
|
||||
} ),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if ( result.data.id ) {
|
||||
return result.data.id;
|
||||
}
|
||||
|
||||
console.error( result );
|
||||
},
|
||||
onApprove: async ( { vaultSetupToken } ) => {
|
||||
const response = await fetch(
|
||||
this.config.ajax.create_payment_token_for_guest.endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
nonce: this.config.ajax
|
||||
.create_payment_token_for_guest.nonce,
|
||||
vault_setup_token: vaultSetupToken,
|
||||
} ),
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if ( result.success === true ) {
|
||||
document.querySelector( '#place_order' ).click();
|
||||
return;
|
||||
}
|
||||
|
||||
console.error( result );
|
||||
},
|
||||
onError: ( error ) => {
|
||||
console.error( error );
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default CheckoutActionHandler;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* global PayPalCommerceGateway */
|
||||
|
||||
import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler';
|
||||
import { setVisible, setVisibleByClass } from '../Helper/Hiding';
|
||||
import {
|
||||
|
@ -7,6 +9,11 @@ import {
|
|||
PaymentMethods,
|
||||
} from '../Helper/CheckoutMethodState';
|
||||
import BootstrapHelper from '../Helper/BootstrapHelper';
|
||||
import { addPaymentMethodConfiguration } from '../../../../../ppcp-save-payment-methods/resources/js/Configuration';
|
||||
import {
|
||||
ButtonEvents,
|
||||
dispatchButtonEvent,
|
||||
} from '../Helper/PaymentButtonHelpers';
|
||||
|
||||
class CheckoutBootstap {
|
||||
constructor( gateway, renderer, spinner, errorHandler ) {
|
||||
|
@ -68,6 +75,7 @@ class CheckoutBootstap {
|
|||
jQuery( document.body ).on(
|
||||
'updated_checkout payment_method_selected',
|
||||
() => {
|
||||
this.invalidatePaymentMethods();
|
||||
this.updateUi();
|
||||
}
|
||||
);
|
||||
|
@ -160,7 +168,7 @@ class CheckoutBootstap {
|
|||
PayPalCommerceGateway.vault_v3_enabled
|
||||
) {
|
||||
this.renderer.render(
|
||||
actionHandler.addPaymentMethodConfiguration(),
|
||||
addPaymentMethodConfiguration( PayPalCommerceGateway ),
|
||||
{},
|
||||
actionHandler.configuration()
|
||||
);
|
||||
|
@ -174,6 +182,14 @@ class CheckoutBootstap {
|
|||
);
|
||||
}
|
||||
|
||||
invalidatePaymentMethods() {
|
||||
/**
|
||||
* Custom JS event to notify other modules that the payment button on the checkout page
|
||||
* has become irrelevant or invalid.
|
||||
*/
|
||||
dispatchButtonEvent( { event: ButtonEvents.INVALIDATE } );
|
||||
}
|
||||
|
||||
updateUi() {
|
||||
const currentPaymentMethod = getCurrentPaymentMethod();
|
||||
const isPaypal = currentPaymentMethod === PaymentMethods.PAYPAL;
|
||||
|
@ -181,9 +197,17 @@ class CheckoutBootstap {
|
|||
const isSeparateButtonGateway = [ PaymentMethods.CARD_BUTTON ].includes(
|
||||
currentPaymentMethod
|
||||
);
|
||||
const isGooglePayMethod =
|
||||
currentPaymentMethod === PaymentMethods.GOOGLEPAY;
|
||||
const isApplePayMethod =
|
||||
currentPaymentMethod === PaymentMethods.APPLEPAY;
|
||||
const isSavedCard = isCard && isSavedCardSelected();
|
||||
const isNotOurGateway =
|
||||
! isPaypal && ! isCard && ! isSeparateButtonGateway;
|
||||
! isPaypal &&
|
||||
! isCard &&
|
||||
! isSeparateButtonGateway &&
|
||||
! isGooglePayMethod &&
|
||||
! isApplePayMethod;
|
||||
const isFreeTrial = PayPalCommerceGateway.is_free_trial_cart;
|
||||
const hasVaultedPaypal =
|
||||
PayPalCommerceGateway.vaulted_paypal_email !== '';
|
||||
|
@ -227,7 +251,20 @@ class CheckoutBootstap {
|
|||
}
|
||||
}
|
||||
|
||||
jQuery( document.body ).trigger( 'ppcp_checkout_rendered' );
|
||||
/**
|
||||
* Custom JS event that is observed by the relevant payment gateway.
|
||||
*
|
||||
* Dynamic part of the event name is the payment method ID, for example
|
||||
* "ppcp-credit-card-gateway" or "ppcp-googlepay"
|
||||
*/
|
||||
dispatchButtonEvent( {
|
||||
event: ButtonEvents.RENDER,
|
||||
paymentMethod: currentPaymentMethod,
|
||||
} );
|
||||
|
||||
setVisible( '#ppc-button-ppcp-applepay', isApplePayMethod );
|
||||
|
||||
document.body.dispatchEvent( new Event( 'ppcp_checkout_rendered' ) );
|
||||
}
|
||||
|
||||
shouldShowMessages() {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { getPlanIdFromVariation } from '../Helper/Subscriptions';
|
|||
import SimulateCart from '../Helper/SimulateCart';
|
||||
import { strRemoveWord, strAddWord, throttle } from '../Helper/Utils';
|
||||
import merge from 'deepmerge';
|
||||
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
|
||||
|
||||
class SingleProductBootstap {
|
||||
constructor( gateway, renderer, errorHandler ) {
|
||||
|
@ -20,9 +21,13 @@ class SingleProductBootstap {
|
|||
|
||||
// Prevent simulate cart being called too many times in a burst.
|
||||
this.simulateCartThrottled = throttle(
|
||||
this.simulateCart,
|
||||
this.simulateCart.bind( this ),
|
||||
this.gateway.simulate_cart.throttling || 5000
|
||||
);
|
||||
this.debouncedHandleChange = debounce(
|
||||
this.handleChange.bind( this ),
|
||||
100
|
||||
);
|
||||
|
||||
this.renderer.onButtonsInit(
|
||||
this.gateway.button.wrapper,
|
||||
|
@ -74,7 +79,7 @@ class SingleProductBootstap {
|
|||
}
|
||||
|
||||
jQuery( document ).on( 'change', this.formSelector, () => {
|
||||
this.handleChange();
|
||||
this.debouncedHandleChange();
|
||||
} );
|
||||
this.mutationObserver.observe( form, {
|
||||
childList: true,
|
||||
|
|
|
@ -26,8 +26,11 @@ export function setupButtonEvents( refresh ) {
|
|||
document.addEventListener( REFRESH_BUTTON_EVENT, debouncedRefresh );
|
||||
|
||||
// Listen for cart and checkout update events.
|
||||
document.body.addEventListener( 'updated_cart_totals', debouncedRefresh );
|
||||
document.body.addEventListener( 'updated_checkout', debouncedRefresh );
|
||||
// Note: we need jQuery here, because WooCommerce uses jQuery.trigger() to dispatch the events.
|
||||
window
|
||||
.jQuery( 'body' )
|
||||
.on( 'updated_cart_totals', debouncedRefresh )
|
||||
.on( 'updated_checkout', debouncedRefresh );
|
||||
|
||||
// Use setTimeout for fragment events to avoid unnecessary refresh on initial render.
|
||||
setTimeout( () => {
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
export const cardFieldStyles = ( field ) => {
|
||||
const allowedProperties = [
|
||||
'appearance',
|
||||
'color',
|
||||
'direction',
|
||||
'font',
|
||||
'font-family',
|
||||
'font-size',
|
||||
'font-size-adjust',
|
||||
'font-stretch',
|
||||
'font-style',
|
||||
'font-variant',
|
||||
'font-variant-alternates',
|
||||
'font-variant-caps',
|
||||
'font-variant-east-asian',
|
||||
'font-variant-ligatures',
|
||||
'font-variant-numeric',
|
||||
'font-weight',
|
||||
'letter-spacing',
|
||||
'line-height',
|
||||
'opacity',
|
||||
'outline',
|
||||
'padding',
|
||||
'padding-bottom',
|
||||
'padding-left',
|
||||
'padding-right',
|
||||
'padding-top',
|
||||
'text-shadow',
|
||||
'transition',
|
||||
'-moz-appearance',
|
||||
'-moz-osx-font-smoothing',
|
||||
'-moz-tap-highlight-color',
|
||||
'-moz-transition',
|
||||
'-webkit-appearance',
|
||||
'-webkit-osx-font-smoothing',
|
||||
'-webkit-tap-highlight-color',
|
||||
'-webkit-transition',
|
||||
];
|
||||
|
||||
const stylesRaw = window.getComputedStyle( field );
|
||||
const styles = {};
|
||||
Object.values( stylesRaw ).forEach( ( prop ) => {
|
||||
if ( ! stylesRaw[ prop ] || ! allowedProperties.includes( prop ) ) {
|
||||
return;
|
||||
}
|
||||
styles[ prop ] = '' + stylesRaw[ prop ];
|
||||
} );
|
||||
|
||||
return styles;
|
||||
};
|
|
@ -3,6 +3,32 @@ export const PaymentMethods = {
|
|||
CARDS: 'ppcp-credit-card-gateway',
|
||||
OXXO: 'ppcp-oxxo-gateway',
|
||||
CARD_BUTTON: 'ppcp-card-button-gateway',
|
||||
GOOGLEPAY: 'ppcp-googlepay',
|
||||
APPLEPAY: 'ppcp-applepay',
|
||||
};
|
||||
|
||||
/**
|
||||
* List of valid context values that the button can have.
|
||||
*
|
||||
* The "context" describes the placement or page where a payment button might be displayed.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
export const PaymentContext = {
|
||||
Cart: 'cart', // Classic cart.
|
||||
Checkout: 'checkout', // Classic checkout.
|
||||
BlockCart: 'cart-block', // Block cart.
|
||||
BlockCheckout: 'checkout-block', // Block checkout.
|
||||
Product: 'product', // Single product page.
|
||||
MiniCart: 'mini-cart', // Mini cart available on all pages except checkout & cart.
|
||||
PayNow: 'pay-now', // Pay for order, via admin generated link.
|
||||
Preview: 'preview', // Layout preview on settings page.
|
||||
|
||||
// Contexts that use blocks to render payment methods.
|
||||
Blocks: [ 'cart-block', 'checkout-block' ],
|
||||
|
||||
// Contexts that display "classic" payment gateways.
|
||||
Gateways: [ 'checkout', 'pay-now' ],
|
||||
};
|
||||
|
||||
export const ORDER_BUTTON_SELECTOR = '#place_order';
|
||||
|
|
179
modules/ppcp-button/resources/js/modules/Helper/LocalStorage.js
Normal file
179
modules/ppcp-button/resources/js/modules/Helper/LocalStorage.js
Normal file
|
@ -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 );
|
||||
}
|
||||
}
|
|
@ -1,59 +1,196 @@
|
|||
export const payerData = () => {
|
||||
const payer = PayPalCommerceGateway.payer;
|
||||
/**
|
||||
* 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' ],
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const data = {};
|
||||
|
||||
Object.entries( FIELD_MAP ).forEach( ( [ selector, path ] ) => {
|
||||
const value = getElementValue( selector );
|
||||
if ( value ) {
|
||||
setNestedValue( data, path, value );
|
||||
}
|
||||
} );
|
||||
|
||||
if ( data.phone && 'string' === typeof data.phone ) {
|
||||
data.phone = {
|
||||
phone_type: 'HOME',
|
||||
phone_number: { national_number: data.phone },
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function setCheckoutBillingDetails( payer ) {
|
||||
const setValue = ( path, field, value ) => {
|
||||
if ( null === value || undefined === value || ! field ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'phone' === path[ 0 ] && 'object' === typeof value ) {
|
||||
value = value.phone_number?.national_number;
|
||||
}
|
||||
|
||||
field.value = value;
|
||||
};
|
||||
|
||||
const getNestedValue = ( obj, path ) =>
|
||||
path.reduce( ( current, key ) => current?.[ key ], obj );
|
||||
|
||||
Object.entries( FIELD_MAP ).forEach( ( [ selector, path ] ) => {
|
||||
const value = getNestedValue( payer, path );
|
||||
const element = document.querySelector( selector );
|
||||
|
||||
setValue( path, element, value );
|
||||
} );
|
||||
}
|
||||
|
||||
export function getWooCommerceCustomerDetails() {
|
||||
// Populated on server-side with details about the current WooCommerce customer.
|
||||
return window?.PayPalCommerceGateway?.payer;
|
||||
}
|
||||
|
||||
export function getSessionBillingDetails() {
|
||||
// Populated by JS via `setSessionBillingDetails()`
|
||||
return window._PpcpPayerSessionDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 setSessionBillingDetails( details ) {
|
||||
if ( ! details || 'object' !== typeof details ) {
|
||||
return;
|
||||
}
|
||||
|
||||
window._PpcpPayerSessionDetails = normalizePayerDetails( details );
|
||||
}
|
||||
|
||||
export function payerData() {
|
||||
const payer = getWooCommerceCustomerDetails() ?? getSessionBillingDetails();
|
||||
|
||||
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,
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
};
|
||||
const formData = getCheckoutBillingDetails();
|
||||
|
||||
if ( phone ) {
|
||||
payerData.phone = phone;
|
||||
if ( formData ) {
|
||||
return mergePayerDetails( payer, formData );
|
||||
}
|
||||
return payerData;
|
||||
};
|
||||
|
||||
return normalizePayerDetails( payer );
|
||||
}
|
||||
|
||||
export function setPayerData( payerDetails, updateCheckoutForm = false ) {
|
||||
setSessionBillingDetails( payerDetails );
|
||||
|
||||
if ( updateCheckoutForm ) {
|
||||
setCheckoutBillingDetails( payerDetails );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* Helper function used by PaymentButton instances.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
/**
|
||||
* Collection of recognized event names for payment button events.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
export const ButtonEvents = Object.freeze( {
|
||||
INVALIDATE: 'ppcp_invalidate_methods',
|
||||
RENDER: 'ppcp_render_method',
|
||||
REDRAW: 'ppcp_redraw_method',
|
||||
} );
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} defaultId - Default wrapper ID.
|
||||
* @param {string} miniCartId - Wrapper inside the mini-cart.
|
||||
* @param {string} smartButtonId - ID of the smart button wrapper.
|
||||
* @param {string} blockId - Block wrapper ID (express checkout, block cart).
|
||||
* @param {string} gatewayId - Gateway wrapper ID (classic checkout).
|
||||
* @return {{MiniCart, Gateway, Block, SmartButton, Default}} List of all wrapper IDs, by context.
|
||||
*/
|
||||
export function combineWrapperIds(
|
||||
defaultId = '',
|
||||
miniCartId = '',
|
||||
smartButtonId = '',
|
||||
blockId = '',
|
||||
gatewayId = ''
|
||||
) {
|
||||
const sanitize = ( id ) => id.replace( /^#/, '' );
|
||||
|
||||
return {
|
||||
Default: sanitize( defaultId ),
|
||||
SmartButton: sanitize( smartButtonId ),
|
||||
Block: sanitize( blockId ),
|
||||
Gateway: sanitize( gatewayId ),
|
||||
MiniCart: sanitize( miniCartId ),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns full payment button styles by combining the global ppcpConfig with
|
||||
* payment-method-specific styling provided via buttonConfig.
|
||||
*
|
||||
* @param {Object} ppcpConfig - Global plugin configuration.
|
||||
* @param {Object} buttonConfig - Payment method specific configuration.
|
||||
* @return {{MiniCart: (*), Default: (*)}} Combined styles, separated by context.
|
||||
*/
|
||||
export function combineStyles( ppcpConfig, buttonConfig ) {
|
||||
return {
|
||||
Default: {
|
||||
...ppcpConfig.style,
|
||||
...buttonConfig.style,
|
||||
},
|
||||
MiniCart: {
|
||||
...ppcpConfig.mini_cart_style,
|
||||
...buttonConfig.mini_cart_style,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if the given event name is a valid Payment Button event.
|
||||
*
|
||||
* @param {string} event - The event name to verify.
|
||||
* @return {boolean} True, if the event name is valid.
|
||||
*/
|
||||
export function isValidButtonEvent( event ) {
|
||||
const buttonEventValues = Object.values( ButtonEvents );
|
||||
|
||||
return buttonEventValues.includes( event );
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a payment button event.
|
||||
*
|
||||
* @param {Object} options - The options for dispatching the event.
|
||||
* @param {string} options.event - Event to dispatch.
|
||||
* @param {string} [options.paymentMethod] - Optional. Name of payment method, to target a specific button only.
|
||||
* @throws {Error} Throws an error if the event is invalid.
|
||||
*/
|
||||
export function dispatchButtonEvent( { event, paymentMethod = '' } ) {
|
||||
if ( ! isValidButtonEvent( event ) ) {
|
||||
throw new Error( `Invalid event: ${ event }` );
|
||||
}
|
||||
|
||||
const fullEventName = paymentMethod
|
||||
? `${ event }-${ paymentMethod }`
|
||||
: event;
|
||||
|
||||
document.body.dispatchEvent( new Event( fullEventName ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener for the provided button event.
|
||||
*
|
||||
* @param {Object} options - The options for the event listener.
|
||||
* @param {string} options.event - Event to observe.
|
||||
* @param {string} [options.paymentMethod] - The payment method name (optional).
|
||||
* @param {Function} options.callback - The callback function to execute when the event is triggered.
|
||||
* @throws {Error} Throws an error if the event is invalid.
|
||||
*/
|
||||
export function observeButtonEvent( { event, paymentMethod = '', callback } ) {
|
||||
if ( ! isValidButtonEvent( event ) ) {
|
||||
throw new Error( `Invalid event: ${ event }` );
|
||||
}
|
||||
|
||||
const fullEventName = paymentMethod
|
||||
? `${ event }-${ paymentMethod }`
|
||||
: event;
|
||||
|
||||
document.body.addEventListener( fullEventName, callback );
|
||||
}
|
|
@ -71,7 +71,10 @@ export const loadPaypalScript = ( config, onLoaded, onError = null ) => {
|
|||
}
|
||||
|
||||
// Load PayPal script for special case with data-client-token
|
||||
if ( config.data_client_id?.set_attribute ) {
|
||||
if (
|
||||
config.data_client_id?.set_attribute &&
|
||||
config.vault_v3_enabled !== '1'
|
||||
) {
|
||||
dataClientIdAttributeHandler(
|
||||
scriptOptions,
|
||||
config.data_client_id,
|
||||
|
|
|
@ -1,34 +1,49 @@
|
|||
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();
|
||||
} )
|
||||
.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;
|
||||
} );
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import { loadCustomScript } from '@paypal/paypal-js';
|
||||
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
|
||||
import widgetBuilder from '../Renderer/WidgetBuilder';
|
||||
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
|
||||
import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger';
|
||||
import DummyPreviewButton from './DummyPreviewButton';
|
||||
|
||||
/**
|
||||
* Manages all PreviewButton instances of a certain payment method on the page.
|
||||
*/
|
||||
class PreviewButtonManager {
|
||||
/**
|
||||
* @type {ConsoleLogger}
|
||||
*/
|
||||
#logger;
|
||||
|
||||
/**
|
||||
* Resolves the promise.
|
||||
* Used by `this.boostrap()` to process enqueued initialization logic.
|
||||
|
@ -40,6 +46,9 @@ class PreviewButtonManager {
|
|||
this.apiConfig = null;
|
||||
this.apiError = '';
|
||||
|
||||
this.#logger = new ConsoleLogger( this.methodName, 'preview-manager' );
|
||||
this.#logger.enabled = true; // Manually set this to true for development.
|
||||
|
||||
this.#onInit = new Promise( ( resolve ) => {
|
||||
this.#onInitResolver = resolve;
|
||||
} );
|
||||
|
@ -69,9 +78,11 @@ class PreviewButtonManager {
|
|||
* Responsible for fetching and returning the PayPal configuration object for this payment
|
||||
* method.
|
||||
*
|
||||
* @abstract
|
||||
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
|
||||
* @return {Promise<{}>}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async fetchConfig( payPal ) {
|
||||
throw new Error(
|
||||
'The "fetchConfig" method must be implemented by the derived class'
|
||||
|
@ -82,9 +93,11 @@ class PreviewButtonManager {
|
|||
* Protected method that needs to be implemented by the derived class.
|
||||
* This method is responsible for creating a new PreviewButton instance and returning it.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} wrapperId - CSS ID of the wrapper element.
|
||||
* @return {PreviewButton}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
createButtonInstance( wrapperId ) {
|
||||
throw new Error(
|
||||
'The "createButtonInstance" method must be implemented by the derived class'
|
||||
|
@ -97,7 +110,7 @@ class PreviewButtonManager {
|
|||
*
|
||||
* This dummy is only visible on the admin side, and not rendered on the front-end.
|
||||
*
|
||||
* @param wrapperId
|
||||
* @param {string} wrapperId
|
||||
* @return {any}
|
||||
*/
|
||||
createDummyButtonInstance( wrapperId ) {
|
||||
|
@ -124,13 +137,24 @@ class PreviewButtonManager {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a debug message to the console, with a module-specific prefix.
|
||||
*
|
||||
* @param {string} message - Log message.
|
||||
* @param {...any} args - Optional. Additional args to output.
|
||||
*/
|
||||
log( message, ...args ) {
|
||||
this.#logger.log( message, ...args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Output an error message to the console, with a module-specific prefix.
|
||||
* @param message
|
||||
* @param {...any} args
|
||||
*
|
||||
* @param {string} message - Log message.
|
||||
* @param {...any} args - Optional. Additional args to output.
|
||||
*/
|
||||
error( message, ...args ) {
|
||||
console.error( `${ this.methodName } ${ message }`, ...args );
|
||||
this.#logger.error( message, ...args );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -238,21 +262,21 @@ class PreviewButtonManager {
|
|||
}
|
||||
|
||||
if ( ! this.shouldInsertPreviewButton( id ) ) {
|
||||
this.log( 'Skip preview rendering for this preview-box', id );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! this.buttons[ id ] ) {
|
||||
this._addButton( id, ppcpConfig );
|
||||
} else {
|
||||
// This is a debounced method, that fires after 100ms.
|
||||
this._configureAllButtons( ppcpConfig );
|
||||
this._configureButton( id, ppcpConfig );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the preview box supports the current button.
|
||||
*
|
||||
* When this function returns false, this manager instance does not create a new preview button.
|
||||
* E.g. "Should the current preview-box display Google Pay buttons?"
|
||||
*
|
||||
* @param {string} previewId - ID of the inner preview box container.
|
||||
* @return {boolean} True if the box is eligible for the preview button, false otherwise.
|
||||
|
@ -267,10 +291,14 @@ class PreviewButtonManager {
|
|||
|
||||
/**
|
||||
* Applies a new configuration to an existing preview button.
|
||||
*
|
||||
* @private
|
||||
* @param id
|
||||
* @param ppcpConfig
|
||||
*/
|
||||
_configureButton( id, ppcpConfig ) {
|
||||
this.log( 'configureButton', id, ppcpConfig );
|
||||
|
||||
this.buttons[ id ]
|
||||
.setDynamic( this.isDynamic() )
|
||||
.setPpcpConfig( ppcpConfig )
|
||||
|
@ -279,9 +307,13 @@ class PreviewButtonManager {
|
|||
|
||||
/**
|
||||
* Apples the provided configuration to all existing preview buttons.
|
||||
* @param ppcpConfig
|
||||
*
|
||||
* @private
|
||||
* @param ppcpConfig - The new styling to use for the preview buttons.
|
||||
*/
|
||||
_configureAllButtons( ppcpConfig ) {
|
||||
this.log( 'configureAllButtons', ppcpConfig );
|
||||
|
||||
Object.entries( this.buttons ).forEach( ( [ id, button ] ) => {
|
||||
const limitWrapper = ppcpConfig.button?.wrapper;
|
||||
|
||||
|
@ -309,12 +341,18 @@ class PreviewButtonManager {
|
|||
|
||||
/**
|
||||
* Creates a new preview button, that is rendered once the bootstrapping Promise resolves.
|
||||
* @param id
|
||||
* @param ppcpConfig
|
||||
*
|
||||
* @private
|
||||
* @param id - The button to add.
|
||||
* @param ppcpConfig - The styling to apply to the preview button.
|
||||
*/
|
||||
_addButton( id, ppcpConfig ) {
|
||||
this.log( 'addButton', id, ppcpConfig );
|
||||
|
||||
const createButton = () => {
|
||||
if ( ! this.buttons[ id ] ) {
|
||||
this.log( 'createButton.new', id );
|
||||
|
||||
let newInst;
|
||||
|
||||
if ( this.apiConfig && 'object' === typeof this.apiConfig ) {
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
import { show } from '../Helper/Hiding';
|
||||
import { renderFields } from '../../../../../ppcp-card-fields/resources/js/Render';
|
||||
import {
|
||||
addPaymentMethodConfiguration,
|
||||
cardFieldsConfiguration,
|
||||
} from '../../../../../ppcp-save-payment-methods/resources/js/Configuration';
|
||||
|
||||
class CardFieldsFreeTrialRenderer {
|
||||
constructor( defaultConfig, errorHandler, spinner ) {
|
||||
this.defaultConfig = defaultConfig;
|
||||
this.errorHandler = errorHandler;
|
||||
this.spinner = spinner;
|
||||
}
|
||||
|
||||
render( wrapper, contextConfig ) {
|
||||
if (
|
||||
( this.defaultConfig.context !== 'checkout' &&
|
||||
this.defaultConfig.context !== 'pay-now' ) ||
|
||||
wrapper === null ||
|
||||
document.querySelector( wrapper ) === null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonSelector = wrapper + ' button';
|
||||
|
||||
const gateWayBox = document.querySelector(
|
||||
'.payment_box.payment_method_ppcp-credit-card-gateway'
|
||||
);
|
||||
if ( ! gateWayBox ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldDisplayStyle = gateWayBox.style.display;
|
||||
gateWayBox.style.display = 'block';
|
||||
|
||||
const hideDccGateway = document.querySelector( '#ppcp-hide-dcc' );
|
||||
if ( hideDccGateway ) {
|
||||
hideDccGateway.parentNode.removeChild( hideDccGateway );
|
||||
}
|
||||
|
||||
this.errorHandler.clear();
|
||||
|
||||
let cardFields = paypal.CardFields(
|
||||
addPaymentMethodConfiguration( this.defaultConfig )
|
||||
);
|
||||
if ( this.defaultConfig.user.is_logged ) {
|
||||
cardFields = paypal.CardFields(
|
||||
cardFieldsConfiguration( this.defaultConfig, this.errorHandler )
|
||||
);
|
||||
}
|
||||
|
||||
if ( cardFields.isEligible() ) {
|
||||
renderFields( cardFields );
|
||||
}
|
||||
|
||||
gateWayBox.style.display = oldDisplayStyle;
|
||||
|
||||
show( buttonSelector );
|
||||
|
||||
if ( this.defaultConfig.cart_contains_subscription ) {
|
||||
const saveToAccount = document.querySelector(
|
||||
'#wc-ppcp-credit-card-gateway-new-payment-method'
|
||||
);
|
||||
if ( saveToAccount ) {
|
||||
saveToAccount.checked = true;
|
||||
saveToAccount.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector( buttonSelector )
|
||||
?.addEventListener( 'click', ( event ) => {
|
||||
event.preventDefault();
|
||||
this.spinner.block();
|
||||
this.errorHandler.clear();
|
||||
|
||||
cardFields.submit().catch( ( error ) => {
|
||||
console.error( error );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
disableFields() {}
|
||||
enableFields() {}
|
||||
}
|
||||
|
||||
export default CardFieldsFreeTrialRenderer;
|
|
@ -1,5 +1,5 @@
|
|||
import { show } from '../Helper/Hiding';
|
||||
import { cardFieldStyles } from '../Helper/CardFieldsHelper';
|
||||
import { renderFields } from '../../../../../ppcp-card-fields/resources/js/Render';
|
||||
|
||||
class CardFieldsRenderer {
|
||||
constructor(
|
||||
|
@ -45,7 +45,7 @@ class CardFieldsRenderer {
|
|||
hideDccGateway.parentNode.removeChild( hideDccGateway );
|
||||
}
|
||||
|
||||
const cardField = paypal.CardFields( {
|
||||
const cardFields = paypal.CardFields( {
|
||||
createOrder: contextConfig.createOrder,
|
||||
onApprove( data ) {
|
||||
return contextConfig.onApprove( data );
|
||||
|
@ -56,79 +56,8 @@ class CardFieldsRenderer {
|
|||
},
|
||||
} );
|
||||
|
||||
if ( cardField.isEligible() ) {
|
||||
const nameField = document.getElementById(
|
||||
'ppcp-credit-card-gateway-card-name'
|
||||
);
|
||||
if ( nameField ) {
|
||||
const styles = cardFieldStyles( nameField );
|
||||
const fieldOptions = {
|
||||
style: { input: styles },
|
||||
};
|
||||
if ( nameField.getAttribute( 'placeholder' ) ) {
|
||||
fieldOptions.placeholder =
|
||||
nameField.getAttribute( 'placeholder' );
|
||||
}
|
||||
cardField
|
||||
.NameField( fieldOptions )
|
||||
.render( nameField.parentNode );
|
||||
nameField.remove();
|
||||
}
|
||||
|
||||
const numberField = document.getElementById(
|
||||
'ppcp-credit-card-gateway-card-number'
|
||||
);
|
||||
if ( numberField ) {
|
||||
const styles = cardFieldStyles( numberField );
|
||||
const fieldOptions = {
|
||||
style: { input: styles },
|
||||
};
|
||||
if ( numberField.getAttribute( 'placeholder' ) ) {
|
||||
fieldOptions.placeholder =
|
||||
numberField.getAttribute( 'placeholder' );
|
||||
}
|
||||
cardField
|
||||
.NumberField( fieldOptions )
|
||||
.render( numberField.parentNode );
|
||||
numberField.remove();
|
||||
}
|
||||
|
||||
const expiryField = document.getElementById(
|
||||
'ppcp-credit-card-gateway-card-expiry'
|
||||
);
|
||||
if ( expiryField ) {
|
||||
const styles = cardFieldStyles( expiryField );
|
||||
const fieldOptions = {
|
||||
style: { input: styles },
|
||||
};
|
||||
if ( expiryField.getAttribute( 'placeholder' ) ) {
|
||||
fieldOptions.placeholder =
|
||||
expiryField.getAttribute( 'placeholder' );
|
||||
}
|
||||
cardField
|
||||
.ExpiryField( fieldOptions )
|
||||
.render( expiryField.parentNode );
|
||||
expiryField.remove();
|
||||
}
|
||||
|
||||
const cvvField = document.getElementById(
|
||||
'ppcp-credit-card-gateway-card-cvc'
|
||||
);
|
||||
if ( cvvField ) {
|
||||
const styles = cardFieldStyles( cvvField );
|
||||
const fieldOptions = {
|
||||
style: { input: styles },
|
||||
};
|
||||
if ( cvvField.getAttribute( 'placeholder' ) ) {
|
||||
fieldOptions.placeholder =
|
||||
cvvField.getAttribute( 'placeholder' );
|
||||
}
|
||||
cardField
|
||||
.CVVField( fieldOptions )
|
||||
.render( cvvField.parentNode );
|
||||
cvvField.remove();
|
||||
}
|
||||
|
||||
if ( cardFields.isEligible() ) {
|
||||
renderFields( cardFields );
|
||||
document.dispatchEvent( new CustomEvent( 'hosted_fields_loaded' ) );
|
||||
}
|
||||
|
||||
|
@ -169,7 +98,7 @@ class CardFieldsRenderer {
|
|||
return;
|
||||
}
|
||||
|
||||
cardField.submit().catch( ( error ) => {
|
||||
cardFields.submit().catch( ( error ) => {
|
||||
this.spinner.unblock();
|
||||
console.error( error );
|
||||
this.errorHandler.message(
|
||||
|
|
|
@ -0,0 +1,865 @@
|
|||
import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger';
|
||||
import { apmButtonsInit } from '../Helper/ApmButtons';
|
||||
import {
|
||||
getCurrentPaymentMethod,
|
||||
PaymentContext,
|
||||
PaymentMethods,
|
||||
} from '../Helper/CheckoutMethodState';
|
||||
import {
|
||||
ButtonEvents,
|
||||
dispatchButtonEvent,
|
||||
observeButtonEvent,
|
||||
} from '../Helper/PaymentButtonHelpers';
|
||||
|
||||
/**
|
||||
* Collection of all available styling options for this button.
|
||||
*
|
||||
* @typedef {Object} StylesCollection
|
||||
* @property {string} Default - Default button styling.
|
||||
* @property {string} MiniCart - Styles for mini-cart button.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Collection of all available wrapper IDs that are possible for the button.
|
||||
*
|
||||
* @typedef {Object} WrapperCollection
|
||||
* @property {string} Default - Default button wrapper.
|
||||
* @property {string} Gateway - Wrapper for separate gateway.
|
||||
* @property {string} Block - Wrapper for block checkout button.
|
||||
* @property {string} MiniCart - Wrapper for mini-cart button.
|
||||
* @property {string} SmartButton - Wrapper for smart button container.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds the provided PaymentButton instance to a global payment-button collection.
|
||||
*
|
||||
* This is debugging logic that should not be used on a production site.
|
||||
*
|
||||
* @param {string} methodName - Used to group the buttons.
|
||||
* @param {PaymentButton} button - Appended to the button collection.
|
||||
*/
|
||||
const addToDebuggingCollection = ( methodName, button ) => {
|
||||
window.ppcpPaymentButtonList = window.ppcpPaymentButtonList || {};
|
||||
|
||||
const collection = window.ppcpPaymentButtonList;
|
||||
|
||||
collection[ methodName ] = collection[ methodName ] || [];
|
||||
collection[ methodName ].push( button );
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides a context-independent instance Map for `PaymentButton` components.
|
||||
*
|
||||
* This function addresses a potential issue in multi-context environments, such as pages using
|
||||
* Block-components. In these scenarios, multiple React execution contexts can lead to duplicate
|
||||
* `PaymentButton` instances. To prevent this, we store instances in a `Map` that is bound to the
|
||||
* document's `body` (the rendering context) rather than to individual React components
|
||||
* (execution contexts).
|
||||
*
|
||||
* The `Map` is created as a non-enumerable, non-writable, and non-configurable property of
|
||||
* `document.body` to ensure its integrity and prevent accidental modifications.
|
||||
*
|
||||
* @return {Map<any, any>} A Map containing all `PaymentButton` instances for the current page.
|
||||
*/
|
||||
const getInstances = () => {
|
||||
const collectionKey = '__ppcpPBInstances';
|
||||
|
||||
if ( ! document.body[ collectionKey ] ) {
|
||||
Object.defineProperty( document.body, collectionKey, {
|
||||
value: new Map(),
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
} );
|
||||
}
|
||||
|
||||
return document.body[ collectionKey ];
|
||||
};
|
||||
|
||||
/**
|
||||
* Base class for APM payment buttons, like GooglePay and ApplePay.
|
||||
*
|
||||
* This class is not intended for the PayPal button.
|
||||
*/
|
||||
export default class PaymentButton {
|
||||
/**
|
||||
* Defines the implemented payment method.
|
||||
*
|
||||
* Used to identify and address the button internally.
|
||||
* Overwrite this in the derived class.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
static methodId = 'generic';
|
||||
|
||||
/**
|
||||
* CSS class that is added to the payment button wrapper.
|
||||
*
|
||||
* Overwrite this in the derived class.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
static cssClass = '';
|
||||
|
||||
/**
|
||||
* @type {ConsoleLogger}
|
||||
*/
|
||||
#logger;
|
||||
|
||||
/**
|
||||
* Whether the payment button is initialized.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
#isInitialized = false;
|
||||
|
||||
/**
|
||||
* Whether the one-time initialization of the payment gateway is complete.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
#gatewayInitialized = false;
|
||||
|
||||
/**
|
||||
* The button's context.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
#context;
|
||||
|
||||
/**
|
||||
* Object containing the IDs of all possible wrapper elements that might contain this
|
||||
* button; only one wrapper is relevant, depending on the value of the context.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
#wrappers;
|
||||
|
||||
/**
|
||||
* @type {StylesCollection}
|
||||
*/
|
||||
#styles;
|
||||
|
||||
/**
|
||||
* Keeps track of CSS classes that were added to the wrapper element.
|
||||
* We use this list to remove CSS classes that we've added, e.g. to change shape from
|
||||
* pill to rect in the preview.
|
||||
*
|
||||
* @type {string[]}
|
||||
*/
|
||||
#appliedClasses = [];
|
||||
|
||||
/**
|
||||
* APM relevant configuration; e.g., configuration of the GooglePay button.
|
||||
*/
|
||||
#buttonConfig;
|
||||
|
||||
/**
|
||||
* Plugin-wide configuration; i.e., PayPal client ID, shop currency, etc.
|
||||
*/
|
||||
#ppcpConfig;
|
||||
|
||||
/**
|
||||
* A variation of a context bootstrap handler.
|
||||
*/
|
||||
#externalHandler;
|
||||
|
||||
/**
|
||||
* A variation of a context handler object, like CheckoutHandler.
|
||||
* This handler provides a standardized interface for certain standardized checks and actions.
|
||||
*/
|
||||
#contextHandler;
|
||||
|
||||
/**
|
||||
* Whether the current browser/website support the payment method.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
#isEligible = false;
|
||||
|
||||
/**
|
||||
* Whether this button is visible. Modified by `show()` and `hide()`
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
#isVisible = true;
|
||||
|
||||
/**
|
||||
* The currently visible payment button.
|
||||
*
|
||||
* @see {PaymentButton.insertButton}
|
||||
* @type {HTMLElement|null}
|
||||
*/
|
||||
#button = null;
|
||||
|
||||
/**
|
||||
* Factory method to create a new PaymentButton while limiting a single instance per context.
|
||||
*
|
||||
* @param {string} context - Button context name.
|
||||
* @param {unknown} externalHandler - Handler object.
|
||||
* @param {Object} buttonConfig - Payment button specific configuration.
|
||||
* @param {Object} ppcpConfig - Plugin wide configuration object.
|
||||
* @param {unknown} contextHandler - Handler object.
|
||||
* @return {PaymentButton} The button instance.
|
||||
*/
|
||||
static createButton(
|
||||
context,
|
||||
externalHandler,
|
||||
buttonConfig,
|
||||
ppcpConfig,
|
||||
contextHandler
|
||||
) {
|
||||
const buttonInstances = getInstances();
|
||||
const instanceKey = `${ this.methodId }.${ context }`;
|
||||
|
||||
if ( ! buttonInstances.has( instanceKey ) ) {
|
||||
const button = new this(
|
||||
context,
|
||||
externalHandler,
|
||||
buttonConfig,
|
||||
ppcpConfig,
|
||||
contextHandler
|
||||
);
|
||||
|
||||
buttonInstances.set( instanceKey, button );
|
||||
}
|
||||
|
||||
return buttonInstances.get( instanceKey );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list with all wrapper IDs for the implemented payment method, categorized by
|
||||
* context.
|
||||
*
|
||||
* @abstract
|
||||
* @param {Object} buttonConfig - Payment method specific configuration.
|
||||
* @param {Object} ppcpConfig - Global plugin configuration.
|
||||
* @return {{MiniCart, Gateway, Block, SmartButton, Default}} The wrapper ID collection.
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
static getWrappers( buttonConfig, ppcpConfig ) {
|
||||
throw new Error( 'Must be implemented in the child class' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all button styles for the implemented payment method, categorized by
|
||||
* context.
|
||||
*
|
||||
* @abstract
|
||||
* @param {Object} buttonConfig - Payment method specific configuration.
|
||||
* @param {Object} ppcpConfig - Global plugin configuration.
|
||||
* @return {{MiniCart: (*), Default: (*)}} Combined styles, separated by context.
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
static getStyles( buttonConfig, ppcpConfig ) {
|
||||
throw new Error( 'Must be implemented in the child class' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the payment button instance.
|
||||
*
|
||||
* Do not create new button instances directly; use the `createButton` method instead
|
||||
* to avoid multiple button instances handling the same context.
|
||||
*
|
||||
* @private
|
||||
* @param {string} context - Button context name.
|
||||
* @param {Object} externalHandler - Handler object.
|
||||
* @param {Object} buttonConfig - Payment button specific configuration.
|
||||
* @param {Object} ppcpConfig - Plugin wide configuration object.
|
||||
* @param {Object} contextHandler - Handler object.
|
||||
*/
|
||||
constructor(
|
||||
context,
|
||||
externalHandler = null,
|
||||
buttonConfig = {},
|
||||
ppcpConfig = {},
|
||||
contextHandler = null
|
||||
) {
|
||||
if ( this.methodId === PaymentButton.methodId ) {
|
||||
throw new Error( 'Cannot initialize the PaymentButton base class' );
|
||||
}
|
||||
|
||||
if ( ! buttonConfig ) {
|
||||
buttonConfig = {};
|
||||
}
|
||||
|
||||
const isDebugging = !! buttonConfig?.is_debug;
|
||||
const methodName = this.methodId.replace( /^ppcp?-/, '' );
|
||||
|
||||
this.#context = context;
|
||||
this.#buttonConfig = buttonConfig;
|
||||
this.#ppcpConfig = ppcpConfig;
|
||||
this.#externalHandler = externalHandler;
|
||||
this.#contextHandler = contextHandler;
|
||||
|
||||
this.#logger = new ConsoleLogger( methodName, context );
|
||||
|
||||
if ( isDebugging ) {
|
||||
this.#logger.enabled = true;
|
||||
addToDebuggingCollection( methodName, this );
|
||||
}
|
||||
|
||||
this.#wrappers = this.constructor.getWrappers(
|
||||
this.#buttonConfig,
|
||||
this.#ppcpConfig
|
||||
);
|
||||
this.applyButtonStyles( this.#buttonConfig );
|
||||
|
||||
apmButtonsInit( this.#ppcpConfig );
|
||||
this.initEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal ID of the payment gateway.
|
||||
*
|
||||
* @readonly
|
||||
* @return {string} The internal gateway ID, defined in the derived class.
|
||||
*/
|
||||
get methodId() {
|
||||
return this.constructor.methodId;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class that is added to the button wrapper.
|
||||
*
|
||||
* @readonly
|
||||
* @return {string} CSS class, defined in the derived class.
|
||||
*/
|
||||
get cssClass() {
|
||||
return this.constructor.cssClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the payment button was fully initialized.
|
||||
*
|
||||
* @readonly
|
||||
* @return {boolean} True indicates, that the button was fully initialized.
|
||||
*/
|
||||
get isInitialized() {
|
||||
return this.#isInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* The button's context.
|
||||
*
|
||||
* TODO: Convert the string to a context-object (primitive obsession smell)
|
||||
*
|
||||
* @readonly
|
||||
* @return {string} The button context.
|
||||
*/
|
||||
get context() {
|
||||
return this.#context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration, specific for the implemented payment button.
|
||||
*
|
||||
* @return {Object} Configuration object.
|
||||
*/
|
||||
get buttonConfig() {
|
||||
return this.#buttonConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin-wide configuration; i.e., PayPal client ID, shop currency, etc.
|
||||
*
|
||||
* @return {Object} Configuration object.
|
||||
*/
|
||||
get ppcpConfig() {
|
||||
return this.#ppcpConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Object} The bootstrap handler instance, or an empty object.
|
||||
*/
|
||||
get externalHandler() {
|
||||
return this.#externalHandler || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the button's context handler.
|
||||
* When no context handler was provided (like for a preview button), an empty object is
|
||||
* returned.
|
||||
*
|
||||
* @return {Object} The context handler instance, or an empty object.
|
||||
*/
|
||||
get contextHandler() {
|
||||
return this.#contextHandler || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether customers need to provide shipping details during payment.
|
||||
*
|
||||
* Can be extended by child classes to take method specific configuration into account.
|
||||
*
|
||||
* @return {boolean} True means, shipping fields are displayed and must be filled.
|
||||
*/
|
||||
get requiresShipping() {
|
||||
// Default check: Is shipping enabled in WooCommerce?
|
||||
return (
|
||||
'function' === typeof this.contextHandler.shippingAllowed &&
|
||||
this.contextHandler.shippingAllowed()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Button wrapper details.
|
||||
*
|
||||
* @readonly
|
||||
* @return {WrapperCollection} Wrapper IDs.
|
||||
*/
|
||||
get wrappers() {
|
||||
return this.#wrappers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the context-relevant button style object.
|
||||
*
|
||||
* @readonly
|
||||
* @return {string} Styling options.
|
||||
*/
|
||||
get style() {
|
||||
if ( PaymentContext.MiniCart === this.context ) {
|
||||
return this.#styles.MiniCart;
|
||||
}
|
||||
|
||||
return this.#styles.Default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the context-relevant wrapper ID.
|
||||
*
|
||||
* @readonly
|
||||
* @return {string} The wrapper-element's ID (without the `#` prefix).
|
||||
*/
|
||||
get wrapperId() {
|
||||
if ( PaymentContext.MiniCart === this.context ) {
|
||||
return this.wrappers.MiniCart;
|
||||
} else if ( this.isSeparateGateway ) {
|
||||
return this.wrappers.Gateway;
|
||||
} else if ( PaymentContext.Blocks.includes( this.context ) ) {
|
||||
return this.wrappers.Block;
|
||||
}
|
||||
|
||||
return this.wrappers.Default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the button is placed inside a classic gateway context.
|
||||
*
|
||||
* Classic gateway contexts are: Classic checkout, Pay for Order page.
|
||||
*
|
||||
* @return {boolean} True indicates, the button is located inside a classic gateway.
|
||||
*/
|
||||
get isInsideClassicGateway() {
|
||||
return PaymentContext.Gateways.includes( this.context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the current payment button should be rendered as a stand-alone gateway.
|
||||
* The return value `false` usually means, that the payment button is bundled with all available
|
||||
* payment buttons.
|
||||
*
|
||||
* The decision depends on the button context (placement) and the plugin settings.
|
||||
*
|
||||
* @return {boolean} True, if the current button represents a stand-alone gateway.
|
||||
*/
|
||||
get isSeparateGateway() {
|
||||
return (
|
||||
this.#buttonConfig.is_wc_gateway_enabled &&
|
||||
this.isInsideClassicGateway
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the currently selected payment gateway is set to the payment method.
|
||||
*
|
||||
* Only relevant on checkout pages where "classic" payment gateways are rendered.
|
||||
*
|
||||
* @return {boolean} True means that this payment method is selected as current gateway.
|
||||
*/
|
||||
get isCurrentGateway() {
|
||||
if ( ! this.isInsideClassicGateway ) {
|
||||
// This means, the button's visibility is managed by another script.
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* We need to rely on `getCurrentPaymentMethod()` here, as the `CheckoutBootstrap.js`
|
||||
* module fires the "ButtonEvents.RENDER" event before any PaymentButton instances are
|
||||
* created. I.e. we cannot observe the initial gateway selection event.
|
||||
*/
|
||||
const currentMethod = getCurrentPaymentMethod();
|
||||
|
||||
if ( this.isSeparateGateway ) {
|
||||
return this.methodId === currentMethod;
|
||||
}
|
||||
|
||||
// Button is rendered inside the Smart Buttons block.
|
||||
return PaymentMethods.PAYPAL === currentMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flags a preview button without actual payment logic.
|
||||
*
|
||||
* @return {boolean} True indicates a preview instance that has no payment logic.
|
||||
*/
|
||||
get isPreview() {
|
||||
return PaymentContext.Preview === this.context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the browser can accept this payment method.
|
||||
*
|
||||
* @return {boolean} True, if payments are technically possible.
|
||||
*/
|
||||
get isEligible() {
|
||||
return this.#isEligible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the eligibility state of this button component.
|
||||
*
|
||||
* @param {boolean} newState Whether the browser can accept payments.
|
||||
*/
|
||||
set isEligible( newState ) {
|
||||
if ( newState === this.#isEligible ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#isEligible = newState;
|
||||
this.triggerRedraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* The visibility state of the button.
|
||||
* This flag does not reflect actual visibility on the page, but rather, if the button
|
||||
* is intended/allowed to be displayed, in case all other checks pass.
|
||||
*
|
||||
* @return {boolean} True indicates, that the button can be displayed.
|
||||
*/
|
||||
get isVisible() {
|
||||
return this.#isVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the visibility of the button.
|
||||
*
|
||||
* A visible button does not always force the button to render on the page. It only means, that
|
||||
* the button is allowed or not allowed to render, if certain other conditions are met.
|
||||
*
|
||||
* @param {boolean} newState Whether rendering the button is allowed.
|
||||
*/
|
||||
set isVisible( newState ) {
|
||||
if ( this.#isVisible === newState ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#isVisible = newState;
|
||||
this.triggerRedraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTML element that wraps the current button
|
||||
*
|
||||
* @readonly
|
||||
* @return {HTMLElement|null} The wrapper element, or null.
|
||||
*/
|
||||
get wrapperElement() {
|
||||
return document.getElementById( this.wrapperId );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the main button-wrapper is present in the current DOM.
|
||||
*
|
||||
* @readonly
|
||||
* @return {boolean} True, if the button context (wrapper element) is found.
|
||||
*/
|
||||
get isPresent() {
|
||||
return this.wrapperElement instanceof HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, if the payment button is still attached to the DOM.
|
||||
*
|
||||
* WooCommerce performs some partial reloads in many cases, which can lead to our payment
|
||||
* button
|
||||
* to move into the browser's memory. In that case, we need to recreate the button in the
|
||||
* updated DOM.
|
||||
*
|
||||
* @return {boolean} True means, the button is still present (and typically visible) on the
|
||||
* page.
|
||||
*/
|
||||
get isButtonAttached() {
|
||||
if ( ! this.#button ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let parent = this.#button.parentElement;
|
||||
while ( parent?.parentElement ) {
|
||||
if ( 'BODY' === parent.tagName ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a debug detail to the browser console.
|
||||
*
|
||||
* @param {any} args
|
||||
*/
|
||||
log( ...args ) {
|
||||
this.#logger.log( ...args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error message to the browser console.
|
||||
*
|
||||
* @param {any} args
|
||||
*/
|
||||
error( ...args ) {
|
||||
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.
|
||||
*
|
||||
* Can be implemented by the derived class.
|
||||
*
|
||||
* @param {boolean} [silent=false] - Set to true to suppress console errors.
|
||||
* @return {boolean} True indicates the config is valid and initialization can continue.
|
||||
*/
|
||||
validateConfiguration( silent = false ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
applyButtonStyles( buttonConfig, ppcpConfig = null ) {
|
||||
if ( ! ppcpConfig ) {
|
||||
ppcpConfig = this.ppcpConfig;
|
||||
}
|
||||
|
||||
this.#styles = this.constructor.getStyles( buttonConfig, ppcpConfig );
|
||||
|
||||
if ( this.isInitialized ) {
|
||||
this.triggerRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the button instance. Must be called before the initial `init()`.
|
||||
*
|
||||
* Parameters are defined by the derived class.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
configure() {}
|
||||
|
||||
/**
|
||||
* Must be named `init()` to simulate "protected" visibility:
|
||||
* Since the derived class also implements a method with the same name, this method can only
|
||||
* be called by the derived class, but not from any other code.
|
||||
*/
|
||||
init() {
|
||||
this.#isInitialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be named `reinit()` to simulate "protected" visibility:
|
||||
* Since the derived class also implements a method with the same name, this method can only
|
||||
* be called by the derived class, but not from any other code.
|
||||
*/
|
||||
reinit() {
|
||||
this.#isInitialized = false;
|
||||
this.#isEligible = false;
|
||||
}
|
||||
|
||||
triggerRedraw() {
|
||||
this.showPaymentGateway();
|
||||
|
||||
dispatchButtonEvent( {
|
||||
event: ButtonEvents.REDRAW,
|
||||
paymentMethod: this.methodId,
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches event listeners to show or hide the payment button when needed.
|
||||
*/
|
||||
initEventListeners() {
|
||||
// Refresh the button - this might show, hide or re-create the payment button.
|
||||
observeButtonEvent( {
|
||||
event: ButtonEvents.REDRAW,
|
||||
paymentMethod: this.methodId,
|
||||
callback: () => this.refresh(),
|
||||
} );
|
||||
|
||||
// Events relevant for buttons inside a payment gateway.
|
||||
if ( this.isInsideClassicGateway ) {
|
||||
const parentMethod = this.isSeparateGateway
|
||||
? this.methodId
|
||||
: PaymentMethods.PAYPAL;
|
||||
|
||||
// Hide the button right after the user selected _any_ gateway.
|
||||
observeButtonEvent( {
|
||||
event: ButtonEvents.INVALIDATE,
|
||||
callback: () => ( this.isVisible = false ),
|
||||
} );
|
||||
|
||||
// Show the button (again) when the user selected the current gateway.
|
||||
observeButtonEvent( {
|
||||
event: ButtonEvents.RENDER,
|
||||
paymentMethod: parentMethod,
|
||||
callback: () => ( this.isVisible = true ),
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the payment button on the page.
|
||||
*/
|
||||
refresh() {
|
||||
if ( ! this.isPresent ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applyWrapperStyles();
|
||||
|
||||
if ( this.isEligible && this.isCurrentGateway && this.isVisible ) {
|
||||
if ( ! this.isButtonAttached ) {
|
||||
this.log( 'refresh.addButton' );
|
||||
this.addButton();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the payment gateway visible by removing initial inline styles from the DOM.
|
||||
* Also, removes the button-placeholder container from the smart button block.
|
||||
*
|
||||
* Only relevant on the checkout page, i.e., when `this.isSeparateGateway` is `true`
|
||||
*/
|
||||
showPaymentGateway() {
|
||||
if (
|
||||
this.#gatewayInitialized ||
|
||||
! this.isSeparateGateway ||
|
||||
! this.isEligible
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const styleSelector = `style[data-hide-gateway="${ this.methodId }"]`;
|
||||
const wrapperSelector = `#${ this.wrappers.Default }`;
|
||||
|
||||
document
|
||||
.querySelectorAll( styleSelector )
|
||||
.forEach( ( el ) => el.remove() );
|
||||
|
||||
document
|
||||
.querySelectorAll( wrapperSelector )
|
||||
.forEach( ( el ) => el.remove() );
|
||||
|
||||
this.log( 'Show gateway' );
|
||||
this.#gatewayInitialized = true;
|
||||
|
||||
// This code runs only once, during button initialization, and fixes the initial visibility.
|
||||
this.isVisible = this.isCurrentGateway;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies CSS classes and inline styling to the payment button wrapper.
|
||||
*/
|
||||
applyWrapperStyles() {
|
||||
const wrapper = this.wrapperElement;
|
||||
const { shape, height } = this.style;
|
||||
|
||||
for ( const classItem of this.#appliedClasses ) {
|
||||
wrapper.classList.remove( classItem );
|
||||
}
|
||||
|
||||
this.#appliedClasses = [];
|
||||
|
||||
const newClasses = [
|
||||
`ppcp-button-${ shape }`,
|
||||
'ppcp-button-apm',
|
||||
this.cssClass,
|
||||
];
|
||||
|
||||
wrapper.classList.add( ...newClasses );
|
||||
this.#appliedClasses.push( ...newClasses );
|
||||
|
||||
if ( height ) {
|
||||
wrapper.style.height = `${ height }px`;
|
||||
}
|
||||
|
||||
// Apply the wrapper visibility.
|
||||
wrapper.style.display = this.isVisible ? 'block' : 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new payment button (HTMLElement) and must call `this.insertButton()` to display
|
||||
* that button in the correct wrapper.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
addButton() {
|
||||
throw new Error( 'Must be implemented by the child class' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the button wrapper element and inserts the provided payment button into the DOM.
|
||||
*
|
||||
* If a payment button was previously inserted to the wrapper, calling this method again will
|
||||
* first remove the previous button.
|
||||
*
|
||||
* @param {HTMLElement} button - The button element to inject.
|
||||
*/
|
||||
insertButton( button ) {
|
||||
if ( ! this.isPresent ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapper = this.wrapperElement;
|
||||
|
||||
if ( this.#button ) {
|
||||
this.removeButton();
|
||||
}
|
||||
|
||||
this.log( 'addButton', button );
|
||||
|
||||
this.#button = button;
|
||||
wrapper.appendChild( this.#button );
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the payment button from the DOM.
|
||||
*/
|
||||
removeButton() {
|
||||
if ( ! this.isPresent || ! this.#button ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log( 'removeButton' );
|
||||
|
||||
try {
|
||||
this.wrapperElement.removeChild( this.#button );
|
||||
} catch ( Exception ) {
|
||||
// Ignore this.
|
||||
}
|
||||
|
||||
this.#button = null;
|
||||
}
|
||||
}
|
|
@ -139,8 +139,9 @@ class Renderer {
|
|||
};
|
||||
|
||||
// Check the condition and add the handler if needed
|
||||
if ( this.defaultSettings.should_handle_shipping_in_paypal ) {
|
||||
if ( this.shouldEnableShippingCallback() ) {
|
||||
options.onShippingOptionsChange = ( data, actions ) => {
|
||||
let shippingOptionsChange =
|
||||
! this.isVenmoButtonClickedWhenVaultingIsEnabled(
|
||||
venmoButtonClicked
|
||||
)
|
||||
|
@ -150,8 +151,11 @@ class Renderer {
|
|||
this.defaultSettings
|
||||
)
|
||||
: null;
|
||||
|
||||
return shippingOptionsChange
|
||||
};
|
||||
options.onShippingAddressChange = ( data, actions ) => {
|
||||
let shippingAddressChange =
|
||||
! this.isVenmoButtonClickedWhenVaultingIsEnabled(
|
||||
venmoButtonClicked
|
||||
)
|
||||
|
@ -161,6 +165,8 @@ class Renderer {
|
|||
this.defaultSettings
|
||||
)
|
||||
: null;
|
||||
|
||||
return shippingAddressChange
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -221,6 +227,12 @@ class Renderer {
|
|||
return venmoButtonClicked && this.defaultSettings.vaultingEnabled;
|
||||
};
|
||||
|
||||
shouldEnableShippingCallback = () => {
|
||||
console.log(this.defaultSettings.context, this.defaultSettings)
|
||||
let needShipping = this.defaultSettings.needShipping || this.defaultSettings.context === 'product'
|
||||
return this.defaultSettings.should_handle_shipping_in_paypal && needShipping
|
||||
};
|
||||
|
||||
isAlreadyRendered( wrapper, fundingSource ) {
|
||||
return this.renderedSources.has( wrapper + ( fundingSource ?? '' ) );
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue