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/Classes/Helper.php +367 -172 1.0.3 → 2.4.1 View file →
@@ -2,8 +2,9 @@
2 2
3 3 namespace Better_Payment\Lite\Classes;
4 4
5 5 use Better_Payment\Lite\Controller;
6 +use Better_Payment\Lite\Traits\Helper as TraitsHelper;
6 7
7 8 /**
8 9 * Exit if accessed directly
9 10 */
@@ -17,188 +18,290 @@
17 18 * get_option and update_option.
18 19 */
19 20 class Helper extends Controller
20 21 {
22 + use TraitsHelper;
23 +
21 24 /**
22 - * Check if elementor plugin is activated
25 + * The statuses of a confirmed payment, as campaign totals, donor counts and Pro's Donors Wall
26 + * count them. See approved_donation_sql() for the one other row that counts.
23 27 *
24 - * @since 0.0.2
28 + * `success` is the status Paystack verification writes. It was missing, so Paystack donations
29 + * never counted toward a campaign's total or donor count, even though the transactions screen
30 + * and analytics already treated it as paid. Only a transaction Paystack reported as succeeded
31 + * is ever stored as `success`.
32 + *
33 + * @since 2.4.0 (was Campaign\CampaignStats::approved_statuses())
25 34 */
26 - public function elementor_not_loaded()
27 - {
28 - if (!current_user_can('activate_plugins')) {
29 - return;
30 - }
35 + const APPROVED_DONATION_STATUSES = array( 'Completed', 'paid', 'complete', 'succeeded', 'success' );
31 36
32 - if (isset($_GET['page']) && $_GET['page'] == 'better-payment-setup-wizard') {
33 - return;
34 - }
37 + /**
38 + * The status rows written before 2.4.0 can carry for a free Stripe checkout (a 100%-off
39 + * promotion code, a subscription trial).
40 + *
41 + * Such a row is one of two things, and the status alone cannot tell them apart:
42 + * - a CONFIRMED free checkout. The old confirmation stored Stripe's `payment_status` as the
43 + * row status and, because `amount_total` was 0 and `! empty()` treated 0 as missing, kept
44 + * the form's full amount. Nothing was charged.
45 + * - a checkout Stripe reported as `no_payment_required` when it was CREATED and that nobody
46 + * completed. The old confirmation only claimed `unpaid` rows, so it was never touched.
47 + * 2.4.0 stores neither: a confirmed row is always `paid` at its real amount, and a pending
48 + * one `unpaid`. Stored rows are left as they are and counted by the rule below.
49 + *
50 + * @since 2.4.0
51 + */
52 + const LEGACY_FREE_CHECKOUT_STATUS = 'no_payment_required';
35 53
36 - if(wp_doing_ajax()){
37 - return;
38 - }
54 + /**
55 + * What only the old Stripe confirmation wrote: the serialized `exact_paid_amount` key of
56 + * `form_fields_info`, followed by its float value.
57 + *
58 + * Every confirmation from the release that introduced discounts (and with them the only way
59 + * to reach a free checkout) onwards set `$form_fields_info['exact_paid_amount']` to a float
60 + * in the same UPDATE that wrote the status. No creation path ever writes that key. The
61 + * columns the confirmation also wrote are no use: `customer_info` and `transaction_id` are
62 + * already set at creation (the created session and its PaymentIntent), and `email` comes from
63 + * the charge's billing details, which a checkout that charged nothing does not have.
64 + *
65 + * The trailing `d:` makes it the key: after a string VALUE the next token is a key (`s:` or
66 + * `i:`) or `}`, so a form field whose value happens to be "exact_paid_amount" cannot match.
67 + *
68 + * @since 2.4.0
69 + */
70 + const LEGACY_CONFIRMATION_MARKER = 's:17:"exact_paid_amount";d:';
39 71
40 - $elementor = 'elementor/elementor.php';
72 + /**
73 + * SQL condition: true only for an unconfirmed legacy free checkout. Null-safe: a NULL status or
74 + * NULL form_fields_info never makes it true.
75 + *
76 + * OR it into the `incomplete` bucket's `status IN (…)` clause, so the Incomplete tab lists the
77 + * rows the Completed tab drops (DB::get_transactions()). Contains no `%`.
78 + *
79 + * @since 2.4.0
80 + */
81 + const UNCONFIRMED_LEGACY_SQL = "( COALESCE(status, '') = '" . self::LEGACY_FREE_CHECKOUT_STATUS . "' AND INSTR(COALESCE(form_fields_info, ''), '" . self::LEGACY_CONFIRMATION_MARKER . "') = 0 )";
41 82
42 - if ($this->is_plugin_installed($elementor)) {
43 - $activation_url = wp_nonce_url('plugins.php?action=activate&plugin=' . $elementor . '&plugin_status=all&paged=1&s', 'activate-plugin_' . $elementor);
83 + /**
84 + * SQL condition: false only for an unconfirmed legacy free checkout, the exact negation of
85 + * UNCONFIRMED_LEGACY_SQL. Null-safe, so a row with a NULL status (PayPal's pending sentinel) or
86 + * NULL form_fields_info is never dropped by it.
87 + *
88 + * AND it onto any aggregation that selects rows by a status bucket containing
89 + * `no_payment_required` (`completed`, `all`). Admin\DB and Pro's AnalyticsModel do; Pro keeps
90 + * the literal value, and AnalyticsModelParityTest compares the two values.
91 + *
92 + * @since 2.4.0
93 + */
94 + const COUNTED_ROW_SQL = 'NOT ' . self::UNCONFIRMED_LEGACY_SQL;
44 95
45 - $message = sprintf(__('%1$sBetter Payment%2$s requires %1$sElementor%2$s plugin to be active. Please activate Elementor to continue.', 'better-payment'), "<strong>", "</strong>");
96 + /**
97 + * SQL expression: what a row contributes to a money total. A legacy free checkout adds 0,
98 + * confirmed or not, because its stored amount (the form's) was never charged.
99 + *
100 + * @since 2.4.0
101 + */
102 + const COUNTABLE_AMOUNT_SQL = "CASE WHEN status = '" . self::LEGACY_FREE_CHECKOUT_STATUS . "' THEN 0 ELSE amount END";
46 103
47 - $button_text = __('Activate Elementor', 'better-payment');
48 - } else {
49 - $activation_url = wp_nonce_url(self_admin_url('update.php?action=install-plugin&plugin=elementor'), 'install-plugin_elementor');
104 + /**
105 + * SQL condition: the row is a confirmed donation.
106 + *
107 + * An approved status, or a CONFIRMED legacy free checkout (see LEGACY_CONFIRMATION_MARKER).
108 + * Pair it with COUNTABLE_AMOUNT_SQL for any sum. Contains no `%`, so it can be interpolated
109 + * into a string passed to `$wpdb->prepare()`.
110 + *
111 + * Pro keeps an identical copy in Models\AnalyticsModel (it supports a Lite older than this
112 + * method), pinned by Pro's AnalyticsModelParityTest.
113 + *
114 + * @since 2.4.0
115 + *
116 + * @return string
117 + */
118 + public static function approved_donation_sql(): string {
119 + return "( status IN ('" . implode( "', '", self::APPROVED_DONATION_STATUSES ) . "')"
120 + . " OR ( status = '" . self::LEGACY_FREE_CHECKOUT_STATUS . "' AND INSTR(COALESCE(form_fields_info, ''), '" . self::LEGACY_CONFIRMATION_MARKER . "') > 0 ) )";
121 + }
50 122
51 - $message = sprintf(__('%1$sBetter Payment%2$s requires %1$sElementor%2$s plugin to be installed and activated. Please install Elementor to continue.', 'better-payment'), '<strong>', '</strong>');
52 - $button_text = __('Install Elementor', 'better-payment');
53 - }
54 -
55 - $button = '<p><a href="' . esc_url_raw($activation_url) . '" class="button-primary">' . $button_text . '</a></p>';
56 -
57 - ob_start();
58 - printf('<div class="error"><p>%1$s</p>%2$s</div>', wp_kses( __( $message, 'better-payment' ), $this->bp_allowed_tags() ), wp_kses( $button, $this->bp_allowed_tags() ) );
59 - ob_end_flush();
123 + /**
124 + * PHP twin of `NOT COUNTED_ROW_SQL`, for rows already loaded: is this a legacy free checkout
125 + * that was never confirmed?
126 + *
127 + * A row without `form_fields_info` loaded is treated as unconfirmed (never counted), the safe
128 + * side of the rule. An already-unserialized `form_fields_info` array is read as the string it was
129 + * stored as, so a caller that unserialized the row first gets the same answer.
130 + *
131 + * @since 2.4.0
132 + *
133 + * @param object|array $row A transaction row.
134 + * @return bool
135 + */
136 + public static function is_unconfirmed_legacy_free_checkout( $row ): bool {
137 + return self::LEGACY_FREE_CHECKOUT_STATUS === self::row_value( $row, 'status' )
138 + && false === strpos( self::form_fields_info_string( $row ), self::LEGACY_CONFIRMATION_MARKER );
60 139 }
61 140
62 141 /**
63 - * Check if a plugin is installed
142 + * The status bucket a loaded transaction row is labelled and listed under.
64 143 *
65 - * @since 0.0.2
144 + * The raw status's bucket (get_type_by_transaction_status()), except for an unconfirmed legacy
145 + * free checkout: no count or total includes it, so it is `incomplete` (v1: `unpaid`) rather than
146 + * the `completed` its `no_payment_required` status maps to. A confirmed one stays `completed`.
147 + * The row itself is never changed. Every PHP surface that labels a row reads this: the admin REST
148 + * rows (`status_type`), the CSV export, UserAPI, the User Dashboard template and the block's
149 + * editor rows. DB::get_transactions() applies the same rule to the Completed / Incomplete tabs.
150 + *
151 + * @since 2.4.0
152 + *
153 + * @param object|array $row A transaction row.
154 + * @param string $version 'v2' (completed / incomplete / refunded) or 'v1'.
155 + * @return string The bucket key; '' for a status no bucket lists, as get_type_by_transaction_status().
66 156 */
67 - public function is_plugin_installed($basename)
68 - {
69 - if (!function_exists('get_plugins')) {
70 - include_once ABSPATH . 'wp-admin/includes/plugin.php';
157 + public static function transaction_status_type( $row, string $version = 'v2' ): string {
158 + if ( self::is_unconfirmed_legacy_free_checkout( $row ) ) {
159 + return 'v1' === $version ? 'unpaid' : 'incomplete';
71 160 }
72 161
73 - $plugins = get_plugins();
162 + return ( new self() )->get_type_by_transaction_status( self::row_value( $row, 'status' ), $version );
163 + }
74 164
75 - return isset($plugins[$basename]);
165 + /**
166 + * The status label of a loaded row ("Completed", "Incomplete", "Refunded"), v2 buckets.
167 + *
168 + * @since 2.4.0
169 + *
170 + * @param object|array $row A transaction row.
171 + * @return string
172 + */
173 + public static function transaction_status_label( $row ): string {
174 + return ucfirst( self::transaction_status_type( $row, 'v2' ) );
76 175 }
77 176
78 177 /**
79 - * check is plugin active or not
80 - *
81 - * @param $plugin
82 - * @return bool
83 - * @since 0.0.2
84 - */
85 - public function is_plugin_active($plugin) {
86 - if ( !function_exists( 'is_plugin_active' ) ){
87 - require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
178 + * The status colour of a loaded row, v2 buckets: the colour of transaction_status_type()'s bucket.
179 + *
180 + * @since 2.4.0
181 + *
182 + * @param object|array $row A transaction row.
183 + * @return string
184 + */
185 + public static function transaction_status_color( $row ): string {
186 + $helper = new self();
187 +
188 + if ( self::is_unconfirmed_legacy_free_checkout( $row ) ) {
189 + $buckets = $helper->transaction_statuses_with_type_v2();
190 +
191 + return $buckets['incomplete']['color'];
88 192 }
89 193
90 - return is_plugin_active( $plugin );
194 + return $helper->get_color_by_transaction_status( self::row_value( $row, 'status' ), 'v2' );
91 195 }
92 196
93 - public static function sanitize_bp_field($key, $arr, $default_value='', $type='text'){
94 - $value = $default_value;
95 -
96 - if( !empty( $arr[ $key ] ) ) {
97 - $value = $arr[ $key ];
197 + /**
198 + * PHP twin of COUNTABLE_AMOUNT_SQL: what a loaded row contributes to a money total.
199 + *
200 + * @since 2.4.0
201 + *
202 + * @param object|array $row A transaction row.
203 + * @return float
204 + */
205 + public static function countable_amount( $row ): float {
206 + if ( self::LEGACY_FREE_CHECKOUT_STATUS === self::row_value( $row, 'status' ) ) {
207 + return 0.0;
208 + }
98 209
99 - switch($type){
100 - case 'text':
101 - $value = sanitize_text_field( $value );
102 - break;
103 - case 'email':
104 - $value = sanitize_email( $value );
105 - break;
106 - case 'textarea':
107 - $value = sanitize_textarea_field( $value );
108 - break;
109 - default:
110 - $value = sanitize_text_field( $value );
111 - break;
112 - }
113 - }
210 + $amount = self::row_value( $row, 'amount' );
114 211
115 - return $value;
212 + return '' === $amount ? 0.0 : floatval( $amount );
116 213 }
117 214
118 215 /**
119 - * Get widget settings
216 + * Raised total and payment count over loaded transaction rows (the Elementor Fundraising
217 + * Campaign widget's progress).
120 218 *
121 - * @since 0.0.1
219 + * Counts the same rows CampaignStats counts: an approved status (Stripe `paid`, Paystack
220 + * `success`, `Completed`, …) or a confirmed legacy free checkout, which adds 0. It used to
221 + * count only `paid`, so Paystack and manually completed donations never moved the bar.
222 + *
223 + * @since 2.4.0
224 + *
225 + * @param mixed $rows Transaction rows (objects or arrays).
226 + * @return array{amount: float, count: int}
122 227 */
123 - public function get_better_payment_widget_settings( $page_id, $widget_id ) {
124 - $document = \Elementor\Plugin::$instance->documents->get( $page_id );
125 - $settings = [];
126 - if ( $document ) {
127 - $elements = \Elementor\Plugin::instance()->documents->get( $page_id )->get_elements_data();
128 - $widget_data = $this->find_element_recursive( $elements, $widget_id );
129 - if ( $widget_data ) {
130 - $widget = \Elementor\Plugin::instance()->elements_manager->create_element_instance( $widget_data );
131 - if ( $widget ) {
132 - $settings = $widget->get_settings_for_display();
133 - }
228 + public static function campaign_totals_from_rows( $rows ): array {
229 + $totals = array(
230 + 'amount' => 0.0,
231 + 'count' => 0,
232 + );
233 +
234 + foreach ( is_array( $rows ) ? $rows : array() as $row ) {
235 + $status = self::row_value( $row, 'status' );
236 +
237 + $counted = in_array( $status, self::APPROVED_DONATION_STATUSES, true )
238 + || ( self::LEGACY_FREE_CHECKOUT_STATUS === $status && ! self::is_unconfirmed_legacy_free_checkout( $row ) );
239 +
240 + if ( ! $counted ) {
241 + continue;
134 242 }
135 -
243 +
244 + $totals['amount'] += self::countable_amount( $row );
245 + ++$totals['count'];
136 246 }
137 - return $settings;
247 +
248 + return $totals;
138 249 }
139 250
140 251 /**
141 - * Find element recursively
252 + * A scalar column of a row object or array, as a string ('' when absent or not scalar).
142 253 *
143 - * @since 0.0.1
254 + * @param object|array $row A transaction row.
255 + * @param string $key Column name.
256 + * @return string
144 257 */
145 - public function find_element_recursive( $elements, $form_id ) {
258 + private static function row_value( $row, string $key ): string {
259 + $value = null;
146 260
147 - foreach ( $elements as $element ) {
148 - if ( $form_id === $element[ 'id' ] ) {
149 - return $element;
150 - }
151 -
152 - if ( !empty( $element[ 'elements' ] ) ) {
153 - $element = $this->find_element_recursive( $element[ 'elements' ], $form_id );
154 -
155 - if ( $element ) {
156 - return $element;
157 - }
158 - }
261 + if ( is_object( $row ) && isset( $row->$key ) ) {
262 + $value = $row->$key;
263 + } elseif ( is_array( $row ) && isset( $row[ $key ] ) ) {
264 + $value = $row[ $key ];
159 265 }
160 266
161 - return false;
267 + return is_scalar( $value ) ? (string) $value : '';
162 268 }
163 269
164 270 /**
165 - * Get currency list
271 + * A row's `form_fields_info` as stored: the serialized string, an already-unserialized array
272 + * serialized back, or '' when absent. Only ever searched for LEGACY_CONFIRMATION_MARKER.
166 273 *
167 - * @since 0.0.2
274 + * @param object|array $row A transaction row.
275 + * @return string
168 276 */
169 - public function get_currency_list(){
170 - $currency_list = [
171 - 'AED' => 'AED',
172 - 'AUD' => 'AUD',
173 - 'CAD' => 'CAD',
174 - 'CZK' => 'CZK',
175 - 'DKK' => 'DKK',
176 - 'EUR' => 'EUR',
177 - 'HKD' => 'HKD',
178 - 'HUF' => 'HUF',
179 - 'ILS' => 'ILS',
180 - 'JPY' => 'JPY',
181 - 'MXN' => 'MXN',
182 - 'NOK' => 'NOK',
183 - 'NZD' => 'NZD',
184 - 'PHP' => 'PHP',
185 - 'PLN' => 'PLN',
186 - 'GBP' => 'GBP',
187 - 'RUB' => 'RUB',
188 - 'SGD' => 'SGD',
189 - 'SEK' => 'SEK',
190 - 'CHF' => 'CHF',
191 - 'TWD' => 'TWD',
192 - 'THB' => 'THB',
193 - 'USD' => 'USD',
194 - 'ZAR' => 'ZAR'
195 - ];
277 + private static function form_fields_info_string( $row ): string {
278 + $value = null;
196 279
197 - return $currency_list;
280 + if ( is_object( $row ) && isset( $row->form_fields_info ) ) {
281 + $value = $row->form_fields_info;
282 + } elseif ( is_array( $row ) && isset( $row['form_fields_info'] ) ) {
283 + $value = $row['form_fields_info'];
284 + }
285 +
286 + if ( is_array( $value ) ) {
287 + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- searched for the marker, never unserialized.
288 + return serialize( $value );
289 + }
290 +
291 + return is_scalar( $value ) ? (string) $value : '';
198 292 }
199 293
294 + // find_element_recursive() comes from Traits\Helper; this class used to carry
295 + // a byte-identical copy of it.
296 +
200 297 public function titleToSnake($text, $divider = '_') {
298 + // Handle null or empty values to prevent deprecated warnings.
299 + if ( null === $text || '' === $text ) {
300 + return 'n_a';
301 + }
302 +
303 + $text = (string) $text;
201 304 $text = preg_replace('~[^\pL\d]+~u', $divider, $text);
202 305 $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
203 306 $text = preg_replace('~[^-\w]+~', '', $text);
204 307 $text = trim($text, $divider);
@@ -211,17 +314,8 @@
211 314
212 315 return $text;
213 316 }
214 317
215 - public function arrayToString($array, $separator = ', ', $extraString = ''){
216 - $string = rtrim( implode($separator, $array ), $separator ) ;
217 - if($extraString) {
218 - $string .= $extraString;
219 - }
220 -
221 - return $string;
222 - }
223 -
224 318 public function transaction_statuses_with_type(){
225 319 $statuses = [
226 320 'completed' => [
227 321 'statuses' => [
@@ -227,8 +321,14 @@
227 321 'statuses' => [
228 322 'paid',
229 323 'Completed',
230 324 'completed',
325 + // Stripe's own status for a fully-discounted / $0 Checkout
326 + // Session (100% coupon or sale, free trial). The payment
327 + // IS settled - there was simply nothing to charge - so it
328 + // must classify as completed, not fall through to the
329 + // unknown-status default. See Helper::get_type_by_transaction_status().
330 + 'no_payment_required',
231 331 ],
232 332 'color' => '#0ECA86',
233 333 ],
234 334 'processing' => [
@@ -247,8 +347,10 @@
247 347 ],
248 348 'unpaid' => [
249 349 'statuses' => [
250 350 'unpaid',
351 + // A payment that failed.
352 + 'failed',
251 353 ],
252 354 'color' => '#999',
253 355 ],
254 356 'all' => [
@@ -255,12 +357,14 @@
255 357 'statuses' => [
256 358 'paid',
257 359 'Completed',
258 360 'completed',
361 + 'no_payment_required',
259 362 'unpaid',
260 363 'refunded',
261 364 'pending',
262 365 'Pending',
366 + 'failed',
263 367 NULL,
264 368 ],
265 369 'color' => '#999',
266 370 ],
@@ -276,8 +380,16 @@
276 380 'paid',
277 381 'Completed',
278 382 'completed',
279 383 'success',
384 + // See the v1 note: a $0 Stripe session settles as
385 + // 'no_payment_required'. Every status bucket is also a
386 + // `status IN (...)` filter in DB::get_transactions(), so a
387 + // status missing from BOTH 'completed' and 'incomplete'
388 + // is invisible under either tab while still showing on
389 + // the unfiltered list - which is exactly how a paid $0
390 + // order looked like a stray "Incomplete" row.
391 + 'no_payment_required',
280 392 ],
281 393 'color' => '#0ECA86',
282 394 ],
283 395 'incomplete' => [
@@ -286,8 +398,10 @@
286 398 'Pending',
287 399 'unpaid',
288 400 'incomplete',
289 401 'Incomplete',
402 + // A payment that failed.
403 + 'failed',
290 404 NULL,
291 405 ],
292 406 'color' => '#FFDA15',
293 407 ],
@@ -301,8 +415,9 @@
301 415 'statuses' => [
302 416 'paid',
303 417 'Completed',
304 418 'completed',
419 + 'no_payment_required',
305 420 'unpaid',
306 421 'refunded',
307 422 'pending',
308 423 'Pending',
@@ -308,8 +423,9 @@
308 423 'Pending',
309 424 'success',
310 425 'incomplete',
311 426 'Incomplete',
427 + 'failed',
312 428 NULL,
313 429 ],
314 430 'color' => '#999',
315 431 ],
@@ -366,28 +482,8 @@
366 482 return $color;
367 483
368 484 }
369 485
370 - public function get_color_by_transaction_type($status_type = '', $version = 'v1'){
371 - if('v1' === $version){
372 - $statuses = $this->transaction_statuses_with_type();
373 - } else {
374 - $statuses = $this->transaction_statuses_with_type_v2();
375 - }
376 -
377 - $color = '';
378 - if(!empty($status_type)){
379 - foreach($statuses as $type => $statuses_and_color){
380 - if($type == $status_type){
381 - $color = $statuses_and_color['color'];
382 - break;
383 - }
384 - }
385 - }
386 -
387 - return $color;
388 - }
389 -
390 486 public function get_statuses_by_transaction_type($status_type = 'all', $version = 'v1'){
391 487 if('v1' === $version){
392 488 $statuses = $this->transaction_statuses_with_type();
393 489 } else {
@@ -417,30 +513,129 @@
417 513 $statuses_types = array_keys($statuses);
418 514
419 515 return $statuses_types;
420 516 }
517 +
518 + /**
519 + * Get interval text for recurring and split payment
520 + *
521 + * Lite never calls this. Better Payment Pro <= 1.4.3's layout partials did, from
522 + * templates that no longer load (Pro 1.4.3 fills no `layout_template` filter), and
523 + * tests/Unit/ProCouplingContractTest.php still pins it. Pro 1.4.4+ uses its own
524 + * Admin\Elementor\LayoutRenderer::interval_text().
525 + *
526 + * @since 1.3.0
527 + * @deprecated 2.4.0 Kept as belt-and-braces for Pro 1.4.3. Delete once no supported
528 + * site can run it.
529 + */
530 + public function get_interval_text( $interval = '' ){
531 + $interval_text = '';
532 +
533 + switch( $interval ) {
534 + case 'day':
535 + $interval_text = 'Daily';
536 + break;
537 +
538 + case 'month':
539 + case 'year':
540 + $interval_text = $interval . 'ly';
541 + break;
542 +
543 + default :
544 + $interval_text = ! empty( $interval ) ? "Per " . esc_html( $interval ) : "";
545 + break;
546 + }
547 +
548 + return esc_html( ucfirst( $interval_text ) );
549 + }
550 +
551 + /**
552 + * The payment type label of a transaction, as the User Dashboard REST rows and
553 + * the CSV export print it.
554 + *
555 + * Lite's own checkouts only take one-time payments, so Lite says 'One Time'.
556 + * `better_payment/transaction/payment_type` ($label, $form_fields_info) relabels
557 + * a row: Pro returns 'Subscription' for its recurring and split payments. The CSV
558 + * import reads the label back, so keep these two strings.
559 + *
560 + * @since 2.4.0
561 + *
562 + * @param mixed $form_fields_info The transaction's unserialized form_fields_info.
563 + * @return string
564 + */
565 + public static function payment_type_label( $form_fields_info ): string {
566 + $form_fields_info = is_array( $form_fields_info ) ? $form_fields_info : [];
567 + $label = 'One Time';
568 +
569 + $filtered = apply_filters( 'better_payment/transaction/payment_type', $label, $form_fields_info );
570 +
571 + return is_string( $filtered ) ? $filtered : $label;
572 + }
573 +
574 + /**
575 + * Output the page header, compatible with both classic and block themes.
576 + *
577 + * Block themes don't use get_header() — they rely on block template parts.
578 + * We detect this with wp_is_block_theme() and render the header template
579 + * part via do_blocks() so it appears correctly on CPT single pages that
580 + * bypass the theme's block templates.
581 + *
582 + * @param string $block_header Pre-rendered header markup from do_blocks() (block themes only).
583 + * @return void
584 + */
585 + public static function render_header( string $block_header = '' ): void {
586 + if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
587 + ?>
588 + <!doctype html>
589 + <html <?php language_attributes(); ?>>
590 + <head>
591 + <meta charset="<?php bloginfo( 'charset' ); ?>">
592 + <?php wp_head(); ?>
593 + </head>
594 + <body <?php body_class(); ?>>
595 + <?php
596 + wp_body_open();
597 + echo $block_header; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
598 + } else {
599 + get_header();
600 + }
601 + }
602 +
603 + /**
604 + * Output the page footer, compatible with both classic and block themes.
605 + *
606 + * @param string $block_footer Pre-rendered footer markup from do_blocks() (block themes only).
607 + * @return void
608 + */
609 + public static function render_footer( string $block_footer = '' ): void {
610 + if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
611 + echo $block_footer; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
612 + wp_footer();
613 + ?>
614 + </body>
615 + </html>
616 + <?php
617 + } else {
618 + get_footer();
619 + }
620 + }
421 621 }
422 622
423 623
424 -//Helper Functions
425 -if( !function_exists('better_payment_dd') ){
426 - function better_payment_dd($data, $show_query = 0, $print_only = 0) {
624 +// Helper Functions
625 +if ( ! function_exists( 'better_payment_dd' ) ) {
626 + function better_payment_dd( $data, $show_query = 0, $print_only = 0 ) {
427 627 global $wpdb;
428 628
429 - if(1 === $show_query){
430 - // Print last SQL query string
629 + if ( 1 === $show_query ) {
431 630 echo wp_kses_post( $wpdb->last_query );
432 -
433 - //Print last SQL query result
434 631 echo wp_kses_post( $wpdb->last_result );
435 -
436 - //Print last SQL query Error
437 632 echo wp_kses_post( $wpdb->last_error );
438 633 }
439 -
440 - echo "<pre>";
441 - print_r($data);
442 - if(!$print_only){
443 - wp_die('Printed!');
634 +
635 + echo '<pre>';
636 + print_r( $data ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
637 + if ( ! $print_only ) {
638 + wp_die( 'Printed!' );
444 639 }
445 640 }
446 641 }