PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.1.2
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.1.2
2.4.1 2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 All 68 releases
better-payment / includes / Blocks / BlockManager.php

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

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