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

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

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