PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.2.1
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.2.1
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.1, at includes/Blocks/BlockManager.php

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