# better-payment/trunk/includes/Blocks/BlockManager.php

Better Payment – Instant Payments, Donations, Fundraising with Subscriptions &amp; More, version trunk. 2,067 lines.

- Page: https://pluginprobe.com/plugins/better-payment/trunk/code/includes/Blocks/BlockManager.php
- Raw: https://pluginprobe.com/plugins/better-payment/trunk/raw/includes/Blocks/BlockManager.php
- Modified: 2026-07-09T13:20:06+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/better-payment/trunk/code/includes/Blocks/BlockManager.php#L10-L20`.

```php
<?php
/**
 * Block Manager for Better Payment Gutenberg blocks.
 *
 * Handles registration, enqueuing, and management of all Gutenberg blocks.
 *
 * @package Better_Payment
 * @since 1.0.0
 */

namespace Better_Payment\Lite\Blocks;

use Better_Payment\Lite\Admin\DB;
use Better_Payment\Lite\Classes\Handler;
use Better_Payment\Lite\Traits\Helper as TraitsHelper;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Block Manager class.
 *
 * @since 1.0.0
 */
class BlockManager {
    use TraitsHelper;

    /**
     * Instance of this class.
     *
     * @var BlockManager|null
     */
    private static $instance = null;

    /**
     * List of available blocks.
     *
     * @var array
     */
    private $blocks = array(
        'payment-form' => array(
            'name'            => 'better-payment/payment-form',
            'path'            => 'payment-form',
            'has_styles'      => true,
            'has_scripts'     => true,
            'render_callback' => 'render_payment_form_frontend',
            'editor_data'     => 'get_payment_form_editor_data',
        ),
        'user-dashboard' => array(
            'name'            => 'better-payment/user-dashboard',
            'path'            => 'user-dashboard',
            'has_styles'      => true,
            'has_scripts'     => true,
            'render_callback' => 'render_user_dashboard_frontend',
            'editor_data'     => 'get_user_dashboard_editor_data',
        ),
    );

    /**
     * Get the singleton instance.
     *
     * @return BlockManager
     */
    public static function get_instance() {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Private constructor to prevent direct instantiation.
     */
    private function __construct() {
        $this->init_hooks();
    }

    /**
     * Initialize hooks.
     *
     * @return void
     */
    private function init_hooks() {
        add_action( 'init', array( $this, 'register_blocks' ) );
        add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_editor_assets' ) );
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
        add_filter( 'block_categories_all', array( $this, 'register_block_category' ), 10, 2 );
    }

    /**
     * Render callback for the payment form block frontend.
     *
     * This method contains all the rendering logic including layout template loading,
     * settings mapping, widget proxy object creation, and security measures.
     *
     * @param array    $attributes Block attributes.
     * @param string   $content    Block default content.
     * @param WP_Block $block      Block instance.
     * @return string The rendered HTML output.
     */
    public function render_payment_form_frontend( $attributes, $content = '', $block = null ) {
        // Get block attributes with defaults.
        $form_layout = isset( $attributes['formLayout'] ) ? sanitize_text_field( $attributes['formLayout'] ) : 'layout-1';

        // Validate layout - only allow layout-1, layout-2, layout-3.
        $allowed_layouts = array( 'layout-1', 'layout-2', 'layout-3' );
        if ( ! in_array( $form_layout, $allowed_layouts, true ) ) {
            $form_layout = 'layout-1';
        }

        // Check if layout file exists, fallback to layout-1 if not.
        $template_file = BETTER_PAYMENT_ADMIN_VIEWS_PATH . '/elementor/layouts-block/' . $form_layout . '.php';
        if ( ! file_exists( $template_file ) ) {
            $template_file = BETTER_PAYMENT_ADMIN_VIEWS_PATH . '/elementor/layouts-block/layout-1.php';
            if ( ! file_exists( $template_file ) ) {
                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                    error_log( 'Better Payment: Layout template not found at ' . $template_file );
                }
                return '<div class="better-payment-block better-payment-block--error">' .
                       esc_html__( 'Payment form template not found.', 'better-payment' ) .
                       '</div>';
            }
        }

        // Get global settings from database.
        $global_settings = DB::get_settings();

        // Generate a unique ID for this block instance.
        // Use a deterministic ID based on block attributes to ensure consistency across page loads.
        // This is critical for payment callback matching.
        $block_id = isset( $attributes['blockId'] ) && ! empty( $attributes['blockId'] )
            ? sanitize_text_field( $attributes['blockId'] )
            : 'bp-block-' . md5( get_the_ID() . wp_json_encode( $attributes ) );

        // Build the $settings array that the layout files expect.
        // This maps block attributes to the Elementor widget settings format.
        $settings = $this->build_block_settings( $attributes, $global_settings, $form_layout );

// Check if at least one payment gateway is enabled.
        $has_payment_gateway = 'yes' === $settings['better_payment_form_paypal_enable'] ||
                               'yes' === $settings['better_payment_form_stripe_enable'] ||
                               'yes' === $settings['better_payment_form_paystack_enable'];

        // Build wrapper attributes using WordPress's block wrapper API.
        // get_block_wrapper_attributes() reads supports.align from the block context
        // (set automatically by WP_Block before invoking this render callback) and adds
        // alignwide / alignfull to the class list — no manual $attributes['align'] needed.
        $wrapper_class      = 'better-payment-block bp-form-' . esc_attr( $form_layout );
        $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $wrapper_class ) );

        if ( ! $has_payment_gateway ) {
            return '<div ' . $wrapper_attributes . '>' .
                   '<div class="better-payment-block__notice">' .
                   '<p>' . esc_html__( 'Please configure at least one payment gateway (PayPal, Stripe, or Paystack) in Better Payment settings.', 'better-payment' ) . '</p>' .
                   '</div></div>';
        }

        // Enqueue scripts/styles before manage_response() so they are present even when the
        // block returns the pending or success state early (the AJAX poller needs better-payment.js).
        wp_enqueue_style( 'better-payment-el' );
        wp_enqueue_style( 'bp-icon-front' );
        wp_enqueue_style( 'better-payment-style' );
        wp_enqueue_style( 'better-payment-common-style' );
        wp_enqueue_style( 'better-payment-admin-style' );
        $this->enqueue_font_awesome();
        wp_enqueue_style( 'dashicons' );
        wp_enqueue_script( 'better-payment-common-script' );
        wp_enqueue_script( 'better-payment' );

        // Match Elementor flow:
        // - Handle response before rendering form.
        // - On success: render success notice only.
        // - On error: render error notice and continue rendering the form.
        ob_start();
        $payment_response = Handler::manage_response( $settings, $block_id );
        $manage_response_output = ob_get_clean();

        if ( $payment_response ) {
            return '<div ' . $wrapper_attributes . '>' . $manage_response_output . '</div>';
        }

        // Fire action hook for extensibility (matching Elementor widget behavior).
        do_action( 'better_payment/elementor/editor/manage_response_webhook', null, $settings );

        // Store settings in a transient so the AJAX handler can retrieve them.
        // Only write when missing or expired — avoids a DB write on every page load.
        $transient_key = 'bp_block_settings_' . get_the_ID() . '_' . $block_id;
        if ( false === get_transient( $transient_key ) ) {
            set_transient( $transient_key, $settings, HOUR_IN_SECONDS );
        }

        // Create widget proxy object for layout templates.
        $widgetObj = $this->create_widget_proxy( $block_id, $settings );

        // Prepare extraDatas for the layout.
        $extraDatas = array(
            'action'    => esc_url( admin_url( 'admin-post.php' ) ),
            'block_id'  => $block_id,
            'setting_meta' => wp_json_encode(
                array(
                    'page_id'   => get_the_ID(),
                    'widget_id' => $block_id,
                    'source'    => 'gutenberg',
                )
            ),
        );

        // Use output buffering to capture the layout output.
        ob_start();
        ?>
        <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes() is a safe WordPress core function. ?>>
            <?php echo $manage_response_output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
            <?php
            // Create a closure to include the template with isolated scope.
            $render_layout = function ( $template_file, $settings, $widgetObj, $extraDatas ) {
                include $template_file;
            };

            // Bind the closure to the widget proxy so $this works in the template.
            $bound_render = \Closure::bind( $render_layout, $widgetObj, get_class( $widgetObj ) );
            $bound_render( $template_file, $settings, $widgetObj, $extraDatas );
            ?>
        </div>
        <?php

        return ob_get_clean();
    }

    /**
     * Render callback for the user dashboard block frontend.
     *
     * Maps block attributes to widget settings and renders the user dashboard template.
     *
     * @param array    $attributes Block attributes.
     * @param string   $content    Block default content.
     * @param WP_Block $block      Block instance.
     * @return string The rendered HTML output.
     */
    public function render_user_dashboard_frontend( $attributes, $content = '', $block = null ) {
        // Get block attributes with defaults
        $dashboard_layout = isset( $attributes['dashboardLayout'] ) ? sanitize_text_field( $attributes['dashboardLayout'] ) : 'layout-1';

        // Validate layout - only allow layout-1, layout-2, layout-3 plus pro versions
        $allowed_layouts = array( 'layout-1', 'layout-2', 'layout-3', 'layout-1-pro', 'layout-2-pro', 'layout-3-pro' );
        if ( ! in_array( $dashboard_layout, $allowed_layouts, true ) ) {
            $dashboard_layout = 'layout-1';
        }

        // Check if layout file exists, fallback to layout-1 if not
        $template_file = BETTER_PAYMENT_ADMIN_VIEWS_PATH . '/elementor/user-dashboard/' . $dashboard_layout . '.php';

        // Check for pro layout if pro plugin is active
        $is_pro_layout = strpos( $dashboard_layout, '-pro' ) !== false;
        if ( $is_pro_layout && defined( 'BETTER_PAYMENT_PRO_ADMIN_VIEWS_PATH' ) ) {
            $template_file = BETTER_PAYMENT_PRO_ADMIN_VIEWS_PATH . '/elementor/layouts/' . $dashboard_layout . '.php';
        }

        if ( ! file_exists( $template_file ) ) {
            $template_file = BETTER_PAYMENT_ADMIN_VIEWS_PATH . '/elementor/user-dashboard/layout-1.php';
            if ( ! file_exists( $template_file ) ) {
                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                    error_log( 'Better Payment: Layout template not found at ' . $template_file );
                }
                return '<div class="better-payment-block better-payment-block--error">' .
                       esc_html__( 'User Dashboard template not found.', 'better-payment' ) .
                       '</div>';
            }
        }

        // Generate unique block ID for this instance
        $block_id = isset( $attributes['blockId'] ) && ! empty( $attributes['blockId'] )
            ? sanitize_text_field( $attributes['blockId'] )
            : 'bp-user-dashboard-' . md5( get_the_ID() . wp_json_encode( $attributes ) );

        // Map block attributes to widget settings format
        $settings = $this->build_user_dashboard_settings( $attributes );

        // Build wrapper attributes using WordPress's block wrapper API
        $wrapper_class      = 'better-payment-user-dashboard bp-' . esc_attr( $block_id ) . ' bp-dashboard-' . esc_attr( $dashboard_layout );
        $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $wrapper_class ) );

        // Prepare extra data
        $extraDatas = array(
            'action'    => esc_url( admin_url( 'admin-post.php' ) ),
            'block_id'  => $block_id,
            'setting_meta' => wp_json_encode(
                array(
                    'page_id'   => get_the_ID(),
                    'widget_id' => $block_id,
                    'source'    => 'gutenberg',
                )
            ),
        );

        // Enqueue necessary scripts and styles
        wp_enqueue_style( 'better-payment-el' );
        wp_enqueue_style( 'bp-icon-front' );
        wp_enqueue_style( 'better-payment-style' );
        wp_enqueue_style( 'better-payment-common-style' );
        wp_enqueue_style( 'better-payment-admin-style' );
        $this->enqueue_font_awesome();
        wp_enqueue_style( 'dashicons' );
        wp_enqueue_style( 'bp-dashboard-pagination-style' );
        wp_enqueue_script( 'better-payment-common-script' );
        wp_enqueue_script( 'better-payment' );
        if ( is_user_logged_in() ) {
            wp_localize_script( 'better-payment', 'betterPaymentUserDash', [
                'nonce'   => wp_create_nonce( 'wp_rest' ),
                'restUrl' => get_rest_url( null, 'better-payment/v1/user-transactions' ),
            ] );
        }
        wp_enqueue_script( 'bp-dashboard-pagination' );

        // Use output buffering to capture the layout output
        ob_start();
        ?>
        <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- get_block_wrapper_attributes() is a safe WordPress core function. ?>>
            <?php
            // Create widget proxy object for layout templates
            $widgetObj = $this->create_user_dashboard_widget_proxy( $block_id, $settings );

            // Create a closure to include the template with isolated scope
            $render_layout = function ( $template_file, $settings, $widgetObj, $extraDatas ) {
                // Make $bp_settings available to templates (templates expect this variable)
                $bp_settings = $settings;
                include $template_file;
            };

            // Bind the closure to the widget proxy so $this works in the template
            $bound_render = \Closure::bind( $render_layout, $widgetObj, get_class( $widgetObj ) );
            $bound_render( $template_file, $settings, $widgetObj, $extraDatas );
            ?>
        </div>
        <?php

        return ob_get_clean();
    }

    /**
     * Build the settings array for user dashboard layout templates.
     *
     * Maps block attributes to the format expected by dashboard templates.
     *
     * @param array $attributes Block attributes.
     * @return array Settings array for layout templates.
     */
    public function build_user_dashboard_settings( $attributes ) {
        // Convert block attributes to widget settings format (like get_bp_settings does)
        $settings = array(
            // Visibility toggles
            'sidebar_show'        => isset( $attributes['sidebarShow'] ) ? (bool) $attributes['sidebarShow'] : true,
            'avatar_show'         => isset( $attributes['avatarShow'] ) ? (bool) $attributes['avatarShow'] : true,
            'username_show'       => isset( $attributes['usernameShow'] ) ? (bool) $attributes['usernameShow'] : true,
            'dashboard_show'      => isset( $attributes['dashboardShow'] ) ? (bool) $attributes['dashboardShow'] : true,
            'transactions_show'   => isset( $attributes['transactionsShow'] ) ? (bool) $attributes['transactionsShow'] : true,
            'subscriptions_show'  => isset( $attributes['subscriptionsShow'] ) ? (bool) $attributes['subscriptionsShow'] : true,
            'header_show'         => isset( $attributes['headerShow'] ) ? (bool) $attributes['headerShow'] : true,

            // Labels
            'dashboard_label'     => isset( $attributes['dashboardLabel'] ) && ! empty( $attributes['dashboardLabel'] ) ? sanitize_text_field( $attributes['dashboardLabel'] ) : 'Dashboard',
            'transaction_label'   => isset( $attributes['transactionLabel'] ) && ! empty( $attributes['transactionLabel'] ) ? sanitize_text_field( $attributes['transactionLabel'] ) : 'Transactions',
            'subscription_label'  => isset( $attributes['subscriptionLabel'] ) && ! empty( $attributes['subscriptionLabel'] ) ? sanitize_text_field( $attributes['subscriptionLabel'] ) : 'Subscriptions',
            'refresh_stats_label' => isset( $attributes['refreshStatsLabel'] ) && ! empty( $attributes['refreshStatsLabel'] ) ? sanitize_text_field( $attributes['refreshStatsLabel'] ) : 'Refresh Stats',
            'no_items_label'      => isset( $attributes['noItemsLabel'] ) && ! empty( $attributes['noItemsLabel'] ) ? sanitize_text_field( $attributes['noItemsLabel'] ) : 'No records found!',

            // Dashboard section visibility
            'dashboard_transaction_summary_show'   => isset( $attributes['dashboardTransactionSummaryShow'] ) ? (bool) $attributes['dashboardTransactionSummaryShow'] : true,
            'dashboard_analytics_report_show'     => isset( $attributes['dashboardAnalyticsReportShow'] ) ? (bool) $attributes['dashboardAnalyticsReportShow'] : true,
            'dashboard_recent_transactions_show'  => isset( $attributes['dashboardRecentTransactionsShow'] ) ? (bool) $attributes['dashboardRecentTransactionsShow'] : true,
            'dashboard_recurring_subscription_show' => isset( $attributes['dashboardRecurringSubscriptionShow'] ) ? (bool) $attributes['dashboardRecurringSubscriptionShow'] : true,
            'dashboard_split_subscription_show'   => isset( $attributes['dashboardSplitSubscriptionShow'] ) ? (bool) $attributes['dashboardSplitSubscriptionShow'] : true,

            // Dashboard labels
            'dashboard_total_amount_label'        => isset( $attributes['dashboardTotalAmountLabel'] ) && ! empty( $attributes['dashboardTotalAmountLabel'] ) ? sanitize_text_field( $attributes['dashboardTotalAmountLabel'] ) : 'Total Amount',
            'dashboard_completed_amount_label'    => isset( $attributes['dashboardCompletedAmountLabel'] ) && ! empty( $attributes['dashboardCompletedAmountLabel'] ) ? sanitize_text_field( $attributes['dashboardCompletedAmountLabel'] ) : 'Completed Amount',
            'dashboard_incomplete_amount_label'   => isset( $attributes['dashboardIncompleteAmountLabel'] ) && ! empty( $attributes['dashboardIncompleteAmountLabel'] ) ? sanitize_text_field( $attributes['dashboardIncompleteAmountLabel'] ) : 'Incomplete Amount',
            'dashboard_refunded_amount_label'     => isset( $attributes['dashboardRefundedAmountLabel'] ) && ! empty( $attributes['dashboardRefundedAmountLabel'] ) ? sanitize_text_field( $attributes['dashboardRefundedAmountLabel'] ) : 'Refunded Amount',
            'dashboard_view_all_label'            => isset( $attributes['dashboardViewAllLabel'] ) && ! empty( $attributes['dashboardViewAllLabel'] ) ? sanitize_text_field( $attributes['dashboardViewAllLabel'] ) : 'View All',
            'dashboard_analytics_reports_label'   => isset( $attributes['dashboardAnalyticsReportsLabel'] ) && ! empty( $attributes['dashboardAnalyticsReportsLabel'] ) ? sanitize_text_field( $attributes['dashboardAnalyticsReportsLabel'] ) : 'Analytics Reports',
            'dashboard_recent_transactions_label' => isset( $attributes['dashboardRecentTransactionsLabel'] ) && ! empty( $attributes['dashboardRecentTransactionsLabel'] ) ? sanitize_text_field( $attributes['dashboardRecentTransactionsLabel'] ) : 'Recent Transactions',
            'dashboard_recurring_subscriptions_label' => isset( $attributes['dashboardRecurringSubscriptionsLabel'] ) && ! empty( $attributes['dashboardRecurringSubscriptionsLabel'] ) ? sanitize_text_field( $attributes['dashboardRecurringSubscriptionsLabel'] ) : 'Recurring Subscriptions',
            'dashboard_split_subscriptions_label' => isset( $attributes['dashboardSplitSubscriptionsLabel'] ) && ! empty( $attributes['dashboardSplitSubscriptionsLabel'] ) ? sanitize_text_field( $attributes['dashboardSplitSubscriptionsLabel'] ) : 'Split Subscriptions',

            // Transaction table visibility
            'transaction_table_name_show'          => isset( $attributes['transactionTableNameShow'] ) ? (bool) $attributes['transactionTableNameShow'] : true,
            'transaction_table_email_address_show' => isset( $attributes['transactionTableEmailAddressShow'] ) ? (bool) $attributes['transactionTableEmailAddressShow'] : true,
            'transaction_table_amount_show'        => isset( $attributes['transactionTableAmountShow'] ) ? (bool) $attributes['transactionTableAmountShow'] : true,
            'transaction_table_payment_type_show'  => isset( $attributes['transactionTablePaymentTypeShow'] ) ? (bool) $attributes['transactionTablePaymentTypeShow'] : true,
            'transaction_table_transaction_id_show' => isset( $attributes['transactionTableTransactionIdShow'] ) ? (bool) $attributes['transactionTableTransactionIdShow'] : true,
            'transaction_table_source_show'        => isset( $attributes['transactionTableSourceShow'] ) ? (bool) $attributes['transactionTableSourceShow'] : true,
            'transaction_table_status_show'        => isset( $attributes['transactionTableStatusShow'] ) ? (bool) $attributes['transactionTableStatusShow'] : true,
            'transaction_table_date_show'          => isset( $attributes['transactionTableDateShow'] ) ? (bool) $attributes['transactionTableDateShow'] : true,
            'transaction_table_action_show'        => isset( $attributes['transactionTableActionShow'] ) ? (bool) $attributes['transactionTableActionShow'] : true,

            // Transaction table labels
            'transaction_table_name_label'          => isset( $attributes['transactionTableNameLabel'] ) && ! empty( $attributes['transactionTableNameLabel'] ) ? sanitize_text_field( $attributes['transactionTableNameLabel'] ) : 'Name',
            'transaction_table_email_address_label' => isset( $attributes['transactionTableEmailAddressLabel'] ) && ! empty( $attributes['transactionTableEmailAddressLabel'] ) ? sanitize_text_field( $attributes['transactionTableEmailAddressLabel'] ) : 'Email Address',
            'transaction_table_amount_label'        => isset( $attributes['transactionTableAmountLabel'] ) && ! empty( $attributes['transactionTableAmountLabel'] ) ? sanitize_text_field( $attributes['transactionTableAmountLabel'] ) : 'Amount',
            'transaction_table_payment_type_label'  => isset( $attributes['transactionTablePaymentTypeLabel'] ) && ! empty( $attributes['transactionTablePaymentTypeLabel'] ) ? sanitize_text_field( $attributes['transactionTablePaymentTypeLabel'] ) : 'Payment Type',
            'transaction_table_transaction_id_label' => isset( $attributes['transactionTableTransactionIdLabel'] ) && ! empty( $attributes['transactionTableTransactionIdLabel'] ) ? sanitize_text_field( $attributes['transactionTableTransactionIdLabel'] ) : 'Transaction ID',
            'transaction_table_source_label'        => isset( $attributes['transactionTableSourceLabel'] ) && ! empty( $attributes['transactionTableSourceLabel'] ) ? sanitize_text_field( $attributes['transactionTableSourceLabel'] ) : 'Source',
            'transaction_table_status_label'        => isset( $attributes['transactionTableStatusLabel'] ) && ! empty( $attributes['transactionTableStatusLabel'] ) ? sanitize_text_field( $attributes['transactionTableStatusLabel'] ) : 'Status',
            'transaction_table_date_label'          => isset( $attributes['transactionTableDateLabel'] ) && ! empty( $attributes['transactionTableDateLabel'] ) ? sanitize_text_field( $attributes['transactionTableDateLabel'] ) : 'Date',
            'transaction_table_action_label'        => isset( $attributes['transactionTableActionLabel'] ) && ! empty( $attributes['transactionTableActionLabel'] ) ? sanitize_text_field( $attributes['transactionTableActionLabel'] ) : 'Action',

            // Subscription table column visibility (pro feature — consumed by template-subscriptions-tab.php)
            'subscription_table_subscription_id_show'  => isset( $attributes['subscriptionTableSubscriptionIdShow'] ) ? (bool) $attributes['subscriptionTableSubscriptionIdShow'] : true,
            'subscription_table_plan_id_show'          => isset( $attributes['subscriptionTablePlanIdShow'] ) ? (bool) $attributes['subscriptionTablePlanIdShow'] : true,
            'subscription_table_status_show'           => isset( $attributes['subscriptionTableStatusShow'] ) ? (bool) $attributes['subscriptionTableStatusShow'] : true,
            'subscription_table_amount_show'           => isset( $attributes['subscriptionTableAmountShow'] ) ? (bool) $attributes['subscriptionTableAmountShow'] : true,
            'subscription_table_created_date_show'     => isset( $attributes['subscriptionTableCreatedDateShow'] ) ? (bool) $attributes['subscriptionTableCreatedDateShow'] : true,
            'subscription_table_current_period_show'   => isset( $attributes['subscriptionTableCurrentPeriodShow'] ) ? (bool) $attributes['subscriptionTableCurrentPeriodShow'] : true,
            'subscription_table_action_show'           => isset( $attributes['subscriptionTableActionShow'] ) ? (bool) $attributes['subscriptionTableActionShow'] : true,

            // Subscription table column labels (pro feature)
            'subscription_table_subscription_id_label'  => ! empty( $attributes['subscriptionTableSubscriptionIdLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableSubscriptionIdLabel'] ) : 'Subscription ID',
            'subscription_table_plan_id_label'          => ! empty( $attributes['subscriptionTablePlanIdLabel'] ) ? sanitize_text_field( $attributes['subscriptionTablePlanIdLabel'] ) : 'Product Name',
            'subscription_table_status_label'           => ! empty( $attributes['subscriptionTableStatusLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableStatusLabel'] ) : 'Status',
            'subscription_table_amount_label'           => ! empty( $attributes['subscriptionTableAmountLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableAmountLabel'] ) : 'Amount',
            'subscription_table_created_date_label'     => ! empty( $attributes['subscriptionTableCreatedDateLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableCreatedDateLabel'] ) : 'Payment Date',
            'subscription_table_current_period_label'   => ! empty( $attributes['subscriptionTableCurrentPeriodLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableCurrentPeriodLabel'] ) : 'Renewal Date',
            'subscription_table_action_label'           => ! empty( $attributes['subscriptionTableActionLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableActionLabel'] ) : 'Action',
            'subscription_table_status_active_label'    => ! empty( $attributes['subscriptionTableStatusActiveLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableStatusActiveLabel'] ) : 'Active',
            'subscription_table_status_inactive_label'  => ! empty( $attributes['subscriptionTableStatusInactiveLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableStatusInactiveLabel'] ) : 'Inactive',
            'subscription_table_action_cancel_label'    => ! empty( $attributes['subscriptionTableActionCancelLabel'] ) ? sanitize_text_field( $attributes['subscriptionTableActionCancelLabel'] ) : 'Cancel',
        );

        /**
         * Block-specific filter for merging pro $bp_settings.
         *
         * Passes raw camelCase block $attributes as second arg (unlike the Elementor filter
         * which passes raw Elementor settings with better_payment_user_dashboard_* keys).
         *
         * @since 3.x.x
         *
         * @param array $settings    The built settings array.
         * @param array $attributes  The raw block attributes (camelCase).
         */
        return apply_filters( 'better_payment/block/user_dashboard/bp_settings', $settings, $attributes );
    }

    /**
     * Create a widget proxy object for user dashboard layout templates.
     *
     * This provides the methods that layout files expect from $widgetObj and $this.
     *
     * @param string $block_id The unique block ID.
     * @param array  $settings The settings array.
     * @return object Widget proxy object with required methods.
     */
    private function create_user_dashboard_widget_proxy( $block_id, $settings ) {
        return new class( $block_id, $settings ) {
            /**
             * Block ID.
             *
             * @var string
             */
            private $id;

            /**
             * Settings array.
             *
             * @var array
             */
            private $settings;

            /**
             * Pro enabled flag.
             *
             * @var bool
             */
            public $pro_enabled;

            /**
             * Constructor.
             *
             * @param string $id       Block ID.
             * @param array  $settings Settings array.
             */
            public function __construct( $id, $settings ) {
                $this->id          = $id;
                $this->settings    = $settings;
                $this->pro_enabled = apply_filters( 'better_payment/pro_enabled', false );
            }

            /**
             * Get the block ID.
             *
             * @return string Block ID.
             */
            public function get_id() {
                return $this->id;
            }

            /**
             * Get user transactions by email.
             *
             * @param string $email User email (uses current user if empty).
             * @return array Transaction records.
             */
            public function get_user_transactions( $email = '', $page = 1, $per_page = 10 ) {
                $current_user = wp_get_current_user();
                if ( empty( $email ) ) {
                    $email = $current_user->user_email;
                }
                return \Better_Payment\Lite\Admin\DB::get_user_transactions_paginated( $email, [
                    'page'     => $page,
                    'per_page' => $per_page,
                    'type'     => 'transactions',
                ] );
            }

            /**
             * Get analytics data for transactions.
             *
             * @param array $transactions Transactions array.
             * @return array Analytics data (counts and amounts by status).
             */
            public function get_transactions_analytics( $transactions = array() ) {
                return \Better_Payment\Lite\Admin\DB::get_transactions_analytics_dashboard( $transactions );
            }
        };
    }

    /**
     * Build the settings array for layout templates.
     *
     * Maps block attributes to the Elementor widget settings format expected by layout templates.
     *
     * @param array  $attributes      Block attributes.
     * @param array  $global_settings Global plugin settings from database.
     * @param string $form_layout     The selected form layout.
     * @return array Settings array for layout templates.
     */
    public function build_block_settings( $attributes, $global_settings, $form_layout ) {
        // Convert block formFields array to Elementor format.
        $form_fields = $this->convert_form_fields( $attributes );

        // Convert block amountList array to Elementor format.
        $amount_list = $this->convert_amount_list( $attributes );

        // Get block attribute values with fallbacks to global settings.
        $paypal_enabled = isset( $attributes['paypalEnabled'] )
            ? ( $attributes['paypalEnabled'] ? 'yes' : '' )
            : ( ! empty( $global_settings['better_payment_settings_general_general_paypal'] ) && 'yes' === $global_settings['better_payment_settings_general_general_paypal'] ? 'yes' : '' );

        // Get PayPal business email from block attribute, fall back to global settings.
        $paypal_business_email = isset( $attributes['paypalBusinessEmail'] ) && ! empty( $attributes['paypalBusinessEmail'] )
            ? sanitize_email( $attributes['paypalBusinessEmail'] )
            : ( ! empty( $global_settings['better_payment_settings_payment_paypal_email'] ) ? $global_settings['better_payment_settings_payment_paypal_email'] : '' );

        $stripe_enabled = isset( $attributes['stripeEnabled'] )
            ? ( $attributes['stripeEnabled'] ? 'yes' : '' )
            : ( ! empty( $global_settings['better_payment_settings_general_general_stripe'] ) && 'yes' === $global_settings['better_payment_settings_general_general_stripe'] ? 'yes' : '' );

        $paystack_enabled = isset( $attributes['paystackEnabled'] )
            ? ( $attributes['paystackEnabled'] ? 'yes' : '' )
            : ( ! empty( $global_settings['better_payment_settings_general_general_paystack'] ) && 'yes' === $global_settings['better_payment_settings_general_general_paystack'] ? 'yes' : '' );

        // Get currency from block attribute or global settings.
        $currency = isset( $attributes['currency'] ) && ! empty( $attributes['currency'] )
            ? sanitize_text_field( $attributes['currency'] )
            : ( ! empty( $global_settings['better_payment_settings_general_general_currency'] ) ? $global_settings['better_payment_settings_general_general_currency'] : 'USD' );

        // Get sidebar show setting from block attribute.
        $sidebar_show = isset( $attributes['showSidebar'] )
            ? ( $attributes['showSidebar'] ? 'yes' : '' )
            : 'yes';

        // Get show amount list from block attribute.
        $show_amount_list = isset( $attributes['showAmountList'] )
            ? ( $attributes['showAmountList'] ? 'yes' : '' )
            : '';

        // Get currency alignment from block attribute.
        $currency_alignment = isset( $attributes['currencyAlign'] ) && ! empty( $attributes['currencyAlign'] )
            ? sanitize_text_field( $attributes['currencyAlign'] )
            : 'left';

        // Get payment source from block attribute.
        $payment_source = isset( $attributes['paymentSource'] ) && ! empty( $attributes['paymentSource'] )
            ? sanitize_text_field( $attributes['paymentSource'] )
            : '';

        // Get transaction details from block attributes.
        $transaction_title = isset( $attributes['transactionTitle'] ) && ! empty( $attributes['transactionTitle'] )
            ? sanitize_text_field( $attributes['transactionTitle'] )
            : __( 'Transaction Details', 'better-payment' );

        $transaction_sub_title = isset( $attributes['transactionSubTitle'] ) && ! empty( $attributes['transactionSubTitle'] )
            ? sanitize_text_field( $attributes['transactionSubTitle'] )
            : __( 'Total payment of your product in the following:', 'better-payment' );

        $amount_text = isset( $attributes['amountText'] ) && ! empty( $attributes['amountText'] )
            ? sanitize_text_field( $attributes['amountText'] )
            : __( 'Amount:', 'better-payment' );

        $product_title = isset( $attributes['transactionDetailsProductTitle'] ) && ! empty( $attributes['transactionDetailsProductTitle'] )
            ? sanitize_text_field( $attributes['transactionDetailsProductTitle'] )
            : __( 'Title:', 'better-payment' );

        // Get button text from block attributes.
        $paypal_button_text = isset( $attributes['paypalButtonText'] ) && ! empty( $attributes['paypalButtonText'] )
            ? sanitize_text_field( $attributes['paypalButtonText'] )
            : '';

        $stripe_button_text = isset( $attributes['stripeButtonText'] ) && ! empty( $attributes['stripeButtonText'] )
            ? sanitize_text_field( $attributes['stripeButtonText'] )
            : '';

        $paystack_button_text = isset( $attributes['paystackButtonText'] ) && ! empty( $attributes['paystackButtonText'] )
            ? sanitize_text_field( $attributes['paystackButtonText'] )
            : '';

        // Get product IDs from block attributes.
        $woocommerce_product_id = isset( $attributes['woocommerceProductId'] ) && ! empty( $attributes['woocommerceProductId'] )
            ? intval( $attributes['woocommerceProductId'] )
            : 0;

        $fluentcart_product_id = isset( $attributes['fluentcartProductId'] ) && ! empty( $attributes['fluentcartProductId'] )
            ? intval( $attributes['fluentcartProductId'] )
            : 0;

        // Get Stripe Price ID from block attributes.
        // Block attribute: stripeDefaultPriceId (string)
        // Used when payment source is 'stripe' (Stripe Product).
        $stripe_price_id = isset( $attributes['stripeDefaultPriceId'] ) && ! empty( $attributes['stripeDefaultPriceId'] )
            ? sanitize_text_field( $attributes['stripeDefaultPriceId'] )
            : '';

        // Validate Stripe Price ID format (should start with 'price_').
        if ( ! empty( $stripe_price_id ) && strpos( $stripe_price_id, 'price_' ) !== 0 ) {
            // Invalid format - log warning in debug mode and reset to empty.
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                error_log( 'Better Payment: Invalid Stripe Price ID format (should start with "price_"): ' . esc_html( $stripe_price_id ) );
            }
            $stripe_price_id = '';
        }

        // Get email notification setting from block attributes.
        // The editor default for emailNotificationEnabled is true, but WordPress does not
        // serialize default attribute values into the block markup and the registered
        // block.json declares no schema default, so the attribute is absent at render time
        // unless explicitly toggled. Treat "unset" as enabled to match the editor default,
        // while still honoring an explicit OFF (serialized as false).
        $email_notification_enabled = ! isset( $attributes['emailNotificationEnabled'] ) || $attributes['emailNotificationEnabled']
            ? 'yes'
            : '';

        // Get site domain for default email addresses.
        $site_url_parsed = wp_parse_url( get_site_url() );
        $site_domain     = ! empty( $site_url_parsed['host'] ) ? esc_html( $site_url_parsed['host'] ) : 'example.com';
        $default_from_email = 'wordpress@' . $site_domain;
        $default_email_subject = sprintf( __( 'Better Payment transaction on %s', 'better-payment' ), esc_html( get_option( 'blogname' ) ) );

        // Admin email settings from block attributes with fallback to global settings.
        // Block attribute: adminEmail (string)
        $email_to = isset( $attributes['adminEmail'] ) && ! empty( $attributes['adminEmail'] )
            ? $this->sanitize_email_list( $attributes['adminEmail'] )
            : ( ! 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' ) ) );

        // Block attribute: adminSubject (string)
        $email_subject = isset( $attributes['adminSubject'] ) && ! empty( $attributes['adminSubject'] )
            ? sanitize_text_field( $attributes['adminSubject'] )
            : ( ! empty( $global_settings['better_payment_settings_general_email_subject'] ) ? sanitize_text_field( $global_settings['better_payment_settings_general_email_subject'] ) : $default_email_subject );

        // Block attribute: adminMessage (string)
        $email_content = isset( $attributes['adminMessage'] ) && ! empty( $attributes['adminMessage'] )
            ? wp_kses_post( $attributes['adminMessage'] )
            : ( ! empty( $global_settings['better_payment_settings_general_email_message_admin'] ) ? wp_kses_post( $global_settings['better_payment_settings_general_email_message_admin'] ) : '' );

        // Block attribute: adminFromEmail (string)
        $email_from = isset( $attributes['adminFromEmail'] ) && ! empty( $attributes['adminFromEmail'] )
            ? sanitize_email( $attributes['adminFromEmail'] )
            : ( ! empty( $global_settings['better_payment_settings_general_email_from_email'] ) ? sanitize_email( $global_settings['better_payment_settings_general_email_from_email'] ) : $default_from_email );

        // Block attribute: adminFromName (string)
        $email_from_name = isset( $attributes['adminFromName'] ) && ! empty( $attributes['adminFromName'] )
            ? sanitize_text_field( $attributes['adminFromName'] )
            : ( ! 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' ) ) );

        // Block attribute: adminReplyTo (string)
        $email_reply_to = isset( $attributes['adminReplyTo'] ) && ! empty( $attributes['adminReplyTo'] )
            ? sanitize_email( $attributes['adminReplyTo'] )
            : ( ! empty( $global_settings['better_payment_settings_general_email_reply_to'] ) ? sanitize_email( $global_settings['better_payment_settings_general_email_reply_to'] ) : $default_from_email );

        // Block attribute: adminCc (string)
        $email_cc = isset( $attributes['adminCc'] ) && ! empty( $attributes['adminCc'] )
            ? $this->sanitize_email_list( $attributes['adminCc'] )
            : ( ! empty( $global_settings['better_payment_settings_general_email_cc'] ) ? $this->sanitize_email_list( $global_settings['better_payment_settings_general_email_cc'] ) : '' );

        // Block attribute: adminBcc (string)
        $email_bcc = isset( $attributes['adminBcc'] ) && ! empty( $attributes['adminBcc'] )
            ? $this->sanitize_email_list( $attributes['adminBcc'] )
            : ( ! empty( $global_settings['better_payment_settings_general_email_bcc'] ) ? $this->sanitize_email_list( $global_settings['better_payment_settings_general_email_bcc'] ) : '' );

        // Block attribute: adminSendAs (string: 'html' or 'plain')
        $allowed_content_types = array( 'html', 'plain' );
        $email_content_type = isset( $attributes['adminSendAs'] ) && in_array( $attributes['adminSendAs'], $allowed_content_types, true )
            ? $attributes['adminSendAs']
            : ( ! 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' );

        // Admin email display toggles (boolean attributes converted to 'yes'/'' for Elementor compatibility).
        // Block attribute: adminShowHeaderText (boolean, default: true)
        $email_content_heading = isset( $attributes['adminShowHeaderText'] ) ? ( $attributes['adminShowHeaderText'] ? 'yes' : '' ) : 'yes';
        // Block attribute: adminShowFromSection (boolean, default: true)
        $email_content_from_section = isset( $attributes['adminShowFromSection'] ) ? ( $attributes['adminShowFromSection'] ? 'yes' : '' ) : 'yes';
        // Block attribute: adminShowToSection (boolean, default: true)
        $email_content_to_section = isset( $attributes['adminShowToSection'] ) ? ( $attributes['adminShowToSection'] ? 'yes' : '' ) : 'yes';
        // Block attribute: adminShowTransactionSummary (boolean, default: true)
        $email_content_transaction_summary = isset( $attributes['adminShowTransactionSummary'] ) ? ( $attributes['adminShowTransactionSummary'] ? 'yes' : '' ) : 'yes';
        // Block attribute: adminShowFooterText (boolean, default: true)
        $email_content_footer_text = isset( $attributes['adminShowFooterText'] ) ? ( $attributes['adminShowFooterText'] ? 'yes' : '' ) : 'yes';

        // Customer email settings from block attributes with fallback to global settings.
        // Block attribute: customerSubject (string)
        $email_subject_customer = isset( $attributes['customerSubject'] ) && ! empty( $attributes['customerSubject'] )
            ? sanitize_text_field( $attributes['customerSubject'] )
            : ( ! 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 );

        // Block attribute: customerMessage (string)
        $email_content_customer = isset( $attributes['customerMessage'] ) && ! empty( $attributes['customerMessage'] )
            ? wp_kses_post( $attributes['customerMessage'] )
            : ( ! empty( $global_settings['better_payment_settings_general_email_message_customer'] ) ? wp_kses_post( $global_settings['better_payment_settings_general_email_message_customer'] ) : '' );

        // Block attribute: customerFromEmail (string)
        $email_from_customer = isset( $attributes['customerFromEmail'] ) && ! empty( $attributes['customerFromEmail'] )
            ? sanitize_email( $attributes['customerFromEmail'] )
            : ( ! 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 );

        // Block attribute: customerFromName (string)
        $email_from_name_customer = isset( $attributes['customerFromName'] ) && ! empty( $attributes['customerFromName'] )
            ? sanitize_text_field( $attributes['customerFromName'] )
            : ( ! 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' ) ) );

        // Block attribute: customerReplyTo (string)
        $email_reply_to_customer = isset( $attributes['customerReplyTo'] ) && ! empty( $attributes['customerReplyTo'] )
            ? sanitize_email( $attributes['customerReplyTo'] )
            : ( ! 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 );

        // Block attribute: customerCc (string)
        $email_cc_customer = isset( $attributes['customerCc'] ) && ! empty( $attributes['customerCc'] )
            ? $this->sanitize_email_list( $attributes['customerCc'] )
            : ( ! empty( $global_settings['better_payment_settings_general_email_cc_customer'] ) ? $this->sanitize_email_list( $global_settings['better_payment_settings_general_email_cc_customer'] ) : '' );

        // Block attribute: customerBcc (string)
        $email_bcc_customer = isset( $attributes['customerBcc'] ) && ! empty( $attributes['customerBcc'] )
            ? $this->sanitize_email_list( $attributes['customerBcc'] )
            : ( ! empty( $global_settings['better_payment_settings_general_email_bcc_customer'] ) ? $this->sanitize_email_list( $global_settings['better_payment_settings_general_email_bcc_customer'] ) : '' );

        // Block attribute: customerSendAs (string: 'html' or 'plain')
        $email_content_type_customer = isset( $attributes['customerSendAs'] ) && in_array( $attributes['customerSendAs'], $allowed_content_types, true )
            ? $attributes['customerSendAs']
            : ( ! 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' );

        // Customer email attachment settings.
        // Block attribute: customerPDFAttachment (boolean, default: false)
        $email_attachment_pdf_show = isset( $attributes['customerPDFAttachment'] ) && $attributes['customerPDFAttachment'] ? 'yes' : '';
        // Block attribute: customerFileAttachment (string - URL)
        $email_attachment = isset( $attributes['customerFileAttachment'] ) && ! empty( $attributes['customerFileAttachment'] )
            ? array( 'url' => esc_url( $attributes['customerFileAttachment'] ) )
            : array();

        // Get success message settings from block attributes.
        $success_icon = isset( $attributes['successIcon'] ) && ! empty( $attributes['successIcon'] )
            ? sanitize_text_field( $attributes['successIcon'] )
            : '';

        $success_heading = isset( $attributes['successHeading'] ) && ! empty( $attributes['successHeading'] )
            ? sanitize_text_field( $attributes['successHeading'] )
            : '';

        $success_sub_heading = isset( $attributes['successSubHeading'] ) && ! empty( $attributes['successSubHeading'] )
            ? sanitize_text_field( $attributes['successSubHeading'] )
            : '';

        $transaction_id_text = isset( $attributes['transactionID'] ) && ! empty( $attributes['transactionID'] )
            ? sanitize_text_field( $attributes['transactionID'] )
            : __( 'Transaction ID:', 'better-payment' );

        $thanks_message = isset( $attributes['thanksMessage'] ) && ! empty( $attributes['thanksMessage'] )
            ? sanitize_text_field( $attributes['thanksMessage'] )
            : __( 'Thank you for your payment.', 'better-payment' );

        $amount_message = isset( $attributes['amountMessage'] ) && ! empty( $attributes['amountMessage'] )
            ? sanitize_text_field( $attributes['amountMessage'] )
            : __( 'Amount', 'better-payment' );

        $currency_message = isset( $attributes['currencyMessage'] ) && ! empty( $attributes['currencyMessage'] )
            ? sanitize_text_field( $attributes['currencyMessage'] )
            : __( 'Currency', 'better-payment' );

        $payment_method_message = isset( $attributes['paymentMethodMessage'] ) && ! empty( $attributes['paymentMethodMessage'] )
            ? sanitize_text_field( $attributes['paymentMethodMessage'] )
            : __( 'Payment Method', 'better-payment' );

        $payment_type_message = isset( $attributes['paymentType'] ) && ! empty( $attributes['paymentType'] )
            ? sanitize_text_field( $attributes['paymentType'] )
            : __( 'Payment Type', 'better-payment' );

        $merchant_details_text = isset( $attributes['merchatDetails'] ) && ! empty( $attributes['merchatDetails'] )
            ? sanitize_text_field( $attributes['merchatDetails'] )
            : __( 'Merchant Details', 'better-payment' );

        $paid_amount_text = isset( $attributes['paidAmount'] ) && ! empty( $attributes['paidAmount'] )
            ? sanitize_text_field( $attributes['paidAmount'] )
            : __( 'Paid Amount', 'better-payment' );

        $purchase_details_text = isset( $attributes['purchaseDetails'] ) && ! empty( $attributes['purchaseDetails'] )
            ? sanitize_text_field( $attributes['purchaseDetails'] )
            : __( 'Purchase Details', 'better-payment' );

        $print_btn_text = isset( $attributes['printButtonText'] ) && ! empty( $attributes['printButtonText'] )
            ? sanitize_text_field( $attributes['printButtonText'] )
            : __( 'Print', 'better-payment' );

        $view_details_btn_text = isset( $attributes['viewDetailsButtonText'] ) && ! empty( $attributes['viewDetailsButtonText'] )
            ? sanitize_text_field( $attributes['viewDetailsButtonText'] )
            : __( 'View Details', 'better-payment' );

        $user_dashboard_url = isset( $attributes['userDashboardUrl'] ) && ! empty( $attributes['userDashboardUrl'] )
            ? esc_url( $attributes['userDashboardUrl'] )
            : '';

        $custom_redirect_url = isset( $attributes['customRedirectUrl'] ) && ! empty( $attributes['customRedirectUrl'] )
            ? esc_url( $attributes['customRedirectUrl'] )
            : '';

        // Get error message settings from block attributes.
        $error_icon = isset( $attributes['errorIcon'] ) && ! empty( $attributes['errorIcon'] )
            ? sanitize_text_field( $attributes['errorIcon'] )
            : '';

        // Email icon (block control) maps to Elementor email logo (image URL) when uploaded.
        $email_icon = isset( $attributes['emailIcon'] ) && ! empty( $attributes['emailIcon'] )
            ? sanitize_text_field( $attributes['emailIcon'] )
            : 'bp-icon bp-envelope';

        $error_heading = isset( $attributes['errorHeading'] ) && ! empty( $attributes['errorHeading'] )
            ? sanitize_text_field( $attributes['errorHeading'] )
            : __( 'Payment Failed', 'better-payment' );

        $error_sub_heading = isset( $attributes['errorSubHeading'] ) && ! empty( $attributes['errorSubHeading'] )
            ? sanitize_text_field( $attributes['errorSubHeading'] )
            : __( 'Your payment has failed. Please check your payment details', 'better-payment' );

        $error_transaction_id_text = isset( $attributes['transactionIDText'] ) && ! empty( $attributes['transactionIDText'] )
            ? sanitize_text_field( $attributes['transactionIDText'] )
            : __( 'Transaction ID', 'better-payment' );

        $error_show_details_button = isset( $attributes['showDetailsButton'] ) && $attributes['showDetailsButton'] ? 'yes' : '';

        $error_details_button_text = isset( $attributes['detailsButtonText'] ) && ! empty( $attributes['detailsButtonText'] )
            ? sanitize_text_field( $attributes['detailsButtonText'] )
            : __( 'View Details', 'better-payment' );

        $error_details_button_url = isset( $attributes['detailsButtonUrl'] ) && ! empty( $attributes['detailsButtonUrl'] )
            ? esc_url( $attributes['detailsButtonUrl'] )
            : '';

        $error_redirect_url = isset( $attributes['errorRedirectUrl'] ) && ! empty( $attributes['errorRedirectUrl'] )
            ? esc_url( $attributes['errorRedirectUrl'] )
            : '';

        return array(
            // Layout settings.
            'better_payment_form_layout' => $form_layout,

            // Form title - used as item/product name in PayPal and Stripe checkout.
            // Block attribute is 'formName' (not 'formTitle') — default is 'Better Payment'.
            'better_payment_form_title' => isset( $attributes['formName'] ) && ! empty( $attributes['formName'] )
                ? sanitize_text_field( $attributes['formName'] )
                : '',

            // Payment gateway settings - prioritize block attributes over global settings.
            'better_payment_form_paypal_enable'   => $paypal_enabled,
            'better_payment_form_stripe_enable'   => $stripe_enabled,
            'better_payment_form_paystack_enable' => $paystack_enabled,

            // Stripe API keys - resolve based on live mode (matches Elementor behavior).
            // Handler::stripe_payment_success() uses better_payment_stripe_secret_key directly.
            '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' : '',
            'better_payment_stripe_public_key'      => $this->resolve_stripe_key( $global_settings, 'public' ),
            'better_payment_stripe_secret_key'      => $this->resolve_stripe_key( $global_settings, 'secret' ),
            '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'] : '',
            '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'] : '',

            // PayPal API keys - prioritize block attributes over global settings.
            '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' : '',
            'better_payment_paypal_business_email' => $paypal_business_email,
            'better_payment_paypal_button_type'    => '_xclick', // Default PayPal button type for Buy Now functionality.

            // Paystack API keys - resolve based on live mode (matches Elementor behavior).
            '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' : '',
            'better_payment_paystack_public_key'      => $this->resolve_paystack_key( $global_settings, 'public' ),
            'better_payment_paystack_secret_key'      => $this->resolve_paystack_key( $global_settings, 'secret' ),
            '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'] : '',
            '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'] : '',

            // Currency settings from block attributes with fallback to global settings.
            'better_payment_form_currency'           => $currency,
            'better_payment_form_currency_alignment' => $currency_alignment,

            // Payment source settings from block attributes.
            'better_payment_form_payment_source'                  => $payment_source,
            'better_payment_form_payment_type'                    => '',
            'better_payment_form_woocommerce_product_id'          => $woocommerce_product_id,
            'better_payment_form_fluentcart_product_id'           => $fluentcart_product_id,
            'better_payment_form_payment_source_stripe_price_id'  => $stripe_price_id,

            // Sidebar settings from block attributes.
            'better_payment_form_sidebar_show'                      => $sidebar_show,
            'better_payment_form_transaction_details_heading'       => $transaction_title,
            'better_payment_form_transaction_details_sub_heading'   => $transaction_sub_title,
            'better_payment_form_transaction_details_product_title' => $product_title,
            'better_payment_form_transaction_details_amount_text'   => $amount_text,

            // Amount list settings from block attributes.
            'better_payment_show_amount_list' => $show_amount_list,
            'better_payment_amount'           => $amount_list,

            // Form fields from block attributes (converted to Elementor format).
            'better_payment_form_fields' => $form_fields,

            // Button text settings from block attributes.
            'better_payment_form_form_buttons_paypal_button_text'   => $paypal_button_text,
            'better_payment_form_form_buttons_stripe_button_text'   => $stripe_button_text,
            'better_payment_form_form_buttons_paystack_button_text' => $paystack_button_text,

            // Placeholder settings.
            'better_payment_placeholder_switch' => 'yes',

            // Email notification settings.
            'better_payment_form_email_enable' => $email_notification_enabled,

            // Admin email settings.
            'better_payment_email_to'                          => $email_to,
            'better_payment_email_subject'                     => $email_subject,
            'better_payment_email_content'                     => $email_content,
            'better_payment_email_from'                        => $email_from,
            'better_payment_email_from_name'                   => $email_from_name,
            'better_payment_email_reply_to'                    => $email_reply_to,
            'better_payment_email_cc'                          => $email_cc,
            'better_payment_email_bcc'                         => $email_bcc,
            'better_payment_email_content_type'                => $email_content_type,
            'better_payment_email_content_heading'             => $email_content_heading,
            'better_payment_email_content_from_section'        => $email_content_from_section,
            'better_payment_email_content_to_section'          => $email_content_to_section,
            'better_payment_email_content_transaction_summary' => $email_content_transaction_summary,
            'better_payment_email_content_footer_text'         => $email_content_footer_text,

            // Customer email settings.
            'better_payment_email_subject_customer'         => $email_subject_customer,
            'better_payment_email_content_customer'         => $email_content_customer,
            'better_payment_email_from_customer'            => $email_from_customer,
            'better_payment_email_from_name_customer'       => $email_from_name_customer,
            'better_payment_email_reply_to_customer'        => $email_reply_to_customer,
            'better_payment_email_cc_customer'              => $email_cc_customer,
            'better_payment_email_bcc_customer'             => $email_bcc_customer,
            'better_payment_email_content_type_customer'    => $email_content_type_customer,
            'better_payment_form_email_attachment_pdf_show' => $email_attachment_pdf_show,
            'better_payment_form_email_attachment'          => $email_attachment,
            'better_payment_form_email_logo'                => $this->build_email_logo_setting( $email_icon, 'bp-icon bp-envelope' ),

            // Success message settings from block attributes.
            'better_payment_form_success_message_icon'      => $this->build_icon_control_setting( $success_icon ),
            'better_payment_form_success_message_heading'           => $success_heading,
            'better_payment_form_success_message_sub_heading'       => $success_sub_heading,
            'better_payment_form_success_message_transaction'       => $transaction_id_text,
            'better_payment_form_success_message_thanks'            => $thanks_message,
            'better_payment_form_success_message_amount_text'       => $amount_message,
            'better_payment_form_success_message_currency_text'     => $currency_message,
            'better_payment_form_success_message_pay_method_text'   => $payment_method_message,
            'better_payment_form_success_message_pay_type_text'     => $payment_type_message,
            'better_payment_form_success_message_merchant_details_text' => $merchant_details_text,
            'better_payment_form_success_message_paid_amount_text'  => $paid_amount_text,
            'better_payment_form_success_message_purchase_details_text' => $purchase_details_text,
            'better_payment_form_success_message_print_btn_text'    => $print_btn_text,
            'better_payment_form_success_message_view_details_btn_text' => $view_details_btn_text,
            'better_payment_form_success_page_view_details_url'     => array(
                'url' => $user_dashboard_url,
            ),
            'better_payment_form_success_page_url' => array(
                'url' => $custom_redirect_url,
            ),

            // Error message settings from block attributes.
            'better_payment_form_error_message_icon' => $this->build_icon_control_setting( $error_icon ),
            'better_payment_form_error_message_heading' => $error_heading,
            'better_payment_form_error_message_sub_heading' => $error_sub_heading,
            'better_payment_form_error_message_transaction_id_text' => $error_transaction_id_text,
            'better_payment_form_error_details_button_switch' => $error_show_details_button,
            'better_payment_form_error_details_button_text' => $error_details_button_text,
            'better_payment_form_error_details_page_url' => array(
                'url' => $error_details_button_url,
            ),
            'better_payment_form_error_page_url'        => array(
                'url' => $error_redirect_url,
            ),
            // Flag to identify this form is from Gutenberg block (not Elementor)
            'better_payment_form_source' => 'block',
        );
    }

    /**
     * Resolve the correct Stripe key based on live mode setting.
     *
     * This matches Elementor's behavior where the key is resolved before being stored
     * in better_payment_stripe_public_key/better_payment_stripe_secret_key.
     * Handler::stripe_payment_success() uses better_payment_stripe_secret_key directly.
     *
     * @param array  $global_settings Global settings array.
     * @param string $key_type        Either 'public' or 'secret'.
     * @return string The resolved key.
     */
    private function resolve_stripe_key( $global_settings, $key_type ) {
        $is_live_mode = ! empty( $global_settings['better_payment_settings_payment_stripe_live_mode'] )
            && 'yes' === $global_settings['better_payment_settings_payment_stripe_live_mode'];

        if ( 'public' === $key_type ) {
            return $is_live_mode
                ? ( ! empty( $global_settings['better_payment_settings_payment_stripe_live_public'] ) ? $global_settings['better_payment_settings_payment_stripe_live_public'] : '' )
                : ( ! empty( $global_settings['better_payment_settings_payment_stripe_test_public'] ) ? $global_settings['better_payment_settings_payment_stripe_test_public'] : '' );
        } else {
            return $is_live_mode
                ? ( ! empty( $global_settings['better_payment_settings_payment_stripe_live_secret'] ) ? $global_settings['better_payment_settings_payment_stripe_live_secret'] : '' )
                : ( ! empty( $global_settings['better_payment_settings_payment_stripe_test_secret'] ) ? $global_settings['better_payment_settings_payment_stripe_test_secret'] : '' );
        }
    }

    /**
     * Resolve the correct Paystack key based on live mode setting.
     *
     * This matches Elementor's behavior where the key is resolved before being stored
     * in better_payment_paystack_public_key/better_payment_paystack_secret_key.
     *
     * @param array  $global_settings Global settings array.
     * @param string $key_type        Either 'public' or 'secret'.
     * @return string The resolved key.
     */
    private function resolve_paystack_key( $global_settings, $key_type ) {
        $is_live_mode = ! empty( $global_settings['better_payment_settings_payment_paystack_live_mode'] )
            && 'yes' === $global_settings['better_payment_settings_payment_paystack_live_mode'];

        if ( 'public' === $key_type ) {
            return $is_live_mode
                ? ( ! empty( $global_settings['better_payment_settings_payment_paystack_live_public'] ) ? $global_settings['better_payment_settings_payment_paystack_live_public'] : '' )
                : ( ! empty( $global_settings['better_payment_settings_payment_paystack_test_public'] ) ? $global_settings['better_payment_settings_payment_paystack_test_public'] : '' );
        } else {
            return $is_live_mode
                ? ( ! empty( $global_settings['better_payment_settings_payment_paystack_live_secret'] ) ? $global_settings['better_payment_settings_payment_paystack_live_secret'] : '' )
                : ( ! empty( $global_settings['better_payment_settings_payment_paystack_test_secret'] ) ? $global_settings['better_payment_settings_payment_paystack_test_secret'] : '' );
        }
    }

    /**
     * Sanitize a comma-separated list of email addresses.
     *
     * This method validates and sanitizes email addresses to prevent
     * email header injection attacks and ensure only valid emails are used.
     *
     * @param string $email_list Comma-separated list of email addresses.
     * @return string Sanitized comma-separated list of valid email addresses.
     */
    private function sanitize_email_list( $email_list ) {
        if ( empty( $email_list ) ) {
            return '';
        }

        // Split by comma and sanitize each email.
        $emails = array_map( 'trim', explode( ',', $email_list ) );
        $valid_emails = array();

        foreach ( $emails as $email ) {
            $sanitized = sanitize_email( $email );
            // Only include valid email addresses.
            if ( ! empty( $sanitized ) && is_email( $sanitized ) ) {
                $valid_emails[] = $sanitized;
            }
        }

        return implode( ', ', $valid_emails );
    }

    /**
     * Convert block formFields array to Elementor widget format.
     *
     * @param array $attributes Block attributes.
     * @return array Converted form fields in Elementor format.
     */
    private function convert_form_fields( $attributes ) {
        $form_fields = array();

        if ( empty( $attributes['formFields'] ) || ! is_array( $attributes['formFields'] ) ) {
            // Return default form fields if none provided.
            return array(
                array(
                    '_id'                                => 'first_name_field',
                    'better_payment_primary_field_type'  => 'primary_first_name',
                    'better_payment_field_name_heading'  => __( 'First Name', 'better-payment' ),
                    'better_payment_field_name_placeholder' => __( 'First Name', 'better-payment' ),
                    'better_payment_field_type'          => 'text',
                    'better_payment_field_icon'          => array( 'value' => 'bp-icon bp-user', 'library' => '' ),
                    'better_payment_field_name_required' => '',
                    'better_payment_field_name_show'     => 'yes',
                ),
                array(
                    '_id'                                => 'last_name_field',
                    'better_payment_primary_field_type'  => 'primary_last_name',
                    'better_payment_field_name_heading'  => __( 'Last Name', 'better-payment' ),
                    'better_payment_field_name_placeholder' => __( 'Last Name', 'better-payment' ),
                    'better_payment_field_type'          => 'text',
                    'better_payment_field_icon'          => array( 'value' => 'bp-icon bp-user', 'library' => '' ),
                    'better_payment_field_name_required' => '',
                    'better_payment_field_name_show'     => 'yes',
                ),
                array(
                    '_id'                                => 'email_field',
                    'better_payment_primary_field_type'  => 'primary_email',
                    'better_payment_field_name_heading'  => __( 'Email', 'better-payment' ),
                    'better_payment_field_name_placeholder' => __( 'Email Address', 'better-payment' ),
                    'better_payment_field_type'          => 'email',
                    'better_payment_field_icon'          => array( 'value' => 'bp-icon bp-envelope', 'library' => '' ),
                    'better_payment_field_name_required' => 'yes',
                    'better_payment_field_name_show'     => 'yes',
                ),
                array(
                    '_id'                                       => 'amount_field',
                    'better_payment_primary_field_type'         => 'primary_payment_amount',
                    'better_payment_field_name_heading'         => __( 'Amount', 'better-payment' ),
                    'better_payment_field_name_placeholder'     => __( 'Payment Amount', 'better-payment' ),
                    'better_payment_field_type'                 => 'number',
                    'better_payment_field_icon'                 => array( 'value' => 'bp-icon bp-logo-2', 'library' => '' ),
                    'better_payment_field_name_required'        => 'yes',
                    'better_payment_field_name_show'            => 'yes',
                    'better_payment_field_name_min'             => 1,
                    'better_payment_field_name_max'             => '',
                    'better_payment_field_name_default'         => '',
                    'better_payment_field_name_default_fixed'   => '',
                    'better_payment_field_name_default_dynamic_enable' => '',
                ),
            );
        }

        foreach ( $attributes['formFields'] as $index => $field ) {
            $raw_icon_value = isset( $field['icon'] ) ? sanitize_text_field( $field['icon'] ) : 'bp-icon bp-user';
            $icon_setting   = $this->build_icon_control_setting( $raw_icon_value, 'bp-icon bp-user' );

            $converted_field = array(
                '_id'                                => 'field_' . $index,
                'better_payment_primary_field_type'  => isset( $field['primaryFieldType'] ) ? sanitize_text_field( $field['primaryFieldType'] ) : '',
                'better_payment_field_name_heading'  => isset( $field['label'] ) ? sanitize_text_field( $field['label'] ) : '',
                'better_payment_field_name_placeholder' => isset( $field['placeholder'] ) ? sanitize_text_field( $field['placeholder'] ) : '',
                'better_payment_field_type'          => isset( $field['type'] ) ? sanitize_text_field( $field['type'] ) : 'text',
                'better_payment_field_icon'          => array(
                    'value'   => $icon_setting['value'],
                    'library' => $icon_setting['library'],
                ),
                'better_payment_field_name_required' => isset( $field['required'] ) && $field['required'] ? 'yes' : '',
                'better_payment_field_name_show'     => isset( $field['show'] ) && $field['show'] ? 'yes' : '',
                'better_payment_field_name_display_inline' => isset( $field['displayInline'] ) && $field['displayInline'] ? 'inline-block' : '',
            );

            // Add amount field specific settings.
            if ( isset( $field['primaryFieldType'] ) && 'primary_payment_amount' === $field['primaryFieldType'] ) {
                $converted_field['better_payment_field_name_min']     = isset( $field['min'] ) ? intval( $field['min'] ) : 1;
                $converted_field['better_payment_field_name_max']     = isset( $field['max'] ) ? intval( $field['max'] ) : '';
                $converted_field['better_payment_field_name_default'] = isset( $field['default'] ) ? intval( $field['default'] ) : '';
                $converted_field['better_payment_field_name_default_fixed'] = isset( $field['defaultFixed'] ) && $field['defaultFixed'] ? 'yes' : '';
                $converted_field['better_payment_field_name_default_dynamic_enable'] = isset( $field['defaultDynamicEnable'] ) && $field['defaultDynamicEnable'] ? 'yes' : '';
            }

            $form_fields[] = $converted_field;
        }

        return $form_fields;
    }

    /**
     * Convert block amountList array to Elementor widget format.
     *
     * @param array $attributes Block attributes.
     * @return array Converted amount list in Elementor format.
     */
    private function convert_amount_list( $attributes ) {
        $amount_list = array();

        if ( empty( $attributes['amountList'] ) || ! is_array( $attributes['amountList'] ) ) {
            return array();
        }

        foreach ( $attributes['amountList'] as $index => $item ) {
            if ( isset( $item['label'] ) && '' !== $item['label'] ) {
                $amount_list[] = array(
                    '_id'                       => 'amount_' . $index,
                    'better_payment_amount_val' => sanitize_text_field( $item['label'] ),
                );
            }
        }

        return $amount_list;
    }

    /**
     * Normalize icon classes for rendering compatibility.
     *
     * Dashicons require the base `dashicons` class plus the icon class.
     *
     * @param string $icon Icon class string.
     * @return string
     */
    private function normalize_icon_class( $icon ) {
        $icon = trim( (string) $icon );
        if ( '' === $icon ) {
            return $icon;
        }

        if ( false !== strpos( $icon, 'dashicons-' ) && false === strpos( $icon, 'dashicons ' ) ) {
            $icon = 'dashicons ' . $icon;
        }

        return $icon;
    }

    /**
     * Convert block icon value to Elementor ICONS control format.
     *
     * Keeps default state empty-library so existing default notice icons are shown.
     *
     * @param string $icon_value    Icon class or uploaded icon URL.
     * @param string $default_value Default icon class for this control.
     * @return array
     */
    private function build_icon_control_setting( $icon_value, $default_value = '' ) {
        $icon_value = trim( (string) $icon_value );

        // Only return empty library when icon_value is truly empty (no selection)
        // Don't treat icons matching the default as empty - they should render as custom icons
        if ( '' === $icon_value ) {
            return array(
                'value'   => '',
                'library' => '',
            );
        }

        // Check if the value is an image URL (SVG, PNG, JPG, etc.)
        if ( $this->is_icon_url( $icon_value ) ) {
            return array(
                'value'   => array(
                    'url' => esc_url_raw( $icon_value ),
                ),
                'library' => 'svg',
            );
        }

        // For all other non-empty values (custom CSS classes, FontAwesome, etc.)
        // Detect the library type and return it so Handler can render correctly
        return array(
            'value'   => $this->normalize_icon_class( $icon_value ),
            'library' => $this->detect_icon_library( $icon_value ),
        );
    }

    /**
     * Build email logo setting from block email icon value.
     *
     * Email template expects a media-like structure with a `url` key.
     *
     * @param string $icon_value    Icon class or uploaded icon URL.
     * @param string $default_value Default icon class for this control.
     * @return array
     */
    private function build_email_logo_setting( $icon_value, $default_value = '' ) {
        $icon_value = trim( (string) $icon_value );

        if ( '' === $icon_value || ( '' !== $default_value && $icon_value === $default_value ) ) {
            return array();
        }

        if ( $this->is_icon_url( $icon_value ) ) {
            return array(
                'url' => esc_url_raw( $icon_value ),
            );
        }

        return array();
    }

    /**
     * Detect Elementor-style icon library key from icon class string.
     *
     * @param string $icon_value Icon class.
     * @return string
     */
    private function detect_icon_library( $icon_value ) {
        $icon_value = (string) $icon_value;

        // Check for FontAwesome icons (has 'fa-' or 'fa ' prefix)
        if ( false !== strpos( $icon_value, 'fa-' ) || false !== strpos( $icon_value, 'fa ' ) ) {
            // Check for specific FontAwesome variants (fas, far, fab, fal, fad)
            if ( preg_match( '/\bfab\b/', $icon_value ) ) {
                return 'fab';  // FontAwesome Brands
            }
            if ( preg_match( '/\bfar\b/', $icon_value ) ) {
                return 'far';  // FontAwesome Regular
            }
            if ( preg_match( '/\bfal\b/', $icon_value ) ) {
                return 'fal';  // FontAwesome Light
            }
            if ( preg_match( '/\bfad\b/', $icon_value ) ) {
                return 'fad';  // FontAwesome Duotone
            }

            // Default to FontAwesome Solid
            return 'fas';
        }

        // Check for Dashicons
        if ( false !== strpos( $icon_value, 'dashicon' ) ) {
            return 'dashicons';
        }

        // Any other CSS class (custom icons like 'bp-icon bp-check-circle')
        return 'custom';
    }

    /**
     * Check whether an icon value is a URL-like image path.
     *
     * @param string $icon_value Icon value.
     * @return bool
     */
    private function is_icon_url( $icon_value ) {
        $icon_value = trim( (string) $icon_value );

        if ( '' === $icon_value ) {
            return false;
        }

        return 0 === strpos( $icon_value, 'http://' )
            || 0 === strpos( $icon_value, 'https://' )
            || 0 === strpos( $icon_value, '/' )
            || 0 === strpos( $icon_value, 'data:image/' );
    }

    /**
     * Create a widget proxy object for layout templates.
     *
     * This provides the methods that layout files expect from $widgetObj and $this.
     *
     * @param string $block_id The unique block ID.
     * @param array  $settings The settings array.
     * @return object Widget proxy object with required methods.
     */
    private function create_widget_proxy( $block_id, $settings ) {
        return new class( $block_id, $settings ) {
            /**
             * Block ID.
             *
             * @var string
             */
            private $id;

            /**
             * Settings array.
             *
             * @var array
             */
            private $settings;

            /**
             * Constructor.
             *
             * @param string $id       Block ID.
             * @param array  $settings Settings array.
             */
            public function __construct( $id, $settings ) {
                $this->id       = $id;
                $this->settings = $settings;
            }

            /**
             * Get the block ID.
             *
             * @return string Block ID.
             */
            public function get_id() {
                return $this->id;
            }

            /**
             * Get default payment amount text.
             *
             * @param array $settings Settings array.
             * @return string Default amount text.
             */
            public function render_attribute_default_text( $settings ) {
                $render_attribute_default_text = '';

                $items = ! empty( $settings['better_payment_form_fields'] ) ? $settings['better_payment_form_fields'] : array();

                foreach ( $items as $item ) {
                    // Check for primary_payment_amount field type.
                    if ( ! empty( $item['better_payment_primary_field_type'] ) && 'primary_payment_amount' === $item['better_payment_primary_field_type'] ) {
                        $render_attribute_default_text = ! empty( $item['better_payment_field_name_default'] )
                            ? $item['better_payment_field_name_default']
                            : '';
                        break;
                    }
                }

                return $render_attribute_default_text;
            }

            /**
             * Render amount selection element.
             *
             * @param array $settings Settings array.
             * @param array $args     Additional arguments.
             * @return void
             */
            public function render_amount_element( $settings, $args = array() ) {
                if ( empty( $settings['better_payment_amount'] ) ) {
                    return;
                }

                $items = $settings['better_payment_amount'];

                // Get currency symbol and alignment for display.
                $bp_helper_obj          = new \Better_Payment\Lite\Classes\Helper();
                $currency               = ! empty( $settings['better_payment_form_currency'] ) ? $settings['better_payment_form_currency'] : 'USD';
                $currency_symbol        = $bp_helper_obj->get_currency_symbol( esc_html( $currency ) );
                $currency_alignment     = ! empty( $settings['better_payment_form_currency_alignment'] ) ? $settings['better_payment_form_currency_alignment'] : 'left';
                $currency_left          = 'left' === $currency_alignment ? $currency_symbol : '';
                $currency_right         = 'right' === $currency_alignment ? $currency_symbol : '';

                foreach ( $items as $item ) :
                    if ( ! empty( $item['better_payment_amount_val'] ) ) :
                        $uid   = uniqid();
                        $value = floatval( $item['better_payment_amount_val'] );
                        ?>
                        <div class="bp-form__group pt-5">
                            <input type="radio" value="<?php echo esc_attr( $value ); ?>"
                                id="bp_payment_amount-<?php echo esc_attr( $uid ); ?>"
                                class="bp-form__control bp-form_pay-radio"
                                name="primary_payment_amount_radio">
                            <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>
                        </div>
                        <?php
                    endif;
                endforeach;
            }

            /**
             * Render campaign ID hidden field.
             *
             * @param array $settings Settings array.
             * @return string Empty string for blocks.
             */
            public function render_campaign_id_hidden_field( $settings ) {
                $campaign_id       = ! empty( $_GET['campaign_id'] ) ? sanitize_text_field( $_GET['campaign_id'] ) : '';
                $campaign_currency = ! empty( $_GET['campaign_currency'] ) ? sanitize_text_field( $_GET['campaign_currency'] ) : '';

                return '
                    <input type="hidden" name="campaign_id" value="' . esc_attr( $campaign_id ) . '">
                    <input type="hidden" name="campaign_currency" value="' . esc_attr( $campaign_currency ) . '">
                ';
            }
        };
    }

    /**
     * Enqueue editor assets (controls library).
     *
     * @return void
     */
    public function enqueue_editor_assets() {
        // Only load on block editor pages
        $screen = get_current_screen();
        if ( ! $screen || ! $screen->is_block_editor() ) {
            return;
        }

        global $pagenow;
        $editor_type = false;
        if ( $pagenow === 'post-new.php' || $pagenow === 'post.php' ) {
            $editor_type = 'edit-post';
        } elseif ( $pagenow === 'site-editor.php' || ( $pagenow === 'themes.php' && isset( $_GET[ 'page' ] ) && sanitize_text_field( wp_unslash( $_GET[ 'page' ] ) ) === 'gutenberg-edit-site' ) ) {
            $editor_type = 'edit-site';
        } elseif ( $pagenow === 'widgets.php' ) {
            $editor_type = 'edit-widgets';
        }

        // Define EssentialBlocksLocalize before controls load (controls access it at module level)
        $localize_data = array(
            'ajaxurl'               => admin_url( 'admin-ajax.php' ),
            'ajax_url'              => admin_url( 'admin-ajax.php' ),
            'nonce'                 => wp_create_nonce( 'better_payment_block_nonce' ),
            'admin_nonce'           => wp_create_nonce( 'better_payment_admin_nonce' ),
            'rest_rootURL'          => esc_url_raw( rest_url() ),
            'is_pro_active'         => 'false',
            'eb_plugins_url'        => BETTER_PAYMENT_URL . '/',
            'image_url'             => BETTER_PAYMENT_ASSETS . '/img',
            'fontAwesome'           => 'true',
            'googleFont'            => 'true',
            'quickToolbar'          => 'false',
            'enableGenerateImage'   => '0',
            'enableWriteAIInputField' => '0',
            'enableWriteAIRichtext' => '0',
            'enableRewriteAIContent' => '0',
            'hasOpenAiApiKey'       => '0',
            'all_blocks_default'   => array(),
            'all_blocks'           => array(),
            'quick_toolbar_blocks' => array(),
            'responsiveBreakpoints' => array(
                'tablet' => 1024,
                'mobile' => 767,
            ),
        );

        // Add inline script to wp-editor which is guaranteed to load in block editor
        wp_add_inline_script(
            'wp-editor',
            'window.EssentialBlocksLocalize = window.EssentialBlocksLocalize || ' . wp_json_encode( $localize_data ) . ';'
        );

        // Enqueue necessary CSS files for form styles in editor (used in render.php)
        wp_enqueue_style( 'better-payment-el' );
        wp_enqueue_style( 'bp-icon-front' );
        wp_enqueue_style( 'better-payment-style' );
        wp_enqueue_style( 'better-payment-common-style' );
        wp_enqueue_style( 'better-payment-admin-style' );
        wp_enqueue_style( 'bp-dashboard-pagination-style' );
        $this->enqueue_font_awesome();
        wp_enqueue_style( 'dashicons' );

        // Build block data: start with common keys, then merge each block's own data.
        // To add data for a new block, implement get_{block-key}_editor_data() and add
        // 'editor_data' => 'get_{block-key}_editor_data' to the $blocks registry above.
        $block_data = $this->get_common_editor_data();
        foreach ( $this->blocks as $block_config ) {
            if ( ! empty( $block_config['editor_data'] ) && method_exists( $this, $block_config['editor_data'] ) ) {
                $block_data = array_merge( $block_data, $this->{$block_config['editor_data']}() );
            }
        }

        // Canonical filter for the full merged data; the legacy alias preserves backwards
        // compatibility with any pro-plugin hooks on the old per-block filter name.
        $block_data = apply_filters( 'better_payment/block/editor_block_data', $block_data );
        $block_data = apply_filters( 'better_payment/block/user_dashboard/editor_block_data', $block_data );

        wp_add_inline_script(
            'wp-editor',
            'window.betterPaymentBlockData = ' . wp_json_encode( $block_data ) . ';'
        );

        wp_add_inline_script(
            'wp-editor',
            'window.eb_conditional_localize = ' . wp_json_encode(
                $editor_type !== false ? [ 'editor_type' => $editor_type ] : []
            ) . ';'
        );
    }

    /**
     * Enqueue frontend assets for blocks
     * Called on wp_enqueue_scripts hook to allow pro plugin to enqueue scripts early
     *
     * @return void
     */
    public function enqueue_frontend_assets() {
        // Allow pro plugin to enqueue scripts for the user dashboard block
        do_action( 'better_payment/block/user_dashboard/wp_enqueue_scripts' );
    }

    /**
     * Return the site domain (host only, no protocol) for use in editor data.
     *
     * @return string
     */
    private function get_site_domain() {
        $parsed = wp_parse_url( get_site_url() );
        return ! empty( $parsed['host'] ) ? esc_html( $parsed['host'] ) : 'example.com';
    }

    /**
     * Return plugin settings with all gateway secret/private keys removed.
     *
     * Safe to expose to any block editor user regardless of role. Only the keys
     * that the editor UI actually reads (email defaults, gateway enable flags,
     * public keys) are kept — secrets never leave the server.
     *
     * @return array
     */
    private function get_safe_settings_for_editor() {
        $settings = get_option( 'better_payment_settings', array() );
        if ( ! is_array( $settings ) ) {
            return array();
        }

        $sensitive_keys = array(
            'better_payment_settings_payment_paypal_live_secret',
            'better_payment_settings_payment_paypal_test_secret',
            'better_payment_settings_payment_stripe_live_secret',
            'better_payment_settings_payment_stripe_test_secret',
            'better_payment_settings_payment_paystack_live_secret',
            'better_payment_settings_payment_paystack_test_secret',
        );

        foreach ( $sensitive_keys as $key ) {
            unset( $settings[ $key ] );
        }

        return $settings;
    }

    /**
     * Return data shared by every registered block in the editor.
     *
     * All blocks can read these keys from window.betterPaymentBlockData.
     * Adding a new block? Common data lives here; block-specific data goes in
     * get_{block-key}_editor_data() and is registered via $blocks['editor_data'].
     *
     * @return array
     */
    private function get_common_editor_data() {
        return array(
            'ajaxurl'    => admin_url( 'admin-ajax.php' ),
            'nonce'      => wp_create_nonce( 'better_payment_block_nonce' ),
            'currencies' => $this->get_currency_list(),
            'assetUrl'   => BETTER_PAYMENT_ASSETS,
            'assetsUrl'  => BETTER_PAYMENT_ASSETS,
            'siteName'   => get_bloginfo( 'name' ),
            'siteDomain' => $this->get_site_domain(),
            'proEnabled' => apply_filters( 'better_payment/pro_enabled', false ),
        );
    }

    /**
     * Return editor data specific to the payment-form block.
     *
     * Provides the global settings (secrets stripped) so the inspector can
     * pre-populate email defaults, and the site admin email for the To field.
     *
     * @return array
     */
    private function get_payment_form_editor_data() {
        return array(
            'betterPaymentSettings' => $this->get_safe_settings_for_editor(),
            'adminEmail'            => sanitize_email( get_option( 'admin_email' ) ),
        );
    }

    /**
     * Return editor data specific to the user-dashboard block.
     *
     * Fetches the current editor user's transactions, subscriptions, and analytics
     * so the block preview shows real data immediately on insertion without a REST
     * round-trip. Normalises each row to the same flat shape the UserAPI returns so
     * the JS preview component only needs to handle one structure.
     *
     * @return array
     */
    private function get_user_dashboard_editor_data() {
        $current_user = wp_get_current_user();

        // Cache the 5 DB queries for the duration of the browser session in the editor.
        // The cache is keyed per user so each editor sees only their own data.
        // TTL: 5 minutes — short enough to reflect recent transactions without hammering
        // the DB on every page navigation inside the block editor.
        $cache_key   = 'bp_editor_dash_' . ( $current_user->ID ?? 0 );
        $cache_group = 'better_payment';
        $cached      = wp_cache_get( $cache_key, $cache_group );
        if ( false !== $cached ) {
            // Nonces are request-scoped and must never be cached; regenerate on every hit.
            $cached['restNonce'] = wp_create_nonce( 'wp_rest' );
            return $cached;
        }

        $user_transactions      = array();
        $user_subscriptions     = array();
        $user_analytics         = array();
        $user_transactions_meta = array( 'total' => 0, 'pages' => 1, 'perPage' => 20 );
        // Safe default ensures userSubscriptionsMeta is always fully defined even when
        // the user has no subscriptions or is not logged in.
        $sub_result             = array( 'total' => 0, 'pages' => 1, 'per_page' => 20, 'transactions' => array() );

        // Always fetch in the editor — has_block() only checks saved content and returns
        // false before first save, leaving the block with empty data on insertion.
        if ( $current_user && $current_user->ID ) {
            $user_analytics = \Better_Payment\Lite\Admin\DB::get_user_analytics_by_email( $current_user->user_email );

            $paged_result = \Better_Payment\Lite\Admin\DB::get_user_transactions_paginated(
                $current_user->user_email,
                array(
                    'page'     => 1,
                    'per_page' => 20,
                    'type'     => 'transactions',
                )
            );

            $raw_transactions       = $paged_result['transactions'];
            $user_transactions_meta = array(
                'total'   => $paged_result['total'],
                'pages'   => $paged_result['pages'],
                'perPage' => $paged_result['per_page'],
            );

            // Unserialize and normalise to match UserAPI REST response format.
            foreach ( $raw_transactions as $tx ) {
                if ( ! empty( $tx->form_fields_info ) ) {
                    $tx->form_fields_info = maybe_unserialize( $tx->form_fields_info );
                }
                if ( ! empty( $tx->customer_info ) ) {
                    $tx->customer_info = maybe_unserialize( $tx->customer_info );
                }

                $ffi           = is_array( $tx->form_fields_info ) ? $tx->form_fields_info : array();
                $customer_name = isset( $ffi['primary_first_name'] )
                    ? trim( sanitize_text_field( $ffi['primary_first_name'] ) . ' ' . sanitize_text_field( $ffi['primary_last_name'] ?? '' ) )
                    : '';
                if ( empty( $customer_name ) ) {
                    $customer_name = isset( $ffi['first_name'] )
                        ? trim( sanitize_text_field( $ffi['first_name'] ) . ' ' . sanitize_text_field( $ffi['last_name'] ?? '' ) )
                        : '';
                }
                $tx->customer_name  = $customer_name;
                $tx->customer_email = sanitize_text_field( $ffi['primary_email'] ?? ( $ffi['email'] ?? '' ) );
            }

            $user_transactions = apply_filters( 'better_payment/block/user_dashboard/enrich_user_transactions', $raw_transactions );

            // Separate query for subscriptions (not a subset of transactions page 1).
            $sub_result        = \Better_Payment\Lite\Admin\DB::get_user_transactions_paginated(
                $current_user->user_email,
                array(
                    'page'     => 1,
                    'per_page' => 20,
                    'type'     => 'subscriptions',
                )
            );

            $raw_subscriptions = $sub_result['transactions'];
            foreach ( $raw_subscriptions as $tx ) {
                if ( ! empty( $tx->form_fields_info ) ) {
                    $tx->form_fields_info = maybe_unserialize( $tx->form_fields_info );
                }
            }
            $user_subscriptions = apply_filters( 'better_payment/block/user_dashboard/enrich_user_subscriptions', $raw_subscriptions );

            // Flatten each subscription to match UserAPI flat format so JS handles one structure.
            foreach ( $user_subscriptions as $tx ) {
                $ffi = is_array( $tx->form_fields_info ) ? $tx->form_fields_info : array();

                $tx->subscription_id                 = sanitize_text_field( $ffi['subscription_id'] ?? '' );
                $tx->subscription_plan_id            = sanitize_text_field( $ffi['subscription_plan_id'] ?? '' );
                $tx->subscription_status             = sanitize_text_field( $ffi['subscription_status'] ?? '' );
                $tx->subscription_interval           = sanitize_text_field( $ffi['subscription_interval'] ?? '' );
                $tx->subscription_created_date       = intval( $ffi['subscription_created_date'] ?? 0 );
                $tx->subscription_current_period_end = intval( $ffi['subscription_current_period_end'] ?? 0 );
                $tx->is_payment_split_payment        = ! empty( $ffi['is_payment_split_payment'] ) ? intval( $ffi['is_payment_split_payment'] ) : 0;
                $tx->subscription_product_name       = sanitize_text_field( $ffi['subscription_product_name'] ?? '' );
                $tx->is_subscription                 = ! empty( $ffi['subscription_id'] ) ? 'Subscription' : 'One Time';

                $customer_name = isset( $ffi['primary_first_name'] )
                    ? trim( sanitize_text_field( $ffi['primary_first_name'] ) . ' ' . sanitize_text_field( $ffi['primary_last_name'] ?? '' ) )
                    : '';
                if ( empty( $customer_name ) ) {
                    $customer_name = isset( $ffi['first_name'] )
                        ? trim( sanitize_text_field( $ffi['first_name'] ) . ' ' . sanitize_text_field( $ffi['last_name'] ?? '' ) )
                        : '';
                }
                $tx->customer_name  = $customer_name;
                $tx->customer_email = sanitize_text_field( $ffi['primary_email'] ?? ( $ffi['email'] ?? '' ) );

                unset( $tx->form_fields_info );
            }
        }

        $data = array(
            'restNonce'             => wp_create_nonce( 'wp_rest' ),
            'restUrl'               => get_rest_url( null, 'better-payment/v1/user-transactions' ),
            'currentUser'           => array(
                'id'             => $current_user->ID,
                'user_email'     => $current_user->user_email,
                'user_login'     => $current_user->user_login,
                'user_avatar_url'=> get_avatar_url( $current_user->user_email, array( 'size' => 32 ) ),
                'email'          => $current_user->user_email,
                'login'          => $current_user->user_login,
                'avatar'         => get_avatar_url( $current_user->user_email, array( 'size' => 32 ) ),
            ),
            'userTransactions'      => $user_transactions,
            'userTransactionsMeta'  => $user_transactions_meta,
            'userSubscriptions'     => $user_subscriptions,
            'userSubscriptionsMeta' => array(
                'total'   => (int) $sub_result['total'],
                'pages'   => (int) $sub_result['pages'],
                'perPage' => 20,
            ),
            'userAnalytics'         => $user_analytics,
            'proAssets'             => array(
                'analyticsReportsBanner'      => BETTER_PAYMENT_ASSETS . '/img/user-dashboard-analytics-reports-pro-banner.png',
                'recurringSubscriptionBanner' => BETTER_PAYMENT_ASSETS . '/img/user-dashboard-recurring-subscription-pro-banner.png',
                'splitSubscriptionBanner'     => BETTER_PAYMENT_ASSETS . '/img/user-dashboard-split-subscription-pro-banner.png',
                'subscriptionProBanner'       => BETTER_PAYMENT_ASSETS . '/img/user-dashboard-subscription-pro-banner.png',
            ),
        );

        // Store in the object cache for 5 minutes. nonce is excluded from the cached payload
        // because nonces are request-scoped — storing them would return stale nonces on cache hit.
        // The restNonce above is regenerated fresh on every request (not cached).
        $cacheable = $data;
        unset( $cacheable['restNonce'] );
        wp_cache_set( $cache_key, $cacheable, $cache_group, 5 * MINUTE_IN_SECONDS );

        return $data;
    }

    /**
     * Get the current editor type.
     *
     * @return string
     */
    private function get_editor_type() {
        if ( function_exists( 'get_current_screen' ) ) {
            $screen = get_current_screen();
            if ( $screen && method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() ) {
                return 'edit-post';
            }
        }
        return 'edit-post';
    }

    /**
     * Register the Better Payment block category.
     *
     * @param array                   $categories Block categories.
     * @param WP_Block_Editor_Context $context    Block editor context.
     * @return array Modified block categories.
     */
    public function register_block_category( $categories, $context ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
        // Check if our category already exists
        foreach ( $categories as $category ) {
            if ( 'better-payment' === $category['slug'] ) {
                return $categories;
            }
        }

        // Add our category at the beginning
        array_unshift(
            $categories,
            array(
                'slug'  => 'better-payment',
                'title' => __( 'Better Payment', 'better-payment' ),
                'icon'  => '',
            )
        );

        return $categories;
    }

    /**
     * Register all blocks.
     *
     * @return void
     */
    public function register_blocks() {
        if ( ! function_exists( 'register_block_type' ) ) {
            return;
        }

        foreach ( $this->blocks as $block_key => $block_config ) {
            if ( ! $this->is_block_registered( $block_config['name'] ) ) {
                $this->register_single_block( $block_key, $block_config );
            }
        }
    }

    /**
     * Register a single block.
     *
     * @param string $block_key    Block key.
     * @param array  $block_config Block configuration.
     * @return void
     */
    private function register_single_block( $block_key, $block_config ) {
        // Use assets/blocks path for block.json since src/ is excluded from production builds.
        // The block.json is copied to assets/blocks/ during the build process.
        $block_json_path = BETTER_PAYMENT_PATH . '/assets/blocks/blocks/' . $block_config['path'] . '/block.json';

        // Fallback to src/blocks for development environment (assets may not be built yet)
        if ( ! file_exists( $block_json_path ) ) {
            $block_json_path = BETTER_PAYMENT_PATH . '/src/blocks/' . $block_config['path'] . '/block.json';
        }

        // Check if block.json exists
        if ( ! file_exists( $block_json_path ) ) {
            return;
        }

        // Get asset file for dependencies
        $asset_file = $this->get_asset_file( $block_config['path'] );

        // Add controls as a dependency
        $dependencies = array_merge( $asset_file['dependencies'], array( 'better-payment-controls' ) );

        // Register editor script
        $editor_script_handle = 'better-payment-' . $block_key . '-editor';

        if ( file_exists( BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_config['path'] . '/index.min.js' ) ) {
            wp_register_script(
                $editor_script_handle,
                BETTER_PAYMENT_ASSETS . '/blocks/' . $block_config['path'] . '/index.min.js',
                $dependencies,
                $asset_file['version'],
                true
            );
        } elseif ( file_exists( BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_config['path'] . '/index.js' ) ) {
            wp_register_script(
                $editor_script_handle,
                BETTER_PAYMENT_ASSETS . '/blocks/' . $block_config['path'] . '/index.js',
                $dependencies,
                $asset_file['version'],
                true
            );
        }

        // Register frontend/editor style
        $style_handle = 'better-payment-' . $block_key . '-style';
        $style_dependencies = array(
            'better-payment-controls-style',
            'better-payment-el',
            'bp-icon-front',
            'better-payment-style',
            'better-payment-common-style',
            'better-payment-admin-style',
            'dashicons',
        );
        if ( ! $this->is_font_awesome_provided() ) {
            $style_dependencies[] = 'bp-font-awesome';
        }
        if ( file_exists( BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_config['path'] . '/style.min.css' ) ) {
            wp_register_style(
                $style_handle,
                BETTER_PAYMENT_ASSETS . '/blocks/' . $block_config['path'] . '/style.min.css',
                $style_dependencies,
                $asset_file['version']
            );
        } elseif ( file_exists( BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_config['path'] . '/style.css' ) ) {
            wp_register_style(
                $style_handle,
                BETTER_PAYMENT_ASSETS . '/blocks/' . $block_config['path'] . '/style.css',
                $style_dependencies,
                $asset_file['version']
            );
        }

        // Build block registration args
        $block_args = array(
            'editor_script' => $editor_script_handle,
            'style'         => $style_handle,
        );

        // Add render callback if defined in block config
        if ( ! empty( $block_config['render_callback'] ) && method_exists( $this, $block_config['render_callback'] ) ) {
            $block_args['render_callback'] = array( $this, $block_config['render_callback'] );
        }

        // Register block with WordPress
        $result = register_block_type( $block_json_path, $block_args );

        if ( false === $result ) {
            // Log error in debug mode
            if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                error_log( 'Better Payment: Failed to register block ' . $block_config['name'] );
            }
        }
    }

    /**
     * Get asset file with dependencies and version.
     *
     * @param string $block_path Block path.
     * @return array Asset file data.
     */
    private function get_asset_file( $block_path ) {
        $asset_file_path = BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_path . '/index.min.asset.php';

        if ( ! file_exists( $asset_file_path ) ) {
            $asset_file_path = BETTER_PAYMENT_PATH . '/assets/blocks/' . $block_path . '/index.asset.php';
        }

        if ( file_exists( $asset_file_path ) ) {
            return include $asset_file_path;
        }

        return array(
            'dependencies' => array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n' ),
            'version'      => BETTER_PAYMENT_VERSION,
        );
    }


    /**
     * Get the blocks directory path.
     *
     * @return string
     */
    public function get_blocks_path() {
        return BETTER_PAYMENT_PATH . '/src/blocks/';
    }

    /**
     * Get the blocks assets URL.
     *
     * @return string
     */
    public function get_blocks_url() {
        return BETTER_PAYMENT_ASSETS . '/blocks/';
    }

    /**
     * Check if a block is registered.
     *
     * @param string $block_name Full block name (e.g., 'better-payment/payment-form').
     * @return bool
     */
    public function is_block_registered( $block_name ) {
        return \WP_Block_Type_Registry::get_instance()->is_registered( $block_name );
    }

    /**
     * Check if Font Awesome is already provided by another plugin (e.g. Elementor).
     *
     * @return bool
     */
    private function is_font_awesome_provided() {
        $handles = [
            'font-awesome-5-all',
            'font-awesome-6-all',
            'elementor-icons-fa-solid',
            'elementor-icons-fa-brands',
            'elementor-icons-fa-regular',
        ];

        foreach ( $handles as $handle ) {
            if ( wp_style_is( $handle, 'registered' ) || wp_style_is( $handle, 'enqueued' ) ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Enqueue Font Awesome only if not already provided by another plugin.
     *
     * @return void
     */
    private function enqueue_font_awesome() {
        if ( ! $this->is_font_awesome_provided() ) {
            wp_enqueue_style( 'bp-font-awesome' );
        }
    }
}

```
