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 '
' . esc_html__( 'Payment form template not found.', 'better-payment' ) . '
'; } } // 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 ) { // A visitor gets nothing: the block has no form to show, and the // instruction that follows is addressed to whoever can act on it. // It used to be printed for everyone, so a public page advertised // that the site owner had not finished setting up payments and named // the admin screen where it is done. if ( ! current_user_can( 'manage_options' ) ) { return ''; } return '
' . $this->render_gateway_notice( $attributes, $global_settings ) . '
'; } // 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 '
' . $manage_response_output . '
'; } // 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(); ?>
>
' . esc_html__( 'User Dashboard template not found.', 'better-payment' ) . ''; } } // 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(); ?>
> 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 ); ?>
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', ); } /** * Why a Payment Form block has no gateway, decided per gateway. * * Each gateway resolves in `build_block_settings()` from the block's own * toggle when the block saved one, and from the site setting only when it * did not. So a gateway is off for one of two reasons, and they are fixed in * different places: * * - `block` — the site has a gateway enabled, but this block switches it * off. Settings cannot fix it: the gateway is already on there. * - `global` — no gateway is enabled site-wide. Settings fixes it on its own * for any gateway the block leaves to the site (`settings_fixes`); a * gateway the block also switched off needs both (`needs_block`). A new * block seeds its toggles from the settings, so on a site with nothing * enabled `needs_block` is the usual case. * * Mirrors the editor's `gatewayNoticeState()` (helpers/gatewayNotice.js), so * the editor and the page never give two different answers. * * @param array $attributes Block attributes. * @param array $global_settings Global plugin settings. * @return array{reason: string, site_gateways: string[], settings_fixes: string[], needs_block: bool} */ public static function gateway_notice_state( $attributes, $global_settings ) { $gateways = array( 'PayPal' => array( 'paypalEnabled', 'better_payment_settings_general_general_paypal' ), 'Stripe' => array( 'stripeEnabled', 'better_payment_settings_general_general_stripe' ), 'Paystack' => array( 'paystackEnabled', 'better_payment_settings_general_general_paystack' ), ); $site_gateways = array(); $settings_fixes = array(); foreach ( $gateways as $label => $keys ) { if ( isset( $global_settings[ $keys[1] ] ) && 'yes' === $global_settings[ $keys[1] ] ) { $site_gateways[] = $label; } if ( ! isset( $attributes[ $keys[0] ] ) ) { $settings_fixes[] = $label; } } $reason = empty( $site_gateways ) ? 'global' : 'block'; return array( 'reason' => $reason, 'site_gateways' => $site_gateways, 'settings_fixes' => $settings_fixes, 'needs_block' => 'block' === $reason || empty( $settings_fixes ), ); } /** * The admin-only notice printed in place of a form with no gateway. * * Names the gateways and the place that fixes them, and links to each place * the fix needs: Better Payment settings, this block's editor, or both. The * editor link is offered only when the block is really in the current post's * content — in a template, template part, synced pattern or widget area the * current post's editor does not contain it, so the notice says where to look * instead of linking to the wrong screen. * * @param array $attributes Block attributes. * @param array $global_settings Global plugin settings. * @return string Notice HTML. */ private function render_gateway_notice( $attributes, $global_settings ) { $state = self::gateway_notice_state( $attributes, $global_settings ); if ( 'block' === $state['reason'] ) { $message = sprintf( /* translators: %s: gateway names, e.g. "PayPal or Stripe". */ __( 'Payment form: every payment method is switched off in this block’s settings. Turn on %s under Payment Settings in the block sidebar.', 'better-payment' ), self::join_gateway_names( $state['site_gateways'] ) ); } elseif ( $state['needs_block'] ) { $message = __( 'Payment form: no payment gateway is enabled in Better Payment settings, and every payment method is switched off in this block’s settings. Enable PayPal, Stripe or Paystack in Better Payment settings, then turn the same gateway on under Payment Settings in the block sidebar.', 'better-payment' ); } else { $message = sprintf( /* translators: %s: gateway names, e.g. "PayPal, Stripe or Paystack". */ __( 'Payment form: enable at least one payment gateway (%s) in Better Payment settings.', 'better-payment' ), self::join_gateway_names( $state['settings_fixes'] ) ); } $links = array(); if ( 'global' === $state['reason'] ) { $links[] = '' . esc_html__( 'Open Better Payment settings', 'better-payment' ) . ''; } $hint = ''; if ( $state['needs_block'] ) { $edit_url = self::block_edit_url(); if ( '' !== $edit_url ) { $links[] = '' . esc_html__( 'Edit this form in the block editor', 'better-payment' ) . ''; } else { $hint = '

' . esc_html__( 'This form is not part of this page’s content. Open the template, template part, pattern or widget area that contains it to change its settings.', 'better-payment' ) . '

'; } } return '
' . '

' . esc_html( $message ) . ' ' . esc_html__( 'This notice is only visible to you.', 'better-payment' ) . '

' . $hint . ( $links ? '

' . implode( ' ', $links ) . '

' : '' ) . '
'; } /** * The editor URL of the current post, when it holds a Payment Form block. * * Empty when there is no current post, the user may not edit it, or its * content has no Payment Form block (the block came from a template, template * part, synced pattern or widget area). * * @return string Raw edit URL, or ''. */ private static function block_edit_url() { $post_id = (int) get_the_ID(); if ( $post_id <= 0 || ! current_user_can( 'edit_post', $post_id ) ) { return ''; } if ( ! has_block( 'better-payment/payment-form', $post_id ) ) { return ''; } return (string) get_edit_post_link( $post_id, 'raw' ); } /** * "A", "A or B", "A, B or C". * * @param string[] $names Gateway names (brand names, not translated). * @return string Joined list. */ private static function join_gateway_names( $names ) { $names = array_values( $names ); $last = array_pop( $names ); if ( empty( $names ) ) { return (string) $last; } return sprintf( /* translators: 1: comma-separated gateway names, 2: the last gateway name. */ __( '%1$s or %2$s', 'better-payment' ), implode( ', ', $names ), $last ); } /** * 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'] ); ?>
'; } }; } /** * 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', // AI provider API keys (Campaign Builder); the block editor never needs them. 'better_payment_settings_ai_api_key_openai', 'better_payment_settings_ai_api_key_claude', 'better_payment_settings_ai_api_key_gemini', 'better_payment_settings_ai_api_key_openrouter', ); 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' ) ), // A form with no gateway enabled shows an editor notice instead of // nothing (components/gateway-notice.js). Both keys are about what to // tell the user: the link is only offered to someone who can act on // it, since an author who cannot open Better Payment's settings is // better told to ask an administrator than handed a link to a screen // they will be refused. 'settingsUrl' => admin_url( 'admin.php?page=better-payment-admin&tab=settings§ion=general' ), 'canManageSettings' => current_user_can( 'manage_options' ), ); } /** * 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. // Entitlement is part of the key: subscription rows are fetched only for // Pro (below), so a cached free payload must never be served to Pro. $pro_enabled = (bool) apply_filters( 'better_payment/pro_enabled', false ); $cache_key = 'bp_editor_dash_' . ( $current_user->ID ?? 0 ) . ( $pro_enabled ? '_pro' : '' ); $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 ) { // The status bucket, as UserAPI's `status_type`: the preview labels the row from it // (an unconfirmed pre-2.4.0 free checkout reads Incomplete). $tx->status_type = \Better_Payment\Lite\Classes\Helper::transaction_status_type( $tx, 'v2' ); 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'] ?? '' ) ); // 'One Time' unless Pro labels the row (better_payment/transaction/payment_type), as UserAPI does. $tx->payment_type_label = \Better_Payment\Lite\Classes\Helper::payment_type_label( $ffi ); $tx->is_subscription = $tx->payment_type_label; } $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). // These rows are Better Payment's own Stripe recurring and split payments — // Pro features that only Pro's Subscriptions tab displays — so a free // install skips the query and keeps the empty default. $raw_subscriptions = array(); if ( $pro_enabled ) { $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(); // The subscription columns come from the same place as UserAPI's REST rows: Pro fills // `better_payment/user_api/subscription_row`. $row = array(); $built = apply_filters( 'better_payment/user_api/subscription_row', $row, $ffi, $tx ); $row = is_array( $built ) ? $built : $row; foreach ( $row as $key => $value ) { $tx->{$key} = $value; } // The enrich_user_subscriptions filter above stores the product name in form_fields_info. if ( ! empty( $ffi['subscription_product_name'] ) ) { $tx->subscription_product_name = sanitize_text_field( $ffi['subscription_product_name'] ); } $tx->is_subscription = Helper::payment_type_label( $ffi ); $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' ); } } }