|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 ); } }