PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.3.0
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.3.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 1.0.5 1.0.6 All 66 releases
better-payment / includes / Blocks / BlockManager.php

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

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