Skip to content

Advanced Card Processing payments get stuck on the first attempt when using CheckoutWC (2689) #2393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions modules/ppcp-button/resources/js/button.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* global paypal */

import MiniCartBootstap from './modules/ContextBootstrap/MiniCartBootstap';
import SingleProductBootstap from './modules/ContextBootstrap/SingleProductBootstap';
import CartBootstrap from './modules/ContextBootstrap/CartBootstap';
Expand Down
153 changes: 140 additions & 13 deletions modules/ppcp-button/resources/js/modules/Renderer/CardFieldsRenderer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
import { show } from '../Helper/Hiding';
import { renderFields } from '../../../../../ppcp-card-fields/resources/js/Render';
import { cardFieldStyles } from '../../../../../ppcp-card-fields/resources/js/CardFieldsHelper';

/**
* @typedef {'NameField'|'NumberField'|'ExpiryField'|'CVVField'} FieldName
*/

/**
* @typedef {Object} FieldInfo
* @property {FieldName} name - The field name, a valid property of the CardField instance.
* @property {HTMLElement} wrapper - The field's wrapper element (parent of `el`).
* @property {HTMLElement} el - The current input field, which is replaced by `insertField()`.
* @property {Object} options - Rendering options passed to the CardField instance.
*/

/**
* @typedef {Object} CardFields
* @property {() => boolean} isEligible
* @property {() => Promise} submit
* @property {(options: {}) => {}} NameField
* @property {(options: {}) => {}} NumberField
* @property {(options: {}) => {}} ExpiryField
* @property {(options: {}) => {}} CVVField
*/

class CardFieldsRenderer {
/**
* A Map that contains details about all input fields for the card checkout.
*
* @type {Map<FieldName, FieldInfo>|null}
*/
#fields = null;

constructor(
defaultConfig,
errorHandler,
Expand All @@ -18,6 +47,50 @@ class CardFieldsRenderer {
this.onCardFieldsBeforeSubmit = onCardFieldsBeforeSubmit;
}

/**
* Returns a Map with details about all form fields for the CardField element.
*
* @return {Map<FieldName, FieldInfo>}
*/
get fieldInfos() {
if ( ! this.#fields ) {
this.#fields = new Map();

const domFields = {
NameField: 'ppcp-credit-card-gateway-card-name',
NumberField: 'ppcp-credit-card-gateway-card-number',
ExpiryField: 'ppcp-credit-card-gateway-card-expiry',
CVVField: 'ppcp-credit-card-gateway-card-cvc',
};

Object.entries( domFields ).forEach( ( [ fieldName, fieldId ] ) => {
const el = document.getElementById( fieldId );
if ( ! el ) {
return;
}

const wrapper = el.parentNode;
const styles = cardFieldStyles( el );
const options = {
style: { input: styles },
};

if ( el.getAttribute( 'placeholder' ) ) {
options.placeholder = el.getAttribute( 'placeholder' );
}

this.#fields.set( fieldName, {
name: fieldName,
wrapper,
options,
el,
} );
} );
}

return this.#fields;
}

