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
better-payment / includes / Blocks / BlockManager.php

BlockManager.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.4.1, at includes/Blocks/BlockManager.php

2,266 lines 118.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Block Manager for Better Payment Gutenberg blocks.
4 *
5 * Handles registration, enqueuing, and management of all Gutenberg blocks.
6 *
7 * @package Better_Payment
8 * @since 1.0.0
9 */
10
11 namespace Better_Payment\Lite\Blocks;
12
13 use Better_Payment\Lite\Admin\DB;
14 use Better_Payment\Lite\Classes\Handler;
15 use Better_Payment\Lite\Classes\Helper;
16 use Better_Payment\Lite\Traits\Helper as TraitsHelper;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 /**
23 * Block Manager class.
24 *
25 * @since 1.0.0
26 */
27 class BlockManager {
28 use TraitsHelper;
29
30 /**
31 * Instance of this class.
32 *
33 * @var BlockManager|null
34 */
35 private static $instance = null;
36
37 /**
38 * List of available blocks.
39 *
40 * @var array
41 */
42 private $blocks = array(
43 'payment-form' => array(
44 'name' => 'better-payment/payment-form',
45 'path' => 'payment-form',
46 'has_styles' => true,
47 'has_scripts' => true,
48 'render_callback' => 'render_payment_form_frontend',
49 'editor_data' => 'get_payment_form_editor_data',
50 ),
51 'user-dashboard' => array(
52 'name' => 'better-payment/user-dashboard',
53 'path' => 'user-dashboard',
54 'has_styles' => true,
55 'has_scripts' => true,
56 'render_callback' => 'render_user_dashboard_frontend',
57 'editor_data' => 'get_user_dashboard_editor_data',
58 ),
59 );
60
61 /**
62 * Get the singleton instance.
63 *
64 * @return BlockManager
65 */
66 public static function get_instance() {
67 if ( null === self::$instance ) {
68 self::$instance = new self();
69 }
70 return self::$instance;
71 }
72
73 /**
74 * Private constructor to prevent direct instantiation.
75 */
76 private function __construct() {
77 $this->init_hooks();
78 }
79
80 /**
81 * Initialize hooks.
82 *
83 * @return void
84 */
85 private function init_hooks() {
86 add_action( 'init', array( $this, 'register_blocks' ) );
87 add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_editor_assets' ) );
88 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
89 add_filter( 'block_categories_all', array( $this, 'register_block_category' ), 10, 2 );
90 }
91
92 /**
93 * Render callback for the payment form block frontend.
94 *
95 * This method contains all the rendering logic including layout template loading,
96 * settings mapping, widget proxy object creation, and security measures.
97 *
98 * @param array $attributes Block attributes.
99 * @param string $content Block default content.
100 * @param WP_Block $block Block instance.
101 * @return string The rendered HTML output.
102 */
103 public function render_payment_form_frontend( $attributes, $content = '', $block = null ) {
104 // Get block attributes with defaults.
105 $form_layout = isset( $attributes['formLayout'] ) ? sanitize_text_field( $attributes['formLayout'] ) : 'layout-1';
106
107 // Validate layout - only allow layout-1, layout-2, layout-3.
108 $allowed_layouts = array( 'layout-1', 'layout-2', 'layout-3' );
109 if ( ! in_array( $form_layout, $allowed_layouts, true ) ) {
110 $form_layout = 'layout-1';
111 }
112
113 // Check if layout file exists, fallback to layout-1 if not.
114 $template_file = BETTER_PAYMENT_ADMIN_VIEWS_PATH . '/elementor/layouts-block/' . $form_layout . '.php';
115 if ( ! file_exists( $template_file ) ) {
116 $template_file = BETTER_PAYMENT_ADMIN_VIEWS_PATH . '/elementor/layouts-block/layout-1.php';
117 if ( ! file_exists( $template_file ) ) {
118 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
119 error_log( 'Better Payment: Layout template not found at ' . $template_file );
120 }
121 return '<div class="better-payment-block better-payment-block--error">' .
122 esc_html__( 'Payment form template not found.', 'better-payment' ) .
123 '</div>';
124 }
125 }
126
127 // Get global settings from database.
128 $global_settings = DB::get_settings();
129
130 // Generate a unique ID for this block instance.
131 // Use a deterministic ID based on block attributes to ensure consistency across page loads.
132 // This is critical for payment callback matching.
133 $block_id = isset( $attributes['blockId'] ) && ! empty( $attributes['blockId'] )
134 ? sanitize_text_field( $attributes['blockId'] )
135 : 'bp-block-' . md5( get_the_ID() . wp_json_encode( $attributes ) );
136
137 // Build the $settings array that the layout files expect.
138 // This maps block attributes to the Elementor widget settings format.
139 $settings = $this->build_block_settings( $attributes, $global_settings, $form_layout );
140
141 // Check if at least one payment gateway is enabled.
142 $has_payment_gateway = 'yes' === $settings['better_payment_form_paypal_enable'] ||
143 'yes' === $settings['better_payment_form_stripe_enable'] ||
144 'yes' === $settings['better_payment_form_paystack_enable'];
145
146 // Build wrapper attributes using WordPress's block wrapper API.
147 // get_block_wrapper_attributes() reads supports.align from the block context
148 // (set automatically by WP_Block before invoking this render callback) and adds
149 // alignwide / alignfull to the class list — no manual $attributes['align'] needed.
150 $wrapper_class = 'better-payment-block bp-form-' . esc_attr( $form_layout );
151 $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $wrapper_class ) );
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
163 return '<div ' . $wrapper_attributes . '>' .
164 $this->render_gateway_notice( $attributes, $global_settings ) .
165 '</div>';
166 }
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
180 // Match Elementor flow:
181 // - Handle response before rendering form.
182 // - On success: render success notice only.
183 // - On error: render error notice and continue rendering the form.
184 ob_start();
185 $payment_response = Handler::manage_response( $settings, $block_id );
186 $manage_response_output = ob_get_clean();
187
188 if ( $payment_response ) {
189 return '<div ' . $wrapper_attributes . '>' . $manage_response_output . '</div>';
190 }
191
192 // Fire action hook for extensibility (matching Elementor widget behavior).
193 do_action( 'better_payment/elementor/editor/manage_response_webhook', null, $settings );
194
195 // Store settings in a transient so the AJAX handler can retrieve them.
196 // Only write when missing or expired — avoids a DB write on every page load.
197 $transient_key = 'bp_block_settings_' . get_the_ID() . '_' . $block_id;
198 if ( false === get_transient( $transient_key ) ) {
199 set_transient( $transient_key, $settings, HOUR_IN_SECONDS );
200 }
201
202 // Create widget proxy object for layout templates.
203 $widgetObj = $this->create_widget_proxy( $block_id, $settings );
204
205 // Prepare extraDatas for the layout.
206 $extraDatas = array(
207 'action' => esc_url( admin_url( 'admin-post.php' ) ),
208 'block_id' => $block_id,
209 'setting_meta' => wp_json_encode(
210 array(
211 'page_id' => get_the_ID(),
212 'widget_id' => $block_id,
213 'source' => 'gutenberg',
214 )
215 ),
216 );
217
218 // Use output buffering to capture the layout output.
219 ob_start();
220 ?>
221 <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes() is a safe WordPress core function. ?>>
222 <?php echo $manage_response_output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
223 <?php
224 // Create a closure to include the template with isolated scope.
225 $render_layout = function ( $template_file, $settings, $widgetObj, $extraDatas ) {
226 include $template_file;
227 };
228
229 // Bind the closure to the widget proxy so $this works in the template.
230 $bound_render = \Closure::bind( $render_layout, $widgetObj, get_class( $widgetObj ) );
231 $bound_render( $template_file, $settings, $widgetObj, $extraDatas );
232 ?>
233 </div>
234 <?php
235
236 return ob_get_clean();
237 }
238
239 /**
240 * Render callback for the user dashboard block frontend.
241 *
242 * Maps block attributes to widget settings and renders the user dashboard template.
243 *
244 * @param array $attributes Block attributes.
245 * @param string $content Block default content.
246 * @param WP_Block $block Block instance.
247 * @return string The rendered HTML output.
248 */
249 public function render_user_dashboard_frontend( $attributes, $content = '', $block = null ) {
250 // Get block attributes with defaults
251 $dashboard_layout = isset( $attributes['dashboardLayout'] ) ? sanitize_text_field( $attributes['dashboardLayout'] ) : 'layout-1';
252
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' );
257 if ( ! in_array( $dashboard_layout, $allowed_layouts, true ) ) {
258 $dashboard_layout = 'layout-1';
259 }
260
261 // Check if layout file exists, fallback to layout-1 if not
262 $template_file = BETTER_PAYMENT_ADMIN_VIEWS_PATH . '/elementor/user-dashboard/' . $dashboard_layout . '.php';
263
264 if ( ! file_exists( $template_file ) ) {
265 $template_file = BETTER_PAYMENT_ADMIN_VIEWS_PATH . '/elementor/user-dashboard/layout-1.php';
266 if ( ! file_exists( $template_file ) ) {
267 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
268 error_log( 'Better Payment: Layout template not found at ' . $template_file );
269 }
270 return '<div class="better-payment-block better-payment-block--error">' .
271 esc_html__( 'User Dashboard template not found.', 'better-payment' ) .
272 '</div>';
273 }
274 }
275
276 // Generate unique block ID for this instance
277 $block_id = isset( $attributes['blockId'] ) && ! empty( $attributes['blockId'] )
278 ? sanitize_text_field( $attributes['blockId'] )
279 : 'bp-user-dashboard-' . md5( get_the_ID() . wp_json_encode( $attributes ) );
280
281 // Map block attributes to widget settings format
282 $settings = $this->build_user_dashboard_settings( $attributes );
283
284 // Build wrapper attributes using WordPress's block wrapper API
285 $wrapper_class = 'better-payment-user-dashboard bp-' . esc_attr( $block_id ) . ' bp-dashboard-' . esc_attr( $dashboard_layout );
286 $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $wrapper_class ) );
287
288 // Prepare extra data
289 $extraDatas = array(
290 'action' => esc_url( admin_url( 'admin-post.php' ) ),
291 'block_id' => $block_id,
292 'setting_meta' => wp_json_encode(
293 array(
294 'page_id' => get_the_ID(),
295 'widget_id' => $block_id,
296 'source' => 'gutenberg',
297 )
298 ),
299 );
300
301 // Enqueue necessary scripts and styles
302 wp_enqueue_style( 'better-payment-el' );
303 wp_enqueue_style( 'bp-icon-front' );
304 wp_enqueue_style( 'better-payment-style' );
305 wp_enqueue_style( 'better-payment-common-style' );
306 wp_enqueue_style( 'better-payment-admin-style' );
307 $this->enqueue_font_awesome();
308 wp_enqueue_style( 'dashicons' );
309 wp_enqueue_style( 'bp-dashboard-pagination-style' );
310 wp_enqueue_script( 'better-payment-common-script' );
311 wp_enqueue_script( 'better-payment' );
312 if ( is_user_logged_in() ) {
313 wp_localize_script( 'better-payment', 'betterPaymentUserDash', [
314 'nonce' => wp_create_nonce( 'wp_rest' ),
315 'restUrl' => get_rest_url( null, 'better-payment/v1/user-transactions' ),
316 ] );
317 }
318 wp_enqueue_script( 'bp-dashboard-pagination' );
319
320 // Use output buffering to capture the layout output
321 ob_start();
322 ?>
323 <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes() is a safe WordPress core function. ?>>
324 <?php
325 // Create widget proxy object for layout templates
326 $widgetObj = $this->create_user_dashboard_widget_proxy( $block_id, $settings );
327
328 // Create a closure to include the template with isolated scope
329 $render_layout = function ( $template_file, $settings, $widgetObj, $extraDatas ) {
330 // Make $bp_settings available to templates (templates expect this variable)
331 $bp_settings = $settings;
332 include $template_file;
333 };
334
335 // Bind the closure to the widget proxy so $this works in the template
336 $bound_render = \Closure::bind( $render_layout, $widgetObj, get_class( $widgetObj ) );
337 $bound_render( $template_file, $settings, $widgetObj, $extraDatas );
338 ?>
339 </div>
340 <?php
341
342 return ob_get_clean();
343 }
344
345 /**
346 * Build the settings array for user dashboard layout templates.
347 *
348 * Maps block attributes to the format expected by dashboard templates.
349 *
350 * @param array $attributes Block attributes.
351 * @return array Settings array for layout templates.
352 */
353 public function build_user_dashboard_settings( $attributes ) {
354 // Convert block attributes to widget settings format (like get_bp_settings does)
355 $settings = array(
356 // Visibility toggles
357 'sidebar_show' => isset( $attributes['sidebarShow'] ) ? (bool) $attributes['sidebarShow'] : true,
358 'avatar_show' => isset( $attributes['avatarShow'] ) ? (bool) $attributes['avatarShow'] : true,
359 'username_show' => isset( $attributes['usernameShow'] ) ? (bool) $attributes['usernameShow'] : true,
360 'dashboard_show' => isset( $attributes['dashboardShow'] ) ? (bool) $attributes['dashboardShow'] : true,
361 'transactions_show' => isset( $attributes['transactionsShow'] ) ? (bool) $attributes['transactionsShow'] : true,
362 'subscriptions_show' => isset( $attributes['subscriptionsShow'] ) ? (bool) $attributes['subscriptionsShow'] : true,
363 'header_show' => isset( $attributes['headerShow'] ) ? (bool) $attributes['headerShow'] : true,
364
365 // Labels
366 'dashboard_label' => isset( $attributes['dashboardLabel'] ) && ! empty( $attributes['dashboardLabel'] ) ? sanitize_text_field( $attributes['dashboardLabel'] ) : 'Dashboard',
367 'transaction_label' => isset( $attributes['transactionLabel'] ) && ! empty( $attributes['transactionLabel'] ) ? sanitize_text_field( $attributes['transactionLabel'] ) : 'Transactions',
368 'subscription_label' => isset( $attributes['subscriptionLabel'] ) && ! empty( $attributes['subscriptionLabel'] ) ? sanitize_text_field( $attributes['subscriptionLabel'] ) : 'Subscriptions',
369 'refresh_stats_label' => isset( $attributes['refreshStatsLabel'] ) && ! empty( $attributes['refreshStatsLabel'] ) ? sanitize_text_field( $attributes['refreshStatsLabel'] ) : 'Refresh Stats',
370 'no_items_label' => isset( $attributes['noItemsLabel'] ) && ! empty( $attributes['noItemsLabel'] ) ? sanitize_text_field( $attributes['noItemsLabel'] ) : 'No records found!',
371
372 // Dashboard section visibility
373 'dashboard_transaction_summary_show' => isset( $attributes['dashboardTransactionSummaryShow'] ) ? (bool) $attributes['dashboardTransactionSummaryShow'] : true,
374 'dashboard_analytics_report_show' => isset( $attributes['dashboardAnalyticsReportShow'] ) ? (bool) $attributes['dashboardAnalyticsReportShow'] : true,
375 'dashboard_recent_transactions_show' => isset( $attributes['dashboardRecentTransactionsShow'] ) ? (bool) $attributes['dashboardRecentTransactionsShow'] : true,
376 'dashboard_recurring_subscription_show' => isset( $attributes['dashboardRecurringSubscriptionShow'] ) ? (bool) $attributes['dashboardRecurringSubscriptionShow'] : true,
377 'dashboard_split_subscription_show' => isset( $attributes['dashboardSplitSubscriptionShow'] ) ? (bool) $attributes['dashboardSplitSubscriptionShow'] : true,
378
379 // Dashboard labels
380 'dashboard_total_amount_label' => isset( $attributes['dashboardTotalAmountLabel'] ) && ! empty( $attributes['dashboardTotalAmountLabel'] ) ? sanitize_text_field( $attributes['dashboardTotalAmountLabel'] ) : 'Total Amount',
381 'dashboard_completed_amount_label' => isset( $attributes['dashboardCompletedAmountLabel'] ) && ! empty( $attributes['dashboardCompletedAmountLabel'] ) ? sanitize_text_field( $attributes['dashboardCompletedAmountLabel'] ) : 'Completed Amount',
382 'dashboard_incomplete_amount_label' => isset( $attributes['dashboardIncompleteAmountLabel'] ) && ! empty( $attributes['dashboardIncompleteAmountLabel'] ) ? sanitize_text_field( $attributes['dashboardIncompleteAmountLabel'] ) : 'Incomplete Amount',
383 'dashboard_refunded_amount_label' => isset( $attributes['dashboardRefundedAmountLabel'] ) && ! empty( $attributes['dashboardRefundedAmountLabel'] ) ? sanitize_text_field( $attributes['dashboardRefundedAmountLabel'] ) : 'Refunded Amount',
384 'dashboard_view_all_label' => isset( $attributes['dashboardViewAllLabel'] ) && ! empty( $attributes['dashboardViewAllLabel'] ) ? sanitize_text_field( $attributes['dashboardViewAllLabel'] ) : 'View All',
385 'dashboard_analytics_reports_label' => isset( $attributes['dashboardAnalyticsReportsLabel'] ) && ! empty( $attributes['dashboardAnalyticsReportsLabel'] ) ? sanitize_text_field( $attributes['dashboardAnalyticsReportsLabel'] ) : 'Analytics Reports',
386 'dashboard_recent_transactions_label' => isset( $attributes['dashboardRecentTransactionsLabel'] ) && ! empty( $attributes['dashboardRecentTransactionsLabel'] ) ? sanitize_text_field( $attributes['dashboardRecentTransactionsLabel'] ) : 'Recent Transactions',
387 'dashboard_recurring_subscriptions_label' => isset( $attributes['dashboardRecurringSubscriptionsLabel'] ) && ! empty( $attributes['dashboardRecurringSubscriptionsLabel'] ) ? sanitize_text_field( $attributes['dashboardRecurringSubscriptionsLabel'] ) : 'Recurring Subscriptions',
388 'dashboard_split_subscriptions_label' => isset( $attributes['dashboardSplitSubscriptionsLabel'] ) && ! empty( $attributes['dashboardSplitSubscriptionsLabel'] ) ? sanitize_text_field( $attributes['dashboardSplitSubscriptionsLabel'] ) : 'Split Subscriptions',
389
390 // Transaction table visibility
391 'transaction_table_name_show' => isset( $attributes['transactionTableNameShow'] ) ? (bool) $attributes['transactionTableNameShow'] : true,
392 'transaction_table_email_address_show' => isset( $attributes['transactionTableEmailAddressShow'] ) ? (bool) $attributes['transactionTableEmailAddressShow'] : true,
393 'transaction_table_amount_show' => isset( $attributes['transactionTableAmountShow'] ) ? (bool) $attributes['transactionTableAmountShow'] : true,
394 'transaction_table_payment_type_show' => isset( $attributes['transactionTablePaymentTypeShow'] ) ? (bool) $attributes['transactionTablePaymentTypeShow'] : true,
395 'transaction_table_transaction_id_show' => isset( $attributes['transactionTableTransactionIdShow'] ) ? (bool) $attributes['transactionTableTransactionIdShow'] : true,
396 'transaction_table_source_show' => isset( $attributes['transactionTableSourceShow'] ) ? (bool) $attributes['transactionTableSourceShow'] : true,
397 'transaction_table_status_show' => isset( $attributes['transactionTableStatusShow'] ) ? (bool) $attributes['transactionTableStatusShow'] : true,
398 'transaction_table_date_show' => isset( $attributes['transactionTableDateShow'] ) ? (bool) $attributes['transactionTableDateShow'] : true,
399 'transaction_table_action_show' => isset( $attributes['transactionTableActionShow'] ) ? (bool) $attributes['transactionTableActionShow'] : true,
400
401 // Transaction table labels
402 'transaction_table_name_label' => isset( $attributes['transactionTableNameLabel'] ) && ! empty( $attributes['transactionTableNameLabel'] ) ? sanitize_text_field( $attributes['transactionTableNameLabel'] ) : 'Name',
403 'transaction_table_email_address_label' => isset( $attributes['transactionTableEmailAddressLabel'] ) && ! empty( $attributes['transactionTableEmailAddressLabel'] ) ? sanitize_text_field( $attributes['transactionTableEmailAddressLabel'] ) : 'Email Address',
404 'transaction_table_amount_label' => isset( $attributes['transactionTableAmountLabel'] ) && ! empty( $attributes['transactionTableAmountLabel'] ) ? sanitize_text_field( $attributes['transactionTableAmountLabel'] ) : 'Amount',
405 'transaction_table_payment_type_label' => isset( $attributes['transactionTablePaymentTypeLabel'] ) && ! empty( $attributes['transactionTablePaymentTypeLabel'] ) ? sanitize_text_field( $attributes['transactionTablePaymentTypeLabel'] ) : 'Payment Type',
406 'transaction_table_transaction_id_label' => isset( $attributes['transactionTableTransactionIdLabel'] ) && ! empty( $attributes['transactionTableTransactionIdLabel'] ) ? sanitize_text_field( $attributes['transactionTableTransactionIdLabel'] ) : 'Transaction ID',
407 'transaction_table_source_label' => isset( $attributes['transactionTableSourceLabel'] ) && ! empty( $attributes['transactionTableSourceLabel'] ) ? sanitize_text_field( $attributes['transactionTableSourceLabel'] ) : 'Source',
408 'transaction_table_status_label' => isset( $attributes['transactionTableStatusLabel'] ) && ! empty( $attributes['transactionTableStatusLabel'] ) ? sanitize_text_field( $attributes['transactionTableStatusLabel'] ) : 'Status',
409 'transaction_table_date_label' => isset( $attributes['transactionTableDateLabel'] ) && ! empty( $attributes['transactionTableDateLabel'] ) ? sanitize_text_field( $attributes['transactionTableDateLabel'] ) : 'Date',
410 'transaction_table_action_label' => isset( $attributes['transactionTableActionLabel'] ) && ! empty( $attributes['transactionTableActionLabel'] ) ? sanitize_text_field( $attributes['transactionTableActionLabel'] ) : 'Action',
411
412 // Subscription table column visibility (pro feature — consumed by template-subscriptions-tab.php)
413 'subscription_table_subscription_id_show' => isset( $attributes['subscriptionTableSubscriptionIdShow'] ) ? (bool) $attributes['subscriptionTableSubscriptionIdShow'] : true,
414 'subscription_table_plan_id_show' => isset( $attributes['subscriptionTablePlanIdShow'] ) ? (bool) $attributes['subscriptionTablePlanIdShow'] : true,
415 'subscription_table_status_show' => isset( $attributes['subscriptionTableStatusShow'] ) ? (bool) $attributes['subscriptionTableStatusShow'] : true,
416 'subscription_table_amount_show' => isset( $attributes['subscriptionTableAmountShow'] ) ? (bool) $attributes['subscriptionTableAmountShow'] : true,
417 'subscription_table_created_date_show' => isset( $attributes['subscriptionTableCreatedDateShow'] ) ? (bool) $attributes['subscriptionTableCreatedDateShow'] : true,
418 'subscription_table_current_period_show' => isset( $attributes['subscriptionTableCurrentPeriodShow'] ) ? (bool) $attributes['subscriptionTableCurrentPeriodShow'] : true,
419 'subscription_table_action_show' => isset( $attributes['subscriptionTableActionShow'] ) ? (bool) $attributes['subscriptionTableActionShow'] : true,
420
421 // Subscription table column labels (pro feature)
422 'subscription_table_subscription_id_label' => ! empty( $attributes['subscriptionTableSubscriptionIdLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableSubscriptionIdLabel'] ) : 'Subscription ID',
423 'subscription_table_plan_id_label' => ! empty( $attributes['subscriptionTablePlanIdLabel'] ) ? sanitize_text_field( $attributes['subscriptionTablePlanIdLabel'] ) : 'Product Name',
424 'subscription_table_status_label' => ! empty( $attributes['subscriptionTableStatusLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableStatusLabel'] ) : 'Status',
425 'subscription_table_amount_label' => ! empty( $attributes['subscriptionTableAmountLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableAmountLabel'] ) : 'Amount',
426 'subscription_table_created_date_label' => ! empty( $attributes['subscriptionTableCreatedDateLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableCreatedDateLabel'] ) : 'Payment Date',
427 'subscription_table_current_period_label' => ! empty( $attributes['subscriptionTableCurrentPeriodLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableCurrentPeriodLabel'] ) : 'Renewal Date',
428 'subscription_table_action_label' => ! empty( $attributes['subscriptionTableActionLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableActionLabel'] ) : 'Action',
429 'subscription_table_status_active_label' => ! empty( $attributes['subscriptionTableStatusActiveLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableStatusActiveLabel'] ) : 'Active',
430 'subscription_table_status_inactive_label' => ! empty( $attributes['subscriptionTableStatusInactiveLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableStatusInactiveLabel'] ) : 'Inactive',
431 'subscription_table_action_cancel_label' => ! empty( $attributes['subscriptionTableActionCancelLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableActionCancelLabel'] ) : 'Cancel',
432 );
433
434 /**
435 * Block-specific filter for merging pro $bp_settings.
436 *
437 * Passes raw camelCase block $attributes as second arg (unlike the Elementor filter
438 * which passes raw Elementor settings with better_payment_user_dashboard_* keys).
439 *
440 * @since 3.x.x
441 *
442 * @param array $settings The built settings array.
443 * @param array $attributes The raw block attributes (camelCase).
444 */
445 return apply_filters( 'better_payment/block/user_dashboard/bp_settings', $settings, $attributes );
446 }
447
448 /**
449 * Create a widget proxy object for user dashboard layout templates.
450 *
451 * This provides the methods that layout files expect from $widgetObj and $this.
452 *
453 * @param string $block_id The unique block ID.
454 * @param array $settings The settings array.
455 * @return object Widget proxy object with required methods.
456 */
457 private function create_user_dashboard_widget_proxy( $block_id, $settings ) {
458 return new class( $block_id, $settings ) {
459 /**
460 * Block ID.
461 *
462 * @var string
463 */
464 private $id;
465
466 /**
467 * Settings array.
468 *
469 * @var array
470 */
471 private $settings;
472
473 /**
474 * Pro enabled flag.
475 *
476 * @var bool
477 */
478 public $pro_enabled;
479
480 /**
481 * Constructor.
482 *
483 * @param string $id Block ID.
484 * @param array $settings Settings array.
485 */
486 public function __construct( $id, $settings ) {
487 $this->id = $id;
488 $this->settings = $settings;
489 $this->pro_enabled = apply_filters( 'better_payment/pro_enabled', false );
490 }
491
492 /**
493 * Get the block ID.
494 *
495 * @return string Block ID.
496 */
497 public function get_id() {
498 return $this->id;
499 }
500
501 /**
502 * Get user transactions by email.
503 *
504 * @param string $email User email (uses current user if empty).
505 * @return array Transaction records.
506 */
507 public function get_user_transactions( $email = '', $page = 1, $per_page = 10 ) {
508 $current_user = wp_get_current_user();
509 if ( empty( $email ) ) {
510 $email = $current_user->user_email;
511 }
512 return \Better_Payment\Lite\Admin\DB::get_user_transactions_paginated( $email, [
513 'page' => $page,
514 'per_page' => $per_page,
515 'type' => 'transactions',
516 ] );
517 }
518
519 /**
520 * Get analytics data for transactions.
521 *
522 * @param array $transactions Transactions array.
523 * @return array Analytics data (counts and amounts by status).
524 */
525 public function get_transactions_analytics( $transactions = array() ) {
526 return \Better_Payment\Lite\Admin\DB::get_transactions_analytics_dashboard( $transactions );
527 }
528 };
529 }
530
531 /**
532 * Build the settings array for layout templates.
533 *
534 * Maps block attributes to the Elementor widget settings format expected by layout templates.
535 *
536 * @param array $attributes Block attributes.
537 * @param array $global_settings Global plugin settings from database.
538 * @param string $form_layout The selected form layout.
539 * @return array Settings array for layout templates.
540 */
541 public function build_block_settings( $attributes, $global_settings, $form_layout ) {
542 // Convert block formFields array to Elementor format.
543 $form_fields = $this->convert_form_fields( $attributes );
544
545 // Convert block amountList array to Elementor format.
546 $amount_list = $this->convert_amount_list( $attributes );
547
548 // Get block attribute values with fallbacks to global settings.
549 $paypal_enabled = isset( $attributes['paypalEnabled'] )
550 ? ( $attributes['paypalEnabled'] ? 'yes' : '' )
551 : ( ! empty( $global_settings['better_payment_settings_general_general_paypal'] ) && 'yes' === $global_settings['better_payment_settings_general_general_paypal'] ? 'yes' : '' );
552
553 // Get PayPal business email from block attribute, fall back to global settings.
554 $paypal_business_email = isset( $attributes['paypalBusinessEmail'] ) && ! empty( $attributes['paypalBusinessEmail'] )
555 ? sanitize_email( $attributes['paypalBusinessEmail'] )
556 : ( ! empty( $global_settings['better_payment_settings_payment_paypal_email'] ) ? $global_settings['better_payment_settings_payment_paypal_email'] : '' );
557
558 $stripe_enabled = isset( $attributes['stripeEnabled'] )
559 ? ( $attributes['stripeEnabled'] ? 'yes' : '' )
560 : ( ! empty( $global_settings['better_payment_settings_general_general_stripe'] ) && 'yes' === $global_settings['better_payment_settings_general_general_stripe'] ? 'yes' : '' );
561
562 $paystack_enabled = isset( $attributes['paystackEnabled'] )
563 ? ( $attributes['paystackEnabled'] ? 'yes' : '' )
564 : ( ! empty( $global_settings['better_payment_settings_general_general_paystack'] ) && 'yes' === $global_settings['better_payment_settings_general_general_paystack'] ? 'yes' : '' );
565
566 // Get currency from block attribute or global settings.
567 $currency = isset( $attributes['currency'] ) && ! empty( $attributes['currency'] )
568 ? sanitize_text_field( $attributes['currency'] )
569 : ( ! empty( $global_settings['better_payment_settings_general_general_currency'] ) ? $global_settings['better_payment_settings_general_general_currency'] : 'USD' );
570
571 // Get sidebar show setting from block attribute.
572 $sidebar_show = isset( $attributes['showSidebar'] )
573 ? ( $attributes['showSidebar'] ? 'yes' : '' )
574 : 'yes';
575
576 // Get show amount list from block attribute.
577 $show_amount_list = isset( $attributes['showAmountList'] )
578 ? ( $attributes['showAmountList'] ? 'yes' : '' )
579 : '';
580
581 // Get currency alignment from block attribute.
582 $currency_alignment = isset( $attributes['currencyAlign'] ) && ! empty( $attributes['currencyAlign'] )
583 ? sanitize_text_field( $attributes['currencyAlign'] )
584 : 'left';
585
586 // Get payment source from block attribute.
587 $payment_source = isset( $attributes['paymentSource'] ) && ! empty( $attributes['paymentSource'] )
588 ? sanitize_text_field( $attributes['paymentSource'] )
589 : '';
590
591 // Get transaction details from block attributes.
592 $transaction_title = isset( $attributes['transactionTitle'] ) && ! empty( $attributes['transactionTitle'] )
593 ? sanitize_text_field( $attributes['transactionTitle'] )
594 : __( 'Transaction Details', 'better-payment' );
595
596 $transaction_sub_title = isset( $attributes['transactionSubTitle'] ) && ! empty( $attributes['transactionSubTitle'] )
597 ? sanitize_text_field( $attributes['transactionSubTitle'] )
598 : __( 'Total payment of your product in the following:', 'better-payment' );
599
600 $amount_text = isset( $attributes['amountText'] ) && ! empty( $attributes['amountText'] )
601 ? sanitize_text_field( $attributes['amountText'] )
602 : __( 'Amount:', 'better-payment' );
603
604 $product_title = isset( $attributes['transactionDetailsProductTitle'] ) && ! empty( $attributes['transactionDetailsProductTitle'] )
605 ? sanitize_text_field( $attributes['transactionDetailsProductTitle'] )
606 : __( 'Title:', 'better-payment' );
607
608 // Get button text from block attributes.
609 $paypal_button_text = isset( $attributes['paypalButtonText'] ) && ! empty( $attributes['paypalButtonText'] )
610 ? sanitize_text_field( $attributes['paypalButtonText'] )
611 : '';
612
613 $stripe_button_text = isset( $attributes['stripeButtonText'] ) && ! empty( $attributes['stripeButtonText'] )
614 ? sanitize_text_field( $attributes['stripeButtonText'] )
615 : '';
616
617 $paystack_button_text = isset( $attributes['paystackButtonText'] ) && ! empty( $attributes['paystackButtonText'] )
618 ? sanitize_text_field( $attributes['paystackButtonText'] )
619 : '';
620
621 // Get product IDs from block attributes.
622 $woocommerce_product_id = isset( $attributes['woocommerceProductId'] ) && ! empty( $attributes['woocommerceProductId'] )
623 ? intval( $attributes['woocommerceProductId'] )
624 : 0;
625
626 $fluentcart_product_id = isset( $attributes['fluentcartProductId'] ) && ! empty( $attributes['fluentcartProductId'] )
627 ? intval( $attributes['fluentcartProductId'] )
628 : 0;
629
630 // Get Stripe Price ID from block attributes.
631 // Block attribute: stripeDefaultPriceId (string)
632 // Used when payment source is 'stripe' (Stripe Product).
633 $stripe_price_id = isset( $attributes['stripeDefaultPriceId'] ) && ! empty( $attributes['stripeDefaultPriceId'] )
634 ? sanitize_text_field( $attributes['stripeDefaultPriceId'] )
635 : '';
636
637 // Validate Stripe Price ID format (should start with 'price_').
638 if ( ! empty( $stripe_price_id ) && strpos( $stripe_price_id, 'price_' ) !== 0 ) {
639 // Invalid format - log warning in debug mode and reset to empty.
640 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
641 error_log( 'Better Payment: Invalid Stripe Price ID format (should start with "price_"): ' . esc_html( $stripe_price_id ) );
642 }
643 $stripe_price_id = '';
644 }
645
646 // Get email notification setting from block attributes.
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']
653 ? 'yes'
654 : '';
655
656 // Get site domain for default email addresses.
657 $site_url_parsed = wp_parse_url( get_site_url() );
658 $site_domain = ! empty( $site_url_parsed['host'] ) ? esc_html( $site_url_parsed['host'] ) : 'example.com';
659 $default_from_email = 'wordpress@' . $site_domain;
660 $default_email_subject = sprintf( __( 'Better Payment transaction on %s', 'better-payment' ), esc_html( get_option( 'blogname' ) ) );
661
662 // Admin email settings from block attributes with fallback to global settings.
663 // Block attribute: adminEmail (string)
664 $email_to = isset( $attributes['adminEmail'] ) && ! empty( $attributes['adminEmail'] )
665 ? $this->sanitize_email_list( $attributes['adminEmail'] )
666 : ( ! empty( $global_settings['better_payment_settings_general_email_to'] ) ? sanitize_email( $global_settings['better_payment_settings_general_email_to'] ) : sanitize_email( get_option( 'admin_email' ) ) );
667
668 // Block attribute: adminSubject (string)
669 $email_subject = isset( $attributes['adminSubject'] ) && ! empty( $attributes['adminSubject'] )
670 ? sanitize_text_field( $attributes['adminSubject'] )
671 : ( ! empty( $global_settings['better_payment_settings_general_email_subject'] ) ? sanitize_text_field( $global_settings['better_payment_settings_general_email_subject'] ) : $default_email_subject );
672
673 // Block attribute: adminMessage (string)
674 $email_content = isset( $attributes['adminMessage'] ) && ! empty( $attributes['adminMessage'] )
675 ? wp_kses_post( $attributes['adminMessage'] )
676 : ( ! empty( $global_settings['better_payment_settings_general_email_message_admin'] ) ? wp_kses_post( $global_settings['better_payment_settings_general_email_message_admin'] ) : '' );
677
678 // Block attribute: adminFromEmail (string)
679 $email_from = isset( $attributes['adminFromEmail'] ) && ! empty( $attributes['adminFromEmail'] )
680 ? sanitize_email( $attributes['adminFromEmail'] )
681 : ( ! empty( $global_settings['better_payment_settings_general_email_from_email'] ) ? sanitize_email( $global_settings['better_payment_settings_general_email_from_email'] ) : $default_from_email );
682
683 // Block attribute: adminFromName (string)
684 $email_from_name = isset( $attributes['adminFromName'] ) && ! empty( $attributes['adminFromName'] )
685 ? sanitize_text_field( $attributes['adminFromName'] )
686 : ( ! empty( $global_settings['better_payment_settings_general_email_from_name'] ) ? sanitize_text_field( $global_settings['better_payment_settings_general_email_from_name'] ) : esc_html( get_bloginfo( 'name' ) ) );
687
688 // Block attribute: adminReplyTo (string)
689 $email_reply_to = isset( $attributes['adminReplyTo'] ) && ! empty( $attributes['adminReplyTo'] )
690 ? sanitize_email( $attributes['adminReplyTo'] )
691 : ( ! empty( $global_settings['better_payment_settings_general_email_reply_to'] ) ? sanitize_email( $global_settings['better_payment_settings_general_email_reply_to'] ) : $default_from_email );
692
693 // Block attribute: adminCc (string)
694 $email_cc = isset( $attributes['adminCc'] ) && ! empty( $attributes['adminCc'] )
695 ? $this->sanitize_email_list( $attributes['adminCc'] )
696 : ( ! empty( $global_settings['better_payment_settings_general_email_cc'] ) ? $this->sanitize_email_list( $global_settings['better_payment_settings_general_email_cc'] ) : '' );
697
698 // Block attribute: adminBcc (string)
699 $email_bcc = isset( $attributes['adminBcc'] ) && ! empty( $attributes['adminBcc'] )
700 ? $this->sanitize_email_list( $attributes['adminBcc'] )
701 : ( ! empty( $global_settings['better_payment_settings_general_email_bcc'] ) ? $this->sanitize_email_list( $global_settings['better_payment_settings_general_email_bcc'] ) : '' );
702
703 // Block attribute: adminSendAs (string: 'html' or 'plain')
704 $allowed_content_types = array( 'html', 'plain' );
705 $email_content_type = isset( $attributes['adminSendAs'] ) && in_array( $attributes['adminSendAs'], $allowed_content_types, true )
706 ? $attributes['adminSendAs']
707 : ( ! empty( $global_settings['better_payment_settings_general_email_send_as'] ) && in_array( $global_settings['better_payment_settings_general_email_send_as'], $allowed_content_types, true ) ? $global_settings['better_payment_settings_general_email_send_as'] : 'html' );
708
709 // Admin email display toggles (boolean attributes converted to 'yes'/'' for Elementor compatibility).
710 // Block attribute: adminShowHeaderText (boolean, default: true)
711 $email_content_heading = isset( $attributes['adminShowHeaderText'] ) ? ( $attributes['adminShowHeaderText'] ? 'yes' : '' ) : 'yes';
712 // Block attribute: adminShowFromSection (boolean, default: true)
713 $email_content_from_section = isset( $attributes['adminShowFromSection'] ) ? ( $attributes['adminShowFromSection'] ? 'yes' : '' ) : 'yes';
714 // Block attribute: adminShowToSection (boolean, default: true)
715 $email_content_to_section = isset( $attributes['adminShowToSection'] ) ? ( $attributes['adminShowToSection'] ? 'yes' : '' ) : 'yes';
716 // Block attribute: adminShowTransactionSummary (boolean, default: true)
717 $email_content_transaction_summary = isset( $attributes['adminShowTransactionSummary'] ) ? ( $attributes['adminShowTransactionSummary'] ? 'yes' : '' ) : 'yes';
718 // Block attribute: adminShowFooterText (boolean, default: true)
719 $email_content_footer_text = isset( $attributes['adminShowFooterText'] ) ? ( $attributes['adminShowFooterText'] ? 'yes' : '' ) : 'yes';
720
721 // Customer email settings from block attributes with fallback to global settings.
722 // Block attribute: customerSubject (string)
723 $email_subject_customer = isset( $attributes['customerSubject'] ) && ! empty( $attributes['customerSubject'] )
724 ? sanitize_text_field( $attributes['customerSubject'] )
725 : ( ! empty( $global_settings['better_payment_settings_general_email_subject_customer'] ) ? sanitize_text_field( $global_settings['better_payment_settings_general_email_subject_customer'] ) : $default_email_subject );
726
727 // Block attribute: customerMessage (string)
728 $email_content_customer = isset( $attributes['customerMessage'] ) && ! empty( $attributes['customerMessage'] )
729 ? wp_kses_post( $attributes['customerMessage'] )
730 : ( ! empty( $global_settings['better_payment_settings_general_email_message_customer'] ) ? wp_kses_post( $global_settings['better_payment_settings_general_email_message_customer'] ) : '' );
731
732 // Block attribute: customerFromEmail (string)
733 $email_from_customer = isset( $attributes['customerFromEmail'] ) && ! empty( $attributes['customerFromEmail'] )
734 ? sanitize_email( $attributes['customerFromEmail'] )
735 : ( ! empty( $global_settings['better_payment_settings_general_email_from_email_customer'] ) ? sanitize_email( $global_settings['better_payment_settings_general_email_from_email_customer'] ) : $default_from_email );
736
737 // Block attribute: customerFromName (string)
738 $email_from_name_customer = isset( $attributes['customerFromName'] ) && ! empty( $attributes['customerFromName'] )
739 ? sanitize_text_field( $attributes['customerFromName'] )
740 : ( ! empty( $global_settings['better_payment_settings_general_email_from_name_customer'] ) ? sanitize_text_field( $global_settings['better_payment_settings_general_email_from_name_customer'] ) : esc_html( get_bloginfo( 'name' ) ) );
741
742 // Block attribute: customerReplyTo (string)
743 $email_reply_to_customer = isset( $attributes['customerReplyTo'] ) && ! empty( $attributes['customerReplyTo'] )
744 ? sanitize_email( $attributes['customerReplyTo'] )
745 : ( ! empty( $global_settings['better_payment_settings_general_email_reply_to_customer'] ) ? sanitize_email( $global_settings['better_payment_settings_general_email_reply_to_customer'] ) : $default_from_email );
746
747 // Block attribute: customerCc (string)
748 $email_cc_customer = isset( $attributes['customerCc'] ) && ! empty( $attributes['customerCc'] )
749 ? $this->sanitize_email_list( $attributes['customerCc'] )
750 : ( ! empty( $global_settings['better_payment_settings_general_email_cc_customer'] ) ? $this->sanitize_email_list( $global_settings['better_payment_settings_general_email_cc_customer'] ) : '' );
751
752 // Block attribute: customerBcc (string)
753 $email_bcc_customer = isset( $attributes['customerBcc'] ) && ! empty( $attributes['customerBcc'] )
754 ? $this->sanitize_email_list( $attributes['customerBcc'] )
755 : ( ! empty( $global_settings['better_payment_settings_general_email_bcc_customer'] ) ? $this->sanitize_email_list( $global_settings['better_payment_settings_general_email_bcc_customer'] ) : '' );
756
757 // Block attribute: customerSendAs (string: 'html' or 'plain')
758 $email_content_type_customer = isset( $attributes['customerSendAs'] ) && in_array( $attributes['customerSendAs'], $allowed_content_types, true )
759 ? $attributes['customerSendAs']
760 : ( ! empty( $global_settings['better_payment_settings_general_email_send_as_customer'] ) && in_array( $global_settings['better_payment_settings_general_email_send_as_customer'], $allowed_content_types, true ) ? $global_settings['better_payment_settings_general_email_send_as_customer'] : 'html' );
761
762 // Customer email attachment settings.
763 // Block attribute: customerPDFAttachment (boolean, default: false)
764 $email_attachment_pdf_show = isset( $attributes['customerPDFAttachment'] ) && $attributes['customerPDFAttachment'] ? 'yes' : '';
765 // Block attribute: customerFileAttachment (string - URL)
766 $email_attachment = isset( $attributes['customerFileAttachment'] ) && ! empty( $attributes['customerFileAttachment'] )
767 ? array( 'url' => esc_url( $attributes['customerFileAttachment'] ) )
768 : array();
769
770 // Get success message settings from block attributes.
771 $success_icon = isset( $attributes['successIcon'] ) && ! empty( $attributes['successIcon'] )
772 ? sanitize_text_field( $attributes['successIcon'] )
773 : '';
774
775 $success_heading = isset( $attributes['successHeading'] ) && ! empty( $attributes['successHeading'] )
776 ? sanitize_text_field( $attributes['successHeading'] )
777 : '';
778
779 $success_sub_heading = isset( $attributes['successSubHeading'] ) && ! empty( $attributes['successSubHeading'] )
780 ? sanitize_text_field( $attributes['successSubHeading'] )
781 : '';
782
783 $transaction_id_text = isset( $attributes['transactionID'] ) && ! empty( $attributes['transactionID'] )
784 ? sanitize_text_field( $attributes['transactionID'] )
785 : __( 'Transaction ID:', 'better-payment' );
786
787 $thanks_message = isset( $attributes['thanksMessage'] ) && ! empty( $attributes['thanksMessage'] )
788 ? sanitize_text_field( $attributes['thanksMessage'] )
789 : __( 'Thank you for your payment.', 'better-payment' );
790
791 $amount_message = isset( $attributes['amountMessage'] ) && ! empty( $attributes['amountMessage'] )
792 ? sanitize_text_field( $attributes['amountMessage'] )
793 : __( 'Amount', 'better-payment' );
794
795 $currency_message = isset( $attributes['currencyMessage'] ) && ! empty( $attributes['currencyMessage'] )
796 ? sanitize_text_field( $attributes['currencyMessage'] )
797 : __( 'Currency', 'better-payment' );
798
799 $payment_method_message = isset( $attributes['paymentMethodMessage'] ) && ! empty( $attributes['paymentMethodMessage'] )
800 ? sanitize_text_field( $attributes['paymentMethodMessage'] )
801 : __( 'Payment Method', 'better-payment' );
802
803 $payment_type_message = isset( $attributes['paymentType'] ) && ! empty( $attributes['paymentType'] )
804 ? sanitize_text_field( $attributes['paymentType'] )
805 : __( 'Payment Type', 'better-payment' );
806
807 $merchant_details_text = isset( $attributes['merchatDetails'] ) && ! empty( $attributes['merchatDetails'] )
808 ? sanitize_text_field( $attributes['merchatDetails'] )
809 : __( 'Merchant Details', 'better-payment' );
810
811 $paid_amount_text = isset( $attributes['paidAmount'] ) && ! empty( $attributes['paidAmount'] )
812 ? sanitize_text_field( $attributes['paidAmount'] )
813 : __( 'Paid Amount', 'better-payment' );
814
815 $purchase_details_text = isset( $attributes['purchaseDetails'] ) && ! empty( $attributes['purchaseDetails'] )
816 ? sanitize_text_field( $attributes['purchaseDetails'] )
817 : __( 'Purchase Details', 'better-payment' );
818
819 $print_btn_text = isset( $attributes['printButtonText'] ) && ! empty( $attributes['printButtonText'] )
820 ? sanitize_text_field( $attributes['printButtonText'] )
821 : __( 'Print', 'better-payment' );
822
823 $view_details_btn_text = isset( $attributes['viewDetailsButtonText'] ) && ! empty( $attributes['viewDetailsButtonText'] )
824 ? sanitize_text_field( $attributes['viewDetailsButtonText'] )
825 : __( 'View Details', 'better-payment' );
826
827 $user_dashboard_url = isset( $attributes['userDashboardUrl'] ) && ! empty( $attributes['userDashboardUrl'] )
828 ? esc_url( $attributes['userDashboardUrl'] )
829 : '';
830
831 $custom_redirect_url = isset( $attributes['customRedirectUrl'] ) && ! empty( $attributes['customRedirectUrl'] )
832 ? esc_url( $attributes['customRedirectUrl'] )
833 : '';
834
835 // Get error message settings from block attributes.
836 $error_icon = isset( $attributes['errorIcon'] ) && ! empty( $attributes['errorIcon'] )
837 ? sanitize_text_field( $attributes['errorIcon'] )
838 : '';
839
840 // Email icon (block control) maps to Elementor email logo (image URL) when uploaded.
841 $email_icon = isset( $attributes['emailIcon'] ) && ! empty( $attributes['emailIcon'] )
842 ? sanitize_text_field( $attributes['emailIcon'] )
843 : 'bp-icon bp-envelope';
844
845 $error_heading = isset( $attributes['errorHeading'] ) && ! empty( $attributes['errorHeading'] )
846 ? sanitize_text_field( $attributes['errorHeading'] )
847 : __( 'Payment Failed', 'better-payment' );
848
849 $error_sub_heading = isset( $attributes['errorSubHeading'] ) && ! empty( $attributes['errorSubHeading'] )
850 ? sanitize_text_field( $attributes['errorSubHeading'] )
851 : __( 'Your payment has failed. Please check your payment details', 'better-payment' );
852
853 $error_transaction_id_text = isset( $attributes['transactionIDText'] ) && ! empty( $attributes['transactionIDText'] )
854 ? sanitize_text_field( $attributes['transactionIDText'] )
855 : __( 'Transaction ID', 'better-payment' );
856
857 $error_show_details_button = isset( $attributes['showDetailsButton'] ) && $attributes['showDetailsButton'] ? 'yes' : '';
858
859 $error_details_button_text = isset( $attributes['detailsButtonText'] ) && ! empty( $attributes['detailsButtonText'] )
860 ? sanitize_text_field( $attributes['detailsButtonText'] )
861 : __( 'View Details', 'better-payment' );
862
863 $error_details_button_url = isset( $attributes['detailsButtonUrl'] ) && ! empty( $attributes['detailsButtonUrl'] )
864 ? esc_url( $attributes['detailsButtonUrl'] )
865 : '';
866
867 $error_redirect_url = isset( $attributes['errorRedirectUrl'] ) && ! empty( $attributes['errorRedirectUrl'] )
868 ? esc_url( $attributes['errorRedirectUrl'] )
869 : '';
870
871 return array(
872 // Layout settings.
873 'better_payment_form_layout' => $form_layout,
874
875 // Form title - used as item/product name in PayPal and Stripe checkout.
876 // Block attribute is 'formName' (not 'formTitle') — default is 'Better Payment'.
877 'better_payment_form_title' => isset( $attributes['formName'] ) && ! empty( $attributes['formName'] )
878 ? sanitize_text_field( $attributes['formName'] )
879 : '',
880
881 // Payment gateway settings - prioritize block attributes over global settings.
882 'better_payment_form_paypal_enable' => $paypal_enabled,
883 'better_payment_form_stripe_enable' => $stripe_enabled,
884 'better_payment_form_paystack_enable' => $paystack_enabled,
885
886 // Stripe API keys - resolve based on live mode (matches Elementor behavior).
887 // Handler::stripe_payment_success() uses better_payment_stripe_secret_key directly.
888 'better_payment_stripe_live_mode' => ! empty( $global_settings['better_payment_settings_payment_stripe_live_mode'] ) && 'yes' === $global_settings['better_payment_settings_payment_stripe_live_mode'] ? 'yes' : '',
889 'better_payment_stripe_public_key' => $this->resolve_stripe_key( $global_settings, 'public' ),
890 'better_payment_stripe_secret_key' => $this->resolve_stripe_key( $global_settings, 'secret' ),
891 'better_payment_stripe_public_key_live' => ! empty( $global_settings['better_payment_settings_payment_stripe_live_public'] ) ? $global_settings['better_payment_settings_payment_stripe_live_public'] : '',
892 'better_payment_stripe_secret_key_live' => ! empty( $global_settings['better_payment_settings_payment_stripe_live_secret'] ) ? $global_settings['better_payment_settings_payment_stripe_live_secret'] : '',
893
894 // PayPal API keys - prioritize block attributes over global settings.
895 'better_payment_paypal_live_mode' => ! empty( $global_settings['better_payment_settings_payment_paypal_live_mode'] ) && 'yes' === $global_settings['better_payment_settings_payment_paypal_live_mode'] ? 'yes' : '',
896 'better_payment_paypal_business_email' => $paypal_business_email,
897 'better_payment_paypal_button_type' => '_xclick', // Default PayPal button type for Buy Now functionality.
898
899 // Paystack API keys - resolve based on live mode (matches Elementor behavior).
900 'better_payment_paystack_live_mode' => ! empty( $global_settings['better_payment_settings_payment_paystack_live_mode'] ) && 'yes' === $global_settings['better_payment_settings_payment_paystack_live_mode'] ? 'yes' : '',
901 'better_payment_paystack_public_key' => $this->resolve_paystack_key( $global_settings, 'public' ),
902 'better_payment_paystack_secret_key' => $this->resolve_paystack_key( $global_settings, 'secret' ),
903 'better_payment_paystack_public_key_live' => ! empty( $global_settings['better_payment_settings_payment_paystack_live_public'] ) ? $global_settings['better_payment_settings_payment_paystack_live_public'] : '',
904 'better_payment_paystack_secret_key_live' => ! empty( $global_settings['better_payment_settings_payment_paystack_live_secret'] ) ? $global_settings['better_payment_settings_payment_paystack_live_secret'] : '',
905
906 // Currency settings from block attributes with fallback to global settings.
907 'better_payment_form_currency' => $currency,
908 'better_payment_form_currency_alignment' => $currency_alignment,
909
910 // Payment source settings from block attributes.
911 'better_payment_form_payment_source' => $payment_source,
912 'better_payment_form_payment_type' => '',
913 'better_payment_form_woocommerce_product_id' => $woocommerce_product_id,
914 'better_payment_form_fluentcart_product_id' => $fluentcart_product_id,
915 'better_payment_form_payment_source_stripe_price_id' => $stripe_price_id,
916
917 // Sidebar settings from block attributes.
918 'better_payment_form_sidebar_show' => $sidebar_show,
919 'better_payment_form_transaction_details_heading' => $transaction_title,
920 'better_payment_form_transaction_details_sub_heading' => $transaction_sub_title,
921 'better_payment_form_transaction_details_product_title' => $product_title,
922 'better_payment_form_transaction_details_amount_text' => $amount_text,
923
924 // Amount list settings from block attributes.
925 'better_payment_show_amount_list' => $show_amount_list,
926 'better_payment_amount' => $amount_list,
927
928 // Form fields from block attributes (converted to Elementor format).
929 'better_payment_form_fields' => $form_fields,
930
931 // Button text settings from block attributes.
932 'better_payment_form_form_buttons_paypal_button_text' => $paypal_button_text,
933 'better_payment_form_form_buttons_stripe_button_text' => $stripe_button_text,
934 'better_payment_form_form_buttons_paystack_button_text' => $paystack_button_text,
935
936 // Placeholder settings.
937 'better_payment_placeholder_switch' => 'yes',
938
939 // Email notification settings.
940 'better_payment_form_email_enable' => $email_notification_enabled,
941
942 // Admin email settings.
943 'better_payment_email_to' => $email_to,
944 'better_payment_email_subject' => $email_subject,
945 'better_payment_email_content' => $email_content,
946 'better_payment_email_from' => $email_from,
947 'better_payment_email_from_name' => $email_from_name,
948 'better_payment_email_reply_to' => $email_reply_to,
949 'better_payment_email_cc' => $email_cc,
950 'better_payment_email_bcc' => $email_bcc,
951 'better_payment_email_content_type' => $email_content_type,
952 'better_payment_email_content_heading' => $email_content_heading,
953 'better_payment_email_content_from_section' => $email_content_from_section,
954 'better_payment_email_content_to_section' => $email_content_to_section,
955 'better_payment_email_content_transaction_summary' => $email_content_transaction_summary,
956 'better_payment_email_content_footer_text' => $email_content_footer_text,
957
958 // Customer email settings.
959 'better_payment_email_subject_customer' => $email_subject_customer,
960 'better_payment_email_content_customer' => $email_content_customer,
961 'better_payment_email_from_customer' => $email_from_customer,
962 'better_payment_email_from_name_customer' => $email_from_name_customer,
963 'better_payment_email_reply_to_customer' => $email_reply_to_customer,
964 'better_payment_email_cc_customer' => $email_cc_customer,
965 'better_payment_email_bcc_customer' => $email_bcc_customer,
966 'better_payment_email_content_type_customer' => $email_content_type_customer,
967 'better_payment_form_email_attachment_pdf_show' => $email_attachment_pdf_show,
968 'better_payment_form_email_attachment' => $email_attachment,
969 'better_payment_form_email_logo' => $this->build_email_logo_setting( $email_icon, 'bp-icon bp-envelope' ),
970
971 // Success message settings from block attributes.
972 'better_payment_form_success_message_icon' => $this->build_icon_control_setting( $success_icon ),
973 'better_payment_form_success_message_heading' => $success_heading,
974 'better_payment_form_success_message_sub_heading' => $success_sub_heading,
975 'better_payment_form_success_message_transaction' => $transaction_id_text,
976 'better_payment_form_success_message_thanks' => $thanks_message,
977 'better_payment_form_success_message_amount_text' => $amount_message,
978 'better_payment_form_success_message_currency_text' => $currency_message,
979 'better_payment_form_success_message_pay_method_text' => $payment_method_message,
980 'better_payment_form_success_message_pay_type_text' => $payment_type_message,
981 'better_payment_form_success_message_merchant_details_text' => $merchant_details_text,
982 'better_payment_form_success_message_paid_amount_text' => $paid_amount_text,
983 'better_payment_form_success_message_purchase_details_text' => $purchase_details_text,
984 'better_payment_form_success_message_print_btn_text' => $print_btn_text,
985 'better_payment_form_success_message_view_details_btn_text' => $view_details_btn_text,
986 'better_payment_form_success_page_view_details_url' => array(
987 'url' => $user_dashboard_url,
988 ),
989 'better_payment_form_success_page_url' => array(
990 'url' => $custom_redirect_url,
991 ),
992
993 // Error message settings from block attributes.
994 'better_payment_form_error_message_icon' => $this->build_icon_control_setting( $error_icon ),
995 'better_payment_form_error_message_heading' => $error_heading,
996 'better_payment_form_error_message_sub_heading' => $error_sub_heading,
997 'better_payment_form_error_message_transaction_id_text' => $error_transaction_id_text,
998 'better_payment_form_error_details_button_switch' => $error_show_details_button,
999 'better_payment_form_error_details_button_text' => $error_details_button_text,
1000 'better_payment_form_error_details_page_url' => array(
1001 'url' => $error_details_button_url,
1002 ),
1003 'better_payment_form_error_page_url' => array(
1004 'url' => $error_redirect_url,
1005 ),
1006 // Flag to identify this form is from Gutenberg block (not Elementor)
1007 'better_payment_form_source' => 'block',
1008 );
1009 }
1010
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 /**
1169 * Resolve the correct Stripe key based on live mode setting.
1170 *
1171 * This matches Elementor's behavior where the key is resolved before being stored
1172 * in better_payment_stripe_public_key/better_payment_stripe_secret_key.
1173 * Handler::stripe_payment_success() uses better_payment_stripe_secret_key directly.
1174 *
1175 * @param array $global_settings Global settings array.
1176 * @param string $key_type Either 'public' or 'secret'.
1177 * @return string The resolved key.
1178 */
1179 private function resolve_stripe_key( $global_settings, $key_type ) {
1180 $is_live_mode = ! empty( $global_settings['better_payment_settings_payment_stripe_live_mode'] )
1181 && 'yes' === $global_settings['better_payment_settings_payment_stripe_live_mode'];
1182
1183 if ( 'public' === $key_type ) {
1184 return $is_live_mode
1185 ? ( ! empty( $global_settings['better_payment_settings_payment_stripe_live_public'] ) ? $global_settings['better_payment_settings_payment_stripe_live_public'] : '' )
1186 : ( ! empty( $global_settings['better_payment_settings_payment_stripe_test_public'] ) ? $global_settings['better_payment_settings_payment_stripe_test_public'] : '' );
1187 } else {
1188 return $is_live_mode
1189 ? ( ! empty( $global_settings['better_payment_settings_payment_stripe_live_secret'] ) ? $global_settings['better_payment_settings_payment_stripe_live_secret'] : '' )
1190 : ( ! empty( $global_settings['better_payment_settings_payment_stripe_test_secret'] ) ? $global_settings['better_payment_settings_payment_stripe_test_secret'] : '' );
1191 }
1192 }
1193
1194 /**
1195 * Resolve the correct Paystack key based on live mode setting.
1196 *
1197 * This matches Elementor's behavior where the key is resolved before being stored
1198 * in better_payment_paystack_public_key/better_payment_paystack_secret_key.
1199 *
1200 * @param array $global_settings Global settings array.
1201 * @param string $key_type Either 'public' or 'secret'.
1202 * @return string The resolved key.
1203 */
1204 private function resolve_paystack_key( $global_settings, $key_type ) {
1205 $is_live_mode = ! empty( $global_settings['better_payment_settings_payment_paystack_live_mode'] )
1206 && 'yes' === $global_settings['better_payment_settings_payment_paystack_live_mode'];
1207
1208 if ( 'public' === $key_type ) {
1209 return $is_live_mode
1210 ? ( ! empty( $global_settings['better_payment_settings_payment_paystack_live_public'] ) ? $global_settings['better_payment_settings_payment_paystack_live_public'] : '' )
1211 : ( ! empty( $global_settings['better_payment_settings_payment_paystack_test_public'] ) ? $global_settings['better_payment_settings_payment_paystack_test_public'] : '' );
1212 } else {
1213 return $is_live_mode
1214 ? ( ! empty( $global_settings['better_payment_settings_payment_paystack_live_secret'] ) ? $global_settings['better_payment_settings_payment_paystack_live_secret'] : '' )
1215 : ( ! empty( $global_settings['better_payment_settings_payment_paystack_test_secret'] ) ? $global_settings['better_payment_settings_payment_paystack_test_secret'] : '' );
1216 }
1217 }
1218
1219 /**
1220 * Sanitize a comma-separated list of email addresses.
1221 *
1222 * This method validates and sanitizes email addresses to prevent
1223 * email header injection attacks and ensure only valid emails are used.
1224 *
1225 * @param string $email_list Comma-separated list of email addresses.
1226 * @return string Sanitized comma-separated list of valid email addresses.
1227 */
1228 private function sanitize_email_list( $email_list ) {
1229 if ( empty( $email_list ) ) {
1230 return '';
1231 }
1232
1233 // Split by comma and sanitize each email.
1234 $emails = array_map( 'trim', explode( ',', $email_list ) );
1235 $valid_emails = array();
1236
1237 foreach ( $emails as $email ) {
1238 $sanitized = sanitize_email( $email );
1239 // Only include valid email addresses.
1240 if ( ! empty( $sanitized ) && is_email( $sanitized ) ) {
1241 $valid_emails[] = $sanitized;
1242 }
1243 }
1244
1245 return implode( ', ', $valid_emails );
1246 }
1247
1248 /**
1249 * Convert block formFields array to Elementor widget format.
1250 *
1251 * @param array $attributes Block attributes.
1252 * @return array Converted form fields in Elementor format.
1253 */
1254 private function convert_form_fields( $attributes ) {
1255 $form_fields = array();
1256
1257 if ( empty( $attributes['formFields'] ) || ! is_array( $attributes['formFields'] ) ) {
1258 // Return default form fields if none provided.
1259 return array(
1260 array(
1261 '_id' => 'first_name_field',
1262 'better_payment_primary_field_type' => 'primary_first_name',
1263 'better_payment_field_name_heading' => __( 'First Name', 'better-payment' ),
1264 'better_payment_field_name_placeholder' => __( 'First Name', 'better-payment' ),
1265 'better_payment_field_type' => 'text',
1266 'better_payment_field_icon' => array( 'value' => 'bp-icon bp-user', 'library' => '' ),
1267 'better_payment_field_name_required' => '',
1268 'better_payment_field_name_show' => 'yes',
1269 ),
1270 array(
1271 '_id' => 'last_name_field',
1272 'better_payment_primary_field_type' => 'primary_last_name',
1273 'better_payment_field_name_heading' => __( 'Last Name', 'better-payment' ),
1274 'better_payment_field_name_placeholder' => __( 'Last Name', 'better-payment' ),
1275 'better_payment_field_type' => 'text',
1276 'better_payment_field_icon' => array( 'value' => 'bp-icon bp-user', 'library' => '' ),
1277 'better_payment_field_name_required' => '',
1278 'better_payment_field_name_show' => 'yes',
1279 ),
1280 array(
1281 '_id' => 'email_field',
1282 'better_payment_primary_field_type' => 'primary_email',
1283 'better_payment_field_name_heading' => __( 'Email', 'better-payment' ),
1284 'better_payment_field_name_placeholder' => __( 'Email Address', 'better-payment' ),
1285 'better_payment_field_type' => 'email',
1286 'better_payment_field_icon' => array( 'value' => 'bp-icon bp-envelope', 'library' => '' ),
1287 'better_payment_field_name_required' => 'yes',
1288 'better_payment_field_name_show' => 'yes',
1289 ),
1290 array(
1291 '_id' => 'amount_field',
1292 'better_payment_primary_field_type' => 'primary_payment_amount',
1293 'better_payment_field_name_heading' => __( 'Amount', 'better-payment' ),
1294 'better_payment_field_name_placeholder' => __( 'Payment Amount', 'better-payment' ),
1295 'better_payment_field_type' => 'number',
1296 'better_payment_field_icon' => array( 'value' => 'bp-icon bp-logo-2', 'library' => '' ),
1297 'better_payment_field_name_required' => 'yes',
1298 'better_payment_field_name_show' => 'yes',
1299 'better_payment_field_name_min' => 1,
1300 'better_payment_field_name_max' => '',
1301 'better_payment_field_name_default' => '',
1302 'better_payment_field_name_default_fixed' => '',
1303 'better_payment_field_name_default_dynamic_enable' => '',
1304 ),
1305 );
1306 }
1307
1308 foreach ( $attributes['formFields'] as $index => $field ) {
1309 $raw_icon_value = isset( $field['icon'] ) ? sanitize_text_field( $field['icon'] ) : 'bp-icon bp-user';
1310 $icon_setting = $this->build_icon_control_setting( $raw_icon_value, 'bp-icon bp-user' );
1311
1312 $converted_field = array(
1313 '_id' => 'field_' . $index,
1314 'better_payment_primary_field_type' => isset( $field['primaryFieldType'] ) ? sanitize_text_field( $field['primaryFieldType'] ) : '',
1315 'better_payment_field_name_heading' => isset( $field['label'] ) ? sanitize_text_field( $field['label'] ) : '',
1316 'better_payment_field_name_placeholder' => isset( $field['placeholder'] ) ? sanitize_text_field( $field['placeholder'] ) : '',
1317 'better_payment_field_type' => isset( $field['type'] ) ? sanitize_text_field( $field['type'] ) : 'text',
1318 'better_payment_field_icon' => array(
1319 'value' => $icon_setting['value'],
1320 'library' => $icon_setting['library'],
1321 ),
1322 'better_payment_field_name_required' => isset( $field['required'] ) && $field['required'] ? 'yes' : '',
1323 'better_payment_field_name_show' => isset( $field['show'] ) && $field['show'] ? 'yes' : '',
1324 'better_payment_field_name_display_inline' => isset( $field['displayInline'] ) && $field['displayInline'] ? 'inline-block' : '',
1325 );
1326
1327 // Add amount field specific settings.
1328 if ( isset( $field['primaryFieldType'] ) && 'primary_payment_amount' === $field['primaryFieldType'] ) {
1329 $converted_field['better_payment_field_name_min'] = isset( $field['min'] ) ? intval( $field['min'] ) : 1;
1330 $converted_field['better_payment_field_name_max'] = isset( $field['max'] ) ? intval( $field['max'] ) : '';
1331 $converted_field['better_payment_field_name_default'] = isset( $field['default'] ) ? intval( $field['default'] ) : '';
1332 $converted_field['better_payment_field_name_default_fixed'] = isset( $field['defaultFixed'] ) && $field['defaultFixed'] ? 'yes' : '';
1333 $converted_field['better_payment_field_name_default_dynamic_enable'] = isset( $field['defaultDynamicEnable'] ) && $field['defaultDynamicEnable'] ? 'yes' : '';
1334 }
1335
1336 $form_fields[] = $converted_field;
1337 }
1338
1339 return $form_fields;
1340 }
1341
1342 /**
1343 * Convert block amountList array to Elementor widget format.
1344 *
1345 * @param array $attributes Block attributes.
1346 * @return array Converted amount list in Elementor format.
1347 */
1348 private function convert_amount_list( $attributes ) {
1349 $amount_list = array();
1350
1351 if ( empty( $attributes['amountList'] ) || ! is_array( $attributes['amountList'] ) ) {
1352 return array();
1353 }
1354
1355 foreach ( $attributes['amountList'] as $index => $item ) {
1356 if ( isset( $item['label'] ) && '' !== $item['label'] ) {
1357 $amount_list[] = array(
1358 '_id' => 'amount_' . $index,
1359 'better_payment_amount_val' => sanitize_text_field( $item['label'] ),
1360 );
1361 }
1362 }
1363
1364 return $amount_list;
1365 }
1366
1367 /**
1368 * Normalize icon classes for rendering compatibility.
1369 *
1370 * Dashicons require the base `dashicons` class plus the icon class.
1371 *
1372 * @param string $icon Icon class string.
1373 * @return string
1374 */
1375 private function normalize_icon_class( $icon ) {
1376 $icon = trim( (string) $icon );
1377 if ( '' === $icon ) {
1378 return $icon;
1379 }
1380
1381 if ( false !== strpos( $icon, 'dashicons-' ) && false === strpos( $icon, 'dashicons ' ) ) {
1382 $icon = 'dashicons ' . $icon;
1383 }
1384
1385 return $icon;
1386 }
1387
1388 /**
1389 * Convert block icon value to Elementor ICONS control format.
1390 *
1391 * Keeps default state empty-library so existing default notice icons are shown.
1392 *
1393 * @param string $icon_value Icon class or uploaded icon URL.
1394 * @param string $default_value Default icon class for this control.
1395 * @return array
1396 */
1397 private function build_icon_control_setting( $icon_value, $default_value = '' ) {
1398 $icon_value = trim( (string) $icon_value );
1399
1400 // Only return empty library when icon_value is truly empty (no selection)
1401 // Don't treat icons matching the default as empty - they should render as custom icons
1402 if ( '' === $icon_value ) {
1403 return array(
1404 'value' => '',
1405 'library' => '',
1406 );
1407 }
1408
1409 // Check if the value is an image URL (SVG, PNG, JPG, etc.)
1410 if ( $this->is_icon_url( $icon_value ) ) {
1411 return array(
1412 'value' => array(
1413 'url' => esc_url_raw( $icon_value ),
1414 ),
1415 'library' => 'svg',
1416 );
1417 }
1418
1419 // For all other non-empty values (custom CSS classes, FontAwesome, etc.)
1420 // Detect the library type and return it so Handler can render correctly
1421 return array(
1422 'value' => $this->normalize_icon_class( $icon_value ),
1423 'library' => $this->detect_icon_library( $icon_value ),
1424 );
1425 }
1426
1427 /**
1428 * Build email logo setting from block email icon value.
1429 *
1430 * Email template expects a media-like structure with a `url` key.
1431 *
1432 * @param string $icon_value Icon class or uploaded icon URL.
1433 * @param string $default_value Default icon class for this control.
1434 * @return array
1435 */
1436 private function build_email_logo_setting( $icon_value, $default_value = '' ) {
1437 $icon_value = trim( (string) $icon_value );
1438
1439 if ( '' === $icon_value || ( '' !== $default_value && $icon_value === $default_value ) ) {
1440 return array();
1441 }
1442
1443 if ( $this->is_icon_url( $icon_value ) ) {
1444 return array(
1445 'url' => esc_url_raw( $icon_value ),
1446 );
1447 }
1448
1449 return array();
1450 }
1451
1452 /**
1453 * Detect Elementor-style icon library key from icon class string.
1454 *
1455 * @param string $icon_value Icon class.
1456 * @return string
1457 */
1458 private function detect_icon_library( $icon_value ) {
1459 $icon_value = (string) $icon_value;
1460
1461 // Check for FontAwesome icons (has 'fa-' or 'fa ' prefix)
1462 if ( false !== strpos( $icon_value, 'fa-' ) || false !== strpos( $icon_value, 'fa ' ) ) {
1463 // Check for specific FontAwesome variants (fas, far, fab, fal, fad)
1464 if ( preg_match( '/\bfab\b/', $icon_value ) ) {
1465 return 'fab'; // FontAwesome Brands
1466 }
1467 if ( preg_match( '/\bfar\b/', $icon_value ) ) {
1468 return 'far'; // FontAwesome Regular
1469 }
1470 if ( preg_match( '/\bfal\b/', $icon_value ) ) {
1471 return 'fal'; // FontAwesome Light
1472 }
1473 if ( preg_match( '/\bfad\b/', $icon_value ) ) {
1474 return 'fad'; // FontAwesome Duotone
1475 }
1476
1477 // Default to FontAwesome Solid
1478 return 'fas';
1479 }
1480
1481 // Check for Dashicons
1482 if ( false !== strpos( $icon_value, 'dashicon' ) ) {
1483 return 'dashicons';
1484 }
1485
1486 // Any other CSS class (custom icons like 'bp-icon bp-check-circle')
1487 return 'custom';
1488 }
1489
1490 /**
1491 * Check whether an icon value is a URL-like image path.
1492 *
1493 * @param string $icon_value Icon value.
1494 * @return bool
1495 */
1496 private function is_icon_url( $icon_value ) {
1497 $icon_value = trim( (string) $icon_value );
1498
1499 if ( '' === $icon_value ) {
1500 return false;
1501 }
1502
1503 return 0 === strpos( $icon_value, 'http://' )
1504 || 0 === strpos( $icon_value, 'https://' )
1505 || 0 === strpos( $icon_value, '/' )
1506 || 0 === strpos( $icon_value, 'data:image/' );
1507 }
1508
1509 /**
1510 * Create a widget proxy object for layout templates.
1511 *
1512 * This provides the methods that layout files expect from $widgetObj and $this.
1513 *
1514 * @param string $block_id The unique block ID.
1515 * @param array $settings The settings array.
1516 * @return object Widget proxy object with required methods.
1517 */
1518 private function create_widget_proxy( $block_id, $settings ) {
1519 return new class( $block_id, $settings ) {
1520 /**
1521 * Block ID.
1522 *
1523 * @var string
1524 */
1525 private $id;
1526
1527 /**
1528 * Settings array.
1529 *
1530 * @var array
1531 */
1532 private $settings;
1533
1534 /**
1535 * Constructor.
1536 *
1537 * @param string $id Block ID.
1538 * @param array $settings Settings array.
1539 */
1540 public function __construct( $id, $settings ) {
1541 $this->id = $id;
1542 $this->settings = $settings;
1543 }
1544
1545 /**
1546 * Get the block ID.
1547 *
1548 * @return string Block ID.
1549 */
1550 public function get_id() {
1551 return $this->id;
1552 }
1553
1554 /**
1555 * Get default payment amount text.
1556 *
1557 * @param array $settings Settings array.
1558 * @return string Default amount text.
1559 */
1560 public function render_attribute_default_text( $settings ) {
1561 $render_attribute_default_text = '';
1562
1563 $items = ! empty( $settings['better_payment_form_fields'] ) ? $settings['better_payment_form_fields'] : array();
1564
1565 foreach ( $items as $item ) {
1566 // Check for primary_payment_amount field type.
1567 if ( ! empty( $item['better_payment_primary_field_type'] ) && 'primary_payment_amount' === $item['better_payment_primary_field_type'] ) {
1568 $render_attribute_default_text = ! empty( $item['better_payment_field_name_default'] )
1569 ? $item['better_payment_field_name_default']
1570 : '';
1571 break;
1572 }
1573 }
1574
1575 return $render_attribute_default_text;
1576 }
1577
1578 /**
1579 * Render amount selection element.
1580 *
1581 * @param array $settings Settings array.
1582 * @param array $args Additional arguments.
1583 * @return void
1584 */
1585 public function render_amount_element( $settings, $args = array() ) {
1586 if ( empty( $settings['better_payment_amount'] ) ) {
1587 return;
1588 }
1589
1590 $items = $settings['better_payment_amount'];
1591
1592 // Get currency symbol and alignment for display.
1593 $bp_helper_obj = new \Better_Payment\Lite\Classes\Helper();
1594 $currency = ! empty( $settings['better_payment_form_currency'] ) ? $settings['better_payment_form_currency'] : 'USD';
1595 $currency_symbol = $bp_helper_obj->get_currency_symbol( esc_html( $currency ) );
1596 $currency_alignment = ! empty( $settings['better_payment_form_currency_alignment'] ) ? $settings['better_payment_form_currency_alignment'] : 'left';
1597 $currency_left = 'left' === $currency_alignment ? $currency_symbol : '';
1598 $currency_right = 'right' === $currency_alignment ? $currency_symbol : '';
1599
1600 foreach ( $items as $item ) :
1601 if ( ! empty( $item['better_payment_amount_val'] ) ) :
1602 $uid = uniqid();
1603 $value = floatval( $item['better_payment_amount_val'] );
1604 ?>
1605 <div class="bp-form__group pt-5">
1606 <input type="radio" value="<?php echo esc_attr( $value ); ?>"
1607 id="bp_payment_amount-<?php echo esc_attr( $uid ); ?>"
1608 class="bp-form__control bp-form_pay-radio"
1609 name="primary_payment_amount_radio">
1610 <label for="bp_payment_amount-<?php echo esc_attr( $uid ); ?>"><?php printf( '%s%s%s', esc_html( $currency_left ), esc_html( $value ), esc_html( $currency_right ) ); ?></label>
1611 </div>
1612 <?php
1613 endif;
1614 endforeach;
1615 }
1616
1617 /**
1618 * Render campaign ID hidden field.
1619 *
1620 * @param array $settings Settings array.
1621 * @return string Empty string for blocks.
1622 */
1623 public function render_campaign_id_hidden_field( $settings ) {
1624 $campaign_id = ! empty( $_GET['campaign_id'] ) ? sanitize_text_field( $_GET['campaign_id'] ) : '';
1625 $campaign_currency = ! empty( $_GET['campaign_currency'] ) ? sanitize_text_field( $_GET['campaign_currency'] ) : '';
1626
1627 return '
1628 <input type="hidden" name="campaign_id" value="' . esc_attr( $campaign_id ) . '">
1629 <input type="hidden" name="campaign_currency" value="' . esc_attr( $campaign_currency ) . '">
1630 ';
1631 }
1632 };
1633 }
1634
1635 /**
1636 * Enqueue editor assets (controls library).
1637 *
1638 * @return void
1639 */
1640 public function enqueue_editor_assets() {
1641 // Only load on block editor pages
1642 $screen = get_current_screen();
1643 if ( ! $screen || ! $screen->is_block_editor() ) {
1644 return;
1645 }
1646
1647 global $pagenow;
1648 $editor_type = false;
1649 if ( $pagenow === 'post-new.php' || $pagenow === 'post.php' ) {
1650 $editor_type = 'edit-post';
1651 } elseif ( $pagenow === 'site-editor.php' || ( $pagenow === 'themes.php' && isset( $_GET[ 'page' ] ) && sanitize_text_field( wp_unslash( $_GET[ 'page' ] ) ) === 'gutenberg-edit-site' ) ) {
1652 $editor_type = 'edit-site';
1653 } elseif ( $pagenow === 'widgets.php' ) {
1654 $editor_type = 'edit-widgets';
1655 }
1656
1657 // Define EssentialBlocksLocalize before controls load (controls access it at module level)
1658 $localize_data = array(
1659 'ajaxurl' => admin_url( 'admin-ajax.php' ),
1660 'ajax_url' => admin_url( 'admin-ajax.php' ),
1661 'nonce' => wp_create_nonce( 'better_payment_block_nonce' ),
1662 'admin_nonce' => wp_create_nonce( 'better_payment_admin_nonce' ),
1663 'rest_rootURL' => esc_url_raw( rest_url() ),
1664 'is_pro_active' => 'false',
1665 'eb_plugins_url' => BETTER_PAYMENT_URL . '/',
1666 'image_url' => BETTER_PAYMENT_ASSETS . '/img',
1667 'fontAwesome' => 'true',
1668 'googleFont' => 'true',
1669 'quickToolbar' => 'false',
1670 'enableGenerateImage' => '0',
1671 'enableWriteAIInputField' => '0',
1672 'enableWriteAIRichtext' => '0',
1673 'enableRewriteAIContent' => '0',
1674 'hasOpenAiApiKey' => '0',
1675 'all_blocks_default' => array(),
1676 'all_blocks' => array(),
1677 'quick_toolbar_blocks' => array(),
1678 'responsiveBreakpoints' => array(
1679 'tablet' => 1024,
1680 'mobile' => 767,
1681 ),
1682 );
1683
1684 // Add inline script to wp-editor which is guaranteed to load in block editor
1685 wp_add_inline_script(
1686 'wp-editor',
1687 'window.EssentialBlocksLocalize = window.EssentialBlocksLocalize || ' . wp_json_encode( $localize_data ) . ';'
1688 );
1689
1690 // Enqueue necessary CSS files for form styles in editor (used in render.php)
1691 wp_enqueue_style( 'better-payment-el' );
1692 wp_enqueue_style( 'bp-icon-front' );
1693 wp_enqueue_style( 'better-payment-style' );
1694 wp_enqueue_style( 'better-payment-common-style' );
1695 wp_enqueue_style( 'better-payment-admin-style' );
1696 wp_enqueue_style( 'bp-dashboard-pagination-style' );
1697 $this->enqueue_font_awesome();
1698 wp_enqueue_style( 'dashicons' );
1699
1700 // Build block data: start with common keys, then merge each block's own data.
1701 // To add data for a new block, implement get_{block-key}_editor_data() and add
1702 // 'editor_data' => 'get_{block-key}_editor_data' to the $blocks registry above.
1703 $block_data = $this->get_common_editor_data();
1704 foreach ( $this->blocks as $block_config ) {
1705 if ( ! empty( $block_config['editor_data'] ) && method_exists( $this, $block_config['editor_data'] ) ) {
1706 $block_data = array_merge( $block_data, $this->{$block_config['editor_data']}() );
1707 }
1708 }
1709
1710 // Canonical filter for the full merged data; the legacy alias preserves backwards
1711 // compatibility with any pro-plugin hooks on the old per-block filter name.
1712 $block_data = apply_filters( 'better_payment/block/editor_block_data', $block_data );
1713 $block_data = apply_filters( 'better_payment/block/user_dashboard/editor_block_data', $block_data );
1714
1715 wp_add_inline_script(
1716 'wp-editor',
1717 'window.betterPaymentBlockData = ' . wp_json_encode( $block_data ) . ';'
1718 );
1719
1720 wp_add_inline_script(
1721 'wp-editor',
1722 'window.eb_conditional_localize = ' . wp_json_encode(
1723 $editor_type !== false ? [ 'editor_type' => $editor_type ] : []
1724 ) . ';'
1725 );
1726 }
1727
1728 /**
1729 * Enqueue frontend assets for blocks
1730 * Called on wp_enqueue_scripts hook to allow pro plugin to enqueue scripts early
1731 *
1732 * @return void
1733 */
1734 public function enqueue_frontend_assets() {
1735 // Allow pro plugin to enqueue scripts for the user dashboard block
1736 do_action( 'better_payment/block/user_dashboard/wp_enqueue_scripts' );
1737 }
1738
1739 /**
1740 * Return the site domain (host only, no protocol) for use in editor data.
1741 *
1742 * @return string
1743 */
1744 private function get_site_domain() {
1745 $parsed = wp_parse_url( get_site_url() );
1746 return ! empty( $parsed['host'] ) ? esc_html( $parsed['host'] ) : 'example.com';
1747 }
1748
1749 /**
1750 * Return plugin settings with all gateway secret/private keys removed.
1751 *
1752 * Safe to expose to any block editor user regardless of role. Only the keys
1753 * that the editor UI actually reads (email defaults, gateway enable flags,
1754 * public keys) are kept — secrets never leave the server.
1755 *
1756 * @return array
1757 */
1758 private function get_safe_settings_for_editor() {
1759 $settings = get_option( 'better_payment_settings', array() );
1760 if ( ! is_array( $settings ) ) {
1761 return array();
1762 }
1763
1764 $sensitive_keys = array(
1765 'better_payment_settings_payment_paypal_live_secret',
1766 'better_payment_settings_payment_paypal_test_secret',
1767 'better_payment_settings_payment_stripe_live_secret',
1768 'better_payment_settings_payment_stripe_test_secret',
1769 'better_payment_settings_payment_paystack_live_secret',
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',
1776 );
1777
1778 foreach ( $sensitive_keys as $key ) {
1779 unset( $settings[ $key ] );
1780 }
1781
1782 return $settings;
1783 }
1784
1785 /**
1786 * Return data shared by every registered block in the editor.
1787 *
1788 * All blocks can read these keys from window.betterPaymentBlockData.
1789 * Adding a new block? Common data lives here; block-specific data goes in
1790 * get_{block-key}_editor_data() and is registered via $blocks['editor_data'].
1791 *
1792 * @return array
1793 */
1794 private function get_common_editor_data() {
1795 return array(
1796 'ajaxurl' => admin_url( 'admin-ajax.php' ),
1797 'nonce' => wp_create_nonce( 'better_payment_block_nonce' ),
1798 'currencies' => $this->get_currency_list(),
1799 'assetUrl' => BETTER_PAYMENT_ASSETS,
1800 'assetsUrl' => BETTER_PAYMENT_ASSETS,
1801 'siteName' => get_bloginfo( 'name' ),
1802 'siteDomain' => $this->get_site_domain(),
1803 'proEnabled' => apply_filters( 'better_payment/pro_enabled', false ),
1804 );
1805 }
1806
1807 /**
1808 * Return editor data specific to the payment-form block.
1809 *
1810 * Provides the global settings (secrets stripped) so the inspector can
1811 * pre-populate email defaults, and the site admin email for the To field.
1812 *
1813 * @return array
1814 */
1815 private function get_payment_form_editor_data() {
1816 return array(
1817 'betterPaymentSettings' => $this->get_safe_settings_for_editor(),
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' ),
1827 );
1828 }
1829
1830 /**
1831 * Return editor data specific to the user-dashboard block.
1832 *
1833 * Fetches the current editor user's transactions, subscriptions, and analytics
1834 * so the block preview shows real data immediately on insertion without a REST
1835 * round-trip. Normalises each row to the same flat shape the UserAPI returns so
1836 * the JS preview component only needs to handle one structure.
1837 *
1838 * @return array
1839 */
1840 private function get_user_dashboard_editor_data() {
1841 $current_user = wp_get_current_user();
1842
1843 // Cache the 5 DB queries for the duration of the browser session in the editor.
1844 // The cache is keyed per user so each editor sees only their own data.
1845 // TTL: 5 minutes — short enough to reflect recent transactions without hammering
1846 // the DB on every page navigation inside the block editor.
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' : '' );
1851 $cache_group = 'better_payment';
1852 $cached = wp_cache_get( $cache_key, $cache_group );
1853 if ( false !== $cached ) {
1854 // Nonces are request-scoped and must never be cached; regenerate on every hit.
1855 $cached['restNonce'] = wp_create_nonce( 'wp_rest' );
1856 return $cached;
1857 }
1858
1859 $user_transactions = array();
1860 $user_subscriptions = array();
1861 $user_analytics = array();
1862 $user_transactions_meta = array( 'total' => 0, 'pages' => 1, 'perPage' => 20 );
1863 // Safe default ensures userSubscriptionsMeta is always fully defined even when
1864 // the user has no subscriptions or is not logged in.
1865 $sub_result = array( 'total' => 0, 'pages' => 1, 'per_page' => 20, 'transactions' => array() );
1866
1867 // Always fetch in the editor — has_block() only checks saved content and returns
1868 // false before first save, leaving the block with empty data on insertion.
1869 if ( $current_user && $current_user->ID ) {
1870 $user_analytics = \Better_Payment\Lite\Admin\DB::get_user_analytics_by_email( $current_user->user_email );
1871
1872 $paged_result = \Better_Payment\Lite\Admin\DB::get_user_transactions_paginated(
1873 $current_user->user_email,
1874 array(
1875 'page' => 1,
1876 'per_page' => 20,
1877 'type' => 'transactions',
1878 )
1879 );
1880
1881 $raw_transactions = $paged_result['transactions'];
1882 $user_transactions_meta = array(
1883 'total' => $paged_result['total'],
1884 'pages' => $paged_result['pages'],
1885 'perPage' => $paged_result['per_page'],
1886 );
1887
1888 // Unserialize and normalise to match UserAPI REST response format.
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
1894 if ( ! empty( $tx->form_fields_info ) ) {
1895 $tx->form_fields_info = maybe_unserialize( $tx->form_fields_info );
1896 }
1897 if ( ! empty( $tx->customer_info ) ) {
1898 $tx->customer_info = maybe_unserialize( $tx->customer_info );
1899 }
1900
1901 $ffi = is_array( $tx->form_fields_info ) ? $tx->form_fields_info : array();
1902 $customer_name = isset( $ffi['primary_first_name'] )
1903 ? trim( sanitize_text_field( $ffi['primary_first_name'] ) . ' ' . sanitize_text_field( $ffi['primary_last_name'] ?? '' ) )
1904 : '';
1905 if ( empty( $customer_name ) ) {
1906 $customer_name = isset( $ffi['first_name'] )
1907 ? trim( sanitize_text_field( $ffi['first_name'] ) . ' ' . sanitize_text_field( $ffi['last_name'] ?? '' ) )
1908 : '';
1909 }
1910 $tx->customer_name = $customer_name;
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;
1916 }
1917
1918 $user_transactions = apply_filters( 'better_payment/block/user_dashboard/enrich_user_transactions', $raw_transactions );
1919
1920 // Separate query for subscriptions (not a subset of transactions page 1).
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 }
1936
1937 foreach ( $raw_subscriptions as $tx ) {
1938 if ( ! empty( $tx->form_fields_info ) ) {
1939 $tx->form_fields_info = maybe_unserialize( $tx->form_fields_info );
1940 }
1941 }
1942 $user_subscriptions = apply_filters( 'better_payment/block/user_dashboard/enrich_user_subscriptions', $raw_subscriptions );
1943
1944 // Flatten each subscription to match UserAPI flat format so JS handles one structure.
1945 foreach ( $user_subscriptions as $tx ) {
1946 $ffi = is_array( $tx->form_fields_info ) ? $tx->form_fields_info : array();
1947
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 );
1964
1965 $customer_name = isset( $ffi['primary_first_name'] )
1966 ? trim( sanitize_text_field( $ffi['primary_first_name'] ) . ' ' . sanitize_text_field( $ffi['primary_last_name'] ?? '' ) )
1967 : '';
1968 if ( empty( $customer_name ) ) {
1969 $customer_name = isset( $ffi['first_name'] )
1970 ? trim( sanitize_text_field( $ffi['first_name'] ) . ' ' . sanitize_text_field( $ffi['last_name'] ?? '' ) )
1971 : '';
1972 }
1973 $tx->customer_name = $customer_name;
1974 $tx->customer_email = sanitize_text_field( $ffi['primary_email'] ?? ( $ffi['email'] ?? '' ) );
1975
1976 unset( $tx->form_fields_info );
1977 }
1978 }
1979
1980 $data = array(
1981 'restNonce' => wp_create_nonce( 'wp_rest' ),
1982 'restUrl' => get_rest_url( null, 'better-payment/v1/user-transactions' ),
1983 'currentUser' => array(
1984 'id' => $current_user->ID,
1985 'user_email' => $current_user->user_email,
1986 'user_login' => $current_user->user_login,
1987 'user_avatar_url'=> get_avatar_url( $current_user->user_email, array( 'size' => 32 ) ),
1988 'email' => $current_user->user_email,
1989 'login' => $current_user->user_login,
1990 'avatar' => get_avatar_url( $current_user->user_email, array( 'size' => 32 ) ),
1991 ),
1992 'userTransactions' => $user_transactions,
1993 'userTransactionsMeta' => $user_transactions_meta,
1994 'userSubscriptions' => $user_subscriptions,
1995 'userSubscriptionsMeta' => array(
1996 'total' => (int) $sub_result['total'],
1997 'pages' => (int) $sub_result['pages'],
1998 'perPage' => 20,
1999 ),
2000 'userAnalytics' => $user_analytics,
2001 'proAssets' => array(
2002 'analyticsReportsBanner' => BETTER_PAYMENT_ASSETS . '/img/user-dashboard-analytics-reports-pro-banner.png',
2003 'recurringSubscriptionBanner' => BETTER_PAYMENT_ASSETS . '/img/user-dashboard-recurring-subscription-pro-banner.png',
2004 'splitSubscriptionBanner' => BETTER_PAYMENT_ASSETS . '/img/user-dashboard-split-subscription-pro-banner.png',
2005 'subscriptionProBanner' => BETTER_PAYMENT_ASSETS . '/img/user-dashboard-subscription-pro-banner.png',
2006 ),
2007 );
2008
2009 // Store in the object cache for 5 minutes. nonce is excluded from the cached payload
2010 // because nonces are request-scoped — storing them would return stale nonces on cache hit.
2011 // The restNonce above is regenerated fresh on every request (not cached).
2012 $cacheable = $data;
2013 unset( $cacheable['restNonce'] );
2014 wp_cache_set( $cache_key, $cacheable, $cache_group, 5 * MINUTE_IN_SECONDS );
2015
2016 return $data;
2017 }
2018
2019 /**
2020 * Get the current editor type.
2021 *
2022 * @return string
2023 */
2024 private function get_editor_type() {
2025 if ( function_exists( 'get_current_screen' ) ) {
2026 $screen = get_current_screen();
2027 if ( $screen && method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() ) {
2028 return 'edit-post';
2029 }
2030 }
2031 return 'edit-post';
2032 }
2033
2034 /**
2035 * Register the Better Payment block category.
2036 *
2037 * @param array $categories Block categories.
2038 * @param WP_Block_Editor_Context $context Block editor context.
2039 * @return array Modified block categories.
2040 */
2041 public function register_block_category( $categories, $context ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
2042 // Check if our category already exists
2043 foreach ( $categories as $category ) {
2044 if ( 'better-payment' === $category['slug'] ) {
2045 return $categories;
2046 }
2047 }
2048
2049 // Add our category at the beginning
2050 array_unshift(
2051 $categories,
2052 array(
2053 'slug' => 'better-payment',
2054 'title' => __( 'Better Payment', 'better-payment' ),
2055 'icon' => '',
2056 )
2057 );
2058
2059 return $categories;
2060 }
2061
2062 /**
2063 * Register all blocks.
2064 *
2065 * @return void
2066 */
2067 public function register_blocks() {
2068 if ( ! function_exists( 'register_block_type' ) ) {
2069 return;
2070 }
2071
2072 foreach ( $this->blocks as $block_key => $block_config ) {
2073 if ( ! $this->is_block_registered( $block_config['name'] ) ) {
2074 $this->register_single_block( $block_key, $block_config );
2075 }
2076 }
2077 }
2078
2079 /**
2080 * Register a single block.
2081 *
2082 * @param string $block_key Block key.
2083 * @param array $block_config Block configuration.
2084 * @return void
2085 */
2086 private function register_single_block( $block_key, $block_config ) {
2087 // Use assets/blocks path for block.json since src/ is excluded from production builds.
2088 // The block.json is copied to assets/blocks/ during the build process.
2089 $block_json_path = BETTER_PAYMENT_PATH . '/assets/blocks/blocks/' . $block_config['path'] . '/block.json';
2090
2091 // Fallback to src/blocks for development environment (assets may not be built yet)
2092 if ( ! file_exists( $block_json_path ) ) {
2093 $block_json_path = BETTER_PAYMENT_PATH . '/src/blocks/' . $block_config['path'] . '/block.json';
2094 }
2095
2096 // Check if block.json exists
2097 if ( ! file_exists( $block_json_path ) ) {
2098 return;
2099 }
2100
2101 // Get asset file for dependencies
2102 $asset_file = $this->get_asset_file( $block_config['path'] );
2103
2104 // Add controls as a dependency
2105 $dependencies = array_merge( $asset_file['dependencies'], array( 'better-payment-controls' ) );
2106
2107 // Register editor script
2108 $editor_script_handle = 'better-payment-' . $block_key . '-editor';
2109
2110 if ( file_exists( BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_config['path'] . '/index.min.js' ) ) {
2111 wp_register_script(
2112 $editor_script_handle,
2113 BETTER_PAYMENT_ASSETS . '/blocks/' . $block_config['path'] . '/index.min.js',
2114 $dependencies,
2115 $asset_file['version'],
2116 true
2117 );
2118 } elseif ( file_exists( BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_config['path'] . '/index.js' ) ) {
2119 wp_register_script(
2120 $editor_script_handle,
2121 BETTER_PAYMENT_ASSETS . '/blocks/' . $block_config['path'] . '/index.js',
2122 $dependencies,
2123 $asset_file['version'],
2124 true
2125 );
2126 }
2127
2128 // Register frontend/editor style
2129 $style_handle = 'better-payment-' . $block_key . '-style';
2130 $style_dependencies = array(
2131 'better-payment-controls-style',
2132 'better-payment-el',
2133 'bp-icon-front',
2134 'better-payment-style',
2135 'better-payment-common-style',
2136 'better-payment-admin-style',
2137 'dashicons',
2138 );
2139 if ( ! $this->is_font_awesome_provided() ) {
2140 $style_dependencies[] = 'bp-font-awesome';
2141 }
2142 if ( file_exists( BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_config['path'] . '/style.min.css' ) ) {
2143 wp_register_style(
2144 $style_handle,
2145 BETTER_PAYMENT_ASSETS . '/blocks/' . $block_config['path'] . '/style.min.css',
2146 $style_dependencies,
2147 $asset_file['version']
2148 );
2149 } elseif ( file_exists( BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_config['path'] . '/style.css' ) ) {
2150 wp_register_style(
2151 $style_handle,
2152 BETTER_PAYMENT_ASSETS . '/blocks/' . $block_config['path'] . '/style.css',
2153 $style_dependencies,
2154 $asset_file['version']
2155 );
2156 }
2157
2158 // Build block registration args
2159 $block_args = array(
2160 'editor_script' => $editor_script_handle,
2161 'style' => $style_handle,
2162 );
2163
2164 // Add render callback if defined in block config
2165 if ( ! empty( $block_config['render_callback'] ) && method_exists( $this, $block_config['render_callback'] ) ) {
2166 $block_args['render_callback'] = array( $this, $block_config['render_callback'] );
2167 }
2168
2169 // Register block with WordPress
2170 $result = register_block_type( $block_json_path, $block_args );
2171
2172 if ( false === $result ) {
2173 // Log error in debug mode
2174 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
2175 error_log( 'Better Payment: Failed to register block ' . $block_config['name'] );
2176 }
2177 }
2178 }
2179
2180 /**
2181 * Get asset file with dependencies and version.
2182 *
2183 * @param string $block_path Block path.
2184 * @return array Asset file data.
2185 */
2186 private function get_asset_file( $block_path ) {
2187 $asset_file_path = BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_path . '/index.min.asset.php';
2188
2189 if ( ! file_exists( $asset_file_path ) ) {
2190 $asset_file_path = BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_path . '/index.asset.php';
2191 }
2192
2193 if ( file_exists( $asset_file_path ) ) {
2194 return include $asset_file_path;
2195 }
2196
2197 return array(
2198 'dependencies' => array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n' ),
2199 'version' => BETTER_PAYMENT_VERSION,
2200 );
2201 }
2202
2203
2204 /**
2205 * Get the blocks directory path.
2206 *
2207 * @return string
2208 */
2209 public function get_blocks_path() {
2210 return BETTER_PAYMENT_PATH . '/src/blocks/';
2211 }
2212
2213 /**
2214 * Get the blocks assets URL.
2215 *
2216 * @return string
2217 */
2218 public function get_blocks_url() {
2219 return BETTER_PAYMENT_ASSETS . '/blocks/';
2220 }
2221
2222 /**
2223 * Check if a block is registered.
2224 *
2225 * @param string $block_name Full block name (e.g., 'better-payment/payment-form').
2226 * @return bool
2227 */
2228 public function is_block_registered( $block_name ) {
2229 return \WP_Block_Type_Registry::get_instance()->is_registered( $block_name );
2230 }
2231
2232 /**
2233 * Check if Font Awesome is already provided by another plugin (e.g. Elementor).
2234 *
2235 * @return bool
2236 */
2237 private function is_font_awesome_provided() {
2238 $handles = [
2239 'font-awesome-5-all',
2240 'font-awesome-6-all',
2241 'elementor-icons-fa-solid',
2242 'elementor-icons-fa-brands',
2243 'elementor-icons-fa-regular',
2244 ];
2245
2246 foreach ( $handles as $handle ) {
2247 if ( wp_style_is( $handle, 'registered' ) || wp_style_is( $handle, 'enqueued' ) ) {
2248 return true;
2249 }
2250 }
2251
2252 return false;
2253 }
2254
2255 /**
2256 * Enqueue Font Awesome only if not already provided by another plugin.
2257 *
2258 * @return void
2259 */
2260 private function enqueue_font_awesome() {
2261 if ( ! $this->is_font_awesome_provided() ) {
2262 wp_enqueue_style( 'bp-font-awesome' );
2263 }
2264 }
2265 }
2266