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

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

2,061 lines 108.5 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 $campaign_id = ! empty( $_GET['campaign_id'] ) ? sanitize_text_field( $_GET['campaign_id'] ) : '';
1457 $campaign_currency = ! empty( $_GET['campaign_currency'] ) ? sanitize_text_field( $_GET['campaign_currency'] ) : '';
1458
1459 return '
1460 <input type="hidden" name="campaign_id" value="' . esc_attr( $campaign_id ) . '">
1461 <input type="hidden" name="campaign_currency" value="' . esc_attr( $campaign_currency ) . '">
1462 ';
1463 }
1464 };
1465 }
1466
1467 /**
1468 * Enqueue editor assets (controls library).
1469 *
1470 * @return void
1471 */
1472 public function enqueue_editor_assets() {
1473 // Only load on block editor pages
1474 $screen = get_current_screen();
1475 if ( ! $screen || ! $screen->is_block_editor() ) {
1476 return;
1477 }
1478
1479 global $pagenow;
1480 $editor_type = false;
1481 if ( $pagenow === 'post-new.php' || $pagenow === 'post.php' ) {
1482 $editor_type = 'edit-post';
1483 } elseif ( $pagenow === 'site-editor.php' || ( $pagenow === 'themes.php' && isset( $_GET[ 'page' ] ) && sanitize_text_field( wp_unslash( $_GET[ 'page' ] ) ) === 'gutenberg-edit-site' ) ) {
1484 $editor_type = 'edit-site';
1485 } elseif ( $pagenow === 'widgets.php' ) {
1486 $editor_type = 'edit-widgets';
1487 }
1488
1489 // Define EssentialBlocksLocalize before controls load (controls access it at module level)
1490 $localize_data = array(
1491 'ajaxurl' => admin_url( 'admin-ajax.php' ),
1492 'ajax_url' => admin_url( 'admin-ajax.php' ),
1493 'nonce' => wp_create_nonce( 'better_payment_block_nonce' ),
1494 'admin_nonce' => wp_create_nonce( 'better_payment_admin_nonce' ),
1495 'rest_rootURL' => esc_url_raw( rest_url() ),
1496 'is_pro_active' => 'false',
1497 'eb_plugins_url' => BETTER_PAYMENT_URL . '/',
1498 'image_url' => BETTER_PAYMENT_ASSETS . '/img',
1499 'fontAwesome' => 'true',
1500 'googleFont' => 'true',
1501 'quickToolbar' => 'false',
1502 'enableGenerateImage' => '0',
1503 'enableWriteAIInputField' => '0',
1504 'enableWriteAIRichtext' => '0',
1505 'enableRewriteAIContent' => '0',
1506 'hasOpenAiApiKey' => '0',
1507 'all_blocks_default' => array(),
1508 'all_blocks' => array(),
1509 'quick_toolbar_blocks' => array(),
1510 'responsiveBreakpoints' => array(
1511 'tablet' => 1024,
1512 'mobile' => 767,
1513 ),
1514 );
1515
1516 // Add inline script to wp-editor which is guaranteed to load in block editor
1517 wp_add_inline_script(
1518 'wp-editor',
1519 'window.EssentialBlocksLocalize = window.EssentialBlocksLocalize || ' . wp_json_encode( $localize_data ) . ';'
1520 );
1521
1522 // Enqueue necessary CSS files for form styles in editor (used in render.php)
1523 wp_enqueue_style( 'better-payment-el' );
1524 wp_enqueue_style( 'bp-icon-front' );
1525 wp_enqueue_style( 'better-payment-style' );
1526 wp_enqueue_style( 'better-payment-common-style' );
1527 wp_enqueue_style( 'better-payment-admin-style' );
1528 wp_enqueue_style( 'bp-dashboard-pagination-style' );
1529 $this->enqueue_font_awesome();
1530 wp_enqueue_style( 'dashicons' );
1531
1532 // Build block data: start with common keys, then merge each block's own data.
1533 // To add data for a new block, implement get_{block-key}_editor_data() and add
1534 // 'editor_data' => 'get_{block-key}_editor_data' to the $blocks registry above.
1535 $block_data = $this->get_common_editor_data();
1536 foreach ( $this->blocks as $block_config ) {
1537 if ( ! empty( $block_config['editor_data'] ) && method_exists( $this, $block_config['editor_data'] ) ) {
1538 $block_data = array_merge( $block_data, $this->{$block_config['editor_data']}() );
1539 }
1540 }
1541
1542 // Canonical filter for the full merged data; the legacy alias preserves backwards
1543 // compatibility with any pro-plugin hooks on the old per-block filter name.
1544 $block_data = apply_filters( 'better_payment/block/editor_block_data', $block_data );
1545 $block_data = apply_filters( 'better_payment/block/user_dashboard/editor_block_data', $block_data );
1546
1547 wp_add_inline_script(
1548 'wp-editor',
1549 'window.betterPaymentBlockData = ' . wp_json_encode( $block_data ) . ';'
1550 );
1551
1552 wp_add_inline_script(
1553 'wp-editor',
1554 'window.eb_conditional_localize = ' . wp_json_encode(
1555 $editor_type !== false ? [ 'editor_type' => $editor_type ] : []
1556 ) . ';'
1557 );
1558 }
1559
1560 /**
1561 * Enqueue frontend assets for blocks
1562 * Called on wp_enqueue_scripts hook to allow pro plugin to enqueue scripts early
1563 *
1564 * @return void
1565 */
1566 public function enqueue_frontend_assets() {
1567 // Allow pro plugin to enqueue scripts for the user dashboard block
1568 do_action( 'better_payment/block/user_dashboard/wp_enqueue_scripts' );
1569 }
1570
1571 /**
1572 * Return the site domain (host only, no protocol) for use in editor data.
1573 *
1574 * @return string
1575 */
1576 private function get_site_domain() {
1577 $parsed = wp_parse_url( get_site_url() );
1578 return ! empty( $parsed['host'] ) ? esc_html( $parsed['host'] ) : 'example.com';
1579 }
1580
1581 /**
1582 * Return plugin settings with all gateway secret/private keys removed.
1583 *
1584 * Safe to expose to any block editor user regardless of role. Only the keys
1585 * that the editor UI actually reads (email defaults, gateway enable flags,
1586 * public keys) are kept — secrets never leave the server.
1587 *
1588 * @return array
1589 */
1590 private function get_safe_settings_for_editor() {
1591 $settings = get_option( 'better_payment_settings', array() );
1592 if ( ! is_array( $settings ) ) {
1593 return array();
1594 }
1595
1596 $sensitive_keys = array(
1597 'better_payment_settings_payment_paypal_live_secret',
1598 'better_payment_settings_payment_paypal_test_secret',
1599 'better_payment_settings_payment_stripe_live_secret',
1600 'better_payment_settings_payment_stripe_test_secret',
1601 'better_payment_settings_payment_paystack_live_secret',
1602 'better_payment_settings_payment_paystack_test_secret',
1603 );
1604
1605 foreach ( $sensitive_keys as $key ) {
1606 unset( $settings[ $key ] );
1607 }
1608
1609 return $settings;
1610 }
1611
1612 /**
1613 * Return data shared by every registered block in the editor.
1614 *
1615 * All blocks can read these keys from window.betterPaymentBlockData.
1616 * Adding a new block? Common data lives here; block-specific data goes in
1617 * get_{block-key}_editor_data() and is registered via $blocks['editor_data'].
1618 *
1619 * @return array
1620 */
1621 private function get_common_editor_data() {
1622 return array(
1623 'ajaxurl' => admin_url( 'admin-ajax.php' ),
1624 'nonce' => wp_create_nonce( 'better_payment_block_nonce' ),
1625 'currencies' => $this->get_currency_list(),
1626 'assetUrl' => BETTER_PAYMENT_ASSETS,
1627 'assetsUrl' => BETTER_PAYMENT_ASSETS,
1628 'siteName' => get_bloginfo( 'name' ),
1629 'siteDomain' => $this->get_site_domain(),
1630 'proEnabled' => apply_filters( 'better_payment/pro_enabled', false ),
1631 );
1632 }
1633
1634 /**
1635 * Return editor data specific to the payment-form block.
1636 *
1637 * Provides the global settings (secrets stripped) so the inspector can
1638 * pre-populate email defaults, and the site admin email for the To field.
1639 *
1640 * @return array
1641 */
1642 private function get_payment_form_editor_data() {
1643 return array(
1644 'betterPaymentSettings' => $this->get_safe_settings_for_editor(),
1645 'adminEmail' => sanitize_email( get_option( 'admin_email' ) ),
1646 );
1647 }
1648
1649 /**
1650 * Return editor data specific to the user-dashboard block.
1651 *
1652 * Fetches the current editor user's transactions, subscriptions, and analytics
1653 * so the block preview shows real data immediately on insertion without a REST
1654 * round-trip. Normalises each row to the same flat shape the UserAPI returns so
1655 * the JS preview component only needs to handle one structure.
1656 *
1657 * @return array
1658 */
1659 private function get_user_dashboard_editor_data() {
1660 $current_user = wp_get_current_user();
1661
1662 // Cache the 5 DB queries for the duration of the browser session in the editor.
1663 // The cache is keyed per user so each editor sees only their own data.
1664 // TTL: 5 minutes — short enough to reflect recent transactions without hammering
1665 // the DB on every page navigation inside the block editor.
1666 $cache_key = 'bp_editor_dash_' . ( $current_user->ID ?? 0 );
1667 $cache_group = 'better_payment';
1668 $cached = wp_cache_get( $cache_key, $cache_group );
1669 if ( false !== $cached ) {
1670 // Nonces are request-scoped and must never be cached; regenerate on every hit.
1671 $cached['restNonce'] = wp_create_nonce( 'wp_rest' );
1672 return $cached;
1673 }
1674
1675 $user_transactions = array();
1676 $user_subscriptions = array();
1677 $user_analytics = array();
1678 $user_transactions_meta = array( 'total' => 0, 'pages' => 1, 'perPage' => 20 );
1679 // Safe default ensures userSubscriptionsMeta is always fully defined even when
1680 // the user has no subscriptions or is not logged in.
1681 $sub_result = array( 'total' => 0, 'pages' => 1, 'per_page' => 20, 'transactions' => array() );
1682
1683 // Always fetch in the editor — has_block() only checks saved content and returns
1684 // false before first save, leaving the block with empty data on insertion.
1685 if ( $current_user && $current_user->ID ) {
1686 $user_analytics = \Better_Payment\Lite\Admin\DB::get_user_analytics_by_email( $current_user->user_email );
1687
1688 $paged_result = \Better_Payment\Lite\Admin\DB::get_user_transactions_paginated(
1689 $current_user->user_email,
1690 array(
1691 'page' => 1,
1692 'per_page' => 20,
1693 'type' => 'transactions',
1694 )
1695 );
1696
1697 $raw_transactions = $paged_result['transactions'];
1698 $user_transactions_meta = array(
1699 'total' => $paged_result['total'],
1700 'pages' => $paged_result['pages'],
1701 'perPage' => $paged_result['per_page'],
1702 );
1703
1704 // Unserialize and normalise to match UserAPI REST response format.
1705 foreach ( $raw_transactions as $tx ) {
1706 if ( ! empty( $tx->form_fields_info ) ) {
1707 $tx->form_fields_info = maybe_unserialize( $tx->form_fields_info );
1708 }
1709 if ( ! empty( $tx->customer_info ) ) {
1710 $tx->customer_info = maybe_unserialize( $tx->customer_info );
1711 }
1712
1713 $ffi = is_array( $tx->form_fields_info ) ? $tx->form_fields_info : array();
1714 $customer_name = isset( $ffi['primary_first_name'] )
1715 ? trim( sanitize_text_field( $ffi['primary_first_name'] ) . ' ' . sanitize_text_field( $ffi['primary_last_name'] ?? '' ) )
1716 : '';
1717 if ( empty( $customer_name ) ) {
1718 $customer_name = isset( $ffi['first_name'] )
1719 ? trim( sanitize_text_field( $ffi['first_name'] ) . ' ' . sanitize_text_field( $ffi['last_name'] ?? '' ) )
1720 : '';
1721 }
1722 $tx->customer_name = $customer_name;
1723 $tx->customer_email = sanitize_text_field( $ffi['primary_email'] ?? ( $ffi['email'] ?? '' ) );
1724 }
1725
1726 $user_transactions = apply_filters( 'better_payment/block/user_dashboard/enrich_user_transactions', $raw_transactions );
1727
1728 // Separate query for subscriptions (not a subset of transactions page 1).
1729 $sub_result = \Better_Payment\Lite\Admin\DB::get_user_transactions_paginated(
1730 $current_user->user_email,
1731 array(
1732 'page' => 1,
1733 'per_page' => 20,
1734 'type' => 'subscriptions',
1735 )
1736 );
1737
1738 $raw_subscriptions = $sub_result['transactions'];
1739 foreach ( $raw_subscriptions as $tx ) {
1740 if ( ! empty( $tx->form_fields_info ) ) {
1741 $tx->form_fields_info = maybe_unserialize( $tx->form_fields_info );
1742 }
1743 }
1744 $user_subscriptions = apply_filters( 'better_payment/block/user_dashboard/enrich_user_subscriptions', $raw_subscriptions );
1745
1746 // Flatten each subscription to match UserAPI flat format so JS handles one structure.
1747 foreach ( $user_subscriptions as $tx ) {
1748 $ffi = is_array( $tx->form_fields_info ) ? $tx->form_fields_info : array();
1749
1750 $tx->subscription_id = sanitize_text_field( $ffi['subscription_id'] ?? '' );
1751 $tx->subscription_plan_id = sanitize_text_field( $ffi['subscription_plan_id'] ?? '' );
1752 $tx->subscription_status = sanitize_text_field( $ffi['subscription_status'] ?? '' );
1753 $tx->subscription_interval = sanitize_text_field( $ffi['subscription_interval'] ?? '' );
1754 $tx->subscription_created_date = intval( $ffi['subscription_created_date'] ?? 0 );
1755 $tx->subscription_current_period_end = intval( $ffi['subscription_current_period_end'] ?? 0 );
1756 $tx->is_payment_split_payment = ! empty( $ffi['is_payment_split_payment'] ) ? intval( $ffi['is_payment_split_payment'] ) : 0;
1757 $tx->subscription_product_name = sanitize_text_field( $ffi['subscription_product_name'] ?? '' );
1758 $tx->is_subscription = ! empty( $ffi['subscription_id'] ) ? 'Subscription' : 'One Time';
1759
1760 $customer_name = isset( $ffi['primary_first_name'] )
1761 ? trim( sanitize_text_field( $ffi['primary_first_name'] ) . ' ' . sanitize_text_field( $ffi['primary_last_name'] ?? '' ) )
1762 : '';
1763 if ( empty( $customer_name ) ) {
1764 $customer_name = isset( $ffi['first_name'] )
1765 ? trim( sanitize_text_field( $ffi['first_name'] ) . ' ' . sanitize_text_field( $ffi['last_name'] ?? '' ) )
1766 : '';
1767 }
1768 $tx->customer_name = $customer_name;
1769 $tx->customer_email = sanitize_text_field( $ffi['primary_email'] ?? ( $ffi['email'] ?? '' ) );
1770
1771 unset( $tx->form_fields_info );
1772 }
1773 }
1774
1775 $data = array(
1776 'restNonce' => wp_create_nonce( 'wp_rest' ),
1777 'restUrl' => get_rest_url( null, 'better-payment/v1/user-transactions' ),
1778 'currentUser' => array(
1779 'id' => $current_user->ID,
1780 'user_email' => $current_user->user_email,
1781 'user_login' => $current_user->user_login,
1782 'user_avatar_url'=> get_avatar_url( $current_user->user_email, array( 'size' => 32 ) ),
1783 'email' => $current_user->user_email,
1784 'login' => $current_user->user_login,
1785 'avatar' => get_avatar_url( $current_user->user_email, array( 'size' => 32 ) ),
1786 ),
1787 'userTransactions' => $user_transactions,
1788 'userTransactionsMeta' => $user_transactions_meta,
1789 'userSubscriptions' => $user_subscriptions,
1790 'userSubscriptionsMeta' => array(
1791 'total' => (int) $sub_result['total'],
1792 'pages' => (int) $sub_result['pages'],
1793 'perPage' => 20,
1794 ),
1795 'userAnalytics' => $user_analytics,
1796 'proAssets' => array(
1797 'analyticsReportsBanner' => BETTER_PAYMENT_ASSETS . '/img/user-dashboard-analytics-reports-pro-banner.png',
1798 'recurringSubscriptionBanner' => BETTER_PAYMENT_ASSETS . '/img/user-dashboard-recurring-subscription-pro-banner.png',
1799 'splitSubscriptionBanner' => BETTER_PAYMENT_ASSETS . '/img/user-dashboard-split-subscription-pro-banner.png',
1800 'subscriptionProBanner' => BETTER_PAYMENT_ASSETS . '/img/user-dashboard-subscription-pro-banner.png',
1801 ),
1802 );
1803
1804 // Store in the object cache for 5 minutes. nonce is excluded from the cached payload
1805 // because nonces are request-scoped — storing them would return stale nonces on cache hit.
1806 // The restNonce above is regenerated fresh on every request (not cached).
1807 $cacheable = $data;
1808 unset( $cacheable['restNonce'] );
1809 wp_cache_set( $cache_key, $cacheable, $cache_group, 5 * MINUTE_IN_SECONDS );
1810
1811 return $data;
1812 }
1813
1814 /**
1815 * Get the current editor type.
1816 *
1817 * @return string
1818 */
1819 private function get_editor_type() {
1820 if ( function_exists( 'get_current_screen' ) ) {
1821 $screen = get_current_screen();
1822 if ( $screen && method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() ) {
1823 return 'edit-post';
1824 }
1825 }
1826 return 'edit-post';
1827 }
1828
1829 /**
1830 * Register the Better Payment block category.
1831 *
1832 * @param array $categories Block categories.
1833 * @param WP_Block_Editor_Context $context Block editor context.
1834 * @return array Modified block categories.
1835 */
1836 public function register_block_category( $categories, $context ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
1837 // Check if our category already exists
1838 foreach ( $categories as $category ) {
1839 if ( 'better-payment' === $category['slug'] ) {
1840 return $categories;
1841 }
1842 }
1843
1844 // Add our category at the beginning
1845 array_unshift(
1846 $categories,
1847 array(
1848 'slug' => 'better-payment',
1849 'title' => __( 'Better Payment', 'better-payment' ),
1850 'icon' => '',
1851 )
1852 );
1853
1854 return $categories;
1855 }
1856
1857 /**
1858 * Register all blocks.
1859 *
1860 * @return void
1861 */
1862 public function register_blocks() {
1863 if ( ! function_exists( 'register_block_type' ) ) {
1864 return;
1865 }
1866
1867 foreach ( $this->blocks as $block_key => $block_config ) {
1868 if ( ! $this->is_block_registered( $block_config['name'] ) ) {
1869 $this->register_single_block( $block_key, $block_config );
1870 }
1871 }
1872 }
1873
1874 /**
1875 * Register a single block.
1876 *
1877 * @param string $block_key Block key.
1878 * @param array $block_config Block configuration.
1879 * @return void
1880 */
1881 private function register_single_block( $block_key, $block_config ) {
1882 // Use assets/blocks path for block.json since src/ is excluded from production builds.
1883 // The block.json is copied to assets/blocks/ during the build process.
1884 $block_json_path = BETTER_PAYMENT_PATH . '/assets/blocks/blocks/' . $block_config['path'] . '/block.json';
1885
1886 // Fallback to src/blocks for development environment (assets may not be built yet)
1887 if ( ! file_exists( $block_json_path ) ) {
1888 $block_json_path = BETTER_PAYMENT_PATH . '/src/blocks/' . $block_config['path'] . '/block.json';
1889 }
1890
1891 // Check if block.json exists
1892 if ( ! file_exists( $block_json_path ) ) {
1893 return;
1894 }
1895
1896 // Get asset file for dependencies
1897 $asset_file = $this->get_asset_file( $block_config['path'] );
1898
1899 // Add controls as a dependency
1900 $dependencies = array_merge( $asset_file['dependencies'], array( 'better-payment-controls' ) );
1901
1902 // Register editor script
1903 $editor_script_handle = 'better-payment-' . $block_key . '-editor';
1904
1905 if ( file_exists( BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_config['path'] . '/index.min.js' ) ) {
1906 wp_register_script(
1907 $editor_script_handle,
1908 BETTER_PAYMENT_ASSETS . '/blocks/' . $block_config['path'] . '/index.min.js',
1909 $dependencies,
1910 $asset_file['version'],
1911 true
1912 );
1913 } elseif ( file_exists( BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_config['path'] . '/index.js' ) ) {
1914 wp_register_script(
1915 $editor_script_handle,
1916 BETTER_PAYMENT_ASSETS . '/blocks/' . $block_config['path'] . '/index.js',
1917 $dependencies,
1918 $asset_file['version'],
1919 true
1920 );
1921 }
1922
1923 // Register frontend/editor style
1924 $style_handle = 'better-payment-' . $block_key . '-style';
1925 $style_dependencies = array(
1926 'better-payment-controls-style',
1927 'better-payment-el',
1928 'bp-icon-front',
1929 'better-payment-style',
1930 'better-payment-common-style',
1931 'better-payment-admin-style',
1932 'dashicons',
1933 );
1934 if ( ! $this->is_font_awesome_provided() ) {
1935 $style_dependencies[] = 'bp-font-awesome';
1936 }
1937 if ( file_exists( BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_config['path'] . '/style.min.css' ) ) {
1938 wp_register_style(
1939 $style_handle,
1940 BETTER_PAYMENT_ASSETS . '/blocks/' . $block_config['path'] . '/style.min.css',
1941 $style_dependencies,
1942 $asset_file['version']
1943 );
1944 } elseif ( file_exists( BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_config['path'] . '/style.css' ) ) {
1945 wp_register_style(
1946 $style_handle,
1947 BETTER_PAYMENT_ASSETS . '/blocks/' . $block_config['path'] . '/style.css',
1948 $style_dependencies,
1949 $asset_file['version']
1950 );
1951 }
1952
1953 // Build block registration args
1954 $block_args = array(
1955 'editor_script' => $editor_script_handle,
1956 'style' => $style_handle,
1957 );
1958
1959 // Add render callback if defined in block config
1960 if ( ! empty( $block_config['render_callback'] ) && method_exists( $this, $block_config['render_callback'] ) ) {
1961 $block_args['render_callback'] = array( $this, $block_config['render_callback'] );
1962 }
1963
1964 // Register block with WordPress
1965 $result = register_block_type( $block_json_path, $block_args );
1966
1967 if ( false === $result ) {
1968 // Log error in debug mode
1969 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
1970 error_log( 'Better Payment: Failed to register block ' . $block_config['name'] );
1971 }
1972 }
1973 }
1974
1975 /**
1976 * Get asset file with dependencies and version.
1977 *
1978 * @param string $block_path Block path.
1979 * @return array Asset file data.
1980 */
1981 private function get_asset_file( $block_path ) {
1982 $asset_file_path = BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_path . '/index.min.asset.php';
1983
1984 if ( ! file_exists( $asset_file_path ) ) {
1985 $asset_file_path = BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_path . '/index.asset.php';
1986 }
1987
1988 if ( file_exists( $asset_file_path ) ) {
1989 return include $asset_file_path;
1990 }
1991
1992 return array(
1993 'dependencies' => array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n' ),
1994 'version' => BETTER_PAYMENT_VERSION,
1995 );
1996 }
1997
1998
1999 /**
2000 * Get the blocks directory path.
2001 *
2002 * @return string
2003 */
2004 public function get_blocks_path() {
2005 return BETTER_PAYMENT_PATH . '/src/blocks/';
2006 }
2007
2008 /**
2009 * Get the blocks assets URL.
2010 *
2011 * @return string
2012 */
2013 public function get_blocks_url() {
2014 return BETTER_PAYMENT_ASSETS . '/blocks/';
2015 }
2016
2017 /**
2018 * Check if a block is registered.
2019 *
2020 * @param string $block_name Full block name (e.g., 'better-payment/payment-form').
2021 * @return bool
2022 */
2023 public function is_block_registered( $block_name ) {
2024 return \WP_Block_Type_Registry::get_instance()->is_registered( $block_name );
2025 }
2026
2027 /**
2028 * Check if Font Awesome is already provided by another plugin (e.g. Elementor).
2029 *
2030 * @return bool
2031 */
2032 private function is_font_awesome_provided() {
2033 $handles = [
2034 'font-awesome-5-all',
2035 'font-awesome-6-all',
2036 'elementor-icons-fa-solid',
2037 'elementor-icons-fa-brands',
2038 'elementor-icons-fa-regular',
2039 ];
2040
2041 foreach ( $handles as $handle ) {
2042 if ( wp_style_is( $handle, 'registered' ) || wp_style_is( $handle, 'enqueued' ) ) {
2043 return true;
2044 }
2045 }
2046
2047 return false;
2048 }
2049
2050 /**
2051 * Enqueue Font Awesome only if not already provided by another plugin.
2052 *
2053 * @return void
2054 */
2055 private function enqueue_font_awesome() {
2056 if ( ! $this->is_font_awesome_provided() ) {
2057 wp_enqueue_style( 'bp-font-awesome' );
2058 }
2059 }
2060 }
2061