render( wrapper, contextConfig ) {
if (
( this.defaultConfig.context !== 'checkout' &&
Expand Down Expand Up @@ -45,20 +118,10 @@ class CardFieldsRenderer {
hideDccGateway.parentNode.removeChild( hideDccGateway );
}

const cardFields = paypal.CardFields( {
createOrder: contextConfig.createOrder,
onApprove( data ) {
return contextConfig.onApprove( data );
},
onError( error ) {
console.error( error );
this.spinner.unblock();
},
} );
const cardFields = this.createInstance( contextConfig );

if ( cardFields.isEligible() ) {
renderFields( cardFields );
document.dispatchEvent( new CustomEvent( 'hosted_fields_loaded' ) );
this.insertAllFields( cardFields );
}

gateWayBox.style.display = oldDisplayStyle;
Expand Down Expand Up @@ -100,6 +163,7 @@ class CardFieldsRenderer {

cardFields.submit().catch( ( error ) => {
this.spinner.unblock();

console.error( error );
this.errorHandler.message(
this.defaultConfig.hosted_fields.labels.fields_not_valid
Expand All @@ -109,7 +173,70 @@ class CardFieldsRenderer {
}

disableFields() {}

enableFields() {}

/**
* Creates and returns a new CardFields instance.
*
* @see https://developer.paypal.com/sdk/js/reference/#link-cardfields
* @param {Object} contextConfig
* @return {CardFields}
*/
createInstance( contextConfig ) {
return window.paypal.CardFields( {
createOrder: contextConfig.createOrder,
onApprove( data ) {
return contextConfig.onApprove( data );
},
onError( error ) {
console.error( error );
this.spinner.unblock();
},
} );
}

/**
* Links the provided CardField instance to the local DOM.
*
* Note: If another CardField instance was inserted into the DOM before, that previous instance
* will be removed/unlinked in this process.
*
* @param {CardFields} cardFields
*/
insertAllFields( cardFields ) {
// TODO - due to delayed merge we now have similar logic in renderFields (Render.js) - review and unify logic if possible.

this.fieldInfos.forEach( ( field ) => {
this.insertField( cardFields, field );
} );

document.dispatchEvent( new CustomEvent( 'hosted_fields_loaded' ) );
}

/**
* Renders a single input field from the CardField-instance inside the current document's
* DOM, replacing the previous field.
* On first call, this "previous field" is the input element generated by PHP.
*
* @param {CardFields} cardField
* @param {FieldInfo} field
*/
insertField( cardField, field ) {
if ( 'function' !== typeof cardField[ field.name ] ) {
console.error( `${ field.name } is no valid CardFields property` );
return;
}

// Remove the previous input field from DOM
field.el?.remove();

// Render the CardField input element - a div containing an iframe.
cardField[ field.name ]( field.options ).render( field.wrapper );

// Store a reference to the new input field in our Map.
field.el = field.wrapper.querySelector( 'div[id*="paypal"]' );
}
}

export default CardFieldsRenderer;
43 changes: 43 additions & 0 deletions modules/ppcp-compat/src/CompatModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public function run( ContainerInterface $c ): void {
$this->fix_page_builders();
$this->exclude_cache_plugins_js_minification( $c );
$this->set_elementor_checkout_context();
$this->initialize_cfw_compatibility();

$is_nyp_active = $c->get( 'compat.nyp.is_supported_plugin_version_active' );
if ( $is_nyp_active ) {
Expand Down Expand Up @@ -405,6 +406,48 @@ function( bool $do_tag_minification, string $script_tag, $file ) {
);
}

/**
* Addresses issues with CheckoutWC.
*
* @return void
*/
protected function initialize_cfw_compatibility() : void {
add_action(
'cfw_checkout_loaded_pre_head',
function () {
$this->fix_cfw_checkout_page();
}
);
}

/**
* CheckoutWC.
*
* The plugin renders its own version of the checkout page, regardless of the actual content
* of the page. That custom checkout page uses the logic of a "classic checkout page"; in case
* this website uses the block-checkout, our plugin might return incorrect JS details.
*
* Compat code: Needed to ensure that our JS functions use the classic checkout workflow,
* regardless of the presence of any block-checkout logic.
*
* @return void
*/
protected function fix_cfw_checkout_page() : void {
add_filter(
'woocommerce_paypal_payments_context',
function ( string $context ) : string {
if (
function_exists( 'cfw_is_checkout' )
&& apply_filters( 'cfw_load_checkout_template', cfw_is_checkout() )
) {
$context = 'checkout';
}

return $context;
}
);
}

/**
* Sets up the compatibility layer for PayPal Shipping callback & WooCommerce Name Your Price plugin.
*
Expand Down
Loading