PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 1.7.0.9 All 211 releases
charitable / includes / admin / onboarding / class-charitable-checklist.php

class-charitable-checklist.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at includes/admin/onboarding/class-charitable-checklist.php

1,307 lines 39.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Checklist.
4 *
5 * @package Charitable/Classes/Charitable_Checklist
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.8.2
10 * @version 1.8.2
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 if ( ! class_exists( 'Charitable_Checklist' ) ) :
19
20 /**
21 * Charitable_Checklist
22 *
23 * @final
24 * @since 1.8.2
25 */
26 class Charitable_Checklist {
27
28 /**
29 * The name of the option value.
30 *
31 * @var array
32 */
33 private $checklist_option_name = 'charitable_onboarding_checklist';
34
35 /**
36 * The name of the option value.
37 *
38 * @var array
39 */
40 const PRO_PLUGIN = 'charitable-pro/charitable.php';
41
42 /**
43 * The steps in the checklist.
44 *
45 * @var array
46 */
47 public $steps = array(
48 'connect-gateway',
49 'general-settings',
50 'email-settings',
51 'first-campaign',
52 'first-donation',
53 'next-level',
54 );
55
56 /**
57 * The step urls in the checklist.
58 *
59 * @var array
60 */
61 public $steps_url = array(
62 'connect-gateway' => 'admin.php?page=charitable-settings&tab=gateways&checklist=connect-gateway',
63 'general-settings' => 'admin.php?page=charitable-settings&checklist=general-settings',
64 'email-settings' => 'admin.php?page=charitable-settings&tab=emails&checklist=email-settings',
65 'first-campaign' => 'admin.php?page=charitable-campaign-builder&view=template',
66 'first-donation' => 'admin.php?edit.php?post_type=donation',
67 'next-level' => 'admin.php?page=charitable-addons',
68 );
69
70 /**
71 * The single instance of this class.
72 *
73 * @var Charitable_Checklist|null
74 */
75 private static $instance = null;
76
77 /**
78 * Create object instance.
79 *
80 * @since 1.8.2
81 */
82 public function __construct() {
83 }
84
85 /**
86 * Run things upon init.
87 *
88 * @since 1.8.2
89 *
90 * @return void
91 */
92 public function init() {
93 }
94
95 /**
96 * Enqueue assets.
97 *
98 * @since 1.8.2
99 */
100 public function enqueue_styles_and_scripts() {
101
102 if ( $this->maybe_load_checklist_assets() ) {
103
104 $min = charitable_get_min_suffix(); // 1.8.3
105 $version = charitable()->get_version();
106 $assets_dir = charitable()->get_path( 'assets', false );
107 $style_version = charitable_get_style_version();
108
109 wp_enqueue_style(
110 'charitable-onboarding-checklist',
111 $assets_dir . 'css/onboarding/checklist' . $min . '.css',
112 array(),
113 $version
114 );
115
116 // Enqueue about-us.css for navigation styling
117 wp_enqueue_style(
118 'charitable-admin-about-us',
119 $assets_dir . 'css/admin/about-us.css',
120 array(),
121 $version
122 );
123
124 /* tour */
125
126 wp_enqueue_script(
127 'charitable-float-ui-core',
128 charitable()->get_path( 'directory', false ) . 'assets/js/libraries/floating-ui-core.min.js',
129 array( 'jquery' ),
130 charitable()->get_version()
131 );
132
133 wp_enqueue_script(
134 'charitable-float-ui-dom',
135 charitable()->get_path( 'directory', false ) . 'assets/js/libraries/floating-ui-dom.min.js',
136 array( 'charitable-float-ui-core' ),
137 charitable()->get_version()
138 );
139
140 wp_enqueue_script(
141 'charitable-shepherd',
142 charitable()->get_path( 'directory', false ) . 'assets/js/libraries/shepherd.js',
143 array( 'jquery', 'charitable-float-ui-core', 'charitable-float-ui-dom' ),
144 charitable()->get_version()
145 );
146
147 wp_enqueue_style(
148 'charitable-shepherd',
149 charitable()->get_path( 'directory', false ) . "assets/css/libraries/shepherd{$min}.css",
150 array(),
151 $style_version
152 );
153
154 wp_enqueue_script(
155 'charitable-admin-checklist',
156 charitable()->get_path( 'directory', false ) . 'assets/js/admin/charitable-admin-checklist.js',
157 array( 'jquery', 'charitable-shepherd', 'charitable-float-ui-core', 'charitable-float-ui-dom' ), // 'charitable-admin-utils',
158 charitable()->get_version()
159 );
160
161 wp_localize_script(
162 'charitable-admin-checklist',
163 'charitable_admin_checklist_onboarding',
164 [
165 'nonce' => wp_create_nonce( 'charitable_onboarding_ajax_nonce' ),
166 'ajaxurl' => admin_url( 'admin-ajax.php' ),
167 'option' => array(
168 'window_closed' => $this->get_checklist_option( 'window_closed' ),
169 'tour' => array(
170 'status' => $this->get_checklist_option( 'status' ),
171 'steps_completed' => $this->get_checklist_option( 'steps' ),
172 ),
173 ),
174 'strings' => $this->get_localized_strings(),
175 ]
176 );
177
178 }
179 }
180
181 /**
182 * Add the checklist-completed class to the admin body if it's completed, etc.
183 *
184 * @since 1.8.2
185 *
186 * @param string $classes Body classes.
187 * @return string
188 */
189 public function add_body_class( $classes = '' ) {
190
191 if ( $this->is_checklist_completed() ) {
192 $classes .= ' charitable-checklist-completed ';
193 } elseif ( $this->is_checklist_skipped() ) {
194 $classes .= ' charitable-checklist-skipped ';
195 } elseif ( $this->is_checklist_started() ) {
196 $stats = $this->get_steps_stats();
197 $classes .= ' charitable-checklist-started charitable-checklist-steps-completed-' . intval( $stats['completed'] ) . ' charitable-checklist-steps-total-' . intval( $stats['total'] . ' ' );
198 }
199
200 // add a class if we are on the actual checklist page.
201 if ( isset( $_GET['page'] ) && 'charitable-setup-checklist' === $_GET['page'] ) { // phpcs:ignore
202 $classes .= ' charitable-checklist-page ';
203 }
204
205 return $classes;
206 }
207
208 /**
209 * Get percentage completed.
210 *
211 * @since 1.8.2
212 *
213 * @return int
214 */
215 public function get_percentage_completed() {
216
217 if ( $this->is_checklist_completed() ) {
218 return 100;
219 }
220
221 $stats = $this->get_steps_stats();
222
223 if ( ! is_array( $stats ) || empty( $stats ) || ! isset( $stats['total'] ) || ! isset( $stats['completed'] ) ) {
224 return 0;
225 }
226
227 if ( 0 === intval( $stats['total'] ) ) {
228 return 0;
229 }
230
231 return intval( ( intval( $stats['completed'] ) / intval( $stats['total'] ) ) * 100 );
232 }
233
234 /**
235 * Get the checklist menu HTML.
236 *
237 * @since 1.8.2
238 *
239 * @return string
240 */
241 public function get_checklist_bar_html() {
242
243 $percentage = $this->get_percentage_completed();
244 $percentage = min( 100, max( 0, $percentage ) );
245
246 $html = '<span class="charitable-checklist-progress-bar-container" style="display: block;height: 4px;margin: 5px 0;background-color: #000;"><span style="background-color: #5AA152; width: ' . $percentage . '%; height: 4px; display: block;"></span></span>';
247
248 return $html;
249 }
250
251 /**
252 * Add checklist to WordPress admin menu.
253 *
254 * @since 1.8.2
255 * @version 1.8.8.1
256 *
257 * @param array $submenu The submenu array.
258 *
259 * @return array
260 */
261 public function add_checklist_to_menu( $submenu = array() ) {
262
263 if ( ( defined( 'CHARITABLE_ONBOARDING_NO_CHECKLIST' ) && CHARITABLE_ONBOARDING_NO_CHECKLIST ) ) {
264 return $submenu;
265 }
266
267 // Always show checklist menu item - removed time-based restrictions
268 $checklist_menu_item = array(
269 'page_title' => __( 'Checklist', 'charitable' ),
270 'menu_title' => __( 'Checklist', 'charitable' ) . $this->get_checklist_bar_html(),
271 'menu_slug' => 'charitable-setup-checklist',
272 'function' => array( $this, 'render_setup_checklist_page' ),
273 'capability' => 'manage_charitable_settings',
274 );
275
276 // Add checklist menu item to the beginnnning of the submenu.
277 array_unshift( $submenu, $checklist_menu_item );
278
279 return $submenu;
280 }
281
282 /**
283 * Display the Charitable Growth Tools page.
284 *
285 * @since 1.8.1.15
286 *
287 * @return void
288 */
289 public function render_setup_checklist_page() {
290 charitable_admin_view( 'checklist/checklist' );
291 }
292
293 /**
294 * Save the checklist option via AJAX.
295 *
296 * @since 1.8.2
297 *
298 * @return void
299 */
300 public function save_checklist_option_ajax() {
301
302 if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'charitable_onboarding_ajax_nonce' ) ) { // phpcs:ignore
303 wp_send_json_error( array( 'message' => 'Invalid nonce' ) );
304 }
305
306 if ( ! isset( $_POST['option_data'] ) ) {
307 wp_send_json_error( array( 'message' => 'Missing option or value' ) );
308 }
309
310 if ( isset( $_POST['option_data']['status'] ) && in_array( sanitize_text_field( $_POST['option_data']['status'] ), array( 'completed', 'skipped', 'start', 'init' ) ) ) {
311 $status = sanitize_text_field( wp_unslash( $_POST['option_data']['status'] ) );
312 $this->update_checklist_status( $status );
313 wp_send_json_success( array( 'message' => esc_html__( 'Charitable checklist status saved', 'charitable' ) ) );
314 } else {
315 // This isn't updating the status of the checklist, just passing a value.
316 $window_closed = isset( $_POST['option_data']['window_closed'] ) ? sanitize_text_field( $_POST['option_data']['window_closed'] ) : ''; // phpcs:ignore
317 if ( $window_closed !== '' ) {
318 $options = $this->get_checklist_data();
319 $options['window_closed'] = $window_closed;
320 update_option( $this->checklist_option_name, $options );
321 wp_send_json_success( array( 'message' => esc_html__( 'Charitable checklist information saved', 'charitable' ) ) );
322 }
323 }
324
325 if ( isset( $_POST['option_data']['stepStatus'] ) && 'completed' === sanitize_text_field( $_POST['option_data']['stepStatus'] ) ) { // phpcs:ignore
326 if ( isset( $_POST['option_data']['step'] ) ) {
327 $step = sanitize_text_field( wp_unslash( $_POST['option_data']['step'] ) );
328 $this->mark_step_completed( $step );
329 wp_send_json_success( array( 'message' => esc_html__( 'Charitable checklist information saved', 'charitable' ) ) );
330 } else {
331 wp_send_json_error( array( 'message' => esc_html__( 'No checklist step was updated.', 'charitable' ) ) );
332 }
333
334 }
335
336 wp_send_json_success( array( 'message' => esc_html__( 'No checklist information was updated.', 'charitable' ) ) );
337 }
338
339 /**
340 * Get the CSS classes for the steps.
341 *
342 * @since 1.8.1.5
343 *
344 * @return array
345 */
346 public function get_steps_css() {
347
348 if ( empty( $this->steps ) ) {
349 return array();
350 }
351
352 $checklist_classes = array();
353
354 foreach ( $this->steps as $step ) {
355 $checklist_classes[ $step ] = $this->is_step_completed( $step ) ? 'charitable-checklist-completed charitable-checklist-checked' : 'charitable-checklist-unchecked';
356 }
357
358 return $checklist_classes;
359 }
360
361 /**
362 * Get the checklist urls.
363 *
364 * @since 1.8.2
365 *
366 * @return array
367 */
368 public function get_steps_urls() {
369 return $this->steps_url;
370 }
371
372 /**
373 * Check if the checklist is completed.
374 *
375 * @since 1.8.2
376 *
377 * @return bool
378 */
379 public function is_checklist_completed() {
380
381 $options = $this->get_checklist_data();
382
383 if ( $options['status'] === 'completed' ) {
384 return true;
385 }
386
387 // Go through all the steps, check if the step is completed, and if they are all completed return true.
388 foreach ( $this->steps as $step ) {
389 // ignore first-donation.
390 if ( 'first-donation' === $step ) {
391 continue;
392 }
393 if ( ! $this->is_step_completed( $step ) ) {
394 return false;
395 }
396 }
397
398 return true;
399 }
400
401 /**
402 * Check if the checklist is completed.
403 *
404 * @since 1.8.1.5
405 *
406 * @return bool
407 */
408 public function is_checklist_skipped() {
409
410 $options = $this->get_checklist_data();
411
412 if ( $options['status'] === 'skipped' ) {
413 return true;
414 }
415
416 return false;
417 }
418
419 /**
420 * Check if the checklist should be considered completed based on actual conditions.
421 * This method determines if the checklist should be auto-completed based on:
422 * - Having campaigns created
423 * - Time since activation (14+ days)
424 * - Other completion criteria
425 *
426 * @since 1.8.2
427 * @version 1.8.8.1
428 *
429 * @return bool
430 */
431 public function should_checklist_be_completed() {
432
433 $total_campaigns = wp_count_posts( 'campaign' );
434 $count_campaigns = ! empty( $total_campaigns->publish ) ? $total_campaigns->publish : 0;
435
436 // If there are campaigns created, check activation time
437 if ( intval( $count_campaigns ) > 0 ) {
438 $activation_date = get_option( 'wpcharitable_activated_datetime', false );
439
440 // If wpcharitable_activated_datetime is false, try the backup option
441 if ( false === $activation_date ) {
442 $activation_date = get_option( 'charitable_activated', false );
443 if ( is_array( $activation_date ) ) {
444 foreach ( $activation_date as $date ) {
445 if ( is_numeric( $date ) ) {
446 $activation_date = $date;
447 break;
448 }
449 }
450 }
451 }
452
453 // If we have an activation date and it's been more than 14 days, auto-complete
454 if ( false !== $activation_date && is_numeric( $activation_date ) ) {
455 $difference = time() - $activation_date;
456 if ( $difference > ( 14 * 24 * 60 * 60 ) ) {
457 return true;
458 }
459 }
460 }
461
462 // Also check if all required steps are actually completed
463 $required_steps = array( 'connect-gateway', 'general-settings', 'email-settings', 'first-campaign' );
464 $all_steps_completed = true;
465
466 foreach ( $required_steps as $step ) {
467 if ( ! $this->is_step_completed( $step, false ) ) {
468 $all_steps_completed = false;
469 break;
470 }
471 }
472
473 return $all_steps_completed;
474 }
475
476 /**
477 * Check if the checklist is started.
478 *
479 * @since 1.8.2
480 *
481 * @return bool
482 */
483 public function is_checklist_started() {
484
485 $options = $this->get_checklist_data();
486
487 if ( $options['status'] === 'start' ) {
488 return true;
489 }
490
491 // check if any of the steps have been completed.
492 foreach ( $this->steps as $step ) {
493 if ( $this->is_step_completed( $step ) ) {
494 return true;
495 }
496 }
497
498 return false;
499 }
500
501 /**
502 * Get the stats for the steps.
503 *
504 * @since 1.8.2
505 *
506 * @return array
507 */
508 public function get_steps_stats() {
509
510 $stats = array(
511 'total' => count( $this->steps ) - 1,
512 'completed' => 0,
513 );
514
515 foreach ( $this->steps as $step ) {
516 if ( $this->is_step_completed( $step ) && 'first-donation' !== $step ) {
517 ++$stats['completed'];
518 }
519 }
520
521 return $stats;
522 }
523
524 /**
525 * After saving general settings after a checklist prompt, return to the checklist.
526 *
527 * @since 1.8.2
528 *
529 * @param array $values Current values.
530 * @param array $new_values New values.
531 *
532 * @return array
533 */
534 public function maybe_redirect_to_checklist_after_event( $values, $new_values ) { // phpcs:ignore
535
536 if ( ! function_exists( 'charitable_is_settings_view' ) || ! charitable_is_settings_view() ) {
537 return $values;
538 }
539
540 if ( empty( $_POST['_wp_http_referer'] ) ) { // phpcs:ignore
541 return $values;
542 }
543
544 // if checklist=general-settings is in the _wp_http_referer, then redirect to the checklist page.
545 if ( strpos( $_POST['_wp_http_referer'], 'checklist=general-settings' ) !== false ) { // phpcs:ignore
546 $redirect_url = admin_url( 'admin.php?page=charitable-setup-checklist&step=continue' );
547 wp_safe_redirect( $redirect_url );
548 exit;
549 }
550
551 // if checklist=general-settings is in the _wp_http_referer, then redirect to the checklist page.
552 if ( strpos( $_POST['_wp_http_referer'], 'checklist=email-settings' ) !== false ) { // phpcs:ignore
553 $redirect_url = admin_url( 'admin.php?page=charitable-setup-checklist&step=continue' );
554 wp_safe_redirect( $redirect_url );
555 exit;
556 }
557
558 return $values;
559 }
560
561 /**
562 * After saving general settings after a checklist prompt, maybe return to the checklist.
563 *
564 * @since 1.8.2
565 *
566 * @return void
567 */
568 public function maybe_redirect_to_next_step() {
569
570 // if the page is the checklist page and the next step is simple 'continue' then redirect to the checklist page with an updated step query string.
571 if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'charitable-setup-checklist' || ! isset( $_GET['step'] ) || $_GET['step'] !== 'continue' ) { // phpcs:ignore
572 return;
573 }
574
575 // we need to find the next step that has not been completed.
576 $next_checklist_url = $this->get_next_checklist_url();
577
578 if ( empty( $next_checklist_url ) ) {
579 wp_safe_redirect( admin_url( 'admin.php?page=charitable-setup-checklist' ) );
580 exit;
581 }
582
583 wp_safe_redirect( $next_checklist_url );
584 exit;
585 }
586
587 /**
588 * Confirm general settings was done, and check it off.
589 *
590 * @since 1.8.2
591 *
592 * @param array $values Current values.
593 * @param array $new_values New values.
594 *
595 * @return array
596 */
597 public function confirm_general_settings( $values, $new_values ) { // phpcs:ignore
598
599 // Not the right setting page.
600 if ( ! isset( $_POST['_wp_http_referer'] ) || ! isset( $_POST['option_page'] ) || 'charitable_settings' !== $_POST['option_page'] || empty( $_POST['charitable_settings'] ) ) { // phpcs:ignore
601 return $values;
602 }
603
604 if ( strpos( $_POST['_wp_http_referer'], 'checklist=general-settings' ) === false ) { // phpcs:ignore
605 return $values;
606 }
607
608 $this->mark_step_completed( 'general-settings' );
609
610 return $values;
611 }
612
613 /**
614 * Confirm email settings form was submitted, and check it off.
615 *
616 * @since 1.8.2
617 *
618 * @param array $values Current values.
619 * @param array $new_values New values.
620 *
621 * @return array
622 */
623 public function confirm_email_settings( $values, $new_values ) { // phpcs:ignore
624
625 // Not the right setting page.
626 if ( ! isset( $_POST['_wp_http_referer'] ) || ! isset( $_POST['option_page'] ) || 'charitable_settings' !== $_POST['option_page'] || empty( $_POST['charitable_settings'] ) ) { // phpcs:ignore
627 return $values;
628 }
629
630 if ( strpos( $_POST['_wp_http_referer'], 'checklist=email-settings' ) === false ) { // phpcs:ignore
631 return $values;
632 }
633
634 $this->mark_step_completed( 'email-settings' );
635
636 return $values;
637 }
638
639 /**
640 * If the user enables or disables an email while on the settings page then the step is checked off (even if the user doesn't save the settings form).
641 *
642 * @since 1.8.2
643 *
644 * @return bool
645 */
646 public function confirm_email_changes() {
647
648 // Not the right setting page.
649 if ( ! isset( $_GET['page'] ) || ! isset( $_GET['tab'] ) || 'charitable-settings' !== $_GET['page'] || 'emails' !== $_GET['tab'] ) { // phpcs:ignore
650 return false;
651 }
652
653 // There needs to be an action taking place.
654 if ( ! isset( $_GET['charitable_action'] ) ) { // phpcs:ignore
655 return false;
656 }
657
658 if ( ! $this->is_checklist_started() || $this->is_checklist_completed() || $this->is_checklist_skipped() ) {
659 return false;
660 }
661
662 $completed = $this->mark_step_completed( 'email-settings' );
663
664 return $completed;
665 }
666
667 /**
668 * Save the checklist option.
669 *
670 * @since 1.8.2
671 *
672 * @param string $step The option to save. Could be 'general-settings', 'email-settings', 'connect-gateway', etc.
673 *
674 * @return bool
675 */
676 public function mark_step_completed( $step = '' ) {
677
678 if ( '' === $step ) {
679 return false;
680 }
681
682 $options = $this->get_checklist_data();
683 if ( ! in_array( $step, $options['steps'] ) ) {
684 $options['steps'][] = $step;
685 }
686 update_option( $this->checklist_option_name, $options );
687
688 return true;
689 }
690
691 /**
692 * Mark checklist completed (regardless of steps).
693 *
694 * @since 1.8.2
695 */
696 public function maybe_complete_checklist() {
697
698 // if we are on the checklist page, it's a must.
699 if ( ! isset( $_GET['page'] ) || 'charitable-setup-checklist' !== $_GET['page'] ) { // phpcs:ignore
700 return;
701 }
702
703 if ( ! isset( $_GET['completed'] ) || 'true' !== $_GET['completed'] ) { // phpcs:ignore
704 return;
705 }
706
707 // go through all the steps and mark them completed.
708 foreach ( $this->steps as $step ) {
709 $this->mark_step_completed( $step );
710 }
711
712 $this->update_checklist_status( 'completed' );
713
714 // redirect to the checklist page.
715 wp_safe_redirect( admin_url( 'admin.php?page=charitable-setup-checklist' ) );
716 exit;
717 }
718
719 /**
720 * Manually mark checklist as completed.
721 * This method can be called to force completion of the checklist.
722 *
723 * @since 1.8.2
724 * @version 1.8.8.1
725 *
726 * @return bool True if successful, false otherwise.
727 */
728 public function force_complete_checklist() {
729
730 // Mark all steps as completed
731 foreach ( $this->steps as $step ) {
732 $this->mark_step_completed( $step );
733 }
734
735 // Update status to completed
736 $result = $this->update_checklist_status( 'completed' );
737
738 // Clear any existing dashboard notices
739 $dashboard_notices = get_option( 'charitable_dashboard_notifications', array() );
740 if ( isset( $dashboard_notices['checklist_status'] ) ) {
741 unset( $dashboard_notices['checklist_status'] );
742 update_option( 'charitable_dashboard_notifications', $dashboard_notices );
743 }
744
745 return $result;
746 }
747
748
749 /**
750 * Save the checklist option.
751 *
752 * @since 1.8.2
753 *
754 * @param string $status The status to save. Could be 'completed', 'skipped', 'start', 'init'.
755 *
756 * @return bool
757 */
758 public function update_checklist_status( $status = '' ) {
759
760 if ( '' === $status ) {
761 return false;
762 }
763
764 $options = $this->get_checklist_data();
765 $options['status'] = $status;
766 update_option( $this->checklist_option_name, $options );
767
768 return true;
769 }
770
771 /**
772 * Get the start checklist URL.
773 *
774 * @since 1.8.2
775 *
776 * @return string
777 */
778 public function get_start_checklist_url() {
779
780 return apply_filters( 'charitable_start_checklist_url', admin_url( 'admin.php?page=charitable-setup-checklist' ) );
781 }
782
783 /**
784 * Get the next checklist URL.
785 *
786 * @since 1.8.2
787 *
788 * @return string
789 */
790 public function get_next_checklist_url() {
791
792 // Determine what is the first step that has not been completed.
793 $options = $this->get_checklist_data();
794 $steps = ! empty( $options['steps'] ) ? $options['steps'] : array();
795 $steps = ! empty( $steps ) ? array_values( array_intersect( $this->steps, $steps ) ) : array();
796
797 foreach ( $this->steps as $step ) {
798 if ( $step === 'first-donation' ) {
799 continue;
800 }
801 if ( ! in_array( $step, $steps, true ) ) {
802 // ...find that step in $steps_url and return that URL.
803 if ( isset( $this->steps_url[ $step ] ) ) {
804 // there's an exemption for connect-gateway, check if it's completed and if so skip.
805 if ( 'connect-gateway' === $step && $this->is_step_completed( 'connect-gateway' ) ) {
806 continue;
807 }
808 return apply_filters( 'charitable_start_checklist_url', admin_url( 'admin.php?page=charitable-setup-checklist#' . $step ) );
809 }
810 }
811 }
812 // when in doubt, return the start checklist URL.
813 return apply_filters( 'charitable_start_checklist_url', admin_url( 'admin.php?page=charitable-setup-checklist' ) );
814 }
815
816 /**
817 * Translate strings.
818 *
819 * @since 1.8.2
820 *
821 * @return array
822 */
823 public function get_localized_strings() {
824
825 $strings = array(
826 'version' => '1.8.2',
827 'ok' => esc_html__( 'OK', 'charitable' ),
828 'next' => esc_html__( 'Next', 'charitable' ),
829 'start_tour' => esc_html__( 'Start Tour', 'charitable' ),
830 'watch_video' => esc_html__( 'Watch Video', 'charitable' ),
831 'choose_a_template' => esc_html__( 'Choose a Template', 'charitable' ),
832 'lets_get_started' => esc_html__( 'Get Started', 'charitable' ),
833 'general_settings_step_0_title' => esc_html__( 'Settings Page', 'charitable' ),
834 'general_settings_step_0_text' => '<p style="margin-bottom: 0;">' . esc_html__( 'This is where general settings for your Charitable plugin live. You can change your currency, country, and the behavior of your donation form on this page.', 'charitable' ) . '</p>',
835 'general_settings_step_1_text' => '<p>' . esc_html__( 'Confirm your settings and save changes to complete this item on the checklist.', 'charitable' ) . '</p>',
836 'email_settings_step_0_title' => esc_html__( 'Email Settings', 'charitable' ),
837 'email_settings_step_0_text' => '<p>' . esc_html__( 'Enable and configure email notifications for donors, campaign creators, and site admins (like you).', 'charitable' ) . '</p>',
838 'email_settings_step_1_text' => '<h2>' . esc_html__( 'Donor Donation Receipt', 'charitable' ) . '</h2><p>' . esc_html__( 'This email sends donors a receipt after they make a donation.', 'charitable' ) . '</p>',
839 'email_settings_step_2_text' => '<h2>' . esc_html__( 'Admin New Donation Notification', 'charitable' ) . '</h2><p>' . esc_html__( 'This email sends you (the admin) a notification when a new donation has been received.', 'charitable' ) . '</p><p>' . esc_html__( 'You can ', 'charitable' ) . '<a href="https://www.wpcharitable.com/documentation/start-here/#Emails" target="_blank">' . esc_html__( 'read more about email settings', 'charitable' ) . '</a> ' . esc_html__( 'in our docs', 'charitable' ) . '.</p>',
840 'email_settings_step_3_text' => '<p>' . esc_html__( 'Confirm your email settings and save changes to complete this item on the checklist.', 'charitable' ) . '</p>',
841
842 'gateway_settings_step_0_text' => '<h2>' . esc_html__( 'Gateway Settings', 'charitable' ) . '</h2><p>' . esc_html__( 'Connect to a gateway to start getting donations as soon possible. Want to test before setting up a gateway or taking alternate methods of payment? Enable "Offline Mode".', 'charitable' ) . '</p>',
843 'gateway_settings_step_1_text' => '<p>' . esc_html__( 'We recommend Stripe if it\'s available in your country', 'charitable' ) . '</p>',
844 'gateway_settings_step_2_text' => '<p>' . esc_html__( 'Test mode allows Charitable to connect to Stripe and other payment gateways in their test modes. Make sure to check this off when you are ready to accept live donations.', 'charitable' ) . '</p>',
845 'gateway_settings_step_3_text' => '<p>' . esc_html__( 'Confirm your gateway settings and save changes to complete this item on the checklist.', 'charitable' ) . '</p>',
846
847 );
848
849 $strings = apply_filters( 'charitable_checklist_strings', $strings );
850
851 return $strings;
852 }
853
854 /**
855 * Get tour parameter(s).
856 *
857 * @since 1.8.2
858 *
859 * @param string $option Option key.
860 * @return mixed
861 */
862 public function get_checklist_option( $option = false ) {
863
864 $schema = $this->checklist_option_schema();
865 $options = $this->get_checklist_data();
866 $steps = ! empty( $options['steps'] ) ? $options['steps'] : array();
867
868 if ( 'status' === $option && defined( 'CHARITABLE_ONBOARDING_FORCE_CHECKLIST' ) && CHARITABLE_ONBOARDING_FORCE_CHECKLIST ) {
869 return 'init';
870 }
871
872 // Step in and override here... if 'status' is the option being requested and there is already at least one campaign created then return 'skipped'.
873 if ( 'status' === $option && ( ! empty( $steps ) || ! isset( $options['status'] ) || 'init' === $options['status'] ) ) {
874
875 if ( ( defined( 'CHARITABLE_ONBOARDING_NO_CHECKLIST' ) && CHARITABLE_ONBOARDING_NO_CHECKLIST ) ) {
876 return 'skipped';
877 }
878 }
879
880 if ( ! $options || ! is_array( $options ) ) {
881 return $schema[ $option ] ?? '';
882 }
883
884 return $options[ $option ] ?? $schema[ $option ] ?? '';
885 }
886
887 /**
888 * Check if a step is completed.
889 *
890 * This is how we tell when a step is completed on the checklist.
891 *
892 * "general-settings" - if the user has saved general settings at least once.
893 * "email-settings" - if the user has saved email settings at least once.
894 * "connect-payment" - if any payment gateway is connected.
895 * "first-campaign" - if a campaign has been created.
896 * "first-donation" - if a donation has been created.
897 * "opt-in" - if the user has opted in to the newsletter/sharing data.
898 * "fundraising-next-level" - when pro is active or ???
899 *
900 * @since 1.8.2
901 *
902 * @param string $step The step to check.
903 * @param bool $update_option If true, update the checklist data option if we "double check" the step and it is completed.
904 *
905 * @return bool
906 */
907 public function is_step_completed( $step = '', $update_option = true ) {
908
909 if ( empty( $step ) ) {
910 return false;
911 }
912
913 $checklist_data = $this->get_checklist_data();
914 // we store this as an option, so load the option and check the array key.
915
916 if ( ! isset( $checklist_data['steps'] ) ) {
917 return false;
918 } else {
919 $completed_steps = (array) $checklist_data['steps'];
920 }
921
922 if ( in_array( $step, $completed_steps ) ) {
923 return true;
924 }
925
926 // if the step is not in the array, sometimes we can "double" check depending on the step.
927 switch ( $step ) {
928 case 'first-campaign':
929 // if there is at least one campaign, this is completed.
930 $campaigns_count = Charitable_Campaigns::query(
931 array(
932 'posts_per_page' => -1,
933 'fields' => 'ids',
934 )
935 )->found_posts;
936 if ( $campaigns_count > 0 ) {
937 if ( $update_option ) {
938 $this->mark_step_completed( 'first-campaign' );
939 }
940 return true;
941 }
942 break;
943
944 case 'first-donation':
945 // if the step is false... then if there is at least one donation, this is completed.
946 $total_donations_array = (array) wp_count_posts( 'donation' ); // the function caches this, so we shouldn't have to.
947 $total_donations = ! empty( $total_donations_array ) ? array_sum( $total_donations_array ) : 0;
948 if ( $total_donations > 0 ) {
949 if ( $update_option ) {
950 $this->mark_step_completed( 'first-donation' );
951 }
952 return true;
953 }
954 break;
955
956 case 'connect-gateway':
957 $stripe_gateway = new Charitable_Gateway_Stripe_AM();
958 $stripe_connected = $stripe_gateway->maybe_stripe_connected();
959 $gateway_mode = ( charitable_get_option( 'test_mode' ) ) ? 'test' : 'live';
960
961 if ( $stripe_connected || ! empty( $_POST['charitable_settings']['gateways'] ) ) { // phpcs:ignore
962 if ( $update_option ) {
963 $this->mark_step_completed( 'connect-gateway' );
964 }
965 return true;
966 }
967 return false;
968
969 break;
970
971 default:
972 break;
973 }
974
975 return false;
976 }
977
978 /**
979 * Check if the first campaign step is completed.
980 *
981 * @since 1.8.2
982 *
983 * @return bool
984 */
985 public function check_step_first_campaign() {
986 if ( $this->is_step_completed( 'first-campaign' ) ) {
987 return true;
988 }
989
990 // if there is at least one campaign, this is completed.
991 $count_campaigns = wp_count_posts( 'campaign' );
992 if ( $count_campaigns > 0 ) {
993 $this->mark_step_completed( 'first-campaign' );
994 return true;
995 }
996
997 return false;
998 }
999
1000 /**
1001 * The schema for the checklist option.
1002 *
1003 * @since 1.8.2
1004 *
1005 * @return array
1006 */
1007 public function checklist_option_schema() {
1008
1009 return array(
1010 'status' => 'init',
1011 'steps' => array(),
1012 'window_closed' => '',
1013 );
1014 }
1015
1016 /**
1017 * Retrieve the checklist data (or create it if it doesn't exist).
1018 *
1019 * @since 1.8.2
1020 *
1021 * @return array
1022 */
1023 public function get_checklist_data() {
1024
1025 $checklist_data = (array) get_option( $this->checklist_option_name, array() );
1026
1027 if ( empty( $checklist_data ) || false === $checklist_data ) {
1028 $checklist_data = $this->checklist_option_schema();
1029 update_option( $this->checklist_option_name, $checklist_data );
1030 }
1031
1032 return $checklist_data;
1033 }
1034
1035 /**
1036 * Adds (probably) the checklist HTML to the footer.
1037 *
1038 * @since 1.8.2
1039 *
1040 * @return bool
1041 */
1042 public function maybe_add_checklist_widget_html() {
1043
1044 if ( $this->maybe_load_checklist_assets() ) {
1045
1046 // If the checklist was skipped, do not add the widget.
1047 if ( $this->is_checklist_skipped() ) {
1048 return false;
1049 }
1050
1051 include charitable()->get_path( 'includes' ) . 'admin/templates/checklist-widget.php';
1052
1053 }
1054 }
1055
1056 /**
1057 * Confirm if we can load the checklist assets.
1058 *
1059 * @since 1.8.2
1060 * @version 1.8.8.1
1061 *
1062 * @return bool
1063 */
1064 public function maybe_load_checklist_assets() {
1065
1066 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
1067
1068 // if we are on the checklist page, always allow access
1069 if ( isset( $_GET['page'] ) && 'charitable-setup-checklist' === $_GET['page'] ) { // phpcs:ignore`
1070 return true;
1071 }
1072
1073 // if we aren't on a Charitable "screen" then don't load the checklist assets.
1074 if ( is_null( $screen ) || ! in_array( $screen->id, charitable_get_charitable_screens() ) ) {
1075 return false;
1076 }
1077
1078 // if the constant is set to force the checklist, then load the assets.
1079 if ( defined( 'CHARITABLE_ONBOARDING_FORCE_CHECKLIST' ) && CHARITABLE_ONBOARDING_FORCE_CHECKLIST ) {
1080 return true;
1081 }
1082
1083 // if the checklist is skipped, don't bother.
1084 if ( $this->is_checklist_skipped() || $this->is_checklist_completed() ) {
1085 return false;
1086 }
1087
1088 // Always allow checklist assets to load on Charitable screens - removed time restrictions
1089 if ( ! is_null( $screen ) && in_array( $screen->id, charitable_get_charitable_screens() ) ) {
1090 return true;
1091 }
1092
1093 // false will be the fall back.
1094 return apply_filters( 'charitable_load_checklist_assets', false, $screen );
1095 }
1096
1097 /**
1098 * Get dashboard notices.
1099 *
1100 * @since 1.8.1.6
1101 * @version 1.8.1.9 // Updated doc URL.
1102 * @version 1.8.2
1103 * @version 1.8.8.1
1104 */
1105 public function get_dashboard_notices() {
1106
1107 $dashboard_notices = get_option( 'charitable_dashboard_notifications', array() );
1108
1109 // Check if checklist should be considered completed based on actual conditions
1110 $should_be_completed = $this->should_checklist_be_completed();
1111
1112 // If checklist should be completed but isn't marked as such, mark it as completed
1113 if ( $should_be_completed && ! $this->is_checklist_completed() && ! $this->is_checklist_skipped() ) {
1114 $this->update_checklist_status( 'completed' );
1115 }
1116
1117 // Has user completed the checklist?
1118 if ( $this->is_checklist_completed() || $this->is_checklist_skipped() ) {
1119 // remove the checklist notice if it exists, and then bail.
1120 if ( isset( $dashboard_notices['checklist_status'] ) ) {
1121 unset( $dashboard_notices['checklist_status'] );
1122 update_option( 'charitable_dashboard_notifications', $dashboard_notices );
1123 }
1124 return $dashboard_notices;
1125 }
1126
1127 // if the 'checklist_status' already exists in the dashboard notices, don't add it again.
1128 if ( isset( $dashboard_notices['checklist_status'] ) ) {
1129 return $dashboard_notices;
1130 }
1131
1132 // Only show notice if checklist is actually incomplete and user has started it
1133 if ( ! $this->is_checklist_started() ) {
1134 return $dashboard_notices;
1135 }
1136
1137 $dashboard_notices['checklist_status'] = array(
1138 'type' => 'notice',
1139 'dismiss' => false,
1140 'title' => esc_html__( 'Notice', 'charitable' ),
1141 'custom_css' => 'charitable-notification-type-notice',
1142 'message' => sprintf(
1143 '<p>%1$s <a href="%2$s">%3$s</a></p>',
1144 __( 'Charitable Checklist is not completed.', 'charitable' ),
1145 admin_url( 'admin.php?page=charitable-setup-checklist' ),
1146 __( 'Finish the Checklist.', 'charitable' )
1147 )
1148 );
1149
1150 update_option( 'charitable_dashboard_notifications', $dashboard_notices );
1151
1152 return $dashboard_notices;
1153 }
1154
1155 /**
1156 * Determine if we load checklist (or assets) if there is at least one campaign and X days past activation.
1157 *
1158 * @since 1.8.2
1159 *
1160 * @return string
1161 */
1162 public function maybe_load_checklist_within_activation() {
1163
1164 $total_campaigns = wp_count_posts( 'campaign' );
1165 $count_campaigns = ! empty( $total_campaigns->publish ) ? $total_campaigns->publish : 0;
1166 // if there is a campaign created (but the checklist hasn't been skipped or completed) AND the activation has been past 7 days, then do not load the checklist assets.
1167 if ( intval( $count_campaigns ) > 0 && ( ! $this->is_checklist_skipped() && ! $this->is_checklist_completed() ) ) {
1168 $activation_date = get_option( 'wpcharitable_activated_datetime', false );
1169 // If wpcharitable_activated_datetime is false, then the backup would be to load the charitable_activated option as an array and get the first value in the array that is a timestamp.
1170 if ( false === $activation_date ) {
1171 $activation_date = get_option( 'charitable_activated', false );
1172 if ( is_array( $activation_date ) ) {
1173 foreach ( $activation_date as $date ) {
1174 if ( is_numeric( $date ) ) {
1175 $activation_date = $date;
1176 break;
1177 }
1178 }
1179 }
1180 }
1181 if ( false !== $activation_date && is_numeric( $activation_date ) ) {
1182 // Calculate the difference in seconds.
1183 $difference = time() - $activation_date;
1184 // Determine if more than 14 days have passed (14 days * 24 hours * 60 minutes * 60 seconds).
1185 if ( $difference > ( 14 * 24 * 60 * 60 ) ) {
1186
1187 $cache_last_cleared = get_option( 'charitable_cache_cleared', false );
1188 if ( false !== $cache_last_cleared && ( time() - $cache_last_cleared ) > 3600 ) {
1189 return false;
1190 }
1191 }
1192 }
1193 }
1194
1195 return true;
1196 }
1197
1198 /**
1199 * This will incercept the request and redirect to the dashboard page.
1200 *
1201 * @since 1.8.3
1202 */
1203 public function maybe_redirect_from_checklist_page() {
1204
1205 if ( ! empty( $_GET['page'] ) && 'charitable-checklist' === $_GET['page'] ) { // phpcs:ignore
1206 // is the checklist active?
1207 if ( ! $this->maybe_load_checklist_assets() ) {
1208 // if the checklist is not active, then redirect to the dashboard page.
1209 wp_safe_redirect( admin_url( 'admin.php?page=charitable-dashboard' ) );
1210 exit;
1211 }
1212 }
1213 }
1214
1215 /**
1216 * Activate the pro plugin after onboarding.
1217 * This includes needed flushing of cache so Pro can be updated instantly if need be.
1218 *
1219 * @since 1.8.5
1220 * @version 1.8.5.1
1221 */
1222 public function maybe_activate_pro_after_onboarding() {
1223
1224 // Are we about to visit'charitable-setup-checklist'?
1225 if ( ! isset( $_GET['page'] ) || 'charitable-setup-checklist' !== $_GET['page'] ) { // phpcs:ignore
1226 return;
1227 }
1228
1229 // Is the option for installing pro after onboarding set?
1230 $activate_pro = get_option( 'charitable_activate_pro', false );
1231
1232 if ( ! $activate_pro ) {
1233 return;
1234 }
1235
1236 // CRITICAL SAFETY CHECK: Verify Pro plugin actually exists before proceeding
1237 $pro_plugin_path = WP_PLUGIN_DIR . '/' . self::PRO_PLUGIN;
1238 if ( ! file_exists( $pro_plugin_path ) ) {
1239 // Pro plugin doesn't exist - don't deactivate Lite version
1240 // Clear the option to prevent future attempts
1241 delete_option( 'charitable_activate_pro' );
1242 // Log this for debugging
1243 if ( charitable_is_debug() ) {
1244 error_log( 'Charitable: Pro plugin not found at ' . $pro_plugin_path . ' - preventing Lite deactivation' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
1245 }
1246 return;
1247 }
1248
1249 // Confirm the pro plugin is installed (and not activated).
1250 if ( ! is_plugin_inactive( self::PRO_PLUGIN ) ) {
1251 return;
1252 }
1253
1254 // Deactivate the lite version first.
1255 $plugin = plugin_basename( charitable()->get_path( 'plugin-directory' ) . '/charitable/charitable.php' );
1256
1257 deactivate_plugins( $plugin );
1258
1259 // phpcs:ignore Charitable.Comments.PHPDocHooks.RequiredHookDocumentation, Charitable.PHP.ValidateHooks.InvalidHookName
1260 do_action( 'charitable_plugin_deactivated', $plugin );
1261
1262 // Activate the plugin silently.
1263 $activated = activate_plugin( self::PRO_PLUGIN, '', false, true );
1264
1265 if ( ! is_wp_error( $activated ) ) {
1266
1267 // Add the pro_connect activation date to the activated array.
1268 $activated = (array) get_option( 'charitable_activated', array() );
1269
1270 if ( empty( $activated['pro_connect'] ) ) {
1271 $activated['pro_connect'] = time();
1272 update_option( 'charitable_activated', $activated );
1273 }
1274 }
1275
1276 // remove the option.
1277 delete_option( 'charitable_activate_pro' );
1278
1279 $empty_transient = new \stdClass();
1280 set_site_transient( 'update_plugins', $empty_transient ); // Depreciated item.
1281 delete_site_option( 'wpc_plugin_versions' );
1282 update_option( 'charitable_connect_completed', true );
1283
1284 // redirect again to the checklist page.
1285 wp_safe_redirect( admin_url( 'admin.php?page=charitable-setup-checklist' ) );
1286 exit;
1287
1288 }
1289
1290 /**
1291 * Returns and/or create the single instance of this class.
1292 *
1293 * @since 1.8.2
1294 *
1295 * @return Charitable_Checklist
1296 */
1297 public static function get_instance() {
1298 if ( is_null( self::$instance ) ) {
1299 self::$instance = new self();
1300 }
1301
1302 return self::$instance;
1303 }
1304 }
1305
1306 endif;
1307