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