PluginProbe
Pay with Vipps and MobilePay for WooCommerce / 6.2.0
Pay with Vipps and MobilePay for WooCommerce v6.2.0
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.2.0, at recurring/includes/wc-vipps-recurring.php

1,062 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 if ( ! current_user_can( 'edit_shop_orders' ) ) {
677 wp_die( -1, 403 );
678 }
679
680 $order = wc_get_order( intval( $_REQUEST['orderId'] ) );
681 if ( ! is_a( $order, 'WC_Order' ) ) {
682 return;
683 }
684
685 if ( $order->get_payment_method() != $this->gateway()->id ) {
686 return;
687 }
688
689 $action = isset( $_REQUEST['do'] ) ? sanitize_title( $_REQUEST['do'] ) : 'none';
690
691 if ( $action == 'capture_payment' ) {
692 $this->gateway()->maybe_capture_payment( $order->get_id() );
693 }
694
695 print "1";
696 }
697
698 /**
699 * Check charge statuses scheduled action
700 *
701 * @param int|null $limit
702 *
703 * @return array
704 */
705 public function check_order_statuses( $limit = '' ): array {
706 if ( empty( $limit ) ) {
707 $limit = $this->gateway()->check_charges_amount;
708 }
709
710 $options = [
711 'limit' => $limit,
712 'payment_method' => $this->gateway()->id
713 ];
714
715 $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() );
716
717 if ( $this->gateway()->check_charges_sort_order === 'rand' ) {
718 $options['orderby'] = 'rand';
719 } else {
720 $options['orderby'] = 'post_date';
721 $options['order'] = $this->gateway()->check_charges_sort_order;
722 }
723
724 remove_all_filters( 'posts_orderby' );
725 $orders = wc_get_orders( $options );
726
727 foreach ( $orders as $order ) {
728 $order_id = WC_Vipps_Recurring_Helper::get_id( $order );
729
730 do_action( 'wc_vipps_recurring_before_cron_check_order_status', $order_id );
731 $this->gateway()->check_charge_status( $order_id );
732 do_action( 'wc_vipps_recurring_after_cron_check_order_status', $order_id );
733 }
734
735 return $orders;
736 }
737
738 /**
739 * Check the status of gateway change requests
740 */
741 public function check_gateway_change_agreement_statuses() {
742 $args = [
743 'subscription_status' => [ 'active', 'pending', 'on-hold' ],
744 ];
745
746 $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() );
747
748 $subscriptions = wcs_get_subscriptions( $args );
749
750 foreach ( $subscriptions as $subscription ) {
751 // check charge status
752 $this->gateway()->maybe_process_gateway_change( $subscription->get_id() );
753 }
754 }
755
756 /**
757 * Update a subscription's details in the app
758 */
759 public function update_subscription_details_in_app() {
760 $args = [
761 'subscriptions_per_page' => 5,
762 'subscription_status' => [ 'active', 'pending-cancel', 'cancelled', 'on-hold' ],
763 ];
764
765 $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() );
766
767 $subscriptions = wcs_get_subscriptions( $args );
768
769 foreach ( $subscriptions as $subscription ) {
770 // check charge status
771 $this->gateway()->maybe_update_subscription_details_in_app( $subscription->get_id() );
772 }
773 }
774
775 public function check_subscriptions_marked_for_deletion() {
776 $options = [
777 'limit' => 25,
778 'payment_method' => $this->gateway()->id,
779 ];
780
781 $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());
782
783 $subscriptions = wcs_get_subscriptions( $options );
784
785 WC_Vipps_Recurring_Logger::log( sprintf( "Running check_subscriptions_marked_for_deletion for %s subscriptions", count( $subscriptions ) ) );
786
787 foreach ( $subscriptions as $subscription ) {
788 $subscription_id = WC_Vipps_Recurring_Helper::get_id( $subscription );
789
790 // Check the status of this subscription's latest order charge just in case.
791 $latest_order = $subscription->get_last_order( 'all' );
792 $latest_order_id = WC_Vipps_Recurring_Helper::get_id( $latest_order );
793
794 do_action( 'wc_vipps_recurring_before_cron_check_order_status', $latest_order_id );
795 $this->gateway()->check_charge_status( $latest_order_id );
796 do_action( 'wc_vipps_recurring_after_cron_check_order_status', $latest_order_id );
797
798 // Hydrate objects
799 $latest_order = wc_get_order( $latest_order_id );
800 $subscription = wcs_get_subscription( $subscription_id );
801
802 // If this subscription has been manually updated in the mean-time, we no longer want to delete it.
803 // Similarly, if it has a billing email we don't want to delete it.
804 $empty_email = trim( $subscription->get_billing_email() ) === trim( WC_Vipps_Recurring_Helper::FAKE_USER_EMAIL ) || ! $subscription->get_billing_email();
805
806 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' ) );
807
808 $allowed_statuses = [
809 'pending',
810 'cancelled',
811 'pending-cancel'
812 ];
813
814 if ( ! in_array( $subscription->get_status( 'edit' ), $allowed_statuses )
815 || ! $empty_email
816 || wcs_order_contains_renewal( $latest_order ) ) {
817 WC_Vipps_Recurring_Logger::log( sprintf( "Removing subscription %s from the deletion queue as it should no longer be deleted.", $subscription_id ) );
818
819 WC_Vipps_Recurring_Helper::delete_meta_data( $subscription, WC_Vipps_Recurring_Helper::META_SUBSCRIPTION_MARKED_FOR_DELETION );
820 $subscription->save();
821
822 continue;
823 }
824
825 WC_Vipps_Recurring_Logger::log( sprintf( "Deleting subscription %s and latest order %s.", $subscription_id, $latest_order_id ) );
826 $latest_order->delete();
827 $subscription->delete();
828 }
829 }
830
831 /**
832 * Adds plugin action links.
833 *
834 * @since 1.0.0
835 * @version 4.0.0
836 */
837 public function plugin_action_links( $links ): array {
838 // IOK 2025-01-13 temporarily deactivated, because there is currently two
839 // sets of settings, and not enough room to disambituate between them (at least in norwegian).
840 return $links;
841
842 $plugin_links = [
843 '<a href="admin.php?page=wc-settings&tab=checkout&section=vipps_recurring">' . esc_html__( 'Settings', 'woo-vipps' ) . '</a>',
844 ];
845
846 return array_merge( $plugin_links, $links );
847 }
848
849 /**
850 * Handles upgrade routines.
851 *
852 * @since 3.1.0
853 * @version 3.1.0
854 */
855 public function install() {
856 $this->upgrade();
857 }
858
859 /**
860 * Add the gateways to WooCommerce.
861 *
862 * @param $methods
863 *
864 * @return array
865 * @since 1.0.0
866 * @version 4.0.0
867 */
868 public function add_gateways( $methods ): array {
869 if ( function_exists( 'wcs_create_renewal_order' ) && class_exists( 'WC_Subscriptions_Order' ) ) {
870 require_once( dirname( __FILE__ ) . "/wc-gateway-vipps-recurring.php" );
871
872 $methods[] = WC_Gateway_Vipps_Recurring::get_instance();
873 }
874
875 return $methods;
876 }
877
878 /**
879 * Enqueue our CSS and other assets.
880 */
881 public function wp_enqueue_scripts() {
882 wp_enqueue_style(
883 'woo-vipps-recurring',
884
885 WC_VIPPS_RECURRING_PLUGIN_URL . '/assets/build/main.css', [],
886 filemtime( WC_VIPPS_RECURRING_PLUGIN_PATH . '/assets/build/main.css' )
887 );
888
889 $asset = require WC_VIPPS_RECURRING_PLUGIN_PATH . '/assets/build/main.asset.php';
890
891 wp_enqueue_script(
892 'woo-vipps-recurring',
893 WC_VIPPS_RECURRING_PLUGIN_URL . '/assets/build/main.js',
894 $asset['dependencies'],
895 filemtime( WC_VIPPS_RECURRING_PLUGIN_PATH . '/assets/build/main.js' ),
896 true
897 );
898
899 $continue_shopping_link_page = ! empty( $this->gateway()->continue_shopping_link_page )
900 ? $this->gateway()->continue_shopping_link_page
901 : wc_get_page_id( 'shop' );
902
903 $continue_shopping_url = $continue_shopping_link_page ? get_permalink( $continue_shopping_link_page ) : home_url();
904 if ( empty( $continue_shopping_link_page ) ) {
905 $continue_shopping_url = home_url();
906 }
907
908 wp_localize_script( 'woo-vipps-recurring', 'VippsMobilePaySettings', [
909 'logo' => WC_VIPPS_RECURRING_PLUGIN_URL . '/assets/images/' . $this->gateway()->brand . '-logo.svg',
910 'continueShoppingUrl' => $continue_shopping_url
911 ] );
912
913 wp_set_script_translations( 'woo-vipps-recurring', 'woo-vipps', dirname( WC_VIPPS_MAIN_FILE ) . '/languages' );
914 }
915
916 /**
917 * Enqueue our CSS and other assets.
918 */
919 public function admin_enqueue_scripts() {
920 wp_enqueue_style( 'woo-vipps-recurring', plugins_url( 'assets/css/vipps-recurring-admin.css', WC_VIPPS_RECURRING_MAIN_FILE ), [],
921 filemtime( dirname( WC_VIPPS_RECURRING_MAIN_FILE ) . '/assets/css/vipps-recurring-admin.css' ) );
922
923 $this->ajax_config['nonce'] = wp_create_nonce( 'vipps_recurring_ajax_nonce' );
924 $this->ajax_config['currency'] = get_woocommerce_currency();
925
926 wp_enqueue_script(
927 'woo-vipps-recurring-admin',
928 plugins_url( 'assets/js/vipps-recurring-admin.js', WC_VIPPS_RECURRING_MAIN_FILE ),
929 [ 'wp-i18n' ],
930 filemtime( dirname( WC_VIPPS_RECURRING_MAIN_FILE ) . "/assets/js/vipps-recurring-admin.js" ),
931 true
932 );
933
934 wp_localize_script( 'woo-vipps-recurring-admin', 'VippsRecurringConfig', $this->ajax_config );
935 wp_set_script_translations( 'woo-vipps-recurring-admin', 'woo-vipps', dirname( WC_VIPPS_MAIN_FILE ) . '/languages' );
936 }
937
938 /**
939 * @param $schedules
940 *
941 * @return mixed
942 */
943 public function woocommerce_vipps_recurring_add_cron_schedules( $schedules ) {
944 $schedules['one_minute'] = [
945 'interval' => 60,
946 'display' => esc_html__( 'Every One Minute' ),
947 ];
948
949 return $schedules;
950 }
951
952 /**
953 * @throws WC_Vipps_Recurring_Exception
954 * @throws WC_Vipps_Recurring_Temporary_Exception
955 * @throws WC_Vipps_Recurring_Config_Exception
956 */
957 public function handle_webhook_callback() {
958 wc_nocache_headers();
959 status_header( 202, "Accepted" );
960
961 $raw_input = @file_get_contents( 'php://input' );
962 $body = @json_decode( $raw_input, true );
963
964 // If we have a sessionId, this indicates that we are dealing with a Checkout callback.
965 // Fire a hook and let Checkout deal with its own logic.
966 if ( isset( $body['sessionId'] ) ) {
967 $authorization_token = $_SERVER['HTTP_AUTHORIZATION'];
968
969 do_action( 'wc_vipps_recurring_checkout_callback', $body, $authorization_token );
970
971 // Early return to avoid potential errors downstream.
972 return;
973 }
974
975 $callback = $_REQUEST['callback'] ?? "";
976
977 if ( $callback !== "webhook" ) {
978 return;
979 }
980
981 if ( ! $body ) {
982 $error = json_last_error_msg();
983 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 ) );
984
985 return;
986 }
987
988 $local_webhooks = $this->gateway()->webhook_get_local()[ $this->gateway()->merchant_serial_number ];
989 $local_webhook = array_pop( $local_webhooks );
990 $secret = $local_webhook ? ( $local_webhook['secret'] ?? false ) : false;
991
992 $order_id = $body['chargeId'] ?? $body['agreementId'];
993 if ( ! $order_id ) {
994 WC_Vipps_Recurring_Logger::log( sprintf( "Could not find order id in webhook with body %s", json_encode( $body ) ) );
995
996 return;
997 }
998
999 if ( ! $secret ) {
1000 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 ) );
1001 }
1002
1003 if ( ! $this->verify_webhook( $raw_input, $secret ) ) {
1004 return;
1005 }
1006
1007 do_action( "wc_vipps_recurring_webhook_callback", $body, $raw_input );
1008
1009 // We now have a validated webhook
1010 $this->gateway()->handle_webhook_callback( $body );
1011 }
1012
1013 public function verify_webhook( $serialized, $secret ): bool {
1014 $expected_auth = $_SERVER['HTTP_AUTHORIZATION'] ?? ( $_SERVER['HTTP_X_VIPPS_AUTHORIZATION'] ?? "" );
1015 $expected_date = $_SERVER['HTTP_X_MS_DATE'] ?? '';
1016
1017 $hashed_payload = base64_encode( hash( 'sha256', $serialized, true ) );
1018 $path_and_query = $_SERVER['REQUEST_URI'];
1019 $host = $_SERVER['HTTP_HOST'];
1020 $toSign = "POST\n{$path_and_query}\n$expected_date;$host;$hashed_payload";
1021 $signature = base64_encode( hash_hmac( 'sha256', $toSign, $secret, true ) );
1022 $auth = "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature=$signature";
1023
1024 return ( $auth == $expected_auth );
1025 }
1026
1027 public function generate_order_prefix(): string {
1028 $parts = parse_url( site_url() );
1029 if ( ! $parts ) {
1030 return 'woo-';
1031 }
1032
1033 $domain = explode( ".", $parts['host'] ?? '' );
1034 if ( empty( $domain ) ) {
1035 return 'woo-';
1036 }
1037
1038 $first = strtolower( $domain[0] );
1039 $second = $domain[1] ?? '';
1040
1041 // Select first part of domain unless that has no content, otherwise second. Default to "woo-" again.
1042 $key = $first;
1043 if ( in_array( $first, [ 'www', 'test', 'dev', 'vdev' ] ) && ! empty( $second ) ) {
1044 $key = $second;
1045 }
1046
1047 // Use only 8 chars for the site. Try to make it so by dropping vowels, if that doesn't succeed, just chop it.
1048 $key = sanitize_title( $key );
1049 $len = strlen( $key );
1050 if ( $len <= 8 ) {
1051 return "woo-$key-";
1052 }
1053
1054 $kzk = preg_replace( "/[aeiouæøåüö]/i", "", $key );
1055 if ( strlen( $kzk ) <= 8 ) {
1056 return "woo-$kzk-";
1057 }
1058
1059 return "woo-" . substr( $key, 0, 8 ) . "-";
1060 }
1061 }
1062