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 / Gateways / PaymentMethods / WCGatewayMoneiCC.php

WCGatewayMoneiCC.php in MONEI Payments for WooCommerce 7.1.3, at src/Gateways/PaymentMethods/WCGatewayMoneiCC.php

521 lines 17.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Monei\Gateways\PaymentMethods;
4
5 use Monei\Features\Subscriptions\SubscriptionHandlerInterface;
6 use Monei\Features\Subscriptions\SubscriptionService;
7 use Monei\Gateways\Abstracts\WCMoneiPaymentGatewayComponent;
8 use Monei\Helpers\CardBrandHelper;
9 use Monei\Services\payment\MoneiPaymentServices;
10 use Monei\Services\ApiKeyService;
11 use Monei\Services\MoneiStatusCodeHandler;
12 use Monei\Services\PaymentMethodsService;
13 use Monei\Templates\TemplateManager;
14 use Exception;
15 use WC_Admin_Settings;
16 use WC_Geolocation;
17 use WC_Monei_IPN;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 /**
24 * Class that handle Monei Payment method by default.
25 * It will combine HOSTED and Component versions.
26 * Merchant can decide with a checkbox in settings screen.
27 * We keep MONEI_GATEWAY_ID to have compatibility with old merchants ( with hosted cc selected )
28 *
29 * For Hosted, see:
30 * Form based: This is where the user must click a button on a form that then redirects them to the payment processor on the gateway’s own website.
31 * https://docs.monei.com/docs/integrations/use-prebuilt-payment-page/
32 *
33 * For component, see:
34 * Monei Payment method Card Input component.
35 * https://docs.monei.com/docs/monei-js/reference/#cardinput-component
36 * https://docs.monei.com/docs/payment-methods/card/
37 *
38 * Class WC_Gateway_Monei_CC
39 */
40 class WCGatewayMoneiCC extends WCMoneiPaymentGatewayComponent {
41
42 const PAYMENT_METHOD = 'card';
43
44 /**
45 * Default style for the card fields, shared by the single line and the
46 * separate fields layouts.
47 *
48 * The fields render inside a cross origin iframe and cannot inherit page
49 * CSS, so typography must be declared here to match the surrounding form.
50 * `height` and `lineHeight` need a CSS unit, a bare number is ignored.
51 */
52 const DEFAULT_CARD_INPUT_STYLE = '{"base": {"height": "50px", "lineHeight": "50px", "fontSize": "16px", "fontFamily": "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "color": "#2b2d2f"}, "input": {"background": "none"}}';
53
54 protected static $scripts_enqueued = false;
55
56 /**
57 * @var bool
58 */
59 protected $redirect_flow;
60
61 /**
62 * @var bool
63 */
64 protected $apple_google_pay;
65
66 protected SubscriptionService $subscriptions_service;
67
68 protected CardBrandHelper $cardBrandHelper;
69
70 /**
71 * Constructor for the gateway.
72 *
73 * @access public
74 * @return void
75 */
76 public function __construct(
77 PaymentMethodsService $paymentMethodsService,
78 TemplateManager $templateManager,
79 ApiKeyService $apiKeyService,
80 MoneiPaymentServices $moneiPaymentServices,
81 MoneiStatusCodeHandler $statusCodeHandler,
82 SubscriptionService $subscriptionService,
83 CardBrandHelper $cardBrandHelper
84 ) {
85 parent::__construct( $paymentMethodsService, $templateManager, $apiKeyService, $moneiPaymentServices, $statusCodeHandler );
86 $this->cardBrandHelper = $cardBrandHelper;
87 $this->id = MONEI_GATEWAY_ID;
88 $this->method_title = __( 'MONEI - Credit Card', 'monei' );
89 $this->method_description = __( 'Accept credit card payments.', 'monei' );
90 $this->enabled = ( ! empty( $this->get_option( 'enabled' ) ) && 'yes' === $this->get_option( 'enabled' ) && $this->is_valid_for_use() ) ? 'yes' : 'no';
91
92 // Load the form fields.
93 $this->init_form_fields();
94 // Load the settings.
95 $this->init_settings();
96
97 $description = ! empty( $this->get_option( 'description' ) )
98 ? $this->get_option( 'description' )
99 : ''; // Non-breaking space if description is empty
100
101 // Hosted payment with redirect.
102 $this->has_fields = false;
103 $iconUrl = apply_filters( 'woocommerce_monei_icon', WC_Monei()->image_url( 'monei-cards.svg' ) );
104 $iconMarkup = '<img src="' . esc_url( $iconUrl ) . '" alt="MONEI" class="monei-icons-cc" />';
105 // Settings variable
106 $this->hide_logo = ( ! empty( $this->get_option( 'hide_logo' ) ) && 'yes' === $this->get_option( 'hide_logo' ) ) ? true : false;
107
108 // Hide logo if card brands are available
109 $cardBrands = $this->cardBrandHelper->getCardBrandsConfig();
110 $hasCardBrands = ! empty( $cardBrands ) && count( array_filter( array_keys( $cardBrands ), fn( $key ) => $key !== 'default' ) ) > 0;
111
112 $this->icon = ( $this->hide_logo || $hasCardBrands ) ? '' : $iconMarkup;
113 $this->redirect_flow = ( ! empty( $this->get_option( 'mode' ) ) && 'yes' === $this->get_option( 'mode' ) ) ? true : false;
114 $this->testmode = $this->getTestmode();
115 $hide_title = ( ! empty( $this->get_option( 'hide_title' ) ) && 'yes' === $this->get_option( 'hide_title' ) ) ? true : false;
116 $this->title = ( ! $hide_title && ! empty( $this->get_option( 'title' ) ) ) ? $this->get_option( 'title' ) : '';
117 if ( $this->testmode && ! empty( $this->title ) ) {
118 $this->title .= ' (' . __( 'Test Mode', 'monei' ) . ')';
119 }
120 $this->description = ( ! empty( $this->get_option( 'description' ) ) ) ? $this->get_option( 'description' ) : '';
121 // Backward compatible: try local setting first, then global setting
122 $local_orderdo = $this->get_option( 'orderdo' );
123 $this->status_after_payment = ! empty( $local_orderdo ) ? $local_orderdo : get_option( 'monei_orderdo', 'processing' );
124 $this->account_id = $this->getAccountId();
125 $this->api_key = $this->getApiKey();
126 $this->shop_name = get_bloginfo( 'name' );
127 $this->password = ( ! empty( $this->get_option( 'password' ) ) ) ? $this->get_option( 'password' ) : '';
128 $this->tokenization = ( ! empty( $this->get_option( 'tokenization' ) ) && 'yes' === $this->get_option( 'tokenization' ) ) ? true : false;
129 // Backward compatible: try local setting first, then global setting
130 $local_preauth = $this->get_option( 'pre-authorize' );
131 $global_preauth = get_option( 'monei_pre_authorize', 'no' );
132 $this->pre_auth = ( ! empty( $local_preauth ) && 'yes' === $local_preauth ) || ( empty( $local_preauth ) && 'yes' === $global_preauth );
133 $this->logging = ( ! empty( get_option( 'monei_debug' ) ) && 'yes' === get_option( 'monei_debug' ) ) ? true : false;
134
135 // IPN callbacks
136 $this->notify_url = WC_Monei()->get_ipn_url();
137 new WC_Monei_IPN( $this->logging );
138
139 $this->supports = array(
140 'products',
141 'refunds',
142 );
143
144 if ( $this->tokenization ) {
145 $this->supports[] = 'tokenization';
146 }
147 $this->subscriptions_service = $subscriptionService;
148 $this->handler = $this->subscriptions_service->getHandler();
149 if ( $this->handler ) {
150 $this->supports = $this->handler->init_subscriptions( $this->supports, $this->id );
151 }
152
153 add_action(
154 'woocommerce_update_options_payment_gateways_' . $this->id,
155 array(
156 $this,
157 'process_admin_options',
158 )
159 );
160 add_filter(
161 'woocommerce_save_settings_checkout_' . $this->id,
162 function ( $is_post ) {
163 return $this->checks_before_save( $is_post, 'woocommerce_monei_enabled' );
164 }
165 );
166
167 add_action( 'wp_enqueue_scripts', array( $this, 'monei_scripts' ) );
168
169 // Add new total on checkout updates (ex, selecting different shipping methods)
170 add_filter(
171 'woocommerce_update_order_review_fragments',
172 function ( $fragments ) {
173 return self::add_cart_total_fragments( $fragments );
174 }
175 );
176 }
177
178 /**
179 * Return whether or not this gateway still requires setup to function.
180 *
181 * When this gateway is toggled on via AJAX, if this returns true a
182 * redirect will occur to the settings page instead.
183 *
184 * @return bool
185 * @since 3.4.0
186 */
187 public function needs_setup() {
188 if ( ! $this->account_id || ! $this->api_key ) {
189 return true;
190 }
191
192 return false;
193 }
194
195 /**
196 * Initialise Gateway Settings Form Fields
197 *
198 * @access public
199 * @return void
200 * @since 5.0
201 */
202 public function init_form_fields() {
203 $this->form_fields = require WC_Monei()->plugin_path() . '/includes/admin/monei-cc-settings.php';
204 }
205
206 /**
207 * Validate card_input_style field
208 *
209 * @param string $key
210 * @param string $value
211 * @return string
212 */
213 public function validate_card_input_style_field( $key, $value ) {
214 if ( empty( $value ) ) {
215 return $value;
216 }
217
218 // WordPress adds slashes to $_POST data, we need to remove them before validating JSON
219 $value = stripslashes( $value );
220
221 // Try to decode JSON
222 $decoded = json_decode( $value, true );
223
224 // Check for JSON errors
225 if ( json_last_error() !== JSON_ERROR_NONE ) {
226 WC_Admin_Settings::add_error(
227 sprintf(
228 /* translators: %s: JSON error message */
229 __( 'Card Input Style field contains invalid JSON: %s', 'monei' ),
230 json_last_error_msg()
231 )
232 );
233 return $this->get_option( 'card_input_style', self::DEFAULT_CARD_INPUT_STYLE );
234 }
235
236 // Re-encode with pretty print for better readability in admin
237 return wp_json_encode( $decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
238 }
239
240 /**
241 * Process the payment and return the result
242 *
243 * @access public
244 *
245 * @param int $order_id
246 * @param null $allowed_payment_method
247 *
248 * @return array
249 */
250 public function process_payment( $order_id, $allowed_payment_method = null ) {
251 return parent::process_payment( $order_id, self::PAYMENT_METHOD );
252 }
253
254 /**
255 * Add payment method in Account add_payment_method_page.
256 *
257 * @return array
258 */
259 public function add_payment_method() {
260 $nonce = isset( $_POST['woocommerce-add-payment-method-nonce'] )
261 ? sanitize_text_field( wp_unslash( $_POST['woocommerce-add-payment-method-nonce'] ) )
262 : '';
263 if ( ! $nonce || ! wp_verify_nonce( $nonce, 'woocommerce-add-payment-method' ) ) {
264 return array(
265 'result' => 'failure',
266 'redirect' => wc_get_endpoint_url( 'payment-methods' ),
267 );
268 }
269
270 // Since it is a hosted version, we need to create a 0 EUR payment and send customer to MONEI.
271 try {
272 $zero_payload = $this->create_zero_eur_payload();
273 $payment = $this->moneiPaymentServices->create_payment( $zero_payload );
274 $this->log( 'WC_Monei_API::add_payment_method', 'debug' );
275 $this->log( $zero_payload, 'debug' );
276 $this->log( $payment, 'debug' );
277 do_action( 'wc_gateway_monei_add_payment_method_success', $zero_payload, $payment );
278
279 return array(
280 'result' => 'success',
281 'redirect' => $payment->getNextAction()->getRedirectUrl(),
282 );
283 } catch ( Exception $e ) {
284 $this->log( $e, 'error' );
285 wc_add_notice( $e->getMessage(), 'error' );
286
287 return array(
288 'result' => 'failure',
289 'redirect' => wc_get_endpoint_url( 'payment-methods' ),
290 );
291 }
292 }
293
294 /**
295 * @return array
296 */
297 protected function create_zero_eur_payload() {
298 $current_user_id = (string) get_current_user_id();
299 /** Create 0 EUR Payment Payload */
300 $payload = array(
301 'amount' => 0,
302 'currency' => get_woocommerce_currency(),
303 'orderId' => $current_user_id . 'generatetoken' . wp_rand( 0, 1000000 ),
304 'description' => "User $current_user_id creating empty transaction to generate token",
305 'callbackUrl' => esc_url_raw( $this->notify_url ),
306 'completeUrl' => esc_url_raw( wc_get_account_endpoint_url( 'payment-methods' ) ),
307 'cancelUrl' => esc_url_raw( wc_get_account_endpoint_url( 'payment-methods' ) ),
308 'failUrl' => esc_url_raw( wc_get_account_endpoint_url( 'payment-methods' ) ),
309 'transactionType' => self::VERIFY_TRANSACTION_TYPE,
310 'sessionDetails' => array(
311 'ip' => WC_Geolocation::get_ip_address(),
312 'userAgent' => wc_get_user_agent(),
313 ),
314 'generatePaymentToken' => true,
315 'allowedPaymentMethods' => array( self::PAYMENT_METHOD ),
316 );
317
318 // All Zero payloads ( add payment method ) will use component CC.
319 $monei_token = $this->get_frontend_generated_monei_token();
320 if ( MONEI_GATEWAY_ID === $this->id && $monei_token ) {
321 $payload['paymentToken'] = $monei_token;
322 $payload['sessionId'] = (string) ( WC()->session !== null ? WC()->session->get_customer_id() : '' );
323 }
324
325 return $payload;
326 }
327
328 /**
329 * Check if gateway has fields.
330 * @return bool
331 */
332 public function has_fields() {
333 // Always show fields for component mode or when tokenization is enabled
334 return ! $this->redirect_flow || $this->tokenization;
335 }
336
337 /**
338 * Payments fields, shown on checkout or payment method page (add payment method).
339 */
340 public function payment_fields() {
341 ob_start();
342 if ( is_add_payment_method_page() ) {
343 esc_html_e( 'Pay via MONEI: you can add your payment method for future payments.', 'monei' );
344 // Always use component form in Add Payment method page.
345 $this->render_monei_form();
346 } elseif ( is_checkout_pay_page() ) {
347 // Order-pay page: Don't show saved cards (matches Stripe behavior)
348 // Always require new payment method for failed payment retries
349 // Show description in redirect mode
350 if ( $this->redirect_flow && $this->description ) {
351 echo '<div class="monei-redirect-description">';
352 echo wp_kses_post( wpautop( wptexturize( $this->description ) ) );
353 echo '</div>';
354 }
355 if ( ! $this->redirect_flow ) {
356 $this->render_monei_form();
357 }
358 if ( $this->tokenization ) {
359 $this->save_payment_method_checkbox();
360 }
361 } elseif ( $this->handler !== null && $this->handler->is_subscription_change_payment_page() ) {
362 // On subscription change payment page, we always use component CC.
363 // Description not shown in component mode (non-redirect)
364 if ( $this->tokenization ) {
365 $this->saved_payment_methods();
366 }
367 $this->render_monei_form();
368 if ( $this->tokenization ) {
369 $this->tokenization_script();
370 }
371 } else {
372 // Checkout screen.
373 // Show description only in redirect mode
374 if ( $this->redirect_flow && $this->description ) {
375 echo '<div class="monei-redirect-description">';
376 echo wp_kses_post( wpautop( wptexturize( $this->description ) ) );
377 echo '</div>';
378 }
379 if ( $this->tokenization ) {
380 $this->saved_payment_methods();
381 // If Component CC
382 if ( ! $this->redirect_flow ) {
383 $this->render_monei_form();
384 } else {
385 $this->tokenization_script();
386 }
387 $this->save_payment_method_checkbox();
388 } elseif ( ! $this->redirect_flow ) {
389 // If Component CC
390 $this->render_monei_form();
391 }
392 }
393 ob_end_flush();
394 }
395
396 /**
397 * Form where MONEI JS will render CC Component.
398 */
399 protected function render_monei_form() {
400 ?>
401 <fieldset class="monei-fieldset monei-card-fieldset wc-block-components-form"
402 id="wc-<?php echo esc_attr( $this->id ); ?>-cc-form">
403 <!-- Cardholder Name Input -->
404 <div class="monei-input-container wc-block-components-text-input">
405 <input
406 type="text"
407 id="monei_cardholder_name"
408 name="monei_cardholder_name"
409 data-testid="cardholder-name-input"
410 placeholder="<?php echo esc_attr__( 'Cardholder Name', 'monei' ); ?>"
411 required
412 class="monei-input">
413 <div
414 id="monei-cardholder-name-error"
415 class="wc-block-components-validation-error"
416 ></div>
417 </div>
418 <!-- Card Input Container -->
419 <div id="payment-form" class="monei-input-container wc-block-components-text-input">
420 <?php if ( 'split' === $this->get_option( 'card_field_layout', 'single' ) ) : ?>
421 <!-- Split Card Field Containers, in tab order -->
422 <div class="monei-card-group">
423 <div id="monei-card-number" class="monei-card-group-field monei-card-number"></div>
424 <div id="monei-card-expiry" class="monei-card-group-field monei-card-expiry"></div>
425 <div id="monei-card-cvc" class="monei-card-group-field monei-card-cvc"></div>
426 </div>
427 <?php else : ?>
428 <div id="monei-card-input" class="monei-card-input">
429 </div>
430 <?php endif; ?>
431 <div
432 id="monei-card-error"
433 class="wc-block-components-validation-error"
434 ></div>
435 </div>
436 </fieldset>
437
438 <?php
439 }
440
441 /**
442 * Registering MONEI JS library and plugin js.
443 */
444 public function monei_scripts() {
445 if ( self::$scripts_enqueued || ! $this->should_load_scripts() ) {
446 return;
447 }
448
449 // Return early if redirect flow (doesn't need component scripts)
450 if ( $this->redirect_flow ) {
451 return;
452 }
453
454 // Return early if not on a page that needs scripts
455 $is_required_page = is_checkout() || is_checkout_pay_page() || is_add_payment_method_page();
456 if ( ! $is_required_page ) {
457 return;
458 }
459
460 if ( 'no' === $this->enabled ) {
461 return;
462 }
463
464 // Don't load classic CSS on blocks checkout
465 if ( $this->is_block_checkout_page() ) {
466 return;
467 }
468
469 // Register and enqueue classic checkout CSS
470 wp_register_style(
471 'monei-classic-checkout',
472 plugins_url( 'public/css/monei-classic-checkout.css', MONEI_MAIN_FILE ),
473 array(),
474 MONEI_VERSION,
475 'all'
476 );
477 wp_enqueue_style( 'monei-classic-checkout' );
478
479 if ( ! wp_script_is( 'monei', 'registered' ) ) {
480 wp_register_script( 'monei', 'https://js.monei.com/v3/monei.js', '', '3.0', true );
481 }
482 wp_register_script(
483 'woocommerce_monei',
484 plugins_url( 'public/js/monei-cc-classic.min.js', MONEI_MAIN_FILE ),
485 array(
486 'jquery',
487 'monei',
488 ),
489 MONEI_VERSION,
490 true
491 );
492 wp_enqueue_script( 'monei' );
493 // Determine the total amount to be passed
494 $total = $this->determineTheTotalAmountToBePassed();
495 $card_input_style = $this->get_option( 'card_input_style', self::DEFAULT_CARD_INPUT_STYLE );
496 wp_localize_script(
497 'woocommerce_monei',
498 'wc_monei_params',
499 array(
500 'accountId' => $this->getAccountId(),
501 'sessionId' => WC()->session !== null ? WC()->session->get_customer_id() : '',
502 'total' => monei_price_format( $total ),
503 'currency' => get_woocommerce_currency(),
504 'appleLogo' => WC_Monei()->image_url( 'apple-logo.svg' ),
505 'cardInputStyle' => json_decode( $card_input_style ),
506 'cardFieldLayout' => $this->get_option( 'card_field_layout', 'single' ),
507 'cardBrands' => $this->cardBrandHelper->getCardBrandsConfig(),
508 'nameErrorString' => esc_html__( 'Please enter a valid name. Special characters are not allowed.', 'monei' ),
509 )
510 );
511
512 wp_enqueue_script( 'woocommerce_monei' );
513 $this->tokenization_script();
514 self::$scripts_enqueued = true;
515 }
516
517 protected function should_load_scripts() {
518 return is_checkout() || is_checkout_pay_page() || is_add_payment_method_page();
519 }
520 }
521