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 +229 -30 2.3.3 → 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,12 +150,20 @@
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
159 168 // Enqueue scripts/styles before manage_response() so they are present even when the
160 169 // block returns the pending or success state early (the AJAX poller needs better-payment.js).
@@ -240,10 +249,12 @@
240 249 public function render_user_dashboard_frontend( $attributes, $content = '', $block = null ) {
241 250 // Get block attributes with defaults
242 251 $dashboard_layout = isset( $attributes['dashboardLayout'] ) ? sanitize_text_field( $attributes['dashboardLayout'] ) : 'layout-1';
243 252
244 - // Validate layout - only allow layout-1, layout-2, layout-3 plus pro versions
245 - $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' );
246 257 if ( ! in_array( $dashboard_layout, $allowed_layouts, true ) ) {
247 258 $dashboard_layout = 'layout-1';
248 259 }
249 260
@@ -249,14 +260,8 @@
249 260
250 261 // Check if layout file exists, fallback to layout-1 if not
251 262 $template_file = BETTER_PAYMENT_ADMIN_VIEWS_PATH . '/elementor/user-dashboard/' . $dashboard_layout . '.php';
252 263
253 - // Check for pro layout if pro plugin is active
254 - $is_pro_layout = strpos( $dashboard_layout, '-pro' ) !== false;
255 - if ( $is_pro_layout && defined( 'BETTER_PAYMENT_PRO_ADMIN_VIEWS_PATH' ) ) {
256 - $template_file = BETTER_PAYMENT_PRO_ADMIN_VIEWS_PATH . '/elementor/layouts/' . $dashboard_layout . '.php';
257 - }
258 -
259 264 if ( ! file_exists( $template_file ) ) {
260 265 $template_file = BETTER_PAYMENT_ADMIN_VIEWS_PATH . '/elementor/user-dashboard/layout-1.php';
261 266 if ( ! file_exists( $template_file ) ) {
262 267 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
@@ -1003,8 +1008,165 @@
1003 1008 );
1004 1009 }
1005 1010
1006 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 + /**
1007 1169 * Resolve the correct Stripe key based on live mode setting.
1008 1170 *
1009 1171 * This matches Elementor's behavior where the key is resolved before being stored
1010 1172 * in better_payment_stripe_public_key/better_payment_stripe_secret_key.
@@ -1605,8 +1767,13 @@
1605 1767 'better_payment_settings_payment_stripe_live_secret',
1606 1768 'better_payment_settings_payment_stripe_test_secret',
1607 1769 'better_payment_settings_payment_paystack_live_secret',
1608 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',
1609 1776 );
1610 1777
1611 1778 foreach ( $sensitive_keys as $key ) {
1612 1779 unset( $settings[ $key ] );
@@ -1648,8 +1815,16 @@
1648 1815 private function get_payment_form_editor_data() {
1649 1816 return array(
1650 1817 'betterPaymentSettings' => $this->get_safe_settings_for_editor(),
1651 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' ),
1652 1827 );
1653 1828 }
1654 1829
1655 1830 /**
@@ -1668,9 +1843,12 @@
1668 1843 // Cache the 5 DB queries for the duration of the browser session in the editor.
1669 1844 // The cache is keyed per user so each editor sees only their own data.
1670 1845 // TTL: 5 minutes — short enough to reflect recent transactions without hammering
1671 1846 // the DB on every page navigation inside the block editor.
1672 - $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' : '' );
1673 1851 $cache_group = 'better_payment';
1674 1852 $cached = wp_cache_get( $cache_key, $cache_group );
1675 1853 if ( false !== $cached ) {
1676 1854 // Nonces are request-scoped and must never be cached; regenerate on every hit.
@@ -1708,8 +1886,12 @@
1708 1886 );
1709 1887
1710 1888 // Unserialize and normalise to match UserAPI REST response format.
1711 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 +
1712 1894 if ( ! empty( $tx->form_fields_info ) ) {
1713 1895 $tx->form_fields_info = maybe_unserialize( $tx->form_fields_info );
1714 1896 }
1715 1897 if ( ! empty( $tx->customer_info ) ) {
@@ -1726,23 +1908,33 @@
1726 1908 : '';
1727 1909 }
1728 1910 $tx->customer_name = $customer_name;
1729 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;
1730 1916 }
1731 1917
1732 1918 $user_transactions = apply_filters( 'better_payment/block/user_dashboard/enrich_user_transactions', $raw_transactions );
1733 1919
1734 1920 // Separate query for subscriptions (not a subset of transactions page 1).
1735 - $sub_result = \Better_Payment\Lite\Admin\DB::get_user_transactions_paginated(
1736 - $current_user->user_email,
1737 - array(
1738 - 'page' => 1,
1739 - 'per_page' => 20,
1740 - 'type' => 'subscriptions',
1741 - )
1742 - );
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 + }
1743 1936
1744 - $raw_subscriptions = $sub_result['transactions'];
1745 1937 foreach ( $raw_subscriptions as $tx ) {
1746 1938 if ( ! empty( $tx->form_fields_info ) ) {
1747 1939 $tx->form_fields_info = maybe_unserialize( $tx->form_fields_info );
1748 1940 }
@@ -1752,17 +1944,24 @@
1752 1944 // Flatten each subscription to match UserAPI flat format so JS handles one structure.
1753 1945 foreach ( $user_subscriptions as $tx ) {
1754 1946 $ffi = is_array( $tx->form_fields_info ) ? $tx->form_fields_info : array();
1755 1947
1756 - $tx->subscription_id = sanitize_text_field( $ffi['subscription_id'] ?? '' );
1757 - $tx->subscription_plan_id = sanitize_text_field( $ffi['subscription_plan_id'] ?? '' );
1758 - $tx->subscription_status = sanitize_text_field( $ffi['subscription_status'] ?? '' );
1759 - $tx->subscription_interval = sanitize_text_field( $ffi['subscription_interval'] ?? '' );
1760 - $tx->subscription_created_date = intval( $ffi['subscription_created_date'] ?? 0 );
1761 - $tx->subscription_current_period_end = intval( $ffi['subscription_current_period_end'] ?? 0 );
1762 - $tx->is_payment_split_payment = ! empty( $ffi['is_payment_split_payment'] ) ? intval( $ffi['is_payment_split_payment'] ) : 0;
1763 - $tx->subscription_product_name = sanitize_text_field( $ffi['subscription_product_name'] ?? '' );
1764 - $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 );
1765 1964
1766 1965 $customer_name = isset( $ffi['primary_first_name'] )
1767 1966 ? trim( sanitize_text_field( $ffi['primary_first_name'] ) . ' ' . sanitize_text_field( $ffi['primary_last_name'] ?? '' ) )
1768 1967 : '';