🔀 Merge branch 'PCP-3644’

# Conflicts:
#	modules/ppcp-applepay/resources/js/ApplepayButton.js
This commit is contained in:
Philipp Stracker 2024-11-04 17:36:17 +01:00
commit 73cc6b177e
No known key found for this signature in database
16 changed files with 910 additions and 720 deletions

View file

@ -57,7 +57,3 @@
}
}
}
#ppc-button-ppcp-applepay {
display: none;
}

File diff suppressed because it is too large Load diff

View file

@ -1,54 +1,90 @@
import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
import ApplePayButton from './ApplepayButton';
import ContextHandlerFactory from './Context/ContextHandlerFactory';
class ApplePayManager {
#namespace = '';
#buttonConfig = null;
#ppcpConfig = null;
#applePayConfig = null;
#contextHandler = null;
#transactionInfo = null;
#buttons = [];
constructor( namespace, buttonConfig, ppcpConfig ) {
this.namespace = namespace;
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.ApplePayConfig = null;
this.buttons = [];
this.#namespace = namespace;
this.#buttonConfig = buttonConfig;
this.#ppcpConfig = ppcpConfig;
buttonModuleWatcher.watchContextBootstrap( ( bootstrap ) => {
const button = new ApplePayButton(
bootstrap.context,
bootstrap.handler,
buttonConfig,
ppcpConfig
);
this.buttons.push( button );
if ( this.ApplePayConfig ) {
button.init( this.ApplePayConfig );
}
} );
this.onContextBootstrap = this.onContextBootstrap.bind( this );
buttonModuleWatcher.watchContextBootstrap( this.onContextBootstrap );
}
init() {
( async () => {
await this.config();
for ( const button of this.buttons ) {
button.init( this.ApplePayConfig );
}
} )();
async onContextBootstrap( bootstrap ) {
this.#contextHandler = ContextHandlerFactory.create(
bootstrap.context,
this.#buttonConfig,
this.#ppcpConfig,
bootstrap.handler
);
const button = ApplePayButton.createButton(
bootstrap.context,
bootstrap.handler,
this.#buttonConfig,
this.#ppcpConfig,
this.#contextHandler
);
this.#buttons.push( button );
// Ensure ApplePayConfig is loaded before proceeding.
await this.init();
button.configure( this.#applePayConfig, this.#transactionInfo );
button.init();
}
reinit() {
for ( const button of this.buttons ) {
button.reinit();
async init() {
try {
if ( ! this.#applePayConfig ) {
this.#applePayConfig = await window[ this.#namespace ]
.Applepay()
.config();
if ( ! this.#applePayConfig ) {
console.error( 'No ApplePayConfig received during init' );
}
}
if ( ! this.#transactionInfo ) {
this.#transactionInfo = await this.fetchTransactionInfo();
if ( ! this.#applePayConfig ) {
console.error( 'No transactionInfo found during init' );
}
}
} catch ( error ) {
console.error( 'Error during initialization:', error );
}
}
/**
* Gets Apple Pay configuration of the PayPal merchant.
*/
async config() {
this.ApplePayConfig = await window[ this.namespace ]
.Applepay()
.config();
async fetchTransactionInfo() {
try {
if ( ! this.#contextHandler ) {
throw new Error( 'ContextHandler is not initialized' );
}
return await this.#contextHandler.transactionInfo();
} catch ( error ) {
console.error( 'Error fetching transaction info:', error );
throw error;
}
}
return this.ApplePayConfig;
reinit() {
for ( const button of this.#buttons ) {
button.reinit();
}
}
}

View file

@ -1,37 +1,15 @@
import ApplePayButton from './ApplepayButton';
import ApplepayButton from './Block/components/ApplePayButton';
class ApplePayManagerBlockEditor {
constructor( namespace, buttonConfig, ppcpConfig ) {
this.namespace = namespace;
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
/*
* On the front-end, the init method is called when a new button context was detected
* via `buttonModuleWatcher`. In the block editor, we do not need to wait for the
* context, but can initialize the button in the next event loop.
*/
setTimeout( () => this.init() );
}
async init() {
try {
this.applePayConfig = await window[ this.namespace ]
.Applepay()
.config();
const button = new ApplePayButton(
this.ppcpConfig.context,
null,
this.buttonConfig,
this.ppcpConfig
);
button.init( this.applePayConfig );
} catch ( error ) {
console.error( 'Failed to initialize Apple Pay:', error );
}
}
}
const ApplePayManagerBlockEditor = ( {
namespace,
buttonConfig,
ppcpConfig,
} ) => (
<ApplepayButton
namespace={ namespace }
buttonConfig={ buttonConfig }
ppcpConfig={ ppcpConfig }
/>
);
export default ApplePayManagerBlockEditor;

View file

@ -0,0 +1,51 @@
import { useState, useEffect } from '@wordpress/element';
import useApiToGenerateButton from '../hooks/useApiToGenerateButton';
import usePayPalScript from '../hooks/usePayPalScript';
import useApplepayScript from '../hooks/useApplepayScript';
import useApplepayConfig from '../hooks/useApplepayConfig';
const ApplepayButton = ( { namespace, buttonConfig, ppcpConfig } ) => {
const [ buttonHtml, setButtonHtml ] = useState( '' );
const [ buttonElement, setButtonElement ] = useState( null );
const [ componentFrame, setComponentFrame ] = useState( null );
const isPayPalLoaded = usePayPalScript( namespace, ppcpConfig );
const isApplepayLoaded = useApplepayScript(
componentFrame,
buttonConfig,
isPayPalLoaded
);
const applepayConfig = useApplepayConfig( namespace, isApplepayLoaded );
useEffect( () => {
if ( ! buttonElement ) {
return;
}
setComponentFrame( buttonElement.ownerDocument );
}, [ buttonElement ] );
const applepayButton = useApiToGenerateButton(
componentFrame,
namespace,
buttonConfig,
ppcpConfig,
applepayConfig
);
useEffect( () => {
if ( applepayButton ) {
setButtonHtml( applepayButton.outerHTML );
}
}, [ applepayButton ] );
return (
<div
ref={ setButtonElement }
dangerouslySetInnerHTML={ { __html: buttonHtml } }
/>
);
};
export default ApplepayButton;

View file

@ -0,0 +1,37 @@
import { useEffect, useState } from '@wordpress/element';
import useButtonStyles from './useButtonStyles';
const useApiToGenerateButton = (
componentDocument,
namespace,
buttonConfig,
ppcpConfig,
applepayConfig
) => {
const [ applepayButton, setApplepayButton ] = useState( null );
const buttonStyles = useButtonStyles( buttonConfig, ppcpConfig );
useEffect( () => {
if ( ! buttonConfig || ! applepayConfig ) {
return;
}
const button = document.createElement( 'apple-pay-button' );
button.setAttribute(
'buttonstyle',
buttonConfig.buttonColor || 'black'
);
button.setAttribute( 'type', buttonConfig.buttonType || 'pay' );
button.setAttribute( 'locale', buttonConfig.buttonLocale || 'en' );
setApplepayButton( button );
return () => {
setApplepayButton( null );
};
}, [ namespace, buttonConfig, ppcpConfig, applepayConfig, buttonStyles ] );
return applepayButton;
};
export default useApiToGenerateButton;

View file

@ -0,0 +1,26 @@
import { useState, useEffect } from '@wordpress/element';
const useApplepayConfig = ( namespace, isApplepayLoaded ) => {
const [ applePayConfig, setApplePayConfig ] = useState( null );
useEffect( () => {
const fetchConfig = async () => {
if ( ! isApplepayLoaded ) {
return;
}
try {
const config = await window[ namespace ].Applepay().config();
setApplePayConfig( config );
} catch ( error ) {
console.error( 'Failed to fetch Apple Pay config:', error );
}
};
fetchConfig();
}, [ namespace, isApplepayLoaded ] );
return applePayConfig;
};
export default useApplepayConfig;

View file

@ -0,0 +1,65 @@
import { useState, useEffect } from '@wordpress/element';
import { loadCustomScript } from '@paypal/paypal-js';
const useApplepayScript = (
componentDocument,
buttonConfig,
isPayPalLoaded
) => {
const [ isApplepayLoaded, setIsApplepayLoaded ] = useState( false );
useEffect( () => {
if ( ! componentDocument ) {
return;
}
const injectScriptToFrame = ( scriptSrc ) => {
if ( document === componentDocument ) {
return;
}
const script = document.querySelector(
`script[src^="${ scriptSrc }"]`
);
if ( script ) {
const newScript = componentDocument.createElement( 'script' );
newScript.src = script.src;
newScript.async = script.async;
newScript.type = script.type;
componentDocument.head.appendChild( newScript );
} else {
console.error( 'Script not found in the document:', scriptSrc );
}
};
const loadApplepayScript = async () => {
if ( ! isPayPalLoaded ) {
return;
}
if ( ! buttonConfig || ! buttonConfig.sdk_url ) {
console.error( 'Invalid buttonConfig or missing sdk_url' );
return;
}
try {
await loadCustomScript( { url: buttonConfig.sdk_url } ).then(
() => {
injectScriptToFrame( buttonConfig.sdk_url );
}
);
setIsApplepayLoaded( true );
} catch ( error ) {
console.error( 'Failed to load Applepay script:', error );
}
};
loadApplepayScript();
}, [ componentDocument, buttonConfig, isPayPalLoaded ] );
return isApplepayLoaded;
};
export default useApplepayScript;

View file

@ -0,0 +1,19 @@
import { useMemo } from '@wordpress/element';
import { combineStyles } from '../../../../../ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers';
const useButtonStyles = ( buttonConfig, ppcpConfig ) => {
return useMemo( () => {
const styles = combineStyles(
ppcpConfig?.button || {},
buttonConfig?.button || {}
);
if ( styles.MiniCart && styles.MiniCart.type === 'buy' ) {
styles.MiniCart.type = 'pay';
}
return styles;
}, [ buttonConfig, ppcpConfig ] );
};
export default useButtonStyles;

View file

@ -0,0 +1,25 @@
import { useState, useEffect } from '@wordpress/element';
import { loadPayPalScript } from '../../../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
const usePayPalScript = ( namespace, ppcpConfig ) => {
const [ isPayPalLoaded, setIsPayPalLoaded ] = useState( false );
ppcpConfig.url_params.components += ',applepay';
useEffect( () => {
const loadScript = async () => {
try {
await loadPayPalScript( namespace, ppcpConfig );
setIsPayPalLoaded( true );
} catch ( error ) {
console.error( `Error loading PayPal script: ${ error }` );
}
};
loadScript();
}, [ namespace, ppcpConfig ] );
return isPayPalLoaded;
};
export default usePayPalScript;

View file

@ -5,6 +5,11 @@ import PreviewButton from '../../../../ppcp-button/resources/js/modules/Preview/
* A single Apple Pay preview button instance.
*/
export default class ApplePayPreviewButton extends PreviewButton {
/**
* @type {?PaymentButton}
*/
#button = null;
constructor( args ) {
super( args );
@ -19,14 +24,18 @@ export default class ApplePayPreviewButton extends PreviewButton {
}
createButton( buttonConfig ) {
const button = new ApplepayButton(
'preview',
null,
buttonConfig,
this.ppcpConfig
);
if ( ! this.#button ) {
this.#button = new ApplepayButton(
'preview',
null,
buttonConfig,
this.ppcpConfig
);
}
button.init( this.apiConfig );
this.#button.configure( this.apiConfig, null );
this.#button.applyButtonStyles( buttonConfig, this.ppcpConfig );
this.#button.reinit();
}
/**

View file

@ -1,5 +1,6 @@
import { useEffect, useState } from '@wordpress/element';
import { useEffect, useRef, useState } from '@wordpress/element';
import { registerExpressPaymentMethod } from '@woocommerce/blocks-registry';
import { __ } from '@wordpress/i18n';
import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
import { cartHasSubscriptionProducts } from '../../../ppcp-blocks/resources/js/Helper/Subscription';
import { loadCustomScript } from '@paypal/paypal-js';
@ -18,20 +19,16 @@ if ( typeof window.PayPalCommerceGateway === 'undefined' ) {
window.PayPalCommerceGateway = ppcpConfig;
}
const ApplePayComponent = ( props ) => {
const [ bootstrapped, setBootstrapped ] = useState( false );
const ApplePayComponent = ( { isEditing } ) => {
const [ paypalLoaded, setPaypalLoaded ] = useState( false );
const [ applePayLoaded, setApplePayLoaded ] = useState( false );
const bootstrap = function () {
const ManagerClass = props.isEditing
? ApplePayManagerBlockEditor
: ApplePayManager;
const manager = new ManagerClass( namespace, buttonConfig, ppcpConfig );
manager.init();
};
const wrapperRef = useRef( null );
useEffect( () => {
if ( isEditing ) {
return;
}
// Load ApplePay SDK
loadCustomScript( { url: buttonConfig.sdk_url } ).then( () => {
setApplePayLoaded( true );
@ -47,17 +44,35 @@ const ApplePayComponent = ( props ) => {
.catch( ( error ) => {
console.error( 'Failed to load PayPal script: ', error );
} );
}, [] );
}, [ isEditing ] );
useEffect( () => {
if ( ! bootstrapped && paypalLoaded && applePayLoaded ) {
setBootstrapped( true );
bootstrap();
if ( isEditing || ! paypalLoaded || ! applePayLoaded ) {
return;
}
}, [ paypalLoaded, applePayLoaded ] );
const ManagerClass = isEditing
? ApplePayManagerBlockEditor
: ApplePayManager;
buttonConfig.reactWrapper = wrapperRef.current;
new ManagerClass( namespace, buttonConfig, ppcpConfig );
}, [ paypalLoaded, applePayLoaded, isEditing ] );
if ( isEditing ) {
return (
<ApplePayManagerBlockEditor
namespace={ namespace }
buttonConfig={ buttonConfig }
ppcpConfig={ ppcpConfig }
/>
);
}
return (
<div
ref={ wrapperRef }
id={ buttonConfig.button.wrapper.replace( '#', '' ) }
className="ppcp-button-apm ppcp-button-applepay"
></div>
@ -75,6 +90,11 @@ if (
registerExpressPaymentMethod( {
name: buttonData.id,
title: `PayPal - ${ buttonData.title }`,
description: __(
'Eligible users will see the PayPal button.',
'woocommerce-paypal-payments'
),
label: <div dangerouslySetInnerHTML={ { __html: buttonData.title } } />,
content: <ApplePayComponent isEditing={ false } />,
edit: <ApplePayComponent isEditing={ true } />,

View file

@ -3,33 +3,49 @@ import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Help
import ApplePayManager from './ApplepayManager';
import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper';
( function ( { buttonConfig, ppcpConfig, jQuery } ) {
( function ( { buttonConfig, ppcpConfig } ) {
const namespace = 'ppcpPaypalApplepay';
let manager;
const bootstrap = function () {
manager = new ApplePayManager( namespace, buttonConfig, ppcpConfig );
manager.init();
};
setupButtonEvents( function () {
if ( manager ) {
manager.reinit();
}
} );
document.addEventListener( 'DOMContentLoaded', () => {
if (
typeof buttonConfig === 'undefined' ||
typeof ppcpConfig === 'undefined'
) {
function bootstrapPayButton() {
if ( ! buttonConfig || ! ppcpConfig ) {
return;
}
const isMiniCart = ppcpConfig.mini_cart_buttons_enabled;
const isButton = jQuery( '#' + buttonConfig.button.wrapper ).length > 0;
const manager = new ApplePayManager(
namespace,
buttonConfig,
ppcpConfig
);
setupButtonEvents( function () {
manager.reinit();
} );
}
function bootstrap() {
bootstrapPayButton();
// Other Apple Pay bootstrapping could happen here.
}
document.addEventListener( 'DOMContentLoaded', () => {
if ( ! buttonConfig || ! ppcpConfig ) {
/*
* No PayPal buttons present on this page, but maybe a bootstrap module needs to be
* initialized. Skip loading the SDK or gateway configuration, and directly initialize
* the module.
*/
bootstrap();
return;
}
const usedInMiniCart = ppcpConfig.mini_cart_buttons_enabled;
const pageHasButton =
null !== document.getElementById( buttonConfig.button.wrapper );
// If button wrapper is not present then there is no need to load the scripts.
// minicart loads later?
if ( ! isMiniCart && ! isButton ) {
if ( ! usedInMiniCart && ! pageHasButton ) {
return;
}
@ -63,5 +79,4 @@ import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Hel
} )( {
buttonConfig: window.wc_ppcp_applepay,
ppcpConfig: window.PayPalCommerceGateway,
jQuery: window.jQuery,
} );

View file

@ -1008,7 +1008,7 @@ class ApplePayButton implements ButtonInterface {
*/
protected function hide_gateway_until_eligible(): void {
?>
<style id="ppcp-hide-apple-pay">.wc_payment_method.payment_method_ppcp-applepay{display:none}</style>
<style data-hide-gateway="ppcp-applepay">.wc_payment_method.payment_method_ppcp-applepay{display:none}</style>
<?php
}

View file

@ -10,6 +10,8 @@ import {
dispatchButtonEvent,
observeButtonEvent,
} from '../Helper/PaymentButtonHelpers';
import { isVisible } from '../Helper/Hiding';
import { isDisabled, setEnabled } from '../Helper/ButtonDisabler';
/**
* Collection of all available styling options for this button.
@ -184,6 +186,13 @@ export default class PaymentButton {
*/
#isVisible = true;
/**
* Whether this button is enabled (can be clicked).
*
* @type {boolean}
*/
#isEnabled = true;
/**
* The currently visible payment button.
*
@ -192,6 +201,13 @@ export default class PaymentButton {
*/
#button = null;
/**
* List of checks to perform to verify the PaymentButton has is configured correctly.
*
* @type {{check, errorMessage, shouldPass}[]}
*/
#validationChecks = [];
/**
* Factory method to create a new PaymentButton while limiting a single instance per context.
*
@ -305,6 +321,11 @@ export default class PaymentButton {
);
this.applyButtonStyles( this.#buttonConfig );
this.registerValidationRules(
this.#assertIsInvalid.bind( this ),
this.#assertIsValid.bind( this )
);
apmButtonsInit( this.#ppcpConfig );
this.initEventListeners();
}
@ -559,6 +580,29 @@ export default class PaymentButton {
this.triggerRedraw();
}
/**
* The enabled/disabled state of the button (whether it can be clicked).
*
* @return {boolean} True indicates, that the button is enabled.
*/
get isEnabled() {
return this.#isEnabled;
}
/**
* Change the enabled/disabled state of the button.
*
* @param {boolean} newState Whether the button is enabled.
*/
set isEnabled( newState ) {
if ( this.#isEnabled === newState ) {
return;
}
this.#isEnabled = newState;
this.triggerRedraw();
}
/**
* Returns the HTML element that wraps the current button
*
@ -569,6 +613,23 @@ export default class PaymentButton {
return document.getElementById( this.wrapperId );
}
/**
* Returns the standard PayPal smart button selector for the current context.
*
* @return {string | null} The selector, or null if not available.
*/
get ppcpButtonWrapperSelector() {
if ( PaymentContext.Blocks.includes( this.context ) ) {
return null;
}
if ( this.context === PaymentContext.MiniCart ) {
return this.ppcpConfig?.button?.mini_cart_wrapper;
}
return this.ppcpConfig?.button?.wrapper;
}
/**
* Checks whether the main button-wrapper is present in the current DOM.
*
@ -634,16 +695,75 @@ export default class PaymentButton {
this.#logger.group( label );
}
/**
* Register a validation check that marks the configuration as invalid when passed.
*
* @param {Function} check - A function that returns a truthy value if the check passes.
* @param {string} errorMessage - The error message to display if the check fails.
*/
#assertIsInvalid( check, errorMessage ) {
this.#validationChecks.push( {
check,
errorMessage,
shouldPass: false,
} );
}
/**
* Register a validation check that instantly marks the configuration as valid when passed.
*
* @param {Function} check - A function that returns a truthy value if the check passes.
*/
#assertIsValid( check ) {
this.#validationChecks.push( { check, shouldPass: true } );
}
/**
* Defines a series of validation steps to ensure the payment button is configured correctly.
*
* Each validation step is executed in the order they are defined within this method.
*
* If a validation step using `invalidIf` returns true, the configuration is immediately considered
* invalid, and an error message is logged. Conversely, if a validation step using `validIf`
* returns true, the configuration is immediately considered valid.
*
* If no validation step returns true, the configuration is assumed to be valid by default.
*
* @param {(condition: () => boolean, errorMessage: string) => void} invalidIf - Registers a validation step that fails if the condition returns true.
* @param {(condition: () => boolean) => void} validIf - Registers a validation step that passes if the condition returns true.
*/
// eslint-disable-next-line no-unused-vars
registerValidationRules( invalidIf, validIf ) {}
/**
* 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.
* All required validation steps must be registered in the constructor of the derived class
* using `this.addValidationFailure()` or `this.addValidationSuccess()`.
*
* @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 ) {
for ( const step of this.#validationChecks ) {
const result = step.check();
if ( step.shouldPass && result ) {
// If a success check passes, mark as valid immediately.
return true;
}
if ( ! step.shouldPass && result ) {
// If a failure check passes, mark as invalid.
if ( ! silent && step.errorMessage ) {
this.error( step.errorMessage );
}
return false;
}
}
return true;
}
@ -697,7 +817,23 @@ export default class PaymentButton {
}
/**
* Attaches event listeners to show or hide the payment button when needed.
* Applies the visibility and enabled state from the PayPal button.
* Intended for the product page, may not work correctly on the checkout page.
*/
syncProductButtonsState() {
const ppcpButton = document.querySelector(
this.ppcpButtonWrapperSelector
);
if ( ! ppcpButton ) {
return;
}
this.isVisible = isVisible( ppcpButton );
this.isEnabled = ! isDisabled( ppcpButton );
}
/**
* Attaches event listeners to show/hide or enable/disable the payment button when needed.
*/
initEventListeners() {
// Refresh the button - this might show, hide or re-create the payment button.
@ -726,6 +862,24 @@ export default class PaymentButton {
callback: () => ( this.isVisible = true ),
} );
}
// On the product page, copy the visibility and enabled state from the PayPal button.
if ( this.context === PaymentContext.Product ) {
jQuery( document ).on(
'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
( ev, data ) => {
if (
! jQuery( data.selector ).is(
this.ppcpButtonWrapperSelector
)
) {
return;
}
this.syncProductButtonsState();
}
);
}
}
/**
@ -812,6 +966,12 @@ export default class PaymentButton {
// Apply the wrapper visibility.
wrapper.style.display = this.isVisible ? 'block' : 'none';
// Apply the enabled/disabled state.
// On the product page, use the form to display error messages if clicked while disabled.
const form =
this.context === PaymentContext.Product ? 'form.cart' : null;
setEnabled( wrapper, this.isEnabled, form );
}
/**

View file

@ -215,45 +215,31 @@ class GooglepayButton extends PaymentButton {
/**
* @inheritDoc
*/
validateConfiguration( silent = false ) {
const validEnvs = [ 'PRODUCTION', 'TEST' ];
registerValidationRules( invalidIf, validIf ) {
invalidIf(
() =>
! [ 'TEST', 'PRODUCTION' ].includes(
this.buttonConfig.environment
),
`Invalid environment: ${ this.buttonConfig.environment }`
);
const isInvalid = ( ...args ) => {
if ( ! silent ) {
this.error( ...args );
}
return false;
};
validIf( () => this.isPreview );
if ( ! validEnvs.includes( this.buttonConfig.environment ) ) {
return isInvalid(
'Invalid environment:',
this.buttonConfig.environment
);
}
invalidIf(
() => ! this.googlePayConfig,
'No API configuration - missing configure() call?'
);
// Preview buttons only need a valid environment.
if ( this.isPreview ) {
return true;
}
invalidIf(
() => ! this.transactionInfo,
'No transactionInfo - missing configure() call?'
);
if ( ! this.googlePayConfig ) {
return isInvalid(
'No API configuration - missing configure() call?'
);
}
if ( ! this.transactionInfo ) {
return isInvalid(
'No transactionInfo - missing configure() call?'
);
}
if ( ! typeof this.contextHandler?.validateContext() ) {
return isInvalid( 'Invalid context handler.', this.contextHandler );
}
return true;
invalidIf(
() => ! this.contextHandler?.validateContext(),
`Invalid context handler.`
);
}
/**