PluginProbe
MultiSafepay plugin for WooCommerce / trunk
MultiSafepay plugin for WooCommerce vtrunk
6.11.1 6.12.0 6.13.0 6.2.0 6.2.1 6.3.0 6.3.1 6.4.0 6.4.1 6.4.2 6.4.3 6.5.0 6.5.1 6.6.0 6.6.1 6.6.2 6.7.0 6.7.1 6.7.2 6.7.3 6.8.0 6.8.1 6.8.2 6.8.3 6.9.0 All 84 releases
multisafepay / src / PaymentMethods / Base / BasePaymentMethod.php

BasePaymentMethod.php in MultiSafepay plugin for WooCommerce trunk, at src/PaymentMethods/Base/BasePaymentMethod.php

1,359 lines 59.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php declare( strict_types=1 );
2
3 namespace MultiSafepay\WooCommerce\PaymentMethods\Base;
4
5 use MultiSafepay\Api\PaymentMethods\PaymentMethod;
6 use MultiSafepay\Api\Transactions\TransactionResponse;
7 use MultiSafepay\Exception\ApiException;
8 use MultiSafepay\Exception\InvalidArgumentException;
9 use MultiSafepay\WooCommerce\Services\BlocksPaymentDataService;
10 use MultiSafepay\WooCommerce\Services\OrderService;
11 use MultiSafepay\WooCommerce\Services\PaymentComponentService;
12 use MultiSafepay\WooCommerce\Services\PaymentMethodService;
13 use MultiSafepay\WooCommerce\Services\SdkService;
14 use MultiSafepay\WooCommerce\Services\Blocks\BlocksContextService;
15 use MultiSafepay\WooCommerce\Utils\Logger;
16 use MultiSafepay\WooCommerce\Utils\PaymentMethodTitleBuilder;
17 use Psr\Http\Client\ClientExceptionInterface;
18 use Throwable;
19 use WC_Countries;
20 use WC_Order;
21 use WC_Payment_Gateway;
22
23 /**
24 * Class BasePaymentMethod
25 *
26 * @package MultiSafepay\WooCommerce\PaymentMethods\Base
27 */
28 class BasePaymentMethod extends WC_Payment_Gateway {
29
30 use BaseRefunds;
31
32 public const TRANSACTION_TYPE_DIRECT = 'direct';
33 public const TRANSACTION_TYPE_REDIRECT = 'redirect';
34
35 public const GOOGLEPAY_TEST_MERCHANT_ID = '12345678901234567890';
36 public const GOOGLEPAY_TEST_MERCHANT_NAME = 'Example Merchant';
37 public const APPLEPAY_TEST_MERCHANT_NAME = 'Example Merchant';
38
39 public const DIRECT_PAYMENT_METHODS_WITHOUT_COMPONENTS = array(
40 'BANKTRANS',
41 );
42
43 public const MULTISAFEPAY_COMPONENT_JS_URL = 'https://pay.multisafepay.com/sdk/components/v2/components.js';
44 public const MULTISAFEPAY_COMPONENT_CSS_URL = 'https://pay.multisafepay.com/sdk/components/v2/components.css';
45
46 public const NOT_ALLOW_REFUND_ORDER_STATUSES = array(
47 'pending',
48 'on-hold',
49 'failed',
50 );
51
52 public const NOT_ALLOW_REFUND_PAYMENT_METHODS = array(
53 'MULTIBANCO',
54 );
55
56 /**
57 * A PaymentMethod object with the information of the payment method object
58 *
59 * @var PaymentMethod
60 */
61 protected $payment_method;
62
63 /**
64 * What type of transaction should be 'direct' or 'redirect'
65 *
66 * @var string
67 */
68 protected $type;
69
70 /**
71 * The MultiSafepay gateway code.
72 *
73 * @var string
74 */
75 protected $gateway_code;
76
77 /**
78 * The minimum amount for the payment method
79 *
80 * @var string
81 */
82 public $min_amount;
83
84 /**
85 * A custom initialized order status for this payment method
86 *
87 * @var string
88 */
89 public $initial_order_status;
90
91 /**
92 * User Roles
93 *
94 * @var array
95 */
96 public $user_roles;
97
98 /**
99 * If supports the payment component
100 *
101 * @var bool
102 */
103 public $payment_component = false;
104
105 /**
106 * Merchant name for Google Pay and Apple Pay
107 *
108 * @var string
109 */
110 public $merchant_name = '';
111
112 /**
113 * Merchant ID for Google Pay
114 *
115 * @var string
116 */
117 public $merchant_id = '';
118
119 /**
120 * Is WooCommerce checkout blocks active?
121 *
122 * @var bool
123 */
124 private $is_checkout_blocks;
125
126 /**
127 * @var Logger
128 */
129 private $logger;
130
131 /**
132 * BasePaymentMethod constructor.
133 *
134 * @param PaymentMethod $payment_method
135 * @param Logger|null $logger
136 */
137 public function __construct( PaymentMethod $payment_method, ?Logger $logger = null ) {
138 $this->logger = $logger ?? new Logger();
139 $this->payment_method = $payment_method;
140 $this->supports = array( 'products', 'refunds' );
141 $this->id = $this->get_payment_method_id();
142
143 $this->is_checkout_blocks = $this->is_woocommerce_checkout_block_active();
144
145 // Classic payment-component assets must always be registered, because the
146 // "Pay for order" endpoint (form-pay.php) is rendered with the classic
147 // template even when the regular Checkout page uses the WooCommerce
148 // Checkout block. The enqueue callbacks themselves are responsible for
149 // skipping the standard checkout when Blocks is active to avoid
150 // conflicting with the Blocks integration.
151 if ( $this->is_payment_component_enabled() ) {
152 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_payment_component_styles' ) );
153 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_payment_component_scripts' ) );
154 }
155 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_multisafepay_scripts_by_gateway_code' ) );
156
157 $this->type = $this->get_payment_method_type();
158 $this->method_title = $this->get_payment_method_title();
159 $this->method_description = $this->get_payment_method_description();
160 $this->gateway_code = $this->get_payment_method_gateway_code();
161 $this->has_fields = $this->has_fields();
162 $this->icon = $this->get_logo();
163 $this->form_fields = $this->add_form_fields();
164
165 // Init form fields and load the settings.
166 $this->init_form_fields();
167 $this->init_settings();
168
169 $this->enabled = $this->get_option( 'enabled', 'yes' );
170 $this->title = $this->get_option( 'title', $this->get_method_title() );
171 $this->description = $this->get_option( 'description' );
172 $this->max_amount = $this->get_option( 'max_amount' );
173 $this->min_amount = $this->get_option( 'min_amount' );
174 $this->user_roles = (array) $this->get_option( 'user_roles', array() );
175 $this->countries = $this->get_option( 'countries' );
176 $this->initial_order_status = $this->get_option( 'initial_order_status', false );
177 $this->payment_component = $this->is_payment_component_enabled();
178 $this->merchant_name = $this->get_option( 'merchant_name', false );
179 $this->merchant_id = $this->get_option( 'merchant_id', false );
180 $this->errors = array();
181
182 add_action(
183 'woocommerce_update_options_payment_gateways_' . $this->id,
184 array(
185 $this,
186 'process_admin_options',
187 )
188 );
189 add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'display_errors' ) );
190 }
191
192 /**
193 * @return string
194 */
195 public function get_payment_method_id(): string {
196 return PaymentMethodService::get_legacy_woocommerce_payment_gateway_ids( $this->payment_method->getId() );
197 }
198
199 /**
200 * @return string
201 */
202 public function get_payment_method_gateway_code(): string {
203 return $this->payment_method->getId();
204 }
205
206 /**
207 * Get the defined direct payment methods without components
208 *
209 * @return array
210 */
211 public function get_defined_direct_payment_methods_without_components(): array {
212 return apply_filters( 'multisafepay_direct_payment_methods_without_components', self::DIRECT_PAYMENT_METHODS_WITHOUT_COMPONENTS );
213 }
214
215 /**
216 * Get the custom payment method type
217 *
218 * @return bool
219 */
220 public function is_payment_method_type_direct(): bool {
221 return (bool) $this->get_option( 'direct_transaction', '0' ) ||
222 (bool) $this->get_option( 'use_direct_button', '0' );
223 }
224
225 /**
226 * Checks if the admin is editing the checkout page in the admin area
227 *
228 * This identifies Checkout block editor context in wp-admin, so Blocks gateway
229 * registration can rely on enabled settings instead of runtime availability.
230 *
231 * @return bool
232 *
233 * @phpcs:disable WordPress.Security.NonceVerification.Recommended
234 */
235 public function admin_editing_checkout_page(): bool {
236 // "null" as default value to avoid matching a potential page with id 0, or false
237 $page_post_id = isset( $_GET['post'] ) ? (int) $_GET['post'] : null;
238 $checkout_id = (int) get_option( 'woocommerce_checkout_page_id', false );
239 $page_action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '';
240
241 return $page_post_id &&
242 ( $page_post_id === $checkout_id ) &&
243 ( 'edit' === $page_action ) &&
244 is_admin() &&
245 current_user_can( 'edit_pages' );
246 }
247
248 /**
249 * Get the payment method type
250 *
251 * @return string
252 */
253 public function get_payment_method_type(): string {
254 // Converting to direct the transaction type for iDEAL
255 if ( $this->is_ideal_2_0() ) {
256 return self::TRANSACTION_TYPE_DIRECT;
257 }
258
259 return $this->is_payment_method_type_direct() ||
260 $this->is_payment_component_enabled()
261 ? self::TRANSACTION_TYPE_DIRECT
262 : self::TRANSACTION_TYPE_REDIRECT;
263 }
264
265 /**
266 * Get the status of Google Pay or Apple Pay direct button
267 *
268 * @param string $payment_method
269 *
270 * @return string
271 */
272 public function get_google_apple_pay_use_button( string $payment_method ): string {
273 $settings = get_option( 'woocommerce_multisafepay_' . $payment_method . '_settings' );
274 if ( is_array( $settings ) && isset( $settings['use_direct_button'] ) ) {
275 return '1' === $settings['use_direct_button'] ? self::TRANSACTION_TYPE_DIRECT : self::TRANSACTION_TYPE_REDIRECT;
276 }
277 return self::TRANSACTION_TYPE_REDIRECT;
278 }
279
280 /**
281 * @return string
282 */
283 public function get_payment_method_title(): string {
284 return $this->payment_method->getName();
285 }
286
287 /**
288 * @return string
289 */
290 public function get_payment_method_description(): string {
291 return sprintf(
292 /* translators: %2$: The payment method title */
293 __( 'Read more about %2$s on <a href="%1$s" target="_blank">MultiSafepay\'s Docs</a>.', 'multisafepay' ),
294 'https://docs.multisafepay.com',
295 $this->get_payment_method_title()
296 );
297 }
298
299 /**
300 * @return string
301 */
302 public function get_payment_method_icon(): string {
303 return $this->payment_method->getLargeIconUrl();
304 }
305
306 /**
307 * @return string
308 */
309 protected function get_logo(): string {
310 return $this->get_payment_method_icon();
311 }
312
313 /**
314 * @return bool
315 */
316 public function has_fields(): bool {
317 if ( $this->is_payment_component_enabled() ) {
318 return true;
319 }
320
321 return false;
322 }
323
324 /**
325 * Return if the tokenization card on file is enabled.
326 *
327 * @return bool
328 */
329 public function is_tokenization_enabled(): bool {
330 $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'tokenization' => 'no' ) );
331 if ( ! isset( $settings['tokenization'] ) ) {
332 return false;
333 }
334 return 'yes' === $settings['tokenization'];
335 }
336
337 /**
338 * Return if the payment component is enabled.
339 *
340 * @return bool
341 */
342 public function is_payment_component_enabled(): bool {
343 if ( ! $this->payment_method->supportsPaymentComponent() ) {
344 return false;
345 }
346
347 if ( $this->is_ideal_2_0() ) {
348 $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'tokenization' => 'yes' ) );
349 if ( ! isset( $settings['tokenization'] ) ) {
350 return true;
351 }
352 return 'yes' === $settings['tokenization'];
353 }
354
355 $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'yes' ) );
356 if ( ! isset( $settings['payment_component'] ) ) {
357 return true;
358 }
359
360 if ( $this->is_qr_enabled() || $this->is_qr_only_enabled() ) {
361 return true;
362 }
363
364 return 'yes' === $settings['payment_component'];
365 }
366
367 /**
368 * Return if QR is enabled for payment components.
369 *
370 * @return bool
371 */
372 public function is_qr_enabled(): bool {
373 if ( ! $this->payment_method->supportsQr() ) {
374 return false;
375 }
376
377 $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'yes' ) );
378 if ( ! isset( $settings['payment_component'] ) ) {
379 return false;
380 }
381 return 'qr' === $settings['payment_component'];
382 }
383
384 /**
385 * Return if QR only is enabled for payment components.
386 *
387 * @return bool
388 */
389 public function is_qr_only_enabled(): bool {
390 if ( ! $this->payment_method->supportsQr() ) {
391 return false;
392 }
393
394 $settings = get_option( 'woocommerce_' . $this->id . '_settings', array( 'payment_component' => 'yes' ) );
395 if ( ! isset( $settings['payment_component'] ) ) {
396 return false;
397 }
398 return 'qr_only' === $settings['payment_component'];
399 }
400
401 /**
402 * Return the custom QR width in pixels
403 *
404 * @return string
405 */
406 public function get_qr_width(): string {
407 return $this->get_option( 'qr_width', '' );
408 }
409
410 /**
411 * Enqueue JavaScript related with Payment Component.
412 *
413 * @return void
414 */
415 public function enqueue_payment_component_scripts(): void {
416 if ( ! $this->should_enqueue_classic_assets() ) {
417 return;
418 }
419
420 if ( is_checkout() || is_wc_endpoint_url( 'order-pay' ) ) {
421
422 wp_enqueue_script( 'multisafepay-payment-component-script', self::MULTISAFEPAY_COMPONENT_JS_URL, array(), MULTISAFEPAY_PLUGIN_VERSION, true );
423
424 $multisafepay_payment_component_config = ( new PaymentComponentService() )->get_payment_component_arguments( $this );
425 $gateways_with_payment_component = ( new PaymentMethodService() )->get_woocommerce_payment_gateway_ids_with_payment_component_support();
426
427 $route = MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-payment-component.js';
428 wp_enqueue_script( 'multisafepay-payment-component-js', $route, array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
429
430 static $enabled_gateways_added = false;
431 if ( ! $enabled_gateways_added ) {
432 wp_localize_script( 'multisafepay-payment-component-js', 'multisafepay_payment_component_gateways', $gateways_with_payment_component );
433 $enabled_gateways_added = true;
434 }
435 wp_localize_script( 'multisafepay-payment-component-js', 'payment_component_config_' . $this->id, $multisafepay_payment_component_config );
436 wp_enqueue_script( 'multisafepay-payment-component-js' );
437 }
438 }
439
440 /**
441 * Whether the classic (non-Blocks) payment-component assets should be
442 * enqueued for the current request.
443 *
444 * The "Pay for order" endpoint is always rendered by the classic
445 * `form-pay.php` template, so the classic assets are required there even
446 * when the regular Checkout page uses the WooCommerce Checkout block.
447 * In every other context, when Blocks is active for the Checkout page,
448 * the dedicated Blocks integration is responsible for loading its own
449 * assets, and the classic ones must not be loaded to avoid conflicts.
450 *
451 * @return bool
452 */
453 private function should_enqueue_classic_assets(): bool {
454 if ( function_exists( 'is_wc_endpoint_url' ) && is_wc_endpoint_url( 'order-pay' ) ) {
455 return true;
456 }
457
458 return ! $this->is_checkout_blocks;
459 }
460
461 /**
462 * Check if the woocommerce checkout block is active.
463 *
464 * @return bool
465 */
466 private function is_woocommerce_checkout_block_active(): bool {
467 $checkout_page_id = wc_get_page_id( 'checkout' );
468 $checkout_page = get_post( $checkout_page_id );
469 $has_block = ( new BlocksContextService() )->is_checkout_blocks_active();
470
471 // Fallback: WC_Blocks_Utils::has_block_in_page() doesn't detect nested blocks (e.g. inside columns).
472 // If not detected, check directly in post-content.
473 if ( ! $has_block && $checkout_page ) {
474 $has_block = strpos( $checkout_page->post_content, 'wp:woocommerce/checkout' ) !== false;
475 }
476
477 return $has_block;
478 }
479
480 /**
481 * Enqueue JavaScript related with a MultiSafepay Payment Method.
482 *
483 * @return void
484 */
485 public function enqueue_multisafepay_scripts_by_gateway_code(): void {
486 if ( ! $this->should_enqueue_classic_assets() ) {
487 return;
488 }
489
490 $gateway_code = $this->get_payment_method_gateway_code();
491
492 if ( ( 'APPLEPAY' === $gateway_code ) && ( ( $this->get_google_apple_pay_use_button( 'applepay' ) === self::TRANSACTION_TYPE_REDIRECT ) || is_wc_endpoint_url( 'order-pay' ) ) ) {
493 wp_enqueue_script( 'multisafepay-apple-pay-js', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-apple-pay.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
494 }
495
496 if ( ( 'GOOGLEPAY' === $gateway_code ) && ( ( $this->get_google_apple_pay_use_button( 'googlepay' ) === self::TRANSACTION_TYPE_REDIRECT ) || is_wc_endpoint_url( 'order-pay' ) ) ) {
497 wp_enqueue_script( 'multisafepay-google-pay-js', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-google-pay.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
498 }
499
500 // Static variable to track if the payment variables have been added
501 static $payment_variables_for_applepay_added = false;
502 static $payment_variables_for_googlepay_added = false;
503
504 if ( is_checkout() && ! is_wc_endpoint_url( 'order-pay' ) ) {
505 if ( ( 'APPLEPAY' === $gateway_code ) && ! $payment_variables_for_applepay_added && ( $this->get_google_apple_pay_use_button( 'applepay' ) === self::TRANSACTION_TYPE_DIRECT ) ) {
506 wp_enqueue_script( 'multisafepay-apple-pay-wallet', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-apple-pay-wallet.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
507 $payment_variables = $this->build_applepay_wallet_variables( self::APPLEPAY_TEST_MERCHANT_NAME ) ?? '';
508 wp_add_inline_script( 'multisafepay-apple-pay-wallet', $payment_variables, 'before' );
509 // Mark that the payment variables have been added
510 $payment_variables_for_applepay_added = true;
511 }
512
513 if ( 'GOOGLEPAY' === $gateway_code ) {
514 wp_enqueue_script( 'google-pay-js', 'https://pay.google.com/gp/p/js/pay.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
515 if ( ! $payment_variables_for_googlepay_added && ( $this->get_google_apple_pay_use_button( 'googlepay' ) === self::TRANSACTION_TYPE_DIRECT ) ) {
516 wp_enqueue_script( 'multisafepay-google-pay-wallet', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-google-pay-wallet.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
517 $payment_variables = $this->build_googlepay_wallet_variables( self::GOOGLEPAY_TEST_MERCHANT_ID, self::GOOGLEPAY_TEST_MERCHANT_NAME ) ?? '';
518 wp_add_inline_script( 'multisafepay-google-pay-wallet', $payment_variables, 'before' );
519 // Mark that the payment variables have been added
520 $payment_variables_for_googlepay_added = true;
521 }
522 }
523
524 if (
525 ( ! $payment_variables_for_googlepay_added || ! $payment_variables_for_applepay_added ) &&
526 ( ( 'GOOGLEPAY' === $gateway_code ) || ( 'APPLEPAY' === $gateway_code ) )
527 ) {
528 wp_enqueue_script( 'multisafepay-validator-wallets', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-validator-wallets.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
529 wp_enqueue_script( 'multisafepay-common-wallets', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-common-wallets.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
530 // Add parameters for validator wallets
531 wp_localize_script(
532 'multisafepay-validator-wallets',
533 'multisafepayParams',
534 array(
535 'location' => admin_url( 'admin-ajax.php' ),
536 'nonce' => wp_create_nonce( 'multisafepay_validator_nonce' ),
537 )
538 );
539
540 $admin_url_array = array(
541 'location' => admin_url( 'admin-ajax.php' ),
542 'nonce' => wp_create_nonce( 'total_price_nonce' ),
543 );
544 wp_localize_script( 'multisafepay-common-wallets', 'configAdminUrlAjax', $admin_url_array );
545 wp_enqueue_script( 'multisafepay-jquery-wallets', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-jquery-wallets.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
546 }
547 }
548
549 // On the order-pay endpoint, also load wallet direct scripts so that
550 // Google Pay / Apple Pay direct buttons work for pending-payment orders.
551 if ( is_wc_endpoint_url( 'order-pay' ) && ( ( 'GOOGLEPAY' === $gateway_code ) || ( 'APPLEPAY' === $gateway_code ) ) ) {
552 $this->enqueue_wallet_direct_scripts_for_order_pay( $gateway_code, $payment_variables_for_googlepay_added, $payment_variables_for_applepay_added );
553 }
554 }
555
556 /**
557 * Enqueue wallet direct scripts (Google Pay / Apple Pay) specifically for
558 * the order-pay endpoint. The order-pay page uses a fixed order total
559 * instead of the cart total and does not have customer_details fields.
560 *
561 * @param string $gateway_code The gateway code (GOOGLEPAY or APPLEPAY).
562 * @param bool $payment_variables_for_googlepay_added Whether Google Pay variables were already added.
563 * @param bool $payment_variables_for_applepay_added Whether Apple Pay variables were already added.
564 *
565 * @return void
566 */
567 private function enqueue_wallet_direct_scripts_for_order_pay( string $gateway_code, bool $payment_variables_for_googlepay_added, bool $payment_variables_for_applepay_added ): void {
568 // Use static variables to prevent adding inline scripts more than once,
569 // since wp_add_inline_script() appends content each time it is called.
570 static $order_pay_googlepay_inline_added = false;
571 static $order_pay_applepay_inline_added = false;
572
573 // Load the supporting scripts (validator, common utilities, jQuery handler).
574 wp_enqueue_script( 'multisafepay-validator-wallets', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-validator-wallets.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
575 wp_enqueue_script( 'multisafepay-common-wallets', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-common-wallets.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
576
577 wp_localize_script(
578 'multisafepay-validator-wallets',
579 'multisafepayParams',
580 array(
581 'location' => admin_url( 'admin-ajax.php' ),
582 'nonce' => wp_create_nonce( 'multisafepay_validator_nonce' ),
583 )
584 );
585
586 $admin_url_array = array(
587 'location' => admin_url( 'admin-ajax.php' ),
588 'nonce' => wp_create_nonce( 'total_price_nonce' ),
589 );
590 wp_localize_script( 'multisafepay-common-wallets', 'configAdminUrlAjax', $admin_url_array );
591 wp_enqueue_script( 'multisafepay-jquery-wallets', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-jquery-wallets.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
592
593 // Apple Pay wallet script.
594 if ( ( 'APPLEPAY' === $gateway_code ) && ! $payment_variables_for_applepay_added && ! $order_pay_applepay_inline_added ) {
595 wp_enqueue_script( 'multisafepay-apple-pay-wallet', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-apple-pay-wallet.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
596 $payment_variables = $this->build_applepay_wallet_variables( self::APPLEPAY_TEST_MERCHANT_NAME ) ?? '';
597 wp_add_inline_script( 'multisafepay-apple-pay-wallet', $payment_variables, 'before' );
598 $order_pay_applepay_inline_added = true;
599 }
600
601 // Google Pay wallet script.
602 if ( 'GOOGLEPAY' === $gateway_code ) {
603 wp_enqueue_script( 'google-pay-js', 'https://pay.google.com/gp/p/js/pay.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
604 if ( ! $payment_variables_for_googlepay_added && ! $order_pay_googlepay_inline_added ) {
605 wp_enqueue_script( 'multisafepay-google-pay-wallet', MULTISAFEPAY_PLUGIN_URL . '/assets/public/js/multisafepay-google-pay-wallet.js', array( 'jquery' ), MULTISAFEPAY_PLUGIN_VERSION, true );
606 $payment_variables = $this->build_googlepay_wallet_variables( self::GOOGLEPAY_TEST_MERCHANT_ID, self::GOOGLEPAY_TEST_MERCHANT_NAME ) ?? '';
607 wp_add_inline_script( 'multisafepay-google-pay-wallet', $payment_variables, 'before' );
608 $order_pay_googlepay_inline_added = true;
609 }
610 }
611 }
612
613 /**
614 * @return array
615 */
616 public function add_form_fields(): array {
617 $form_fields = array(
618 'enabled' => array(
619 'title' => __( 'Enable/Disable', 'multisafepay' ),
620 'label' => 'Enable ' . $this->get_method_title() . ' Gateway',
621 'type' => 'checkbox',
622 'default' => 'no',
623 ),
624 'title' => array(
625 'title' => __( 'Title', 'multisafepay' ),
626 'type' => 'text',
627 'desc_tip' => __( 'This controls the title which the user sees during checkout.', 'multisafepay' ),
628 'default' => $this->get_method_title(),
629 ),
630 'description' => array(
631 'title' => __( 'Description', 'multisafepay' ),
632 'type' => 'textarea',
633 'desc_tip' => __( 'This controls the description which the user sees during checkout.', 'multisafepay' ),
634 'default' => '',
635 ),
636 'initial_order_status' => array(
637 'title' => __( 'Initial Order Status', 'multisafepay' ),
638 'type' => 'select',
639 'options' => $this->get_order_statuses(),
640 'desc_tip' => __( 'Initial order status for this payment method.', 'multisafepay' ),
641 'default' => 'wc-default',
642 ),
643 'min_amount' => array(
644 'title' => __( 'Min Amount', 'multisafepay' ),
645 'type' => 'decimal',
646 'desc_tip' => __( 'This payment method is not shown in the checkout if the order total is lower than the defined amount. Leave blank for no restrictions.', 'multisafepay' ),
647 'default' => ( (int) $this->payment_method->getMinAmount() / 100 ),
648 'value' => (float) $this->get_option( 'min_amount', ( (int) $this->payment_method->getMinAmount() / 100 ) ),
649 ),
650 'max_amount' => array(
651 'title' => __( 'Max Amount', 'multisafepay' ),
652 'type' => 'decimal',
653 'desc_tip' => __( 'This payment method is not shown in the checkout if the order total exceeds a certain amount. Leave blank for no restrictions.', 'multisafepay' ),
654 'default' => $this->payment_method->getMaxAmount() ? ( (int) $this->payment_method->getMaxAmount() / 100 ) : '',
655 'value' => (float) $this->get_option( 'max_amount', ( $this->payment_method->getMaxAmount() ? ( (int) $this->payment_method->getMaxAmount() / 100 ) : '' ) ),
656 ),
657 'countries' => array(
658 'title' => __( 'Country', 'multisafepay' ),
659 'type' => 'multiselect',
660 'description' => __( 'If you select one or more countries, this payment method will be shown in the checkout page, if the payment address`s country of the customer match with the selected values. Leave blank for no restrictions.', 'multisafepay' ),
661 'desc_tip' => __( 'For most operating system and configurations, you must hold Ctrl or Cmd in your keyboard, while you click in the options to select more than one value.', 'multisafepay' ),
662 'options' => $this->get_countries(),
663 'default' => $this->get_option( 'countries', array() ),
664 ),
665 'user_roles' => array(
666 'title' => __( 'User Roles', 'multisafepay' ),
667 'type' => 'multiselect',
668 'description' => __( 'If you select one or more user roles, this payment method will be shown in the checkout page, if the user rules of the customer match with the selected values. Leave blank for no restrictions.', 'multisafepay' ),
669 'desc_tip' => __( 'For most operating system and configurations, you must hold Ctrl or Cmd in your keyboard, while you click in the options to select more than one value.', 'multisafepay' ),
670 'options' => $this->get_user_roles(),
671 'default' => $this->get_option( 'user_roles', array() ),
672 ),
673 );
674
675 if ( 'APPLEPAY' === $this->get_payment_method_gateway_code() ) {
676 $form_fields['use_direct_button'] = array(
677 'title' => __( 'Use Apple Pay Direct Button', 'multisafepay' ),
678 'type' => 'select',
679 'options' => array(
680 '0' => 'Disabled',
681 '1' => 'Enabled',
682 ),
683 'description' => __( 'If you enable this, the place order button on the checkout page will be replaced by the Apple Pay button when this method is selected. We strongly recommend you test this feature carefully before enabled. More information about Apple Pay Direct on <a href="https://docs.multisafepay.com/docs/apple-pay-direct" target="_blank">MultiSafepay\'s Documentation Center</a>', 'multisafepay' ),
684 'desc_tip' => __( 'Enable this feature, to replace the place order button on the checkout page with the Apple Pay button, when the customer selects this one.', 'multisafepay' ),
685 'default' => '0',
686 );
687 $form_fields['merchant_name'] = array(
688 'title' => __( 'Apple Pay Merchant Name', 'multisafepay' ),
689 'type' => 'text',
690 'desc_tip' => __( 'Field required by Apple Pay direct transactions.', 'multisafepay' ),
691 'default' => '',
692 );
693 }
694
695 if ( in_array( $this->get_payment_method_gateway_code(), $this->get_defined_direct_payment_methods_without_components(), true ) ) {
696 $form_fields['direct_transaction'] = array(
697 'title' => __( 'Transaction Type', 'multisafepay' ),
698 'type' => 'select',
699 'options' => array(
700 '0' => 'Redirect',
701 '1' => 'Direct',
702 ),
703 'desc_tip' => __( 'If enabled, the consumer receives an e-mail with payment details, and no extra information is required during checkout.', 'multisafepay' ),
704 'default' => '0',
705 );
706 }
707
708 if ( 'GOOGLEPAY' === $this->get_payment_method_gateway_code() ) {
709 $form_fields['use_direct_button'] = array(
710 'title' => __( 'Use Google Pay Direct Button', 'multisafepay' ),
711 'type' => 'select',
712 'options' => array(
713 '0' => 'Disabled',
714 '1' => 'Enabled',
715 ),
716 'description' => __( 'If you enable this, the place order button on the checkout page will be replaced by the Google Pay button when this method is selected. We strongly recommend you test this feature carefully before enabled. More information about Google Pay Direct on <a href="https://docs.multisafepay.com/docs/google-pay-direct" target="_blank">MultiSafepay\'s Documentation Center</a>', 'multisafepay' ),
717 'desc_tip' => __( 'Enable this feature, to replace the place order button on the checkout page with the Google Pay button, when the customer selects this one.', 'multisafepay' ),
718 'default' => '0',
719 );
720 $form_fields['merchant_name'] = array(
721 'title' => __( 'Google Merchant Name', 'multisafepay' ),
722 'type' => 'text',
723 'desc_tip' => __( 'Field required by Google Pay direct transactions.', 'multisafepay' ),
724 'default' => '',
725 );
726 $form_fields['merchant_id'] = array(
727 'title' => __( 'Google Merchant ID', 'multisafepay' ),
728 'type' => 'text',
729 'desc_tip' => __( 'Field required by Google Pay direct transactions.', 'multisafepay' ),
730 'default' => '',
731 );
732 }
733
734 if ( $this->payment_method->supportsPaymentComponent() && ! $this->payment_method->supportsQr() && ! $this->is_ideal_2_0() ) {
735 $form_fields['payment_component'] = array(
736 'title' => __( 'Payment Type', 'multisafepay' ),
737 'type' => 'select',
738 'options' => array(
739 'no' => __( 'Redirect', 'multisafepay' ),
740 'yes' => __( 'Payment component', 'multisafepay' ),
741 ),
742 'description' => __( 'Redirect - Redirect the customer to a payment page to finish the payment. <br /> Payment Component - Payment components let you embed payment checkout fields directly into your checkout. <br /><br /> More information about Payment Components on <a href="https://docs.multisafepay.com/docs/payment-components" target="_blank">MultiSafepay\'s Documentation Center</a>.', 'multisafepay' ),
743 'default' => $this->get_option( 'payment_component', $this->payment_method->supportsPaymentComponent() ? 'yes' : 'no' ),
744 'value' => $this->get_option( 'payment_component', $this->payment_method->supportsPaymentComponent() ? 'yes' : 'no' ),
745 );
746
747 if ( 'BILLINK' === $this->get_payment_method_gateway_code() ) {
748 $form_fields['payment_component']['options']['P'] = __( 'B2C Only', 'multisafepay' );
749 $form_fields['payment_component']['options']['B'] = __( 'B2B Only', 'multisafepay' );
750 $form_fields['payment_component']['description'] .= __( '<br /><br /> B2C Only - For individual customers. <br /> B2B Only - For business customers. <br /><br /> Select the appropriate contract type based on your target customer base.', 'multisafepay' );
751 }
752 }
753
754 if ( $this->payment_method->supportsPaymentComponent() && $this->payment_method->supportsQr() ) {
755 $form_fields['payment_component'] = array(
756 'title' => __( 'Payment Type', 'multisafepay' ),
757 'type' => 'select',
758 'options' => array(
759 'no' => __( 'Redirect', 'multisafepay' ),
760 'yes' => __( 'Payment component', 'multisafepay' ),
761 'qr' => __( 'Payment component with QR', 'multisafepay' ),
762 'qr_only' => __( 'Payment component with QR only', 'multisafepay' ),
763 ),
764 'description' => __( 'Redirect - Redirect the customer to a payment page to finish the payment. <br /> Payment Component - Payment components let you embed payment checkout fields directly into your checkout. <br /> Payment Component with QR (*) – Similar to the previous option, but now includes the ability to pay using a QR code too. <br /> Payment Component with QR only (*) – Payment can only be completed via a QR code. <br /><br /> (*) Payment Components using QR are an experimental feature and may not work as expected. Contact MultiSafepay support for more information. <br /><br /> More information about Payment Components on <a href="https://docs.multisafepay.com/docs/payment-components" target="_blank">MultiSafepay\'s Documentation Center</a>.', 'multisafepay' ),
765 'default' => $this->get_option( 'payment_component', $this->payment_method->supportsPaymentComponent() ? 'yes' : 'no' ),
766 'value' => $this->get_option( 'payment_component', $this->payment_method->supportsPaymentComponent() ? 'yes' : 'no' ),
767 );
768 $form_fields['qr_width'] = array(
769 'title' => __( 'QR image width', 'multisafepay' ),
770 'type' => 'text',
771 'desc_tip' => __( 'Defines the width of the QR code in pixels. An appropriate size improves user experience and makes scanning easier.', 'multisafepay' ),
772 'description' => __( 'The numeric value represents the width in pixels. If left blank, the default size will be used.', 'multisafepay' ),
773 'default' => '',
774 );
775 }
776
777 if ( $this->payment_method->supportsTokenization() && $this->payment_method->supportsPaymentComponent() ) {
778 $form_fields['tokenization'] = array(
779 'title' => __( 'Recurring payments', 'multisafepay' ),
780 'type' => 'select',
781 'options' => array(
782 'no' => __( 'Disabled', 'multisafepay' ),
783 'yes' => __( 'Enabled', 'multisafepay' ),
784 ),
785 'description' => __( 'Ensure that the Payment component is enabled. It won\'t work using redirect payment type.', 'multisafepay' ),
786 'default' => $this->get_option( 'tokenization', 'no' ),
787 'value' => $this->get_option( 'tokenization', 'no' ),
788 );
789
790 if ( $this->is_ideal_2_0() ) {
791 unset( $form_fields['tokenization']['description'] );
792 }
793 }
794
795 return $form_fields;
796 }
797
798 /**
799 * Enqueue CSS styles related with Payment Component.
800 *
801 * @return void
802 */
803 public function enqueue_payment_component_styles() {
804 if ( ! $this->should_enqueue_classic_assets() ) {
805 return;
806 }
807
808 if ( ( is_checkout() || is_wc_endpoint_url( 'order-pay' ) ) ) {
809 wp_enqueue_style(
810 'multisafepay-payment-component-style',
811 self::MULTISAFEPAY_COMPONENT_CSS_URL,
812 array(),
813 MULTISAFEPAY_PLUGIN_VERSION
814 );
815 }
816 }
817
818 /**
819 * Prints checkout custom fields
820 *
821 * @return void
822 */
823 public function payment_fields() {
824 require MULTISAFEPAY_PLUGIN_DIR_PATH . 'templates/multisafepay-checkout-fields-display.php';
825 }
826
827 /**
828 * @param WC_Order $order
829 * @return bool
830 */
831 public function can_refund_order( $order ): bool {
832 if ( in_array( $this->get_payment_method_gateway_code(), self::NOT_ALLOW_REFUND_PAYMENT_METHODS, true ) ) {
833 return false;
834 }
835
836 if ( in_array( $order->get_status(), self::NOT_ALLOW_REFUND_ORDER_STATUSES, true ) ) {
837 return false;
838 }
839
840 return $order && $this->supports( 'refunds' );
841 }
842
843 /**
844 * @param int|string $order_id
845 * @return array
846 */
847 public function process_payment( $order_id ): array {
848 return ( new BlocksContextService() )->is_store_api_request()
849 ? $this->process_blocks_payment( $order_id )
850 : $this->process_classic_payment( $order_id );
851 }
852
853 /**
854 * Handles classic checkout payment processing.
855 *
856 * @param int|string $order_id
857 * @return array
858 */
859 private function process_classic_payment( $order_id ): array {
860 $order = wc_get_order( $order_id );
861 if ( ! $order instanceof WC_Order ) {
862 wc_add_notice( __( 'Invalid order. Please try again.', 'multisafepay' ), 'error' );
863
864 return $this->get_invalid_order_process_payment_response();
865 }
866
867 try {
868 $transaction = $this->create_transaction_for_payment( $order, new OrderService() );
869 } catch ( ApiException |ClientExceptionInterface |InvalidArgumentException $exception ) {
870 $this->logger->log_error( $exception->getMessage() );
871 wc_add_notice( __( 'There was a problem processing your payment. Please try again later or contact with us.', 'multisafepay' ), 'error' );
872
873 return $this->get_failed_process_payment_response();
874 }
875
876 $payment_url = $transaction->getPaymentUrl();
877 $this->logger->log_info( 'Start MultiSafepay transaction for the order ID ' . $order_id . ' on ' . date( 'd/m/Y H:i:s' ) . ' with payment URL ' . $payment_url );
878
879 return array(
880 'result' => 'success',
881 'redirect' => esc_url_raw( $payment_url ),
882 );
883 }
884
885 /**
886 * Handles Store API / Blocks payment processing.
887 *
888 * @param int|string $order_id
889 * @return array
890 */
891 private function process_blocks_payment( $order_id ): array {
892 $order_service = new OrderService();
893 $order = wc_get_order( $order_id );
894
895 if ( ! $order instanceof WC_Order ) {
896 return $this->get_invalid_order_process_payment_response();
897 }
898
899 $is_wallet_gateway = in_array( (string) $this->gateway_code, array( 'APPLEPAY', 'GOOGLEPAY' ), true );
900 $has_wallet_token = $is_wallet_gateway && ! empty( $order_service->get_wallet_payment_token( $order ) );
901 $has_wallet_combined_payment_method_title = false;
902
903 try {
904 $transaction = $this->create_transaction_for_payment( $order, $order_service );
905
906 // In Blocks wallet-direct flows, callback enrichment can arrive after redirect.
907 // Use payment_details from the create() transaction to prebuild and persist the
908 // combined wallet title, so the initial 'Thank You' render does not show a generic wallet label.
909 if ( $is_wallet_gateway && $has_wallet_token ) {
910 $has_wallet_combined_payment_method_title = $this->persist_wallet_combined_payment_method_title_from_transaction( $order, $transaction );
911 }
912
913 $payment_url = $transaction->getPaymentUrl();
914 $this->logger->log_info( 'Start MultiSafepay transaction for the order ID ' . $order_id . ' on ' . date( 'd/m/Y H:i:s' ) . ' with payment URL ' . $payment_url );
915
916 $redirect_url = ( ( $is_wallet_gateway && $has_wallet_token && $has_wallet_combined_payment_method_title ) || empty( $payment_url ) )
917 ? $this->get_return_url( $order )
918 : esc_url_raw( $payment_url );
919
920 return array(
921 'result' => 'success',
922 'redirect' => $redirect_url,
923 );
924 } catch ( ApiException |ClientExceptionInterface |InvalidArgumentException $exception ) {
925 $this->logger->log_error( $exception->getMessage() );
926
927 return $this->get_failed_process_payment_response();
928 } finally {
929 $this->clear_blocks_process_payment_data( $order, $order_id, $order_service );
930 }
931 }
932
933 /**
934 * Creates the MultiSafepay transaction for the given order.
935 *
936 * Exposed as protected so tests can override it to bypass the real SDK call.
937 *
938 * @param WC_Order $order
939 * @param OrderService $order_service
940 * @return TransactionResponse
941 * @throws ApiException
942 * @throws ClientExceptionInterface
943 * @throws InvalidArgumentException
944 */
945 protected function create_transaction_for_payment( WC_Order $order, OrderService $order_service ): TransactionResponse {
946 $transaction_manager = ( new SdkService() )->get_transaction_manager();
947 $order_request = $order_service->create_order_request( $order, $this->gateway_code, $this->type );
948
949 return $transaction_manager->create( $order_request );
950 }
951
952 /**
953 * Best-effort cleanup for request-lifecycle Blocks checkout data.
954 *
955 * @param WC_Order $order
956 * @param int|string $order_id
957 * @param OrderService $order_service
958 * @return void
959 */
960 private function clear_blocks_process_payment_data( WC_Order $order, $order_id, OrderService $order_service ): void {
961 try {
962 $order_service->blocks_payment_data_service->clear_blocks_payment_data_from_order_meta_by_pattern(
963 $order,
964 BlocksPaymentDataService::REQUEST_LIFECYCLE_KEY_PATTERN
965 );
966 } catch ( Throwable $exception ) {
967 $this->logger->log_warning( 'Unable to clear request-lifecycle Blocks payment data for order ID ' . $order_id . ': ' . $exception->getMessage() );
968 }
969 }
970
971 /**
972 * Returns the invalid-order response expected by WooCommerce payment flows.
973 *
974 * @return array
975 */
976 private function get_invalid_order_process_payment_response(): array {
977 return array(
978 'result' => 'failure',
979 'message' => __( 'Invalid order. Please try again.', 'multisafepay' ),
980 );
981 }
982
983 /**
984 * Returns the generic processing failure response expected by WooCommerce payment flows.
985 *
986 * @return array
987 */
988 private function get_failed_process_payment_response(): array {
989 return array(
990 'result' => 'failure',
991 'message' => __( 'There was a problem processing your payment. Please try again later or contact with us.', 'multisafepay' ),
992 );
993 }
994
995 /**
996 * Persist a combined wallet title (e.g., Google Pay (Visa)) using data from
997 * the transaction returned by create().
998 *
999 * The wallet code and payment instrument type come from payment_details; the
1000 * wallet label is resolved from the configured WooCommerce gateway and then
1001 * combined with the instrument label.
1002 *
1003 * This mitigates the Blocks timing gap where the 'Thank You' page can render
1004 * before callback enrichment.
1005 *
1006 * @param WC_Order $order
1007 * @param mixed $transaction
1008 * @return bool
1009 */
1010 protected function persist_wallet_combined_payment_method_title_from_transaction( WC_Order $order, $transaction ): bool {
1011 try {
1012 // Guard against unexpected SDK responses; in edge cases the create() transaction
1013 // may not include payment_details, so a combined wallet title cannot be built.
1014 if ( ! is_object( $transaction ) || ! method_exists( $transaction, 'getPaymentDetails' ) ) {
1015 return false;
1016 }
1017
1018 $payment_details = $transaction->getPaymentDetails();
1019 if ( ! is_object( $payment_details ) || ! method_exists( $payment_details, 'getType' ) || ! method_exists( $payment_details, 'get' ) ) {
1020 return false;
1021 }
1022
1023 // `wallet` identifies the wallet wrapper (Google/Apple Pay), while `type` carries the underlying instrument.
1024 $wallet_code = (string) $payment_details->get( 'wallet' );
1025 $gateway_code = (string) $payment_details->getType();
1026 if ( '' === $wallet_code || '' === $gateway_code ) {
1027 return false;
1028 }
1029
1030 // Resolve the wallet title from the configured WooCommerce gateway to keep labels consistent.
1031 $wallet_payment_method = $this->resolve_wallet_woocommerce_payment_gateway( $wallet_code );
1032 if ( ! $wallet_payment_method ) {
1033 return false;
1034 }
1035
1036 $wallet_title = $wallet_payment_method->get_payment_method_title();
1037 if ( '' === $wallet_title ) {
1038 return false;
1039 }
1040
1041 $payment_method_title_builder = new PaymentMethodTitleBuilder();
1042 // Only combine titles for known instruments (e.g., Visa, Mastercard, Amex).
1043 if ( ! $payment_method_title_builder->can_build_wallet_combined_payment_method_title( $gateway_code ) ) {
1044 return false;
1045 }
1046
1047 $underlying_title = $payment_method_title_builder->get_underlying_payment_method_title( $gateway_code );
1048 $combined_title = $payment_method_title_builder->build_combined_payment_method_title( $wallet_title, $underlying_title );
1049 if ( '' === $combined_title ) {
1050 return false;
1051 }
1052
1053 // Avoid unnecessary order writes when title is already persisted.
1054 if ( $order->get_payment_method_title() !== $combined_title ) {
1055 $order->set_payment_method_title( $combined_title );
1056 $order->save();
1057 }
1058
1059 return true;
1060 } catch ( Throwable $exception ) {
1061 $this->logger->log_warning( 'Unable to persist combined wallet payment title for order ID ' . $order->get_id() . ': ' . $exception->getMessage() );
1062 return false;
1063 }
1064 }
1065
1066 /**
1067 * Resolve a configured WooCommerce gateway for the given MultiSafepay wallet code.
1068 *
1069 * Extracted as a protected seam so tests can supply a stub wallet gateway without
1070 * hitting the MultiSafepay API through PaymentMethodService.
1071 *
1072 * @param string $wallet_code
1073 * @return BasePaymentMethod|null
1074 */
1075 protected function resolve_wallet_woocommerce_payment_gateway( string $wallet_code ): ?BasePaymentMethod {
1076 return ( new PaymentMethodService() )->get_woocommerce_payment_gateway_by_multisafepay_gateway_code( $wallet_code );
1077 }
1078
1079 /**
1080 * Validate_fields
1081 *
1082 * @return boolean
1083 */
1084 public function validate_fields(): bool {
1085
1086 if ( is_wc_endpoint_url( 'add-payment-method' ) ) {
1087 return false;
1088 }
1089
1090 // Checkout Blocks uses the Store API; payloads may be stored in order meta and later
1091 // consumed by OrderService. This POST check applies only to classic checkout, since
1092 // Store API surfaces errors as REST exceptions rather than HTML notices.
1093 $is_store_api_request = ( new BlocksContextService() )->is_store_api_request();
1094
1095 if (
1096 $this->is_payment_component_enabled() &&
1097 ! $is_store_api_request &&
1098 (
1099 ! isset( $_POST[ $this->id . '_payment_component_payload' ] ) ||
1100 empty( $_POST[ $this->id . '_payment_component_payload' ] )
1101 )
1102 ) {
1103 wc_add_notice( '<strong>' . $this->get_payment_method_title() . ' payment details</strong> is a required field.', 'error' );
1104 }
1105
1106 if ( isset( $_POST[ $this->id . '_payment_component_errors' ] ) && '' !== $_POST[ $this->id . '_payment_component_errors' ] ) {
1107 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1108 foreach ( wp_unslash( $_POST[ $this->id . '_payment_component_errors' ] ) as $payment_component_error ) {
1109 wc_add_notice( sanitize_text_field( $payment_component_error ), 'error' );
1110 }
1111 }
1112
1113 if ( wc_get_notices( 'error' ) ) {
1114 return false;
1115 }
1116
1117 return true;
1118 }
1119
1120 /**
1121 * Returns the WooCommerce registered order statuses
1122 *
1123 * @see http://hookr.io/functions/wc_get_order_statuses/
1124 * @return array
1125 */
1126 protected function get_order_statuses(): array {
1127 $order_statuses = wc_get_order_statuses();
1128 $order_statuses['wc-default'] = __( 'Default value set in common settings', 'multisafepay' );
1129 return $order_statuses;
1130 }
1131
1132 /**
1133 * Build the variables required to initialize the Credit Card direct transactions
1134 *
1135 * @param string $payment_method
1136 * @return ?array
1137 */
1138 private function common_wallets_data( string $payment_method ): ?array {
1139 $sdk_service = new SdkService();
1140 $environment = $sdk_service->get_test_mode() ? 'TEST' : 'LIVE';
1141 $debug_mode = (bool) get_option( 'multisafepay_debugmode', false );
1142 $currency_code = get_woocommerce_currency();
1143
1144 // Wallet config is exposed through BasePaymentMethodBlocks::get_payment_method_data()
1145 // in both frontend checkout and the Checkout block editor. In the wp-admin / editor context,
1146 // WooCommerce may not have initialized customer/cart session objects yet, so guard these reads.
1147 $country_code = '';
1148 // On order-pay, prefer the billing country stored in the order.
1149 if ( function_exists( 'is_wc_endpoint_url' ) && is_wc_endpoint_url( 'order-pay' ) ) {
1150 global $wp;
1151 $pay_order_id = isset( $wp->query_vars['order-pay'] ) ? absint( $wp->query_vars['order-pay'] ) : 0;
1152 $pay_order = $pay_order_id ? wc_get_order( $pay_order_id ) : null;
1153 if ( $pay_order instanceof WC_Order ) {
1154 $country_code = $pay_order->get_billing_country() ? $pay_order->get_billing_country() : $pay_order->get_shipping_country();
1155 }
1156 }
1157 if ( empty( $country_code ) && function_exists( 'WC' ) && WC() && WC()->customer ) {
1158 $country_code = WC()->customer->get_billing_country() ? WC()->customer->get_billing_country() : WC()->customer->get_shipping_country();
1159 }
1160 if ( empty( $country_code ) ) {
1161 $default_country = (string) get_option( 'woocommerce_default_country', '' );
1162 $country_prefix = strstr( $default_country, ':', true );
1163 $country_code = ( false !== $country_prefix ) ? $country_prefix : $default_country;
1164 }
1165
1166 $total_price = 0.0;
1167 // On the order-pay endpoint the cart may be empty; use the order total instead.
1168 if ( function_exists( 'is_wc_endpoint_url' ) && is_wc_endpoint_url( 'order-pay' ) ) {
1169 global $wp;
1170 $order_id = isset( $wp->query_vars['order-pay'] ) ? absint( $wp->query_vars['order-pay'] ) : 0;
1171 $order = $order_id ? wc_get_order( $order_id ) : null;
1172 $total_price = $order instanceof WC_Order ? (float) $order->get_total() : 0.0;
1173 }
1174 if ( empty( $total_price ) && function_exists( 'WC' ) && WC() && WC()->cart ) {
1175 $total_price = (float) WC()->cart->get_total( '' );
1176 }
1177
1178 $common_data = array(
1179 'environment' => $environment,
1180 'gateway_merchant_id' => $sdk_service->get_multisafepay_account_id(),
1181 'debug_mode' => $debug_mode,
1182 'country_code' => $country_code,
1183 'currency_code' => $currency_code,
1184 'total_price' => $total_price,
1185 );
1186
1187 if ( 'apple_pay' === $payment_method ) {
1188 unset( $common_data['gateway_merchant_id'] );
1189 }
1190
1191 return $common_data;
1192 }
1193
1194 /**
1195 * Build the variables required to initialize the Google Pay direct transactions
1196 *
1197 * @param string $test_merchant_id
1198 * @param string $test_merchant_name
1199 * @return string|null
1200 */
1201 public function build_googlepay_wallet_variables( string $test_merchant_id, string $test_merchant_name ): ?string {
1202 $google_pay = $this->common_wallets_data( 'google_pay' );
1203 if ( is_null( $google_pay ) ) {
1204 return null;
1205 }
1206 $merchant_id = $test_merchant_id;
1207 $merchant_name = $test_merchant_name;
1208
1209 if ( 'LIVE' === $google_pay['environment'] ) {
1210 $merchant_id = $this->merchant_id;
1211 $merchant_name = $this->merchant_name;
1212 }
1213
1214 return 'var configGooglePay = ' . wp_json_encode(
1215 array(
1216 'environment' => $google_pay['environment'],
1217 'gatewayMerchantId' => $google_pay['gateway_merchant_id'],
1218 'debugMode' => $google_pay['debug_mode'],
1219 'countryCode' => $google_pay['country_code'],
1220 'currencyCode' => $google_pay['currency_code'],
1221 'merchantId' => $merchant_id,
1222 'merchantName' => $merchant_name,
1223 'totalPrice' => $google_pay['total_price'],
1224 )
1225 ) . ';';
1226 }
1227
1228 /**
1229 * Returns Google Pay configuration for frontend usage (including Blocks).
1230 *
1231 * @return array|null
1232 */
1233 public function get_googlepay_wallet_config(): ?array {
1234 $google_pay = $this->common_wallets_data( 'google_pay' );
1235 if ( is_null( $google_pay ) ) {
1236 return null;
1237 }
1238
1239 $merchant_id = self::GOOGLEPAY_TEST_MERCHANT_ID;
1240 $merchant_name = self::GOOGLEPAY_TEST_MERCHANT_NAME;
1241
1242 if ( 'LIVE' === $google_pay['environment'] ) {
1243 $merchant_id = (string) $this->merchant_id;
1244 $merchant_name = (string) $this->merchant_name;
1245 }
1246
1247 return array(
1248 'environment' => $google_pay['environment'],
1249 'gatewayMerchantId' => $google_pay['gateway_merchant_id'],
1250 'debugMode' => (bool) $google_pay['debug_mode'],
1251 'countryCode' => (string) $google_pay['country_code'],
1252 'currencyCode' => (string) $google_pay['currency_code'],
1253 'merchantId' => (string) $merchant_id,
1254 'merchantName' => (string) $merchant_name,
1255 'totalPrice' => (float) $google_pay['total_price'],
1256 );
1257 }
1258
1259 /**
1260 * Build the variables required to initialize the Apple Pay direct transactions
1261 *
1262 * @param string $test_merchant_name
1263 * @return string|null
1264 */
1265 public function build_applepay_wallet_variables( string $test_merchant_name ): ?string {
1266 $apple_pay = $this->common_wallets_data( 'apple_pay' );
1267 if ( is_null( $apple_pay ) ) {
1268 return null;
1269 }
1270 $merchant_name = $test_merchant_name;
1271
1272 if ( 'LIVE' === $apple_pay['environment'] ) {
1273 $merchant_name = $this->merchant_name;
1274 }
1275
1276 return 'var configApplePay = ' . wp_json_encode(
1277 array(
1278 'debugMode' => $apple_pay['debug_mode'],
1279 'countryCode' => $apple_pay['country_code'],
1280 'currencyCode' => $apple_pay['currency_code'],
1281 'merchantName' => $merchant_name,
1282 'totalPrice' => $apple_pay['total_price'],
1283 )
1284 ) . ';';
1285 }
1286
1287 /**
1288 * Returns Apple Pay configuration for frontend usage (including Blocks).
1289 *
1290 * @return array|null
1291 */
1292 public function get_applepay_wallet_config(): ?array {
1293 $apple_pay = $this->common_wallets_data( 'apple_pay' );
1294 if ( is_null( $apple_pay ) ) {
1295 return null;
1296 }
1297
1298 $merchant_name = self::APPLEPAY_TEST_MERCHANT_NAME;
1299
1300 if ( 'LIVE' === $apple_pay['environment'] ) {
1301 $merchant_name = (string) $this->merchant_name;
1302 }
1303
1304 return array(
1305 'environment' => $apple_pay['environment'],
1306 'debugMode' => (bool) $apple_pay['debug_mode'],
1307 'countryCode' => (string) $apple_pay['country_code'],
1308 'currencyCode' => (string) $apple_pay['currency_code'],
1309 'merchantName' => (string) $merchant_name,
1310 'totalPrice' => (float) $apple_pay['total_price'],
1311 );
1312 }
1313
1314 /**
1315 * Get the countries allowed by WooCommerce
1316 *
1317 * @return array
1318 */
1319 protected function get_countries(): array {
1320 $countries = new WC_Countries();
1321 return $countries->get_allowed_countries();
1322 }
1323
1324 /**
1325 * Get the user roles allowed by WordPress
1326 *
1327 * @return array
1328 */
1329 protected function get_user_roles(): array {
1330 $roles = wp_roles()->roles;
1331
1332 return array_map(
1333 static function ( $role ) {
1334 return $role['name'];
1335 },
1336 $roles
1337 );
1338 }
1339
1340 /**
1341 * If the API starts returning that the payment component has no fields for IDEAL,
1342 * it's because the payment component is disabled for this payment method.
1343 *
1344 * @return bool
1345 */
1346 private function is_ideal_2_0(): bool {
1347 if ( $this->payment_method->getId() !== 'IDEAL' ) {
1348 return false;
1349 }
1350
1351 $payment_method_apps = $this->payment_method->getApps();
1352 if ( false === $payment_method_apps[ PaymentMethod::PAYMENT_COMPONENT_KEY ][ PaymentMethod::PAYMENT_COMPONENT_HAS_FIELDS_KEY ] ) {
1353 return true;
1354 }
1355
1356 return false;
1357 }
1358 }
1359