|null
*/
private static $gateways = null;
/**
* Register the frontend hooks.
*
* @return void
*/
public function init() {
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_classic_assets' ) );
add_action( 'woocommerce_before_checkout_form', array( $this, 'render_checkout_buttons' ), 5 );
add_action( 'woocommerce_proceed_to_checkout', array( $this, 'render_cart_buttons' ), 5 );
add_action( 'woocommerce_after_add_to_cart_form', array( $this, 'render_product_buttons' ) );
}
/**
* Loads the classic express bundle on the surfaces the merchant enabled.
*
* @return void
*/
public function enqueue_classic_assets() {
$location = $this->get_classic_location();
if ( null === $location ) {
return;
}
wp_register_style(
'monei-express-checkout',
plugins_url( 'public/css/monei-express-checkout.css', MONEI_MAIN_FILE ),
array(),
MONEI_VERSION,
'all'
);
wp_enqueue_style( 'monei-express-checkout' );
if ( ! wp_script_is( 'monei', 'registered' ) ) {
wp_register_script( 'monei', 'https://js.monei.com/v3/monei.js', '', '3.0', true );
}
wp_enqueue_script( 'monei' );
wp_register_script(
self::SCRIPT_HANDLE,
plugins_url( 'public/js/monei-express-checkout.min.js', MONEI_MAIN_FILE ),
array( 'jquery', 'monei' ),
MONEI_VERSION,
true
);
wp_localize_script(
self::SCRIPT_HANDLE,
'wc_monei_express_params',
array_merge(
self::get_script_data(),
array(
'location' => $location,
'product' => 'product' === $location ? $this->get_product_context() : null,
)
)
);
wp_enqueue_script( self::SCRIPT_HANDLE );
}
/**
* Express button container above the classic checkout form.
*
* @return void
*/
public function render_checkout_buttons() {
if ( 'checkout' !== $this->get_classic_location() ) {
return;
}
$this->render_container( 'checkout' );
}
/**
* Express button container above the classic cart's checkout button.
*
* @return void
*/
public function render_cart_buttons() {
if ( 'cart' !== $this->get_classic_location() ) {
return;
}
$this->render_container( 'cart' );
}
/**
* Express button container below the add-to-cart form.
*
* @return void
*/
public function render_product_buttons() {
if ( 'product' !== $this->get_classic_location() ) {
return;
}
$this->render_container( 'product' );
}
/**
* Prints the mount containers, one per wallet the merchant enabled at this surface.
*
* Everything starts hidden and the script reveals it only once a wallet reports
* itself supported, so an unavailable wallet leaves no gap and no dead control.
*
* @param string $location One of the express location keys.
*
* @return void
*/
private function render_container( $location ) {
$methods = array_keys( self::get_enabled_methods( $location ) );
if ( empty( $methods ) ) {
return;
}
?>
*/
public static function get_script_data() {
$methods = array();
$account_id = '';
foreach ( self::get_express_gateways() as $method => $gateway ) {
$locations = array();
foreach ( array_keys( WCMoneiPaymentGateway::get_express_location_options() ) as $location ) {
$locations[ $location ] = $gateway->is_express_enabled_at( (string) $location );
}
$methods[ $method ] = array(
'locations' => $locations,
// PayPal takes different style keys from PaymentRequest — color, layout,
// size, shape, label — so each wallet carries its own.
'style' => json_decode( self::get_button_style( $gateway ) ),
// Whether the MONEI account offers this wallet at all. The blocks
// registry reserves a grid column per registered express method before
// any component mounts, so a wallet the account cannot serve has to be
// refused at registration; discovering it later through `onLoad` leaves
// an empty column behind that halves the width of its neighbour.
'available' => self::account_offers( $method ),
);
if ( '' === $account_id ) {
$account_id = (string) $gateway->getAccountId();
}
}
return array(
// The `%%endpoint%%` placeholder is how WooCommerce core itself hands a
// wc-ajax URL template to the browser, see wc_cart_fragments_params.
'ajaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
'accountId' => $account_id,
'currency' => get_woocommerce_currency(),
'language' => locale_iso_639_1_code(),
'methods' => $methods,
'i18n' => array(
'genericError' => __( 'Express checkout is unavailable right now. Please use the regular checkout.', 'monei' ),
),
);
}
/**
* Registers the express script for the Cart and Checkout blocks.
*
* Called by both blocks payment methods, so express still loads when only one of
* the two gateways is on. Registering an existing handle twice is a no-op, and the
* handle is deduplicated when WooCommerce merges it into the block bundle.
*
* @return string Handle, or an empty string when no wallet has express on a block
* surface.
*/
public static function register_blocks_script() {
if ( empty( self::get_enabled_methods( 'cart' ) ) && empty( self::get_enabled_methods( 'checkout' ) ) ) {
return '';
}
$handle = self::BLOCKS_SCRIPT_HANDLE;
wp_register_script(
$handle,
WC_Monei()->plugin_url() . '/public/js/monei-block-express-checkout.min.js',
array(
'wc-blocks-checkout',
'wc-blocks-registry',
'wc-settings',
'wp-data',
'wp-element',
'wp-i18n',
'monei',
),
WC_Monei()->version,
true
);
if ( function_exists( 'wp_set_script_translations' ) ) {
wp_set_script_translations( $handle, 'monei', WC_Monei()->plugin_path() . '/languages' );
}
return $handle;
}
/**
* Express gateways that are enabled at a surface, keyed by wallet component.
*
* @param string $location One of the express location keys.
*
* @return array
*/
private static function get_enabled_methods( $location ) {
$enabled = array();
foreach ( self::get_express_gateways() as $method => $gateway ) {
if ( $gateway->is_express_enabled_at( $location ) ) {
$enabled[ $method ] = $gateway;
}
}
return $enabled;
}
/**
* @param WCMoneiPaymentGateway $gateway Express gateway.
*
* @return string
*/
private static function get_button_style( WCMoneiPaymentGateway $gateway ) {
return (string) $gateway->get_option( 'express_button_style', $gateway::DEFAULT_EXPRESS_BUTTON_STYLE );
}
/**
* The gateways that expose express checkout, keyed by the wallet they render.
*
* Resolved on demand rather than injected, for the same reason the AJAX handler
* does it: this service is built during `init`, before WooCommerce assembles its
* payment gateways.
*
* @return array
*/
/**
* Whether the MONEI account can serve the wallet behind an express method.
*
* ⚠️ Fails open. A false negative hides a wallet the merchant has switched on and
* paid to enable; a false positive costs an empty column. So an unreadable answer
* — the API down, the account response cached empty — registers the method and
* lets `onLoad` sort it out, which is the behaviour this replaced.
*
* @param string $method Express method key.
*
* @return bool
*/
private static function account_offers( $method ) {
try {
$methods = ContainerProvider::getContainer()->get( PaymentMethodsService::class );
if ( self::METHOD_PAYPAL === $method ) {
return $methods->isPaypalEnabled();
}
return $methods->isGoogleEnabled() || $methods->isAppleEnabled();
} catch ( Exception $e ) {
return true;
}
}
private static function get_express_gateways() {
if ( null !== self::$gateways ) {
return self::$gateways;
}
$container = ContainerProvider::getContainer();
self::$gateways = array();
$classes = array(
self::METHOD_PAYMENT_REQUEST => WCGatewayMoneiAppleGoogle::class,
self::METHOD_PAYPAL => WCGatewayMoneiPaypal::class,
);
foreach ( $classes as $method => $class_name ) {
$gateway = $container->get( $class_name );
if ( $gateway instanceof WCMoneiPaymentGateway ) {
self::$gateways[ $method ] = $gateway;
}
}
return self::$gateways;
}
/**
* The product the page is showing, for the product page express flow.
*
* @return array|null
*/
private function get_product_context() {
$product = wc_get_product();
if ( ! $product instanceof WC_Product ) {
return null;
}
return array(
'id' => $product->get_id(),
'isVariable' => $product->is_type( 'variable' ),
'purchasable' => $product->is_purchasable() && $product->is_in_stock(),
);
}
/**
* Express location of the current request, or null when express must not render.
*
* Block-rendered cart and checkout pages return null: their buttons come from the
* payment method registry, and loading the classic bundle there would mount a
* second set.
*
* @return string|null
*/
private function get_classic_location() {
if ( is_checkout() && ! is_checkout_pay_page() && ! is_add_payment_method_page() ) {
if ( self::current_page_has_block( 'woocommerce/checkout' ) ) {
return null;
}
return empty( self::get_enabled_methods( 'checkout' ) ) ? null : 'checkout';
}
if ( is_product() ) {
return empty( self::get_enabled_methods( 'product' ) ) ? null : 'product';
}
if ( is_cart() ) {
if ( self::current_page_has_block( 'woocommerce/cart' ) ) {
return null;
}
return empty( self::get_enabled_methods( 'cart' ) ) ? null : 'cart';
}
return null;
}
/**
* Whether the page being rendered right now contains a block.
*
* Deliberately not `WC_Blocks_Utils::has_block_in_page( wc_get_page_id( ... ) )`:
* that asks about the page configured in WooCommerce settings, so a store whose
* configured checkout is a block would answer "block" for a second, shortcode-based
* checkout page too, and the classic buttons would never load there.
*
* @param string $block Block name.
*
* @return bool
*/
private static function current_page_has_block( $block ) {
$post = get_post();
return $post instanceof WP_Post && has_block( $block, $post );
}
}