PluginProbe
MONEI Payments for WooCommerce / 7.1.3
MONEI Payments for WooCommerce v7.1.3
7.3.3 7.3.2 7.3.1 7.3.0 7.2.4 7.2.3 7.2.2 7.2.0 7.2.1 7.1.3 2.1.0 3.0.0 3.1.0 3.1.1 4.0.0 4.1.0 4.1.1 4.2.0 4.2.1 5.0 5.1.0 5.1.1 5.1.2 5.2.2 5.2.3 All 87 releases
monei / src / Services / express / ExpressCheckoutAssets.php

ExpressCheckoutAssets.php in MONEI Payments for WooCommerce 7.1.3, at src/Services/express/ExpressCheckoutAssets.php

446 lines 12.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Express checkout asset loading and classic markup.
4 *
5 * @package Monei
6 */
7
8 namespace Monei\Services\express;
9
10 use Monei\Core\ContainerProvider;
11 use Monei\Gateways\Abstracts\WCMoneiPaymentGateway;
12 use Monei\Gateways\PaymentMethods\WCGatewayMoneiAppleGoogle;
13 use Monei\Gateways\PaymentMethods\WCGatewayMoneiPaypal;
14 use Monei\Services\PaymentMethodsService;
15 use WC_AJAX;
16 use WC_Product;
17 use WP_Post;
18 use Exception;
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23
24 /**
25 * Loads the express checkout button assets and prints the mount containers on the
26 * classic (non-block) product, cart and checkout pages.
27 *
28 * The express surfaces span three pages, while `WCGatewayMoneiAppleGoogle::monei_scripts()`
29 * deliberately only loads on checkout. Widening that method would change when the
30 * regular Apple/Google Pay checkout assets load, so express gets its own service
31 * instead — the same shape as ExpressCheckoutAjaxHandler, bootstrapped from the
32 * plugin's `init()` because the container is lazy.
33 *
34 * Block surfaces load nothing from here: the Cart and Checkout blocks pull their
35 * script through the payment method registry, in MoneiAppleGoogleBlocksSupport.
36 */
37 class ExpressCheckoutAssets {
38
39 /**
40 * Handle of the classic express script.
41 */
42 const SCRIPT_HANDLE = 'monei-express-checkout';
43
44 /**
45 * Handle of the blocks express registration script.
46 */
47 const BLOCKS_SCRIPT_HANDLE = 'wc-monei-express-blocks-integration';
48
49 /**
50 * Name the blocks express method registers under. It must differ from the
51 * `monei_apple_google` entry the regular payment method already occupies, or the
52 * second registration silently replaces the first.
53 */
54 const BLOCKS_METHOD_NAME = 'monei_apple_google_express';
55
56 /**
57 * The wallet component each express gateway renders. These keys are the contract
58 * with the JavaScript: they name the component factory and the mount container.
59 */
60 const METHOD_PAYMENT_REQUEST = 'payment_request';
61 const METHOD_PAYPAL = 'paypal';
62
63 /**
64 * Express gateways by wallet component, or null before they are resolved.
65 *
66 * @var array<string, WCMoneiPaymentGateway>|null
67 */
68 private static $gateways = null;
69
70 /**
71 * Register the frontend hooks.
72 *
73 * @return void
74 */
75 public function init() {
76 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_classic_assets' ) );
77 add_action( 'woocommerce_before_checkout_form', array( $this, 'render_checkout_buttons' ), 5 );
78 add_action( 'woocommerce_proceed_to_checkout', array( $this, 'render_cart_buttons' ), 5 );
79 add_action( 'woocommerce_after_add_to_cart_form', array( $this, 'render_product_buttons' ) );
80 }
81
82 /**
83 * Loads the classic express bundle on the surfaces the merchant enabled.
84 *
85 * @return void
86 */
87 public function enqueue_classic_assets() {
88 $location = $this->get_classic_location();
89
90 if ( null === $location ) {
91 return;
92 }
93
94 wp_register_style(
95 'monei-express-checkout',
96 plugins_url( 'public/css/monei-express-checkout.css', MONEI_MAIN_FILE ),
97 array(),
98 MONEI_VERSION,
99 'all'
100 );
101 wp_enqueue_style( 'monei-express-checkout' );
102
103 if ( ! wp_script_is( 'monei', 'registered' ) ) {
104 wp_register_script( 'monei', 'https://js.monei.com/v3/monei.js', '', '3.0', true );
105 }
106 wp_enqueue_script( 'monei' );
107
108 wp_register_script(
109 self::SCRIPT_HANDLE,
110 plugins_url( 'public/js/monei-express-checkout.min.js', MONEI_MAIN_FILE ),
111 array( 'jquery', 'monei' ),
112 MONEI_VERSION,
113 true
114 );
115
116 wp_localize_script(
117 self::SCRIPT_HANDLE,
118 'wc_monei_express_params',
119 array_merge(
120 self::get_script_data(),
121 array(
122 'location' => $location,
123 'product' => 'product' === $location ? $this->get_product_context() : null,
124 )
125 )
126 );
127
128 wp_enqueue_script( self::SCRIPT_HANDLE );
129 }
130
131 /**
132 * Express button container above the classic checkout form.
133 *
134 * @return void
135 */
136 public function render_checkout_buttons() {
137 if ( 'checkout' !== $this->get_classic_location() ) {
138 return;
139 }
140
141 $this->render_container( 'checkout' );
142 }
143
144 /**
145 * Express button container above the classic cart's checkout button.
146 *
147 * @return void
148 */
149 public function render_cart_buttons() {
150 if ( 'cart' !== $this->get_classic_location() ) {
151 return;
152 }
153
154 $this->render_container( 'cart' );
155 }
156
157 /**
158 * Express button container below the add-to-cart form.
159 *
160 * @return void
161 */
162 public function render_product_buttons() {
163 if ( 'product' !== $this->get_classic_location() ) {
164 return;
165 }
166
167 $this->render_container( 'product' );
168 }
169
170 /**
171 * Prints the mount containers, one per wallet the merchant enabled at this surface.
172 *
173 * Everything starts hidden and the script reveals it only once a wallet reports
174 * itself supported, so an unavailable wallet leaves no gap and no dead control.
175 *
176 * @param string $location One of the express location keys.
177 *
178 * @return void
179 */
180 private function render_container( $location ) {
181 $methods = array_keys( self::get_enabled_methods( $location ) );
182
183 if ( empty( $methods ) ) {
184 return;
185 }
186 ?>
187 <div class="monei-express-checkout is-loading" data-monei-express-location="<?php echo esc_attr( $location ); ?>">
188 <div class="monei-express-checkout__title"><?php esc_html_e( 'Express checkout', 'monei' ); ?></div>
189 <?php foreach ( $methods as $method ) : ?>
190 <div class="monei-express-checkout__button" data-monei-express-method="<?php echo esc_attr( $method ); ?>"></div>
191 <?php endforeach; ?>
192 <div class="monei-express-checkout__error" role="alert"></div>
193 </div>
194 <?php
195 }
196
197 /**
198 * Everything the express scripts need that is identical on classic and blocks.
199 *
200 * Both wallets are described in one payload, and both blocks payment methods carry
201 * the same copy of it, so the express script works whichever gateway happened to
202 * put it on the page.
203 *
204 * @return array<string, mixed>
205 */
206 public static function get_script_data() {
207 $methods = array();
208 $account_id = '';
209
210 foreach ( self::get_express_gateways() as $method => $gateway ) {
211 $locations = array();
212
213 foreach ( array_keys( WCMoneiPaymentGateway::get_express_location_options() ) as $location ) {
214 $locations[ $location ] = $gateway->is_express_enabled_at( (string) $location );
215 }
216
217 $methods[ $method ] = array(
218 'locations' => $locations,
219 // PayPal takes different style keys from PaymentRequest — color, layout,
220 // size, shape, label — so each wallet carries its own.
221 'style' => json_decode( self::get_button_style( $gateway ) ),
222 // Whether the MONEI account offers this wallet at all. The blocks
223 // registry reserves a grid column per registered express method before
224 // any component mounts, so a wallet the account cannot serve has to be
225 // refused at registration; discovering it later through `onLoad` leaves
226 // an empty column behind that halves the width of its neighbour.
227 'available' => self::account_offers( $method ),
228 );
229
230 if ( '' === $account_id ) {
231 $account_id = (string) $gateway->getAccountId();
232 }
233 }
234
235 return array(
236 // The `%%endpoint%%` placeholder is how WooCommerce core itself hands a
237 // wc-ajax URL template to the browser, see wc_cart_fragments_params.
238 'ajaxUrl' => WC_AJAX::get_endpoint( '%%endpoint%%' ),
239 'accountId' => $account_id,
240 'currency' => get_woocommerce_currency(),
241 'language' => locale_iso_639_1_code(),
242 'methods' => $methods,
243 'i18n' => array(
244 'genericError' => __( 'Express checkout is unavailable right now. Please use the regular checkout.', 'monei' ),
245 ),
246 );
247 }
248
249 /**
250 * Registers the express script for the Cart and Checkout blocks.
251 *
252 * Called by both blocks payment methods, so express still loads when only one of
253 * the two gateways is on. Registering an existing handle twice is a no-op, and the
254 * handle is deduplicated when WooCommerce merges it into the block bundle.
255 *
256 * @return string Handle, or an empty string when no wallet has express on a block
257 * surface.
258 */
259 public static function register_blocks_script() {
260 if ( empty( self::get_enabled_methods( 'cart' ) ) && empty( self::get_enabled_methods( 'checkout' ) ) ) {
261 return '';
262 }
263
264 $handle = self::BLOCKS_SCRIPT_HANDLE;
265
266 wp_register_script(
267 $handle,
268 WC_Monei()->plugin_url() . '/public/js/monei-block-express-checkout.min.js',
269 array(
270 'wc-blocks-checkout',
271 'wc-blocks-registry',
272 'wc-settings',
273 'wp-data',
274 'wp-element',
275 'wp-i18n',
276 'monei',
277 ),
278 WC_Monei()->version,
279 true
280 );
281
282 if ( function_exists( 'wp_set_script_translations' ) ) {
283 wp_set_script_translations( $handle, 'monei', WC_Monei()->plugin_path() . '/languages' );
284 }
285
286 return $handle;
287 }
288
289 /**
290 * Express gateways that are enabled at a surface, keyed by wallet component.
291 *
292 * @param string $location One of the express location keys.
293 *
294 * @return array<string, WCMoneiPaymentGateway>
295 */
296 private static function get_enabled_methods( $location ) {
297 $enabled = array();
298
299 foreach ( self::get_express_gateways() as $method => $gateway ) {
300 if ( $gateway->is_express_enabled_at( $location ) ) {
301 $enabled[ $method ] = $gateway;
302 }
303 }
304
305 return $enabled;
306 }
307
308 /**
309 * @param WCMoneiPaymentGateway $gateway Express gateway.
310 *
311 * @return string
312 */
313 private static function get_button_style( WCMoneiPaymentGateway $gateway ) {
314 return (string) $gateway->get_option( 'express_button_style', $gateway::DEFAULT_EXPRESS_BUTTON_STYLE );
315 }
316
317 /**
318 * The gateways that expose express checkout, keyed by the wallet they render.
319 *
320 * Resolved on demand rather than injected, for the same reason the AJAX handler
321 * does it: this service is built during `init`, before WooCommerce assembles its
322 * payment gateways.
323 *
324 * @return array<string, WCMoneiPaymentGateway>
325 */
326 /**
327 * Whether the MONEI account can serve the wallet behind an express method.
328 *
329 * ⚠️ Fails open. A false negative hides a wallet the merchant has switched on and
330 * paid to enable; a false positive costs an empty column. So an unreadable answer
331 * — the API down, the account response cached empty — registers the method and
332 * lets `onLoad` sort it out, which is the behaviour this replaced.
333 *
334 * @param string $method Express method key.
335 *
336 * @return bool
337 */
338 private static function account_offers( $method ) {
339 try {
340 $methods = ContainerProvider::getContainer()->get( PaymentMethodsService::class );
341
342 if ( self::METHOD_PAYPAL === $method ) {
343 return $methods->isPaypalEnabled();
344 }
345
346 return $methods->isGoogleEnabled() || $methods->isAppleEnabled();
347 } catch ( Exception $e ) {
348 return true;
349 }
350 }
351
352 private static function get_express_gateways() {
353 if ( null !== self::$gateways ) {
354 return self::$gateways;
355 }
356
357 $container = ContainerProvider::getContainer();
358 self::$gateways = array();
359
360 $classes = array(
361 self::METHOD_PAYMENT_REQUEST => WCGatewayMoneiAppleGoogle::class,
362 self::METHOD_PAYPAL => WCGatewayMoneiPaypal::class,
363 );
364
365 foreach ( $classes as $method => $class_name ) {
366 $gateway = $container->get( $class_name );
367
368 if ( $gateway instanceof WCMoneiPaymentGateway ) {
369 self::$gateways[ $method ] = $gateway;
370 }
371 }
372
373 return self::$gateways;
374 }
375
376 /**
377 * The product the page is showing, for the product page express flow.
378 *
379 * @return array<string, mixed>|null
380 */
381 private function get_product_context() {
382 $product = wc_get_product();
383
384 if ( ! $product instanceof WC_Product ) {
385 return null;
386 }
387
388 return array(
389 'id' => $product->get_id(),
390 'isVariable' => $product->is_type( 'variable' ),
391 'purchasable' => $product->is_purchasable() && $product->is_in_stock(),
392 );
393 }
394
395 /**
396 * Express location of the current request, or null when express must not render.
397 *
398 * Block-rendered cart and checkout pages return null: their buttons come from the
399 * payment method registry, and loading the classic bundle there would mount a
400 * second set.
401 *
402 * @return string|null
403 */
404 private function get_classic_location() {
405 if ( is_checkout() && ! is_checkout_pay_page() && ! is_add_payment_method_page() ) {
406 if ( self::current_page_has_block( 'woocommerce/checkout' ) ) {
407 return null;
408 }
409
410 return empty( self::get_enabled_methods( 'checkout' ) ) ? null : 'checkout';
411 }
412
413 if ( is_product() ) {
414 return empty( self::get_enabled_methods( 'product' ) ) ? null : 'product';
415 }
416
417 if ( is_cart() ) {
418 if ( self::current_page_has_block( 'woocommerce/cart' ) ) {
419 return null;
420 }
421
422 return empty( self::get_enabled_methods( 'cart' ) ) ? null : 'cart';
423 }
424
425 return null;
426 }
427
428 /**
429 * Whether the page being rendered right now contains a block.
430 *
431 * Deliberately not `WC_Blocks_Utils::has_block_in_page( wc_get_page_id( ... ) )`:
432 * that asks about the page configured in WooCommerce settings, so a store whose
433 * configured checkout is a block would answer "block" for a second, shortcode-based
434 * checkout page too, and the classic buttons would never load there.
435 *
436 * @param string $block Block name.
437 *
438 * @return bool
439 */
440 private static function current_page_has_block( $block ) {
441 $post = get_post();
442
443 return $post instanceof WP_Post && has_block( $block, $post );
444 }
445 }
446