PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.3.4
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.3.4
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 All 66 releases
better-payment / includes / Traits / Helper.php

Helper.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.3.4, at includes/Traits/Helper.php

966 lines 28.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Better_Payment\Lite\Traits;
4
5 use Better_Payment\Lite\Admin\Setup_Wizard;
6 use Better_Payment\Lite\Classes\Plugin_Usage_Tracker;
7
8 /**
9 * Exit if accessed directly
10 */
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15 /**
16 * Helper trait
17 *
18 * @since 0.0.1
19 */
20 trait Helper
21 {
22 use Elements, ElementorHelper, WordPressHelper, TemplateQuery;
23
24 public function bp_template_render($filePath, $variables = array(), $print = true)
25 {
26 $output = NULL;
27
28 if (file_exists($filePath)) {
29 // Extract the variables to a local namespace
30 extract($variables);
31
32 // Start output buffering
33 ob_start();
34
35 // Include the template file
36 include $filePath;
37
38 // End buffering and return its contents
39 $output = ob_get_clean();
40 }
41
42 if ( $print ) {
43 print $output;
44 }
45
46 return $output;
47 }
48
49 /**
50 * List of allowed html tag for wp_kses
51 *
52 * bp_allowed_tags
53 * @return array
54 */
55 public function bp_allowed_tags() {
56 return [
57 'a' => [
58 'href' => [],
59 'title' => [],
60 'class' => [],
61 'rel' => [],
62 'id' => [],
63 'style' => [],
64 'target' => [],
65 ],
66 'q' => [
67 'cite' => [],
68 'class' => [],
69 'id' => [],
70 ],
71 'img' => [
72 'src' => [],
73 'alt' => [],
74 'height' => [],
75 'width' => [],
76 'class' => [],
77 'id' => [],
78 'style' => []
79 ],
80 'span' => [
81 'class' => [],
82 'id' => [],
83 'style' => []
84 ],
85 'dfn' => [
86 'class' => [],
87 'id' => [],
88 'style' => []
89 ],
90 'time' => [
91 'datetime' => [],
92 'class' => [],
93 'id' => [],
94 'style' => [],
95 ],
96 'cite' => [
97 'title' => [],
98 'class' => [],
99 'id' => [],
100 'style' => [],
101 ],
102 'hr' => [
103 'class' => [],
104 'id' => [],
105 'style' => [],
106 ],
107 'b' => [
108 'class' => [],
109 'id' => [],
110 'style' => [],
111 ],
112 'p' => [
113 'class' => [],
114 'id' => [],
115 'style' => []
116 ],
117 'i' => [
118 'class' => [],
119 'id' => [],
120 'style' => []
121 ],
122 'u' => [
123 'class' => [],
124 'id' => [],
125 'style' => []
126 ],
127 's' => [
128 'class' => [],
129 'id' => [],
130 'style' => [],
131 ],
132 'br' => [],
133 'em' => [
134 'class' => [],
135 'id' => [],
136 'style' => []
137 ],
138 'code' => [
139 'class' => [],
140 'id' => [],
141 'style' => [],
142 ],
143 'mark' => [
144 'class' => [],
145 'id' => [],
146 'style' => [],
147 ],
148 'small' => [
149 'class' => [],
150 'id' => [],
151 'style' => []
152 ],
153 'abbr' => [
154 'title' => [],
155 'class' => [],
156 'id' => [],
157 'style' => [],
158 ],
159 'strong' => [
160 'class' => [],
161 'id' => [],
162 'style' => []
163 ],
164 'del' => [
165 'class' => [],
166 'id' => [],
167 'style' => []
168 ],
169 'ins' => [
170 'class' => [],
171 'id' => [],
172 'style' => []
173 ],
174 'sub' => [
175 'class' => [],
176 'id' => [],
177 'style' => [],
178 ],
179 'sup' => [
180 'class' => [],
181 'id' => [],
182 'style' => [],
183 ],
184 'div' => [
185 'class' => [],
186 'id' => [],
187 'style' => []
188 ],
189 'strike' => [
190 'class' => [],
191 'id' => [],
192 'style' => [],
193 ],
194 'acronym' => [],
195 'h1' => [
196 'class' => [],
197 'id' => [],
198 'style' => [],
199 ],
200 'h2' => [
201 'class' => [],
202 'id' => [],
203 'style' => [],
204 ],
205 'h3' => [
206 'class' => [],
207 'id' => [],
208 'style' => [],
209 ],
210 'h4' => [
211 'class' => [],
212 'id' => [],
213 'style' => [],
214 ],
215 'h5' => [
216 'class' => [],
217 'id' => [],
218 'style' => [],
219 ],
220 'h6' => [
221 'class' => [],
222 'id' => [],
223 'style' => [],
224 ],
225 'button' => [
226 'class' => [],
227 'id' => [],
228 'style' => [],
229 'data-paypal-info' => [],
230 ],
231 'center' => [
232 'class' => [],
233 'id' => [],
234 'style' => [],
235 ],
236 'ul' => [
237 'class' => [],
238 'id' => [],
239 'style' => [],
240 ],
241 'ol' => [
242 'class' => [],
243 'id' => [],
244 'style' => [],
245 ],
246 'li' => [
247 'class' => [],
248 'id' => [],
249 'style' => [],
250 ],
251 'table' => [
252 'class' => [],
253 'id' => [],
254 'style' => [],
255 'dir' => [],
256 'align' => [],
257 ],
258 'thead' => [
259 'class' => [],
260 'id' => [],
261 'style' => [],
262 'align' => [],
263 ],
264 'tbody' => [
265 'class' => [],
266 'id' => [],
267 'style' => [],
268 'align' => [],
269 ],
270 'tfoot' => [
271 'class' => [],
272 'id' => [],
273 'style' => [],
274 'align' => [],
275 ],
276 'th' => [
277 'class' => [],
278 'id' => [],
279 'style' => [],
280 'align' => [],
281 'colspan' => [],
282 'rowspan' => [],
283 ],
284 'tr' => [
285 'class' => [],
286 'id' => [],
287 'style' => [],
288 'align' => [],
289 ],
290 'td' => [
291 'class' => [],
292 'id' => [],
293 'style' => [],
294 'align' => [],
295 'colspan' => [],
296 'rowspan' => [],
297 ],
298 ];
299 }
300
301 /**
302 * Optional usage tracker
303 *
304 * @since v1.1.1
305 */
306 public function start_plugin_tracking()
307 {
308 /**
309 * Lite-only, exactly as before — this guard used to live in the
310 * `! $this->pro_enabled` block in Admin::init(). It moved here when
311 * tracking was rewired to also run on cron requests, so the single
312 * remaining caller does not have to repeat it.
313 *
314 * Evaluating the filter at `init` rather than at `plugins_loaded` is
315 * strictly safer too: every plugin has loaded by then, so Pro cannot
316 * lose a race to register its filter and have Lite start tracking on a
317 * Pro site.
318 *
319 * @since 2.3.2
320 */
321 if ( apply_filters( 'better_payment/pro_enabled', false ) ) {
322 return;
323 }
324
325 /**
326 * Must run BEFORE init() below: init() calls maybe_schedule_tracking(),
327 * which only schedules once consent is on record. Recording consent
328 * first means a fresh install gets its daily event in this same request
329 * rather than the next one.
330 */
331 $this->maybe_auto_enable_usage_tracking();
332
333 $tracker = Plugin_Usage_Tracker::get_instance( BETTER_PAYMENT_FILE, [
334 'opt_in' => true,
335 'goodbye_form' => true,
336 'item_id' => '64e1f724b5e14edb343e'
337 ] );
338 $tracker->set_notice_options(array(
339 'notice' => 'Want to help make <strong>Better Payment</strong> even more awesome? You can get a <strong>10% discount coupon</strong> for Pro upgrade if you allow.',
340 'extra_notice' => 'Nothing is sent unless you allow it. If you do: your site URL and name;
341 WordPress, PHP and server versions; language and charset; your <strong>active</strong> plugins and
342 theme (and how many inactive); which Better Payment widgets, blocks and campaign elements you use,
343 and which features and gateways you have switched on; and your admin email, for the coupon.
344 We never collect your payment or transaction data, your customers\' details, your API keys,
345 or your inactive plugin names. No spam, I promise.',
346 ));
347 $tracker->init();
348 }
349
350 /**
351 * Turn usage tracking on for a fresh install that never answered the wizard.
352 *
353 * The wizard's two buttons cover the cases where the user answers: "Proceed
354 * To Next Step" and "Skip It" both call Setup_Wizard::wpins_process(). A user
355 * who closes the wizard, navigates away, or never opens it at all answers
356 * neither, and this is what covers them.
357 *
358 * Three things this deliberately does:
359 *
360 * 1. **Fresh installs only.** The `pending` marker is written by
361 * Installer::enable_setup_wizard() and only on a site with no
362 * `better_payment_setup_wizard` option — i.e. one being installed, not
363 * upgraded. An existing site keeps whatever tracking state it has.
364 * 2. **Exactly once.** The marker flips to `done` before any work, so this
365 * cannot re-assert consent on a later request. That is what keeps a
366 * future opt-out from being silently undone on the next page load, and
367 * it holds even if the call below throws.
368 * 3. **No forced send.** `wpins_process( false )` records consent and stops.
369 * This runs on `init` during an ordinary admin (or cron) request, and
370 * the tracker's send is a blocking POST with a 30s timeout — it belongs
371 * on the scheduled event, which init() arranges immediately after.
372 *
373 * Inherits start_plugin_tracking()'s Pro gate: a site running Better Payment
374 * Pro never reaches here, exactly like every other tracking code path.
375 *
376 * @return void
377 * @since 2.3.2
378 */
379 protected function maybe_auto_enable_usage_tracking()
380 {
381 if ( get_option( 'better_payment_tracking_auto_optin' ) !== 'pending' ) {
382 return;
383 }
384
385 update_option( 'better_payment_tracking_auto_optin', 'done' );
386
387 Setup_Wizard::wpins_process( false );
388 }
389
390 /**
391 * List of currencies not supported by paypal or stripe
392 *
393 * @since v1.3.1
394 */
395 public function bp_unsupported_currencies( $method = '' ) {
396 $method = sanitize_text_field( strtolower( $method ) );
397 $all_currencies = [
398 'paypal' => [
399 'AED',
400 'BGN',
401 'BAM',
402 'KES',
403 'NGN',
404 'RON',
405 'RSD',
406 'ZAR',
407 'GHS',
408 ],
409 'stripe' => [],
410 ];
411
412 return !empty( $all_currencies[ $method ] ) ? $all_currencies[ $method ] : [];
413 }
414
415 /**
416 * List of currencies supported by paystack
417 *
418 * @since v1.3.1
419 */
420 public function bp_supported_currencies( $method = '' ) {
421 $method = sanitize_text_field( strtolower( $method ) );
422 $supported_currencies = [
423 'paystack' => [
424 'GHS',
425 'KES',
426 'NGN',
427 'USD',
428 'ZAR'
429 ],
430 ];
431
432 return !empty( $supported_currencies[ $method ] ) ? $supported_currencies[ $method ] : [];
433 }
434
435 /**
436 * Get comprehensive currency data
437 *
438 * @since 0.0.2
439 * @return array Array of currency data with codes, names, and symbols
440 */
441 private function get_currency_data() {
442 return [
443 'USD' => ['name' => 'US Dollar', 'symbol' => '$'],
444 'EUR' => ['name' => 'Euro', 'symbol' => ''],
445 'GBP' => ['name' => 'British Pound', 'symbol' => '£'],
446 'AED' => ['name' => 'UAE Dirham', 'symbol' => 'د.إ'],
447 'AUD' => ['name' => 'Australian Dollar', 'symbol' => '$'],
448 'BGN' => ['name' => 'Bulgarian Lev', 'symbol' => 'лв'],
449 'BAM' => ['name' => 'Bosnia and Herzegovina Convertible Mark', 'symbol' => 'KM'],
450 'CAD' => ['name' => 'Canadian Dollar', 'symbol' => '$'],
451 'CHF' => ['name' => 'Swiss Franc', 'symbol' => 'CHF'],
452 'CZK' => ['name' => 'Czech Koruna', 'symbol' => ''],
453 'DKK' => ['name' => 'Danish Krone', 'symbol' => 'kr'],
454 'GHS' => ['name' => 'Ghanaian Cedi', 'symbol' => ''],
455 'HKD' => ['name' => 'Hong Kong Dollar', 'symbol' => '$'],
456 'HUF' => ['name' => 'Hungarian Forint', 'symbol' => 'ft'],
457 'ILS' => ['name' => 'Israeli Shekel', 'symbol' => ''],
458 'JPY' => ['name' => 'Japanese Yen', 'symbol' => '¥'],
459 'KES' => ['name' => 'Kenyan Shilling', 'symbol' => 'Ksh.'],
460 'MXN' => ['name' => 'Mexican Peso', 'symbol' => '$'],
461 'MYR' => ['name' => 'Malaysian Ringgit', 'symbol' => 'MYR'],
462 'NGN' => ['name' => 'Nigerian Naira', 'symbol' => ''],
463 'NOK' => ['name' => 'Norwegian Krone', 'symbol' => 'kr'],
464 'NZD' => ['name' => 'New Zealand Dollar', 'symbol' => '$'],
465 'PHP' => ['name' => 'Philippine Peso', 'symbol' => ''],
466 'PLN' => ['name' => 'Polish Zloty', 'symbol' => ''],
467 'RON' => ['name' => 'Romanian Leu', 'symbol' => 'lei'],
468 'RSD' => ['name' => 'Serbian Dinar', 'symbol' => 'din.'],
469 'RUB' => ['name' => 'Russian Ruble', 'symbol' => ''],
470 'SEK' => ['name' => 'Swedish Krona', 'symbol' => 'kr'],
471 'SGD' => ['name' => 'Singapore Dollar', 'symbol' => '$'],
472 'THB' => ['name' => 'Thai Baht', 'symbol' => '฿'],
473 'TRY' => ['name' => 'Turkish Lira', 'symbol' => ''],
474 'TWD' => ['name' => 'Taiwan Dollar', 'symbol' => '$'],
475 'ZAR' => ['name' => 'South African Rand', 'symbol' => 'R'],
476 ];
477 }
478
479 /**
480 * Get currency symbols list
481 *
482 * @since 0.0.2
483 * @return array Array of currency codes and their symbols
484 */
485 public function get_currency_symbols_list() {
486 $currency_data = $this->get_currency_data();
487 $symbols = [];
488
489 foreach ($currency_data as $code => $data) {
490 $symbols[$code] = $data['symbol'];
491 }
492
493 return $symbols;
494 }
495
496 /**
497 * Get currency list
498 *
499 * @since 0.0.2
500 * @return array Array of currency codes as key-value pairs
501 */
502 public function get_currency_list() {
503 $currency_data = $this->get_currency_data();
504 $currency_list = [];
505
506 foreach ($currency_data as $code => $data) {
507 $currency_list[$code] = $code;
508 }
509
510 return $currency_list;
511 }
512
513 /**
514 * Get currency names list
515 *
516 * @since 0.0.2
517 * @return array Array of currency codes and their full names
518 */
519 public function get_currency_names_list() {
520 $currency_data = $this->get_currency_data();
521 $names = [];
522
523 foreach ($currency_data as $code => $data) {
524 $names[$code] = $data['name'];
525 }
526
527 return $names;
528 }
529
530 /**
531 * Get currency symbol by code
532 *
533 * @since 0.0.2
534 * @param string $currency_code The currency code (e.g., 'USD', 'EUR')
535 * @return string The currency symbol or the code if symbol not found
536 */
537 public function get_currency_symbol($currency_code) {
538 $currency_data = $this->get_currency_data();
539
540 if (isset($currency_data[$currency_code])) {
541 return $currency_data[$currency_code]['symbol'];
542 }
543
544 return $currency_code; // Fallback to currency code
545 }
546
547 /**
548 * Get currency name by code
549 *
550 * @since 0.0.2
551 * @param string $currency_code The currency code (e.g., 'USD', 'EUR')
552 * @return string The currency name or the code if name not found
553 */
554 public function get_currency_name($currency_code) {
555 $currency_data = $this->get_currency_data();
556
557 if (isset($currency_data[$currency_code])) {
558 return $currency_data[$currency_code]['name'];
559 }
560
561 return $currency_code; // Fallback to currency code
562 }
563
564 /**
565 * Whether a visitor may pay through a form stored on this post.
566 *
567 * Every payment handler takes the form's page id from the request, and nothing stopped it
568 * pointing at a draft, private, scheduled or password-protected post — so an unpublished
569 * form could be paid, and any form's configuration probed by id. A published post
570 * qualifies; anything else only for a user allowed to read it (an editor testing a draft
571 * in preview), and never while its password has not been entered.
572 *
573 * @since 2.3.4
574 *
575 * @param int $page_id Post id.
576 * @return bool
577 */
578 public function is_payable_form_page( $page_id ) {
579 $post = get_post( absint( $page_id ) );
580
581 if ( ! $post ) {
582 return false;
583 }
584
585 if ( 'publish' !== $post->post_status && ! current_user_can( 'read_post', $post->ID ) ) {
586 return false;
587 }
588
589 return ! post_password_required( $post );
590 }
591
592 /**
593 * Widget settings
594 *
595 * @since 1.0.0
596 */
597 public function get_elementor_widget_settings( $page_id, $widget_id ) {
598 $settings = [];
599
600 // First, check for Gutenberg block settings stored in transient.
601 // This is used when the form is rendered via a Gutenberg block.
602 $transient_key = 'bp_block_settings_' . $page_id . '_' . $widget_id;
603 $block_settings = get_transient( $transient_key );
604 if ( ! empty( $block_settings ) && is_array( $block_settings ) ) {
605 return $block_settings;
606 }
607
608 // Fallback to Elementor widget settings.
609 if ( class_exists( '\Elementor\Plugin' ) && ! empty( \Elementor\Plugin::$instance ) ) {
610 $document = \Elementor\Plugin::$instance->documents->get( $page_id );
611 if ( $document ) {
612 $elements = \Elementor\Plugin::instance()->documents->get( $page_id )->get_elements_data();
613 $widget_data = $this->find_element_recursive( $elements, $widget_id );
614 $widget = ! empty( $widget_data ) && is_array( $widget_data ) ? \Elementor\Plugin::instance()->elements_manager->create_element_instance( $widget_data ) : '';
615 if ( ! empty( $widget ) ) {
616 $settings = $widget->get_settings_for_display();
617 }
618 }
619 }
620
621 return $settings;
622 }
623
624 /**
625 * Find element recursive
626 *
627 * @since 1.0.0
628 */
629 public function find_element_recursive( $elements, $form_id ) {
630
631 foreach ( $elements as $element ) {
632 if ( $form_id === $element[ 'id' ] ) {
633 return $element;
634 }
635
636 if ( !empty( $element[ 'elements' ] ) ) {
637 $element = $this->find_element_recursive( $element[ 'elements' ], $form_id );
638
639 if ( $element ) {
640 return $element;
641 }
642 }
643 }
644
645 return false;
646 }
647
648 public function get_stripe_price_details( $price_id = '', $secret_key = '' ) {
649 if ( empty( $price_id ) || empty( $secret_key ) ) {
650 return [];
651 }
652
653 $api_url = 'https://api.stripe.com/v1/prices/' . $price_id;
654
655 $response = wp_remote_get( $api_url, [
656 'headers' => [
657 'Authorization' => 'Bearer ' . $secret_key,
658 ],
659 'timeout' => 20,
660 ] );
661
662 if ( is_wp_error( $response ) ) {
663 error_log( 'Stripe API error: ' . $response->get_error_message() );
664 return [];
665 }
666
667 $body = wp_remote_retrieve_body( $response );
668 $data = json_decode( $body, true );
669
670 if ( ! isset( $data ) || ! is_array( $data ) ) {
671 error_log( 'Stripe API response format error.' );
672 return [];
673 }
674
675 return $data;
676 }
677
678 /**
679 * Initialize widget usage
680 *
681 * @since 1.0.0
682 */
683 public function bp_init_widget_usage() {
684 // Run initial Elementor widget scan if not done yet
685 if (!get_option('bp_widget_usage_initial_scan_done', false)) {
686 $this->scan_existing_pages_for_widgets();
687 update_option('bp_widget_usage_initial_scan_done', true);
688 }
689
690 // Run initial Gutenberg block scan independently — separate flag so it
691 // still runs on sites where the widget scan already completed.
692 if (!get_option('bp_block_usage_initial_scan_done', false)) {
693 $this->scan_existing_pages_for_blocks();
694 update_option('bp_block_usage_initial_scan_done', true);
695 }
696 }
697
698 /**
699 * Track widget and block usage when a post is saved
700 *
701 * @since 1.0.0
702 * @param int $post_id
703 */
704 public function bp_widget_usage_on_save($post_id) {
705 // add capability check
706 if ( ! current_user_can( 'edit_post', $post_id ) ) {
707 return;
708 }
709
710 // Skip autosaves and revisions
711 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
712 return;
713 }
714
715 if (wp_is_post_revision($post_id)) {
716 return;
717 }
718
719 // Only track pages and posts
720 $post_type = get_post_type($post_id);
721 if (!in_array($post_type, ['page', 'post'])) {
722 return;
723 }
724
725 // Check Elementor widget usage
726 $elementor_data = get_post_meta($post_id, '_elementor_data', true);
727 if (!empty($elementor_data)) {
728 $this->update_widget_usage_flags($post_id, $elementor_data);
729 }
730
731 // Check Gutenberg block usage
732 $post = get_post($post_id);
733 if ($post && has_blocks($post->post_content)) {
734 if ($this->detect_better_payment_blocks(parse_blocks($post->post_content))) {
735 update_option('better_payment_any_block_used', true);
736 }
737 }
738 }
739
740 /**
741 * Scan all existing pages for Better Payment widgets
742 *
743 * @since 1.0.0
744 */
745 public function scan_existing_pages_for_widgets() {
746 global $wpdb;
747
748 // Get all posts/pages with Elementor data
749 $posts_with_elementor = $wpdb->get_results(
750 "SELECT post_id, meta_value FROM {$wpdb->postmeta}
751 WHERE meta_key = '_elementor_data'
752 AND meta_value != ''
753 AND meta_value != '[]'"
754 );
755
756 foreach ($posts_with_elementor as $post_data) {
757 $this->update_widget_usage_flags($post_data->post_id, $post_data->meta_value);
758 }
759 }
760
761 /**
762 * Update widget usage flags based on page content
763 *
764 * @since 1.0.0
765 * @param int $post_id
766 * @param string $elementor_data
767 */
768 public function update_widget_usage_flags($post_id, $elementor_data) {
769 // Parse Elementor data
770 $elements = json_decode($elementor_data, true);
771 if (!is_array($elements)) {
772 return;
773 }
774
775 // Check for Better Payment widgets
776 $widget_usage = $this->detect_better_payment_widgets($elements);
777
778 // Update overall usage flag
779 $any_widget_used = $widget_usage['better-payment'];
780
781 if ($any_widget_used) {
782 update_option('better_payment_any_widget_used', true);
783 } else {
784 delete_option('better_payment_any_widget_used');
785 }
786 }
787
788 /**
789 * Recursively detect Better Payment widgets in Elementor data
790 *
791 * @since 1.0.0
792 * @param array $elements
793 * @return array
794 */
795 public function detect_better_payment_widgets($elements) {
796 $widget_usage = [
797 'better-payment' => false,
798 ];
799
800 foreach ($elements as $element) {
801 // Check if this element is a Better Payment widget
802 if (isset($element['widgetType'])) {
803 $widget_type = $element['widgetType'];
804
805 if ($widget_type === 'better-payment') {
806 $widget_usage['better-payment'] = true;
807 } elseif ($widget_type === 'better-payment-user-dashboard') {
808 $widget_usage['better-payment'] = true;
809 } elseif ($widget_type === 'fundraising-campaign') {
810 $widget_usage['better-payment'] = true;
811 }
812 }
813
814 // Recursively check nested elements
815 if (!empty($element['elements']) && is_array($element['elements'])) {
816 $nested_usage = $this->detect_better_payment_widgets($element['elements']);
817
818 // Merge results (OR operation)
819 $widget_usage['better-payment'] = $widget_usage['better-payment'] || $nested_usage['better-payment'];
820 }
821 }
822
823 return $widget_usage;
824 }
825
826 /**
827 * Check if any Better Payment widget is being used
828 *
829 * @since 1.0.0
830 * @return bool
831 */
832 public function is_any_better_payment_widget_used() {
833 return get_option('better_payment_any_widget_used', false) ? '1' : '0';
834 }
835
836 /**
837 * Scan all existing published posts/pages for Better Payment Gutenberg blocks
838 *
839 * @since 2.0.0
840 */
841 public function scan_existing_pages_for_blocks() {
842 global $wpdb;
843
844 $block_found = $wpdb->get_var(
845 "SELECT ID FROM {$wpdb->posts}
846 WHERE post_status = 'publish'
847 AND post_content LIKE '%<!-- wp:better-payment/%'
848 LIMIT 1"
849 );
850
851 if ( $block_found ) {
852 update_option( 'better_payment_any_block_used', true );
853 }
854 }
855
856 /**
857 * Recursively detect Better Payment blocks in parsed Gutenberg block data
858 *
859 * @since 2.0.0
860 * @param array $blocks Parsed blocks from parse_blocks()
861 * @return bool
862 */
863 public function detect_better_payment_blocks( $blocks ) {
864 $bp_block_names = [ 'better-payment/payment-form', 'better-payment/user-dashboard' ];
865
866 foreach ( $blocks as $block ) {
867 if ( in_array( $block['blockName'], $bp_block_names, true ) ) {
868 return true;
869 }
870
871 // Recursively check inner blocks
872 if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
873 if ( $this->detect_better_payment_blocks( $block['innerBlocks'] ) ) {
874 return true;
875 }
876 }
877 }
878
879 return false;
880 }
881
882 /**
883 * Check if any Better Payment Gutenberg block is being used
884 *
885 * @since 2.0.0
886 * @return string '1' or '0'
887 */
888 public function is_any_better_payment_block_used() {
889 return get_option( 'better_payment_any_block_used', false ) ? '1' : '0';
890 }
891
892 /**
893 * Check if dismissible section should be shown
894 *
895 * @since 1.4.2
896 * @return bool
897 */
898 public function bp_section_dismissed() {
899 $progress_bar_dismissed = get_option('better_payment_progress_bar_dismissed', false);
900 $plugin_installed_fresh = get_option('better_payment_plugin_installed_fresh', false);
901 $progress_bar_dismissed_expiry_date = get_option('better_payment_progress_bar_dismissed_expiry_date', 0);
902
903 if ($progress_bar_dismissed) {
904 return true;
905 }
906
907 return $plugin_installed_fresh === 'yes' && time() > $progress_bar_dismissed_expiry_date;
908 }
909
910 public function bp_calculate_progress_steps($settings) {
911 $steps = [];
912
913 // Step 1: Check if API keys are configured
914 // Check PayPal keys
915 $paypal_business_email = !empty($settings['better_payment_settings_payment_paypal_email']);
916
917 // Check Stripe keys
918 $is_stripe_live_mode = $settings['better_payment_settings_payment_stripe_live_mode'] === 'yes' ? true : false;
919 if ( $is_stripe_live_mode ) {
920 $stripe_public = !empty($settings['better_payment_settings_payment_stripe_live_public']);
921 $stripe_secret = !empty($settings['better_payment_settings_payment_stripe_live_secret']);
922 } else {
923 $stripe_public = !empty($settings['better_payment_settings_payment_stripe_test_public']);
924 $stripe_secret = !empty($settings['better_payment_settings_payment_stripe_test_secret']);
925 }
926
927 // Check Paystack keys
928 $is_paystack_live_mode = $settings['better_payment_settings_payment_paystack_live_mode'] === 'yes' ? true : false;
929 if ( $is_paystack_live_mode ) {
930 $paystack_public = !empty($settings['better_payment_settings_payment_paystack_live_public']);
931 $paystack_secret = !empty($settings['better_payment_settings_payment_paystack_live_secret']);
932 } else {
933 $paystack_public = !empty($settings['better_payment_settings_payment_paystack_test_public']);
934 $paystack_secret = !empty($settings['better_payment_settings_payment_paystack_test_secret']);
935 }
936
937 $api_keys_configured = ( isset($paypal_business_email) && $paypal_business_email ) || ( isset($stripe_public) && isset($stripe_secret) && $stripe_public && $stripe_secret ) || ( isset($paystack_public) && isset($paystack_secret) && $paystack_public && $paystack_secret );
938 $payment_settings_url = admin_url('admin.php?page=better-payment-admin&tab=settings&id=paypal');
939
940 $steps[] = [
941 'completed' => $api_keys_configured,
942 'text' => sprintf(__('Add your Stripe, PayPal, or Paystack API keys in <a href="%s">Payment Settings</a>', 'better-payment'), $payment_settings_url)
943 ];
944
945 // Step 2: Check if Elementor widget has been added
946 $widget_used = $this->is_any_better_payment_widget_used() === '1' ? true : false;
947 // Step 3: Check if any Better Payment Gutenberg block has been added
948 $block_used = $this->is_any_better_payment_block_used() === '1' ? true : false;
949
950 $steps[] = [
951 'completed' => $widget_used || $block_used,
952 'text' => __('Open your page in', 'better-payment') . ' <a target="_blank" href="//betterpayment.co/docs/how-to-style-payment-form-in-elementor/">' . __('Elementor', 'better-payment') . '</a>' . __( ' or ', 'better-payment' ) . '<a target="_blank" href="//betterpayment.co/docs/configure-payment-form-in-gutenberg/">' . __('Gutenberg', 'better-payment') . '</a>' . __( ' and insert the Better Payment widget/block.', 'better-payment' ),
953 ];
954
955 // Step 4: Check if widget has been customized
956 $widget_customized = $widget_used || $block_used;
957
958 $steps[] = [
959 'completed' => $widget_customized,
960 'text' => __('Customize the settings to fit your needs, then save and publish', 'better-payment')
961 ];
962
963 return $steps;
964 }
965 }
966