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/Blocks/BlockManager.php +247 -42 2.2.0 → 2.4.1 View file →
@@ -11,8 +11,9 @@
11 11 namespace Better_Payment\Lite\Blocks;
12 12
13 13 use Better_Payment\Lite\Admin\DB;
14 14 use Better_Payment\Lite\Classes\Handler;
15 +use Better_Payment\Lite\Classes\Helper;
15 16 use Better_Payment\Lite\Traits\Helper as TraitsHelper;
16 17
17 18 if ( ! defined( 'ABSPATH' ) ) {
18 19 exit;
@@ -149,14 +150,34 @@
149 150 $wrapper_class = 'better-payment-block bp-form-' . esc_attr( $form_layout );
150 151 $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $wrapper_class ) );
151 152
152 153 if ( ! $has_payment_gateway ) {
154 + // A visitor gets nothing: the block has no form to show, and the
155 + // instruction that follows is addressed to whoever can act on it.
156 + // It used to be printed for everyone, so a public page advertised
157 + // that the site owner had not finished setting up payments and named
158 + // the admin screen where it is done.
159 + if ( ! current_user_can( 'manage_options' ) ) {
160 + return '';
161 + }
162 +
153 163 return '<div ' . $wrapper_attributes . '>' .
154 - '<div class="better-payment-block__notice">' .
155 - '<p>' . esc_html__( 'Please configure at least one payment gateway (PayPal, Stripe, or Paystack) in Better Payment settings.', 'better-payment' ) . '</p>' .
156 - '</div></div>';
164 + $this->render_gateway_notice( $attributes, $global_settings ) .
165 + '</div>';
157 166 }
158 167
168 + // Enqueue scripts/styles before manage_response() so they are present even when the
169 + // block returns the pending or success state early (the AJAX poller needs better-payment.js).
170 + wp_enqueue_style( 'better-payment-el' );
171 + wp_enqueue_style( 'bp-icon-front' );
172 + wp_enqueue_style( 'better-payment-style' );
173 + wp_enqueue_style( 'better-payment-common-style' );
174 + wp_enqueue_style( 'better-payment-admin-style' );
175 + $this->enqueue_font_awesome();
176 + wp_enqueue_style( 'dashicons' );
177 + wp_enqueue_script( 'better-payment-common-script' );
178 + wp_enqueue_script( 'better-payment' );
179 +
159 180 // Match Elementor flow:
160 181 // - Handle response before rendering form.
161 182 // - On success: render success notice only.
162 183 // - On error: render error notice and continue rendering the form.
@@ -193,19 +214,8 @@
193 214 )
194 215 ),
195 216 );
196 217
197 - // Enqueue the necessary scripts and styles.
198 - wp_enqueue_style( 'better-payment-el' );
199 - wp_enqueue_style( 'bp-icon-front' );
200 - wp_enqueue_style( 'better-payment-style' );
201 - wp_enqueue_style( 'better-payment-common-style' );
202 - wp_enqueue_style( 'better-payment-admin-style' );
203 - $this->enqueue_font_awesome();
204 - wp_enqueue_style( 'dashicons' );
205 - wp_enqueue_script( 'better-payment-common-script' );
206 - wp_enqueue_script( 'better-payment' );
207 -
208 218 // Use output buffering to capture the layout output.
209 219 ob_start();
210 220 ?>
211 221 <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes() is a safe WordPress core function. ?>>
@@ -239,10 +249,12 @@
239 249 public function render_user_dashboard_frontend( $attributes, $content = '', $block = null ) {
240 250 // Get block attributes with defaults
241 251 $dashboard_layout = isset( $attributes['dashboardLayout'] ) ? sanitize_text_field( $attributes['dashboardLayout'] ) : 'layout-1';
242 252
243 - // Validate layout - only allow layout-1, layout-2, layout-3 plus pro versions
244 - $allowed_layouts = array( 'layout-1', 'layout-2', 'layout-3', 'layout-1-pro', 'layout-2-pro', 'layout-3-pro' );
253 + // Validate layout. There are no Pro dashboard layouts — Pro extends the
254 + // dashboard through the `better_payment/widget/user-dashboard/*` actions —
255 + // so any other key (a stale `layout-*-pro` included) falls back to layout-1.
256 + $allowed_layouts = array( 'layout-1', 'layout-2', 'layout-3' );
245 257 if ( ! in_array( $dashboard_layout, $allowed_layouts, true ) ) {
246 258 $dashboard_layout = 'layout-1';
247 259 }
248 260
@@ -248,14 +260,8 @@
248 260
249 261 // Check if layout file exists, fallback to layout-1 if not
250 262 $template_file = BETTER_PAYMENT_ADMIN_VIEWS_PATH . '/elementor/user-dashboard/' . $dashboard_layout . '.php';
251 263
252 - // Check for pro layout if pro plugin is active
253 - $is_pro_layout = strpos( $dashboard_layout, '-pro' ) !== false;
254 - if ( $is_pro_layout && defined( 'BETTER_PAYMENT_PRO_ADMIN_VIEWS_PATH' ) ) {
255 - $template_file = BETTER_PAYMENT_PRO_ADMIN_VIEWS_PATH . '/elementor/layouts/' . $dashboard_layout . '.php';
256 - }
257 -
258 264 if ( ! file_exists( $template_file ) ) {
259 265 $template_file = BETTER_PAYMENT_ADMIN_VIEWS_PATH . '/elementor/user-dashboard/layout-1.php';
260 266 if ( ! file_exists( $template_file ) ) {
261 267 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
@@ -637,9 +643,14 @@
637 643 $stripe_price_id = '';
638 644 }
639 645
640 646 // Get email notification setting from block attributes.
641 - $email_notification_enabled = isset( $attributes['emailNotificationEnabled'] ) && $attributes['emailNotificationEnabled']
647 + // The editor default for emailNotificationEnabled is true, but WordPress does not
648 + // serialize default attribute values into the block markup and the registered
649 + // block.json declares no schema default, so the attribute is absent at render time
650 + // unless explicitly toggled. Treat "unset" as enabled to match the editor default,
651 + // while still honoring an explicit OFF (serialized as false).
652 + $email_notification_enabled = ! isset( $attributes['emailNotificationEnabled'] ) || $attributes['emailNotificationEnabled']
642 653 ? 'yes'
643 654 : '';
644 655
645 656 // Get site domain for default email addresses.
@@ -997,8 +1008,165 @@
997 1008 );
998 1009 }
999 1010
1000 1011 /**
1012 + * Why a Payment Form block has no gateway, decided per gateway.
1013 + *
1014 + * Each gateway resolves in `build_block_settings()` from the block's own
1015 + * toggle when the block saved one, and from the site setting only when it
1016 + * did not. So a gateway is off for one of two reasons, and they are fixed in
1017 + * different places:
1018 + *
1019 + * - `block` — the site has a gateway enabled, but this block switches it
1020 + * off. Settings cannot fix it: the gateway is already on there.
1021 + * - `global` — no gateway is enabled site-wide. Settings fixes it on its own
1022 + * for any gateway the block leaves to the site (`settings_fixes`); a
1023 + * gateway the block also switched off needs both (`needs_block`). A new
1024 + * block seeds its toggles from the settings, so on a site with nothing
1025 + * enabled `needs_block` is the usual case.
1026 + *
1027 + * Mirrors the editor's `gatewayNoticeState()` (helpers/gatewayNotice.js), so
1028 + * the editor and the page never give two different answers.
1029 + *
1030 + * @param array $attributes Block attributes.
1031 + * @param array $global_settings Global plugin settings.
1032 + * @return array{reason: string, site_gateways: string[], settings_fixes: string[], needs_block: bool}
1033 + */
1034 + public static function gateway_notice_state( $attributes, $global_settings ) {
1035 + $gateways = array(
1036 + 'PayPal' => array( 'paypalEnabled', 'better_payment_settings_general_general_paypal' ),
1037 + 'Stripe' => array( 'stripeEnabled', 'better_payment_settings_general_general_stripe' ),
1038 + 'Paystack' => array( 'paystackEnabled', 'better_payment_settings_general_general_paystack' ),
1039 + );
1040 +
1041 + $site_gateways = array();
1042 + $settings_fixes = array();
1043 +
1044 + foreach ( $gateways as $label => $keys ) {
1045 + if ( isset( $global_settings[ $keys[1] ] ) && 'yes' === $global_settings[ $keys[1] ] ) {
1046 + $site_gateways[] = $label;
1047 + }
1048 + if ( ! isset( $attributes[ $keys[0] ] ) ) {
1049 + $settings_fixes[] = $label;
1050 + }
1051 + }
1052 +
1053 + $reason = empty( $site_gateways ) ? 'global' : 'block';
1054 +
1055 + return array(
1056 + 'reason' => $reason,
1057 + 'site_gateways' => $site_gateways,
1058 + 'settings_fixes' => $settings_fixes,
1059 + 'needs_block' => 'block' === $reason || empty( $settings_fixes ),
1060 + );
1061 + }
1062 +
1063 + /**
1064 + * The admin-only notice printed in place of a form with no gateway.
1065 + *
1066 + * Names the gateways and the place that fixes them, and links to each place
1067 + * the fix needs: Better Payment settings, this block's editor, or both. The
1068 + * editor link is offered only when the block is really in the current post's
1069 + * content — in a template, template part, synced pattern or widget area the
1070 + * current post's editor does not contain it, so the notice says where to look
1071 + * instead of linking to the wrong screen.
1072 + *
1073 + * @param array $attributes Block attributes.
1074 + * @param array $global_settings Global plugin settings.
1075 + * @return string Notice HTML.
1076 + */
1077 + private function render_gateway_notice( $attributes, $global_settings ) {
1078 + $state = self::gateway_notice_state( $attributes, $global_settings );
1079 +
1080 + if ( 'block' === $state['reason'] ) {
1081 + $message = sprintf(
1082 + /* translators: %s: gateway names, e.g. "PayPal or Stripe". */
1083 + __( 'Payment form: every payment method is switched off in this block’s settings. Turn on %s under Payment Settings in the block sidebar.', 'better-payment' ),
1084 + self::join_gateway_names( $state['site_gateways'] )
1085 + );
1086 + } elseif ( $state['needs_block'] ) {
1087 + $message = __( 'Payment form: no payment gateway is enabled in Better Payment settings, and every payment method is switched off in this block’s settings. Enable PayPal, Stripe or Paystack in Better Payment settings, then turn the same gateway on under Payment Settings in the block sidebar.', 'better-payment' );
1088 + } else {
1089 + $message = sprintf(
1090 + /* translators: %s: gateway names, e.g. "PayPal, Stripe or Paystack". */
1091 + __( 'Payment form: enable at least one payment gateway (%s) in Better Payment settings.', 'better-payment' ),
1092 + self::join_gateway_names( $state['settings_fixes'] )
1093 + );
1094 + }
1095 +
1096 + $links = array();
1097 +
1098 + if ( 'global' === $state['reason'] ) {
1099 + $links[] = '<a href="' . esc_url( admin_url( 'admin.php?page=better-payment-admin&tab=settings&section=general' ) ) . '">' .
1100 + esc_html__( 'Open Better Payment settings', 'better-payment' ) . '</a>';
1101 + }
1102 +
1103 + $hint = '';
1104 +
1105 + if ( $state['needs_block'] ) {
1106 + $edit_url = self::block_edit_url();
1107 +
1108 + if ( '' !== $edit_url ) {
1109 + $links[] = '<a href="' . esc_url( $edit_url ) . '">' .
1110 + esc_html__( 'Edit this form in the block editor', 'better-payment' ) . '</a>';
1111 + } else {
1112 + $hint = '<p>' . esc_html__( 'This form is not part of this page’s content. Open the template, template part, pattern or widget area that contains it to change its settings.', 'better-payment' ) . '</p>';
1113 + }
1114 + }
1115 +
1116 + return '<div class="better-payment-block__notice">' .
1117 + '<p>' . esc_html( $message ) . ' ' . esc_html__( 'This notice is only visible to you.', 'better-payment' ) . '</p>' .
1118 + $hint .
1119 + ( $links ? '<p class="better-payment-block__notice-actions">' . implode( ' ', $links ) . '</p>' : '' ) .
1120 + '</div>';
1121 + }
1122 +
1123 + /**
1124 + * The editor URL of the current post, when it holds a Payment Form block.
1125 + *
1126 + * Empty when there is no current post, the user may not edit it, or its
1127 + * content has no Payment Form block (the block came from a template, template
1128 + * part, synced pattern or widget area).
1129 + *
1130 + * @return string Raw edit URL, or ''.
1131 + */
1132 + private static function block_edit_url() {
1133 + $post_id = (int) get_the_ID();
1134 +
1135 + if ( $post_id <= 0 || ! current_user_can( 'edit_post', $post_id ) ) {
1136 + return '';
1137 + }
1138 +
1139 + if ( ! has_block( 'better-payment/payment-form', $post_id ) ) {
1140 + return '';
1141 + }
1142 +
1143 + return (string) get_edit_post_link( $post_id, 'raw' );
1144 + }
1145 +
1146 + /**
1147 + * "A", "A or B", "A, B or C".
1148 + *
1149 + * @param string[] $names Gateway names (brand names, not translated).
1150 + * @return string Joined list.
1151 + */
1152 + private static function join_gateway_names( $names ) {
1153 + $names = array_values( $names );
1154 + $last = array_pop( $names );
1155 +
1156 + if ( empty( $names ) ) {
1157 + return (string) $last;
1158 + }
1159 +
1160 + return sprintf(
1161 + /* translators: 1: comma-separated gateway names, 2: the last gateway name. */
1162 + __( '%1$s or %2$s', 'better-payment' ),
1163 + implode( ', ', $names ),
1164 + $last
1165 + );
1166 + }
1167 +
1168 + /**
1001 1169 * Resolve the correct Stripe key based on live mode setting.
1002 1170 *
1003 1171 * This matches Elementor's behavior where the key is resolved before being stored
1004 1172 * in better_payment_stripe_public_key/better_payment_stripe_secret_key.
@@ -1599,8 +1767,13 @@
1599 1767 'better_payment_settings_payment_stripe_live_secret',
1600 1768 'better_payment_settings_payment_stripe_test_secret',
1601 1769 'better_payment_settings_payment_paystack_live_secret',
1602 1770 'better_payment_settings_payment_paystack_test_secret',
1771 + // AI provider API keys (Campaign Builder); the block editor never needs them.
1772 + 'better_payment_settings_ai_api_key_openai',
1773 + 'better_payment_settings_ai_api_key_claude',
1774 + 'better_payment_settings_ai_api_key_gemini',
1775 + 'better_payment_settings_ai_api_key_openrouter',
1603 1776 );
1604 1777
1605 1778 foreach ( $sensitive_keys as $key ) {
1606 1779 unset( $settings[ $key ] );
@@ -1642,8 +1815,16 @@
1642 1815 private function get_payment_form_editor_data() {
1643 1816 return array(
1644 1817 'betterPaymentSettings' => $this->get_safe_settings_for_editor(),
1645 1818 'adminEmail' => sanitize_email( get_option( 'admin_email' ) ),
1819 + // A form with no gateway enabled shows an editor notice instead of
1820 + // nothing (components/gateway-notice.js). Both keys are about what to
1821 + // tell the user: the link is only offered to someone who can act on
1822 + // it, since an author who cannot open Better Payment's settings is
1823 + // better told to ask an administrator than handed a link to a screen
1824 + // they will be refused.
1825 + 'settingsUrl' => admin_url( 'admin.php?page=better-payment-admin&tab=settings&section=general' ),
1826 + 'canManageSettings' => current_user_can( 'manage_options' ),
1646 1827 );
1647 1828 }
1648 1829
1649 1830 /**
@@ -1662,9 +1843,12 @@
1662 1843 // Cache the 5 DB queries for the duration of the browser session in the editor.
1663 1844 // The cache is keyed per user so each editor sees only their own data.
1664 1845 // TTL: 5 minutes — short enough to reflect recent transactions without hammering
1665 1846 // the DB on every page navigation inside the block editor.
1666 - $cache_key = 'bp_editor_dash_' . ( $current_user->ID ?? 0 );
1847 + // Entitlement is part of the key: subscription rows are fetched only for
1848 + // Pro (below), so a cached free payload must never be served to Pro.
1849 + $pro_enabled = (bool) apply_filters( 'better_payment/pro_enabled', false );
1850 + $cache_key = 'bp_editor_dash_' . ( $current_user->ID ?? 0 ) . ( $pro_enabled ? '_pro' : '' );
1667 1851 $cache_group = 'better_payment';
1668 1852 $cached = wp_cache_get( $cache_key, $cache_group );
1669 1853 if ( false !== $cached ) {
1670 1854 // Nonces are request-scoped and must never be cached; regenerate on every hit.
@@ -1702,8 +1886,12 @@
1702 1886 );
1703 1887
1704 1888 // Unserialize and normalise to match UserAPI REST response format.
1705 1889 foreach ( $raw_transactions as $tx ) {
1890 + // The status bucket, as UserAPI's `status_type`: the preview labels the row from it
1891 + // (an unconfirmed pre-2.4.0 free checkout reads Incomplete).
1892 + $tx->status_type = \Better_Payment\Lite\Classes\Helper::transaction_status_type( $tx, 'v2' );
1893 +
1706 1894 if ( ! empty( $tx->form_fields_info ) ) {
1707 1895 $tx->form_fields_info = maybe_unserialize( $tx->form_fields_info );
1708 1896 }
1709 1897 if ( ! empty( $tx->customer_info ) ) {
@@ -1720,23 +1908,33 @@
1720 1908 : '';
1721 1909 }
1722 1910 $tx->customer_name = $customer_name;
1723 1911 $tx->customer_email = sanitize_text_field( $ffi['primary_email'] ?? ( $ffi['email'] ?? '' ) );
1912 +
1913 + // 'One Time' unless Pro labels the row (better_payment/transaction/payment_type), as UserAPI does.
1914 + $tx->payment_type_label = \Better_Payment\Lite\Classes\Helper::payment_type_label( $ffi );
1915 + $tx->is_subscription = $tx->payment_type_label;
1724 1916 }
1725 1917
1726 1918 $user_transactions = apply_filters( 'better_payment/block/user_dashboard/enrich_user_transactions', $raw_transactions );
1727 1919
1728 1920 // Separate query for subscriptions (not a subset of transactions page 1).
1729 - $sub_result = \Better_Payment\Lite\Admin\DB::get_user_transactions_paginated(
1730 - $current_user->user_email,
1731 - array(
1732 - 'page' => 1,
1733 - 'per_page' => 20,
1734 - 'type' => 'subscriptions',
1735 - )
1736 - );
1921 + // These rows are Better Payment's own Stripe recurring and split payments —
1922 + // Pro features that only Pro's Subscriptions tab displays — so a free
1923 + // install skips the query and keeps the empty default.
1924 + $raw_subscriptions = array();
1925 + if ( $pro_enabled ) {
1926 + $sub_result = \Better_Payment\Lite\Admin\DB::get_user_transactions_paginated(
1927 + $current_user->user_email,
1928 + array(
1929 + 'page' => 1,
1930 + 'per_page' => 20,
1931 + 'type' => 'subscriptions',
1932 + )
1933 + );
1934 + $raw_subscriptions = $sub_result['transactions'];
1935 + }
1737 1936
1738 - $raw_subscriptions = $sub_result['transactions'];
1739 1937 foreach ( $raw_subscriptions as $tx ) {
1740 1938 if ( ! empty( $tx->form_fields_info ) ) {
1741 1939 $tx->form_fields_info = maybe_unserialize( $tx->form_fields_info );
1742 1940 }
@@ -1746,17 +1944,24 @@
1746 1944 // Flatten each subscription to match UserAPI flat format so JS handles one structure.
1747 1945 foreach ( $user_subscriptions as $tx ) {
1748 1946 $ffi = is_array( $tx->form_fields_info ) ? $tx->form_fields_info : array();
1749 1947
1750 - $tx->subscription_id = sanitize_text_field( $ffi['subscription_id'] ?? '' );
1751 - $tx->subscription_plan_id = sanitize_text_field( $ffi['subscription_plan_id'] ?? '' );
1752 - $tx->subscription_status = sanitize_text_field( $ffi['subscription_status'] ?? '' );
1753 - $tx->subscription_interval = sanitize_text_field( $ffi['subscription_interval'] ?? '' );
1754 - $tx->subscription_created_date = intval( $ffi['subscription_created_date'] ?? 0 );
1755 - $tx->subscription_current_period_end = intval( $ffi['subscription_current_period_end'] ?? 0 );
1756 - $tx->is_payment_split_payment = ! empty( $ffi['is_payment_split_payment'] ) ? intval( $ffi['is_payment_split_payment'] ) : 0;
1757 - $tx->subscription_product_name = sanitize_text_field( $ffi['subscription_product_name'] ?? '' );
1758 - $tx->is_subscription = ! empty( $ffi['subscription_id'] ) ? 'Subscription' : 'One Time';
1948 + // The subscription columns come from the same place as UserAPI's REST rows: Pro fills
1949 + // `better_payment/user_api/subscription_row`.
1950 + $row = array();
1951 + $built = apply_filters( 'better_payment/user_api/subscription_row', $row, $ffi, $tx );
1952 + $row = is_array( $built ) ? $built : $row;
1953 +
1954 + foreach ( $row as $key => $value ) {
1955 + $tx->{$key} = $value;
1956 + }
1957 +
1958 + // The enrich_user_subscriptions filter above stores the product name in form_fields_info.
1959 + if ( ! empty( $ffi['subscription_product_name'] ) ) {
1960 + $tx->subscription_product_name = sanitize_text_field( $ffi['subscription_product_name'] );
1961 + }
1962 +
1963 + $tx->is_subscription = Helper::payment_type_label( $ffi );
1759 1964
1760 1965 $customer_name = isset( $ffi['primary_first_name'] )
1761 1966 ? trim( sanitize_text_field( $ffi['primary_first_name'] ) . ' ' . sanitize_text_field( $ffi['primary_last_name'] ?? '' ) )
1762 1967 : '';