PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.0.1
Pay with Vipps and MobilePay for WooCommerce v6.0.1
6.2.3 6.2.2 6.2.1 6.2.0 6.1.10 6.1.9 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1.0 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 6.0.0 5.4.3 5.4.2 5.4.1 5.4.0 All 185 releases
woo-vipps / recurring / includes / wc-vipps-recurring.php

wc-vipps-recurring.php in Pay with Vipps and MobilePay for WooCommerce 6.0.1, at recurring/includes/wc-vipps-recurring.php

1,058 lines 35.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 defined( 'ABSPATH' ) || exit;
4
5 class WC_Vipps_Recurring {
6 /**
7 * The reference the *Singleton* instance of this class
8 */
9 private static ?WC_Vipps_Recurring $instance = null;
10
11 public WC_Vipps_Recurring_Admin_Notices $notices;
12
13 public array $ajax_config = [];
14
15 /**
16 * Returns the *Singleton* instance of this class.
17 *
18 * @return WC_Vipps_Recurring
19 */
20 public static function get_instance(): WC_Vipps_Recurring {
21 if ( null === self::$instance ) {
22 self::$instance = new self();
23 }
24
25 return self::$instance;
26 }
27
28 public static function register_hooks() {
29 $instance = WC_Vipps_Recurring::get_instance();
30
31 // No longer handled directly here
32 // register_activation_hook( WC_VIPPS_MAIN_FILE, [ $instance, 'activate' ] );
33 // register_deactivation_hook( WC_VIPPS_MAIN_FILE, [ $instance, 'deactivate' ] );
34
35 if ( is_admin() ) {
36 add_action( 'admin_init', [ $instance, 'admin_init' ] );
37 add_action( 'admin_menu', [ $instance, 'admin_menu' ] );
38 add_action( 'wp_ajax_vipps_recurring_force_check_charge_statuses', [
39 $instance,
40 'wp_ajax_vipps_recurring_force_check_charge_statuses'
41 ] );
42 }
43
44 add_action( 'plugins_loaded', [ $instance, 'plugins_loaded' ] );
45 // Declare compatibility with the WooCommerce checkout block
46 add_action( 'woocommerce_blocks_loaded', [ $instance, 'woocommerce_blocks_loaded' ] );
47 add_action( 'init', [ $instance, 'init' ] );
48 }
49
50 public function activate() {
51 global $wp_rewrite;
52
53 $this->install();
54
55 $wp_rewrite->flush_rules();
56 add_option( 'woo-vipps-recurring-version', WC_VIPPS_RECURRING_VERSION );
57 }
58
59 /**
60 * Private clone method to prevent cloning of the instance of the
61 * *Singleton* instance.
62 *
63 * @return void
64 */
65 private function __clone() {
66 }
67
68 /**
69 * Private un-serialize method to prevent un-serializing of the *Singleton*
70 * instance.
71 *
72 * @return void
73 */
74 public function __wakeup() {
75 }
76
77 /**
78 * Protected constructor to prevent creating a new instance of the
79 * *Singleton* via the `new` operator from outside of this class.
80 */
81 private function __construct() {
82 }
83
84 /**
85 * @throws WC_Vipps_Recurring_Exception
86 * @throws WC_Vipps_Recurring_Temporary_Exception
87 * @throws WC_Vipps_Recurring_Config_Exception
88 */
89 public static function deactivate() {
90 WC_Vipps_Recurring::get_instance()->gateway()->webhook_teardown();
91 }
92
93 public function plugins_loaded() {
94 add_filter( 'woocommerce_payment_gateways', [ $this, 'add_gateways' ] );
95
96 // Add custom product settings for Vipps Recurring.
97 add_filter( 'woocommerce_product_data_tabs', [ $this, 'woocommerce_product_data_tabs' ] );
98 add_filter( 'woocommerce_product_data_panels', [ $this, 'woocommerce_product_data_panels' ] );
99 add_filter( 'woocommerce_process_product_meta', [ $this, 'woocommerce_process_product_meta' ] );
100
101 // Disable this gateway unless we're purchasing at least one subscription product.
102 add_filter( 'woocommerce_available_payment_gateways', [ $this, 'maybe_disable_gateway' ] );
103
104 add_action( 'woocommerce_api_wc_gateway_vipps_recurring', [ $this, 'handle_webhook_callback' ] );
105
106 // Create a real page for Vipps Checkout
107 add_filter( 'woocommerce_create_pages', [ $this, 'woocommerce_create_pages' ], 50 );
108
109 }
110
111 // Runs possibly before plugins_loaded
112 public function woocommerce_blocks_loaded() {
113 if ( class_exists( 'Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType' ) ) {
114 require_once 'wc-vipps-recurring-blocks-support.php';
115 add_action(
116 'woocommerce_blocks_payment_method_type_registration',
117 function ( Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry $payment_method_registry ) {
118 $payment_method_registry->register( new WC_Vipps_Recurring_Blocks_Support() );
119 }
120 );
121 }
122 }
123
124 /**
125 * Init the plugin after plugins_loaded so environment variables are set.
126 *
127 * @since 1.0.0
128 * @version 4.0.0
129 */
130 public function init() {
131 require_once __DIR__ . '/wc-vipps-recurring-rest-api.php';
132 WC_Vipps_Recurring_Rest_Api::get_instance();
133
134 $this->notices = WC_Vipps_Recurring_Admin_Notices::get_instance( WC_VIPPS_RECURRING_MAIN_FILE );
135
136 add_action( 'wp_enqueue_scripts', [ $this, 'wp_enqueue_scripts' ] );
137
138 add_filter( 'plugin_action_links_' . plugin_basename( WC_VIPPS_MAIN_FILE ), [
139 $this,
140 'plugin_action_links'
141 ] );
142
143 // Add custom cron schedules for Vipps/MobilePay charge polling
144 add_filter( 'cron_schedules', [
145 $this,
146 'woocommerce_vipps_recurring_add_cron_schedules'
147 ] );
148
149 // schedule recurring payment charge status checking event
150 if ( ! wp_next_scheduled( 'woocommerce_vipps_recurring_check_order_statuses' ) ) {
151 wp_schedule_event( time(), 'one_minute', 'woocommerce_vipps_recurring_check_order_statuses' );
152 }
153
154 add_action( 'woocommerce_vipps_recurring_check_order_statuses', [
155 $this,
156 'check_order_statuses'
157 ] );
158
159 add_action( 'woocommerce_vipps_recurring_cancel_subscription', [
160 $this->gateway(),
161 'cancel_subscription'
162 ], 10, 2 );
163
164 // Schedule checking if gateway change went through
165 if ( ! wp_next_scheduled( 'woocommerce_vipps_recurring_check_gateway_change_request' ) ) {
166 wp_schedule_event( time(), 'one_minute', 'woocommerce_vipps_recurring_check_gateway_change_request' );
167 }
168
169 add_action( 'woocommerce_vipps_recurring_check_gateway_change_request', [
170 $this,
171 'check_gateway_change_agreement_statuses'
172 ] );
173
174 // Schedule checking for updating payment details
175 if ( ! wp_next_scheduled( 'woocommerce_vipps_recurring_update_subscription_details_in_app' ) ) {
176 wp_schedule_event( time(), 'one_minute', 'woocommerce_vipps_recurring_update_subscription_details_in_app' );
177 }
178
179 add_action( 'woocommerce_vipps_recurring_update_subscription_details_in_app', [
180 $this,
181 'update_subscription_details_in_app'
182 ] );
183
184 // Schedule cleaning up subscriptions that are marked for deletion
185 if ( ! wp_next_scheduled( 'woocommerce_vipps_recurring_check_subscriptions_marked_for_deletion' ) ) {
186 wp_schedule_event( time(), 'hourly', 'woocommerce_vipps_recurring_check_subscriptions_marked_for_deletion' );
187 }
188
189 add_action( 'woocommerce_vipps_recurring_check_subscriptions_marked_for_deletion', [
190 $this,
191 'check_subscriptions_marked_for_deletion'
192 ] );
193
194 add_action( 'woocommerce_vipps_recurring_delete_pending_subscription', [
195 $this->gateway(),
196 'maybe_delete_subscription'
197 ], 10, 1 );
198
199 // Add our own ajax actions
200 add_action( 'wp_ajax_wc_vipps_recurring_order_action', [
201 $this,
202 'order_handle_vipps_recurring_action'
203 ] );
204
205 // Custom actions
206 add_filter( 'generate_rewrite_rules', [ $this, 'custom_action_endpoints' ] );
207 add_filter( 'query_vars', [ $this, 'custom_action_query_vars' ] );
208 add_filter( 'template_include', [ $this, 'custom_action_page_template' ] );
209 }
210
211 /**
212 * Admin only dashboard
213 */
214 public function admin_init() {
215 add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] );
216
217 add_action( 'admin_head', [ $this, 'admin_head' ] );
218
219 // Add capture button if order is not captured
220 add_action( 'woocommerce_order_item_add_action_buttons', [
221 $this,
222 'order_item_add_action_buttons'
223 ] );
224
225 if ( $this->gateway()->test_mode ) {
226 $notice = __( 'Vipps/MobilePay Recurring Payments is currently in test mode - no real transactions will occur. Disable test mode when you are ready to go live!', 'woo-vipps' );
227 $this->notices->warning( $notice );
228 }
229
230 // Load correct list table classes for current screen.
231 add_action( 'current_screen', [ $this, 'setup_screen' ] );
232
233 if ( isset( $_REQUEST['statuses_checked'] ) ) {
234 $this->notices->success( __( 'Successfully checked the status of these charges', 'woo-vipps' ) );
235 }
236
237 // Initialize webhooks if we haven't already
238 if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) {
239 if ( WC_Vipps_Recurring_Helper::is_connected() ) {
240 if ( empty( get_option( WC_Vipps_Recurring_Helper::OPTION_WEBHOOKS ) ) ) {
241 $this->gateway()->webhook_initialize();
242 } else {
243 $ok = $this->gateway()->webhook_ensure_this_site();
244
245 if ( ! $ok ) {
246 $this->gateway()->webhook_initialize();
247 }
248 }
249 }
250 }
251
252 // $test = $this->gateway()->check_charge_status(931);
253
254 // Show Vipps Login notice for a maximum of 10 days
255 // 1636066799 = 04-11-2021 23:59:59 UTC
256 // if ( ! class_exists( 'VippsWooLogin' ) && time() < 1636066799 ) {
257 // $vipps_login_plugin_url = 'https://wordpress.org/plugins/login-with-vipps';
258 // if ( get_locale() === 'nb_NO' ) {
259 // $vipps_login_plugin_url = 'https://nb.wordpress.org/plugins/login-with-vipps';
260 // }
261 //
262 // $this->notices->campaign(
263 // /* translators: %1$s URL to login-with-vipps, %2$s translation for "here" */
264 // sprintf( __( 'Login with Vipps is available for WooCommerce. Super-easy and safer login for your customers - no more usernames and passwords. Get started <a href="%1$s" target="_blank">%2$s</a>!', 'woo-vipps' ), $vipps_login_plugin_url, __( 'here', 'woo-vipps' ) ),
265 // 'login_promotion',
266 // true,
267 // 'assets/images/vipps-logg-inn-neg.png',
268 // 'login-promotion'
269 // );
270 // }
271 }
272
273 public function gateway(): WC_Gateway_Vipps_Recurring {
274 require_once( "wc-gateway-vipps-recurring.php" );
275
276 return WC_Gateway_Vipps_Recurring::get_instance();
277 }
278
279 /**
280 * Upgrade routines
281 */
282 public function upgrade() {
283 global $wpdb, $wp_rewrite;
284
285 $version = get_option( 'woo-vipps-recurring-version' );
286
287 // Update 1.8.1: add back _vipps_recurring_pending_charge and _charge_id
288 if ( version_compare( $version, '1.8.1', '<' ) ) {
289 $results = $wpdb->get_results( "SELECT wp_posts.id FROM (
290 SELECT DISTINCT post_id as id FROM wp_postmeta as m
291 WHERE EXISTS (SELECT * FROM wp_postmeta WHERE post_id = m.post_id AND meta_key = '_vipps_recurring_failed_charge_reason')
292 AND NOT EXISTS (SELECT * FROM wp_postmeta WHERE post_id = m.post_id AND meta_key = '_vipps_recurring_pending_charge')
293 ) as lookup
294 JOIN wp_posts ON (wp_posts.id = lookup.id)
295 ORDER BY wp_posts.post_date DESC", ARRAY_A );
296
297 WC_Vipps_Recurring_Logger::log( sprintf( 'Running 1.8.1 update, affecting orders with IDs: %s', implode( ',', array_map( function ( $item ) {
298 return $item['id'];
299 }, $results ) ) ) );
300
301 foreach ( $results as $row ) {
302 $order = wc_get_order( $row['id'] );
303 WC_Vipps_Recurring_Helper::set_order_charge_not_failed( $order, WC_Vipps_Recurring_Helper::get_transaction_id_for_order( $order ) );
304 $order->save();
305 }
306 }
307
308 // Update 1.8.2: migrate failed statuses to subscription too
309 if ( version_compare( $version, '1.8.2', '<' ) ) {
310 $results = $wpdb->get_results( "SELECT wp_posts.id
311 FROM (
312 SELECT DISTINCT post_id as id
313 FROM wp_postmeta as m
314 WHERE EXISTS(SELECT *
315 FROM wp_postmeta
316 WHERE post_id = m.post_id
317 AND meta_key = '_vipps_recurring_failed_charge_reason')
318 ) as lookup
319 JOIN wp_posts ON (wp_posts.id = lookup.id)
320 ORDER BY wp_posts.post_date DESC", ARRAY_A );
321
322 WC_Vipps_Recurring_Logger::log( sprintf( 'Running 1.8.2 update, affecting orders with IDs: %s', implode( ',', array_map( function ( $item ) {
323 return $item['id'];
324 }, $results ) ) ) );
325
326 foreach ( $results as $row ) {
327 $order = wc_get_order( $row['id'] );
328
329 $subscriptions = WC_Vipps_Recurring_Helper::get_subscriptions_for_order( $order );
330 $subscription = $subscriptions[ array_key_first( $subscriptions ) ];
331
332 if ( ! $subscription ) {
333 continue;
334 }
335
336 $failure_reason = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_CHARGE_FAILED_REASON );
337 if ( $failure_reason ) {
338 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_LATEST_FAILED_CHARGE_REASON, $failure_reason );
339 }
340
341 $failure_description = WC_Vipps_Recurring_Helper::get_meta( $order, WC_Vipps_Recurring_Helper::META_CHARGE_FAILED_DESCRIPTION );
342 if ( $failure_description ) {
343 WC_Vipps_Recurring_Helper::update_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_LATEST_FAILED_CHARGE_DESCRIPTION, $failure_description );
344 }
345
346 $subscription->save();
347 }
348 }
349
350 // Flush permalinks when updating to version 1.20.2
351 if ( version_compare( $version, '1.20.2', '<' ) ) {
352 $wp_rewrite->flush_rules();
353 }
354
355 // Copy MSN, client id, client secret and subscription key to our new test fields if test mode is enabled.
356 if ( $this->gateway()->test_mode && version_compare( $version, '2.0.0', '<' ) ) {
357 $this->gateway()->update_option( 'test_merchant_serial_number', $this->gateway()->get_option( 'merchant_serial_number' ) );
358 $this->gateway()->update_option( 'test_client_id', $this->gateway()->get_option( 'client_id' ) );
359 $this->gateway()->update_option( 'test_secret_key', $this->gateway()->get_option( 'secret_key' ) );
360 $this->gateway()->update_option( 'test_subscription_key', $this->gateway()->get_option( 'subscription_key' ) );
361 }
362
363 if ( $version !== WC_VIPPS_RECURRING_VERSION ) {
364 update_option( 'woo-vipps-recurring-version', WC_VIPPS_RECURRING_VERSION );
365 }
366 }
367
368 /**
369 * Inject admin ahead
370 */
371 public function admin_head() {
372 $icon = plugins_url( 'assets/images/' . $this->gateway()->brand . '-mark-icon.svg', WC_VIPPS_RECURRING_MAIN_FILE );
373
374 ?>
375 <style>
376 #woocommerce-product-data ul.wc-tabs li.wc_vipps_recurring_options a:before {
377 background-image: url( <?php echo $icon ?> );
378 }
379 </style>
380 <?php
381 }
382
383 public function gateway_should_be_active( array $methods = [] ) {
384 // Disable our gateway if the standard Vipps payment method is present.
385 $active = ! isset( $methods['vipps'] );
386
387 return apply_filters( 'wc_vipps_recurring_cart_has_subscription_product', $active, WC()->cart->get_cart_contents() );
388 }
389
390 public function maybe_disable_gateway( $methods ) {
391 if ( is_admin() || ! is_checkout() ) {
392 return $methods;
393 }
394
395 $show_gateway = $this->gateway_should_be_active( $methods );
396
397 if ( ! $show_gateway ) {
398 unset( $methods['vipps_recurring'] );
399 }
400
401 return $methods;
402 }
403
404 /**
405 * @return string
406 */
407 public function handle_check_statuses_bulk_action(): string {
408 $sendback = remove_query_arg( [ 'orders' ], wp_get_referer() );
409
410 if ( isset( $_GET['orders'] ) ) {
411 $order_ids = $_GET['orders'];
412
413 foreach ( $order_ids as $order_id ) {
414 // check charge status
415 $this->gateway()->check_charge_status( $order_id, true );
416 }
417
418 $sendback = add_query_arg( 'statuses_checked', 1, $sendback );
419 }
420
421 return $sendback;
422 }
423
424 /**
425 * Setup the screen for our special setting and action tables
426 */
427 public function setup_screen() {
428 global $wc_vipps_recurring_list_table_pending_charges,
429 $wc_vipps_recurring_list_table_failed_charges;
430
431 $screen_id = false;
432
433 if ( function_exists( 'get_current_screen' ) ) {
434 $screen = get_current_screen();
435 $screen_id = isset( $screen, $screen->id ) ? $screen->id : '';
436 }
437
438 if ( ! empty( $_REQUEST['screen'] ) ) {
439 $screen_id = wc_clean( wp_unslash( $_REQUEST['screen'] ) );
440 }
441
442 if ( $screen_id === 'settings_page_woo-vipps-recurring' ) {
443 include_once 'admin/list-tables/wc-vipps-recurring-list-table-pending-charges.php';
444 include_once 'admin/list-tables/wc-vipps-recurring-list-table-failed-charges.php';
445
446 $wc_vipps_recurring_list_table_pending_charges = new WC_Vipps_Recurring_Admin_List_Pending_Charges( [
447 'screen' => $screen_id . '_pending-charges'
448 ] );
449 $wc_vipps_recurring_list_table_failed_charges = new WC_Vipps_Recurring_Admin_List_Failed_Charges( [
450 'screen' => $screen_id . '_failed-charges'
451 ] );
452 }
453
454 if ( $wc_vipps_recurring_list_table_pending_charges
455 && $wc_vipps_recurring_list_table_pending_charges->current_action()
456 && $wc_vipps_recurring_list_table_pending_charges->current_action() === 'check_status' ) {
457 $sendback = $this->handle_check_statuses_bulk_action();
458
459 wp_redirect( $sendback );
460 }
461
462 if ( $wc_vipps_recurring_list_table_failed_charges
463 && $wc_vipps_recurring_list_table_failed_charges->current_action()
464 && $wc_vipps_recurring_list_table_failed_charges->current_action() === 'check_status' ) {
465 $sendback = $this->handle_check_statuses_bulk_action();
466
467 wp_redirect( $sendback );
468 }
469
470 // Ensure the table handler is only loaded once. Prevents multiple loads if a plugin calls check_ajax_referer many times.
471 remove_action( 'current_screen', [ $this, 'setup_screen' ] );
472 }
473
474 /**
475 * Make admin menu entry
476 */
477 public function admin_menu() {
478 add_options_page(
479 __( 'Vipps/MobilePay Recurring Payments', 'woo-vipps' ),
480 __( 'Vipps/MobilePay Recurring Payments', 'woo-vipps' ),
481 'manage_options',
482 'woo-vipps-recurring',
483 [ $this, 'admin_menu_page_html' ]
484 );
485 }
486
487 /**
488 * Admin menu page HTML
489 */
490 public function admin_menu_page_html() {
491 if ( ! current_user_can( 'manage_options' ) ) {
492 return;
493 }
494
495 include __DIR__ . '/pages/admin/vipps-recurring-admin-menu-page.php';
496 }
497
498 public function custom_action_endpoints( $rewrites ) {
499 $rewrites->rules = array_merge(
500 [ 'vipps-mobilepay-recurring-payment/?$' => 'index.php?vipps_recurring_action=payment-redirect' ],
501 $rewrites->rules
502 );
503
504 return $rewrites;
505 }
506
507 public function custom_action_query_vars( $query_vars ) {
508 $query_vars[] = 'vipps_recurring_action';
509
510 return $query_vars;
511 }
512
513 public function custom_action_page_template( $original_template ) {
514 $custom_action = get_query_var( 'vipps_recurring_action' );
515
516 if ( ! empty ( $custom_action ) ) {
517 include WC_VIPPS_RECURRING_PLUGIN_PATH . '/includes/pages/payment-redirect-page.php';
518 die;
519 }
520
521 return $original_template;
522 }
523
524 public function maybe_create_checkout_page() {
525 $checkout_page_id = wc_get_page_id( 'vipps_mobilepay_recurring_checkout' );
526 $should_create_pages = ! $checkout_page_id || ! get_post_status( $checkout_page_id );
527
528 if ( ! $should_create_pages ) {
529 return;
530 }
531
532 // Piggybacks off of WooCommerce's default logic for creating the checkout page.
533 // This is possible because we override the shortcode.
534 delete_option( 'woocommerce_vipps_recurring_checkout_page_id' );
535 WC_Install::create_pages();
536 }
537
538 /**
539 * Vipps Checkout replaces the default checkout page, and currently uses its own page for this which needs to exist
540 */
541 public function woocommerce_create_pages( $data ) {
542 $checkout_enabled = get_option( WC_Vipps_Recurring_Helper::OPTION_CHECKOUT_ENABLED, false );
543 if ( ! $checkout_enabled ) {
544 return $data;
545 }
546
547 $data['vipps_recurring_checkout'] = array(
548 'name' => _x( 'vipps_recurring_checkout', 'Page slug', 'woo-vipps' ),
549 'title' => _x( 'Vipps MobilePay Recurring Checkout', 'Page title', 'woo-vipps' ),
550 'content' => '<!-- wp:shortcode -->[' . 'vipps_recurring_checkout' . ']<!-- /wp:shortcode -->',
551 );
552
553 return $data;
554 }
555
556 /**
557 * Force check status of all pending charges
558 */
559 public function wp_ajax_vipps_recurring_force_check_charge_statuses(): void {
560 try {
561 /* translators: amount of orders checked */
562 echo sprintf( __( 'Done. Checked the status of %s orders', 'woo-vipps' ), count( $this->check_order_statuses( - 1 ) ) );
563 } catch ( Exception $e ) {
564 echo __( 'Failed to finish checking the status of all orders. Please try again.', 'woo-vipps' );
565 }
566
567 wp_die();
568 }
569
570 /**
571 * @param $tabs
572 *
573 * @return mixed
574 */
575 public function woocommerce_product_data_tabs( $tabs ) {
576 $tabs['wc_vipps_recurring'] = [
577 'label' => __( 'Vipps/MobilePay Recurring Payments', 'woo-vipps' ),
578 'target' => 'wc_vipps_recurring_product_data',
579 'priority' => 100,
580 ];
581
582 return $tabs;
583 }
584
585 /**
586 * Tab content
587 */
588 public function woocommerce_product_data_panels(): void {
589 echo '<div id="wc_vipps_recurring_product_data" class="panel woocommerce_options_panel hidden">';
590
591 woocommerce_wp_checkbox( [
592 'id' => WC_Vipps_Recurring_Helper::META_PRODUCT_DIRECT_CAPTURE,
593 'value' => get_post_meta( get_the_ID(), WC_Vipps_Recurring_Helper::META_PRODUCT_DIRECT_CAPTURE, true ),
594 'label' => __( 'Capture payment instantly', 'woo-vipps' ),
595 'description' => __( 'Capture payment instantly even if the product is not virtual. Please make sure you are following the local jurisdiction in your country when using this option.', 'woo-vipps' ),
596 'desc_tip' => true,
597 ] );
598
599 woocommerce_wp_select( [
600 'id' => WC_Vipps_Recurring_Helper::META_PRODUCT_DESCRIPTION_SOURCE,
601 'value' => get_post_meta( get_the_ID(), WC_Vipps_Recurring_Helper::META_PRODUCT_DESCRIPTION_SOURCE, true ) ?: 'title',
602 'label' => __( 'Description source', 'woo-vipps' ),
603 'description' => __( 'Where we should source the agreement description from. Displayed in the Vipps/MobilePay app.', 'woo-vipps' ),
604 'desc_tip' => true,
605 'options' => [
606 'none' => __( 'None', 'woo-vipps' ),
607 'short_description' => __( 'Product short description', 'woo-vipps' ),
608 'custom' => __( 'Custom', 'woo-vipps' )
609 ]
610 ] );
611
612 woocommerce_wp_text_input( [
613 'id' => WC_Vipps_Recurring_Helper::META_PRODUCT_DESCRIPTION_TEXT,
614 'value' => get_post_meta( get_the_ID(), WC_Vipps_Recurring_Helper::META_PRODUCT_DESCRIPTION_TEXT, true ),
615 'label' => __( 'Custom description', 'woo-vipps' ),
616 'description' => __( 'If the description source is set to "custom" this text will be used.', 'woo-vipps' ),
617 'placeholder' => __( 'Max 100 characters', 'woo-vipps' ),
618 'desc_tip' => true,
619 ] );
620
621 echo '</div>';
622 }
623
624 /**
625 * Save our custom fields
626 *
627 * @param $post_id
628 */
629 public function woocommerce_process_product_meta( $post_id ) {
630 $capture_instantly = isset( $_POST[ WC_Vipps_Recurring_Helper::META_PRODUCT_DIRECT_CAPTURE ] ) ? 'yes' : 'no';
631 update_post_meta( $post_id, WC_Vipps_Recurring_Helper::META_PRODUCT_DIRECT_CAPTURE, $capture_instantly );
632
633 update_post_meta( $post_id, WC_Vipps_Recurring_Helper::META_PRODUCT_DESCRIPTION_SOURCE, $_POST[ WC_Vipps_Recurring_Helper::META_PRODUCT_DESCRIPTION_SOURCE ] );
634 update_post_meta( $post_id, WC_Vipps_Recurring_Helper::META_PRODUCT_DESCRIPTION_TEXT, $_POST[ WC_Vipps_Recurring_Helper::META_PRODUCT_DESCRIPTION_TEXT ] ?? '' );
635 }
636
637 /**
638 * @param $order
639 */
640 public function order_item_add_action_buttons( $order ): void {
641 $this->order_item_add_capture_button( $order );
642 }
643
644 /**
645 * @param $order
646 */
647 public function order_item_add_capture_button( $order ): void {
648 if ( $order->get_type() !== 'shop_order' ) {
649 return;
650 }
651
652 $payment_method = WC_Vipps_Recurring_Helper::get_payment_method( $order );
653 if ( $payment_method !== $this->gateway()->id ) {
654 // If this is not the payment method, an agreement would not be available.
655 return;
656 }
657
658 $show_capture_button = WC_Vipps_Recurring_Helper::can_capture_charge_for_order( $order );
659
660 if ( ! apply_filters( 'wc_vipps_recurring_show_capture_button', $show_capture_button, $order ) ) {
661 return;
662 }
663
664 $is_captured = WC_Vipps_Recurring_Helper::is_charge_captured_for_order( $order );
665
666 if ( $show_capture_button && ! $is_captured ) {
667 $logo = plugins_url( 'assets/images/' . $this->gateway()->brand . '-logo-white.svg', WC_VIPPS_RECURRING_MAIN_FILE );
668
669 print '<button type="button" data-order-id="' . $order->get_id() . '" data-action="capture_payment" class="button generate-items capture-payment-button ' . $this->gateway()->brand . '"><img border="0" style="display:inline;height:2ex;vertical-align:text-bottom" class="inline" alt="0" src="' . $logo . '"/> ' . __( 'Capture payment', 'woo-vipps' ) . '</button>';
670 }
671 }
672
673 public function order_handle_vipps_recurring_action() {
674 check_ajax_referer( 'vipps_recurring_ajax_nonce', 'nonce' );
675
676 $order = wc_get_order( intval( $_REQUEST['orderId'] ) );
677 if ( ! is_a( $order, 'WC_Order' ) ) {
678 return;
679 }
680
681 if ( $order->get_payment_method() != $this->gateway()->id ) {
682 return;
683 }
684
685 $action = isset( $_REQUEST['do'] ) ? sanitize_title( $_REQUEST['do'] ) : 'none';
686
687 if ( $action == 'capture_payment' ) {
688 $this->gateway()->maybe_capture_payment( $order->get_id() );
689 }
690
691 print "1";
692 }
693
694 /**
695 * Check charge statuses scheduled action
696 *
697 * @param int|null $limit
698 *
699 * @return array
700 */
701 public function check_order_statuses( $limit = '' ): array {
702 if ( empty( $limit ) ) {
703 $limit = $this->gateway()->check_charges_amount;
704 }
705
706 $options = [
707 'limit' => $limit,
708 'payment_method' => $this->gateway()->id
709 ];
710
711 $options = WC_Vipps_Recurring_Helper::add_meta_query_to_args( $options, WC_Vipps_Recurring_Helper::META_CHARGE_PENDING, '=', 1, $this->gateway()->use_high_performance_order_storage() );
712
713 if ( $this->gateway()->check_charges_sort_order === 'rand' ) {
714 $options['orderby'] = 'rand';
715 } else {
716 $options['orderby'] = 'post_date';
717 $options['order'] = $this->gateway()->check_charges_sort_order;
718 }
719
720 remove_all_filters( 'posts_orderby' );
721 $orders = wc_get_orders( $options );
722
723 foreach ( $orders as $order ) {
724 $order_id = WC_Vipps_Recurring_Helper::get_id( $order );
725
726 do_action( 'wc_vipps_recurring_before_cron_check_order_status', $order_id );
727 $this->gateway()->check_charge_status( $order_id );
728 do_action( 'wc_vipps_recurring_after_cron_check_order_status', $order_id );
729 }
730
731 return $orders;
732 }
733
734 /**
735 * Check the status of gateway change requests
736 */
737 public function check_gateway_change_agreement_statuses() {
738 $args = [
739 'subscription_status' => [ 'active', 'pending', 'on-hold' ],
740 ];
741
742 $args = WC_Vipps_Recurring_Helper::add_meta_query_to_args( $args, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_WAITING_FOR_GATEWAY_CHANGE, '=', 1, $this->gateway()->use_high_performance_order_storage() );
743
744 $subscriptions = wcs_get_subscriptions( $args );
745
746 foreach ( $subscriptions as $subscription ) {
747 // check charge status
748 $this->gateway()->maybe_process_gateway_change( $subscription->get_id() );
749 }
750 }
751
752 /**
753 * Update a subscription's details in the app
754 */
755 public function update_subscription_details_in_app() {
756 $args = [
757 'subscriptions_per_page' => 5,
758 'subscription_status' => [ 'active', 'pending-cancel', 'cancelled', 'on-hold' ],
759 ];
760
761 $args = WC_Vipps_Recurring_Helper::add_meta_query_to_args( $args, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_UPDATE_IN_APP, '=', 1, $this->gateway()->use_high_performance_order_storage() );
762
763 $subscriptions = wcs_get_subscriptions( $args );
764
765 foreach ( $subscriptions as $subscription ) {
766 // check charge status
767 $this->gateway()->maybe_update_subscription_details_in_app( $subscription->get_id() );
768 }
769 }
770
771 public function check_subscriptions_marked_for_deletion() {
772 $options = [
773 'limit' => 25,
774 'payment_method' => $this->gateway()->id,
775 ];
776
777 $options = WC_Vipps_Recurring_Helper::add_meta_query_to_args($options, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_MARKED_FOR_DELETION, '=', 1, $this->gateway()->use_high_performance_order_storage());
778
779 $subscriptions = wcs_get_subscriptions( $options );
780
781 WC_Vipps_Recurring_Logger::log( sprintf( "Running check_subscriptions_marked_for_deletion for %s subscriptions", count( $subscriptions ) ) );
782
783 foreach ( $subscriptions as $subscription ) {
784 $subscription_id = WC_Vipps_Recurring_Helper::get_id( $subscription );
785
786 // Check the status of this subscription's latest order charge just in case.
787 $latest_order = $subscription->get_last_order( 'all' );
788 $latest_order_id = WC_Vipps_Recurring_Helper::get_id( $latest_order );
789
790 do_action( 'wc_vipps_recurring_before_cron_check_order_status', $latest_order_id );
791 $this->gateway()->check_charge_status( $latest_order_id );
792 do_action( 'wc_vipps_recurring_after_cron_check_order_status', $latest_order_id );
793
794 // Hydrate objects
795 $latest_order = wc_get_order( $latest_order_id );
796 $subscription = wcs_get_subscription( $subscription_id );
797
798 // If this subscription has been manually updated in the mean-time, we no longer want to delete it.
799 // Similarly, if it has a billing email we don't want to delete it.
800 $empty_email = trim( $subscription->get_billing_email() ) === trim( WC_Vipps_Recurring_Helper::FAKE_USER_EMAIL ) || ! $subscription->get_billing_email();
801
802 WC_Vipps_Recurring_Logger::log( sprintf( "Checking if subscription %s should be deleted (status: %s, empty email: %s, is renewal: %s).", $subscription_id, $subscription->get_status(), $empty_email ? 'Yes' : 'No', wcs_order_contains_renewal( $latest_order ) ? 'Yes' : 'No' ) );
803
804 $allowed_statuses = [
805 'pending',
806 'cancelled',
807 'pending-cancel'
808 ];
809
810 if ( ! in_array( $subscription->get_status( 'edit' ), $allowed_statuses )
811 || ! $empty_email
812 || wcs_order_contains_renewal( $latest_order ) ) {
813 WC_Vipps_Recurring_Logger::log( sprintf( "Removing subscription %s from the deletion queue as it should no longer be deleted.", $subscription_id ) );
814
815 WC_Vipps_Recurring_Helper::delete_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_MARKED_FOR_DELETION );
816 $subscription->save();
817
818 continue;
819 }
820
821 WC_Vipps_Recurring_Logger::log( sprintf( "Deleting subscription %s and latest order %s.", $subscription_id, $latest_order_id ) );
822 $latest_order->delete();
823 $subscription->delete();
824 }
825 }
826
827 /**
828 * Adds plugin action links.
829 *
830 * @since 1.0.0
831 * @version 4.0.0
832 */
833 public function plugin_action_links( $links ): array {
834 // IOK 2025-01-13 temporarily deactivated, because there is currently two
835 // sets of settings, and not enough room to disambituate between them (at least in norwegian).
836 return $links;
837
838 $plugin_links = [
839 '<a href="admin.php?page=wc-settings&tab=checkout&section=vipps_recurring">' . esc_html__( 'Settings', 'woo-vipps' ) . '</a>',
840 ];
841
842 return array_merge( $plugin_links, $links );
843 }
844
845 /**
846 * Handles upgrade routines.
847 *
848 * @since 3.1.0
849 * @version 3.1.0
850 */
851 public function install() {
852 $this->upgrade();
853 }
854
855 /**
856 * Add the gateways to WooCommerce.
857 *
858 * @param $methods
859 *
860 * @return array
861 * @since 1.0.0
862 * @version 4.0.0
863 */
864 public function add_gateways( $methods ): array {
865 if ( function_exists( 'wcs_create_renewal_order' ) && class_exists( 'WC_Subscriptions_Order' ) ) {
866 require_once( dirname( __FILE__ ) . "/wc-gateway-vipps-recurring.php" );
867
868 $methods[] = WC_Gateway_Vipps_Recurring::get_instance();
869 }
870
871 return $methods;
872 }
873
874 /**
875 * Enqueue our CSS and other assets.
876 */
877 public function wp_enqueue_scripts() {
878 wp_enqueue_style(
879 'woo-vipps-recurring',
880
881 WC_VIPPS_RECURRING_PLUGIN_URL . '/assets/build/main.css', [],
882 filemtime( WC_VIPPS_RECURRING_PLUGIN_PATH . '/assets/build/main.css' )
883 );
884
885 $asset = require WC_VIPPS_RECURRING_PLUGIN_PATH . '/assets/build/main.asset.php';
886
887 wp_enqueue_script(
888 'woo-vipps-recurring',
889 WC_VIPPS_RECURRING_PLUGIN_URL . '/assets/build/main.js',
890 $asset['dependencies'],
891 filemtime( WC_VIPPS_RECURRING_PLUGIN_PATH . '/assets/build/main.js' ),
892 true
893 );
894
895 $continue_shopping_link_page = ! empty( $this->gateway()->continue_shopping_link_page )
896 ? $this->gateway()->continue_shopping_link_page
897 : wc_get_page_id( 'shop' );
898
899 $continue_shopping_url = $continue_shopping_link_page ? get_permalink( $continue_shopping_link_page ) : home_url();
900 if ( empty( $continue_shopping_link_page ) ) {
901 $continue_shopping_url = home_url();
902 }
903
904 wp_localize_script( 'woo-vipps-recurring', 'VippsMobilePaySettings', [
905 'logo' => WC_VIPPS_RECURRING_PLUGIN_URL . '/assets/images/' . $this->gateway()->brand . '-logo.svg',
906 'continueShoppingUrl' => $continue_shopping_url
907 ] );
908
909 wp_set_script_translations( 'woo-vipps-recurring', 'woo-vipps', dirname( WC_VIPPS_MAIN_FILE ) . '/languages' );
910 }
911
912 /**
913 * Enqueue our CSS and other assets.
914 */
915 public function admin_enqueue_scripts() {
916 wp_enqueue_style( 'woo-vipps-recurring', plugins_url( 'assets/css/vipps-recurring-admin.css', WC_VIPPS_RECURRING_MAIN_FILE ), [],
917 filemtime( dirname( WC_VIPPS_RECURRING_MAIN_FILE ) . '/assets/css/vipps-recurring-admin.css' ) );
918
919 $this->ajax_config['nonce'] = wp_create_nonce( 'vipps_recurring_ajax_nonce' );
920 $this->ajax_config['currency'] = get_woocommerce_currency();
921
922 wp_enqueue_script(
923 'woo-vipps-recurring-admin',
924 plugins_url( 'assets/js/vipps-recurring-admin.js', WC_VIPPS_RECURRING_MAIN_FILE ),
925 [ 'wp-i18n' ],
926 filemtime( dirname( WC_VIPPS_RECURRING_MAIN_FILE ) . "/assets/js/vipps-recurring-admin.js" ),
927 true
928 );
929
930 wp_localize_script( 'woo-vipps-recurring-admin', 'VippsRecurringConfig', $this->ajax_config );
931 wp_set_script_translations( 'woo-vipps-recurring-admin', 'woo-vipps', dirname( WC_VIPPS_MAIN_FILE ) . '/languages' );
932 }
933
934 /**
935 * @param $schedules
936 *
937 * @return mixed
938 */
939 public function woocommerce_vipps_recurring_add_cron_schedules( $schedules ) {
940 $schedules['one_minute'] = [
941 'interval' => 60,
942 'display' => esc_html__( 'Every One Minute' ),
943 ];
944
945 return $schedules;
946 }
947
948 /**
949 * @throws WC_Vipps_Recurring_Exception
950 * @throws WC_Vipps_Recurring_Temporary_Exception
951 * @throws WC_Vipps_Recurring_Config_Exception
952 */
953 public function handle_webhook_callback() {
954 wc_nocache_headers();
955 status_header( 202, "Accepted" );
956
957 $raw_input = @file_get_contents( 'php://input' );
958 $body = @json_decode( $raw_input, true );
959
960 // If we have a sessionId, this indicates that we are dealing with a Checkout callback.
961 // Fire a hook and let Checkout deal with its own logic.
962 if ( isset( $body['sessionId'] ) ) {
963 $authorization_token = $_SERVER['HTTP_AUTHORIZATION'];
964
965 do_action( 'wc_vipps_recurring_checkout_callback', $body, $authorization_token );
966
967 // Early return to avoid potential errors downstream.
968 return;
969 }
970
971 $callback = $_REQUEST['callback'] ?? "";
972
973 if ( $callback !== "webhook" ) {
974 return;
975 }
976
977 if ( ! $body ) {
978 $error = json_last_error_msg();
979 WC_Vipps_Recurring_Logger::log( sprintf( "Did not understand callback from Vipps/MobilePay with body: %s – error: %s", empty( $raw_input ) ? "(empty string)" : $raw_input, $error ) );
980
981 return;
982 }
983
984 $local_webhooks = $this->gateway()->webhook_get_local()[ $this->gateway()->merchant_serial_number ];
985 $local_webhook = array_pop( $local_webhooks );
986 $secret = $local_webhook ? ( $local_webhook['secret'] ?? false ) : false;
987
988 $order_id = $body['chargeId'] ?? $body['agreementId'];
989 if ( ! $order_id ) {
990 WC_Vipps_Recurring_Logger::log( sprintf( "Could not find order id in webhook with body %s", json_encode( $body ) ) );
991
992 return;
993 }
994
995 if ( ! $secret ) {
996 WC_Vipps_Recurring_Logger::log( sprintf( "Cannot verify webhook callback for order %s - this shop does not know the secret. You should delete all unwanted webhooks. If you are using the same MSN on several shops, this callback is probably for one of the others.", $order_id ) );
997 }
998
999 if ( ! $this->verify_webhook( $raw_input, $secret ) ) {
1000 return;
1001 }
1002
1003 do_action( "wc_vipps_recurring_webhook_callback", $body, $raw_input );
1004
1005 // We now have a validated webhook
1006 $this->gateway()->handle_webhook_callback( $body );
1007 }
1008
1009 public function verify_webhook( $serialized, $secret ): bool {
1010 $expected_auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ( $_SERVER['HTTP_X_VIPPS_AUTHORIZATION'] ?? "" );
1011 $expected_date = $_SERVER['HTTP_X_MS_DATE'] ?? '';
1012
1013 $hashed_payload = base64_encode( hash( 'sha256', $serialized, true ) );
1014 $path_and_query = $_SERVER['REQUEST_URI'];
1015 $host = $_SERVER['HTTP_HOST'];
1016 $toSign = "POST\n{$path_and_query}\n$expected_date;$host;$hashed_payload";
1017 $signature = base64_encode( hash_hmac( 'sha256', $toSign, $secret, true ) );
1018 $auth = "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=$signature";
1019
1020 return ( $auth == $expected_auth );
1021 }
1022
1023 public function generate_order_prefix(): string {
1024 $parts = parse_url( site_url() );
1025 if ( ! $parts ) {
1026 return 'woo-';
1027 }
1028
1029 $domain = explode( ".", $parts['host'] ?? '' );
1030 if ( empty( $domain ) ) {
1031 return 'woo-';
1032 }
1033
1034 $first = strtolower( $domain[0] );
1035 $second = $domain[1] ?? '';
1036
1037 // Select first part of domain unless that has no content, otherwise second. Default to "woo-" again.
1038 $key = $first;
1039 if ( in_array( $first, [ 'www', 'test', 'dev', 'vdev' ] ) && ! empty( $second ) ) {
1040 $key = $second;
1041 }
1042
1043 // Use only 8 chars for the site. Try to make it so by dropping vowels, if that doesn't succeed, just chop it.
1044 $key = sanitize_title( $key );
1045 $len = strlen( $key );
1046 if ( $len <= 8 ) {
1047 return "woo-$key-";
1048 }
1049
1050 $kzk = preg_replace( "/[aeiouæøåüö]/i", "", $key );
1051 if ( strlen( $kzk ) <= 8 ) {
1052 return "woo-$kzk-";
1053 }
1054
1055 return "woo-" . substr( $key, 0, 8 ) . "-";
1056 }
1057 }
1058