* @since 0.0.1 */ protected $subscription_plan; /** * Amount type. * * @var string * @since 0.0.1 */ protected $amount_type; /** * Fixed amount. * * @var float * @since 0.0.1 */ protected $fixed_amount; /** * Minimum amount. * * @var float * @since 0.0.1 */ protected $minimum_amount; /** * Customer name field slug. * * @var string * @since 0.0.1 */ protected $customer_name_field; /** * Customer email field slug. * * @var string * @since 0.0.1 */ protected $customer_email_field; /** * Variable amount field slug. * * @var string * @since 0.0.1 */ protected $variable_amount_field; /** * Payment methods enabled for this block. * * @var array * @since 1.0.0 */ protected $payment_methods = []; /** * Aria-describedby attribute value. * * @var string * @since 0.0.1 */ protected $aria_described_by = ''; /** * Constructor for the Payment Markup class. * * @param array $attributes Block attributes. * @since 0.0.1 */ public function __construct( $attributes ) { // Get payment settings from Stripe Helper. $this->stripe_connected = Stripe_Helper::is_stripe_connected(); $this->payment_mode = Payment_Helper::get_payment_mode(); $this->slug = 'payment'; $this->set_properties( $attributes ); $this->set_unique_slug(); $this->set_markup_properties(); // Build aria-describedby attribute. $this->aria_described_by = $this->get_aria_describedby(); // Set payment-specific properties. $this->amount = $attributes['amount'] ?? 10; $this->currency = $attributes['currency'] ?? 'USD'; // Use currency from settings if not specified in block. if ( empty( $this->currency ) || 'USD' === $this->currency ) { $this->currency = Payment_Helper::get_currency(); } // Get appropriate Stripe publishable key based on mode. $this->stripe_publishable_key = Stripe_Helper::get_stripe_publishable_key(); // Fall back to one-time if subscription is configured but pro is not active. $configured_type = $attributes['paymentType'] ?? 'one-time'; $this->payment_type = 'subscription' === $configured_type && ! defined( 'SUREDONATION_PRO_VER' ) ? 'one-time' : $configured_type; $this->subscription_plan = $attributes['subscriptionPlan'] ?? []; $this->amount_type = $attributes['amountType'] ?? 'fixed'; $this->fixed_amount = $attributes['fixedAmount'] ?? 10; $this->minimum_amount = $attributes['minimumAmount'] ?? 0; // Set customer field mappings. $this->customer_name_field = $attributes['customerNameField'] ?? ''; $this->customer_email_field = $attributes['customerEmailField'] ?? ''; // Set variable amount field mapping. $this->variable_amount_field = $attributes['variableAmountField'] ?? ''; // Parse payment methods from block attributes (with backward compat fallback). $block_payment_methods = $attributes['paymentMethods'] ?? null; if ( ! is_array( $block_payment_methods ) || empty( $block_payment_methods ) ) { $gateway = $attributes['gateway'] ?? 'stripe'; $block_payment_methods = [ is_string( $gateway ) ? $gateway : 'stripe' ]; } // Filter to only globally-available gateways. $available = []; foreach ( $block_payment_methods as $method ) { if ( 'stripe' === $method && $this->stripe_connected && ! empty( $this->stripe_publishable_key ) ) { $available[] = 'stripe'; } elseif ( 'offline' === $method && Offline_Helper::is_offline_enabled() ) { $available[] = 'offline'; } } /** * Filter the list of available payment methods for this block. * * Allows extensions (e.g., PayPal) to add themselves when connected. * * @param array $available Currently available method IDs. * @param array $block_payment_methods Methods selected in the block. * @param array $attributes Block attributes. * @since 1.0.0 */ $available = apply_filters( 'suredonation_available_payment_methods', $available, $block_payment_methods, $attributes ); $this->payment_methods = ! empty( $available ) ? $available : $block_payment_methods; // BACKWARD COMPATIBILITY: Migrate customer fields from subscriptionPlan. if ( empty( $this->customer_name_field ) && ! empty( $this->subscription_plan['customer_name'] ) ) { $this->customer_name_field = $this->subscription_plan['customer_name']; } if ( empty( $this->customer_email_field ) && ! empty( $this->subscription_plan['customer_email'] ) ) { $this->customer_email_field = $this->subscription_plan['customer_email']; } } /** * Render the payment field markup. * * @return string * @since 0.0.1 */ public function markup() { $has_stripe = in_array( 'stripe', $this->payment_methods, true ); // Determine which gateways are actually connected/available for this form. // get_registered_payment_methods() returns only enabled methods (Stripe when // connected, Offline when enabled, plus any added by extensions such as // PayPal), so it is the single gateway-agnostic source of truth. $methods = $this->get_registered_payment_methods(); // No payment gateway is connected/available. Show a clear message instead of // returning an empty string, which previously left donors with a silently // broken form and no way to donate. See issue #219. if ( empty( $methods ) ) { return $this->render_gateway_unavailable_notice(); } // Validate payment field requirements. if ( ! $this->validate_payment_requirements() ) { return ''; } $field_classes = $this->get_field_classes(); $payment_methods_csv = implode( ',', $this->payment_methods ); $default_gateway = $this->payment_methods[0]; ob_start(); ?>
stripe_publishable_key ) ) { ?> data-stripe-key="stripe_publishable_key ); ?>" data-currency="currency ) ); ?>" data-currency-symbol="currency ) ); ?>" data-payment-mode="payment_mode ); ?>" data-amount-type="amount_type ); ?>" data-fixed-amount="fixed_amount ); ?>" data-payment-type="payment_type ); ?>" data-customer-name-field="customer_name_field ); ?>" data-customer-email-field="customer_email_field ); ?>" data-nonce="" amount_type ) { ?> data-variable-amount-field="variable_amount_field ); ?>" minimum_amount > 0 ) { ?> data-minimum-amount="minimum_amount ); ?>" payment_type && ! empty( $this->subscription_plan ) ) { ?> data-subscription-plan-name="subscription_plan['name'] ) ? Helper::get_string_value( $this->subscription_plan['name'] ) : __( 'Subscription Plan', 'suredonation' ) ); ?>" data-subscription-interval="subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month' ); ?>" data-subscription-billing-cycles="subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0' ); ?>" > label_markup ); ?> help_markup ); ?>
render_amount_display(), Helper::get_allowed_form_html() ); // Test mode notice. if ( 'test' === $this->payment_mode && $has_stripe ) { echo wp_kses_post( $this->get_test_mode_notice() ); } // Payment methods accordion ( $methods computed above ). echo $this->render_payment_methods_accordion( $methods ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Method builds markup with esc_attr/esc_html on all dynamic values and wp_kses_post on content. ?>
Array of payment method configurations. * @since 1.0.0 */ private function get_registered_payment_methods() { $available_methods = [ 'stripe' => [ 'id' => 'stripe', 'label' => __( 'Credit / Debit Card', 'suredonation' ), 'enabled' => $this->stripe_connected && ! empty( $this->stripe_publishable_key ), 'container_class' => 'sd-payment-element', 'container_id' => 'sd-payment-element-' . $this->block_id, ], 'offline' => [ 'id' => 'offline', 'label' => __( 'Offline Donation', 'suredonation' ), 'enabled' => Offline_Helper::is_offline_enabled(), 'container_class' => 'sd-offline-instructions', 'content' => Offline_Helper::get_offline_instructions( $this->get_campaign_name() ), ], ]; $methods = []; foreach ( $this->payment_methods as $method_id ) { if ( isset( $available_methods[ $method_id ] ) && $available_methods[ $method_id ]['enabled'] ) { $methods[ $method_id ] = $available_methods[ $method_id ]; } } /** * Filter the registered payment methods and their display config. * * Allows extensions to add payment methods (e.g., PayPal) with their * label, container class, and enabled state for frontend rendering. * * @param array> $methods Registered methods keyed by ID. * @param array $payment_methods Methods selected in this block. * @param string $block_id The block ID. * @since 1.0.0 */ return apply_filters( 'suredonation_registered_payment_methods', $methods, $this->payment_methods, $this->block_id ); } /** * Get the campaign name for this form. * * @return string Campaign name or empty string. * @since 1.0.0 */ private function get_campaign_name() { if ( empty( $this->form_id ) ) { return ''; } $campaign_id = Donation_Form::get_form_campaign_id( absint( $this->form_id ) ); if ( empty( $campaign_id ) ) { return ''; } return get_the_title( $campaign_id ); } /** * Render payment methods as accordion. * Each payment method is an accordion item with header and collapsible content. * * @param array $methods Array of payment methods. * @return string Payment methods accordion markup. * @since 1.0.0 */ private function render_payment_methods_accordion( $methods ) { if ( empty( $methods ) || ! is_array( $methods ) ) { return ''; } $is_single_method = count( $methods ) === 1; $is_first = true; ob_start(); ?>
aria-label="" />
id="" class="" >
amount_type ) { ?>
payment_type && ! empty( $this->subscription_plan ) ) { $interval = isset( $this->subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month'; $billing_cycles = isset( $this->subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0'; $interval_label = $this->get_interval_label( $interval ); // Build subscription text. if ( 'ongoing' === $billing_cycles ) { echo esc_html( sprintf( /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */ __( '%1$s per %2$s (until cancelled)', 'suredonation' ), $this->format_currency( $this->fixed_amount, $this->currency ), $interval_label ) ); } elseif ( (int) $billing_cycles > 0 ) { echo esc_html( sprintf( /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */ __( '%1$s per %2$s (%3$s payments)', 'suredonation' ), $this->format_currency( $this->fixed_amount, $this->currency ), $interval_label, $billing_cycles ) ); } else { echo esc_html( sprintf( /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */ __( '%1$s per %2$s', 'suredonation' ), $this->format_currency( $this->fixed_amount, $this->currency ), $interval_label ) ); } } else { echo esc_html( $this->format_currency( $this->fixed_amount, $this->currency ) ); } ?>
payment_type && ! empty( $this->subscription_plan ) ) { $interval = isset( $this->subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month'; $billing_cycles = isset( $this->subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0'; $interval_label = $this->get_interval_label( $interval ); // Build message format based on billing cycles. if ( 'ongoing' === $billing_cycles ) { /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */ $message_format = sprintf( __( '{amount} per %s (until cancelled)', 'suredonation' ), $interval_label ); } elseif ( (int) $billing_cycles > 0 ) { /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */ $message_format = sprintf( __( '{amount} per %1$s (%2$s payments)', 'suredonation' ), $interval_label, $billing_cycles ); } else { /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */ $message_format = sprintf( __( '{amount} per %s', 'suredonation' ), $interval_label ); } } ?>
minimum_amount > 0 ) { ?> format_currency( $this->minimum_amount, $this->currency ) ) ); ?>
customer_email_field ) ) { return false; } // Check subscription-specific requirements. if ( 'subscription' === $this->payment_type ) { if ( empty( $this->customer_name_field ) ) { return false; } if ( empty( $this->subscription_plan ) || empty( $this->subscription_plan['name'] ) ) { return false; } } return true; } /** * Format currency for display. * * @param float $amount Amount to format. * @param string $currency Currency code. * @return string * @since 0.0.1 */ private function format_currency( $amount, $currency ) { $symbol = Payment_Helper::get_currency_symbol( $currency ); // Format based on currency. if ( in_array( $currency, [ 'JPY', 'KRW' ], true ) ) { // No decimal places for these currencies. return $symbol . number_format( $amount, 0 ); } return $symbol . number_format( $amount, 2 ); } /** * Get the human-readable label for a payment interval slug. * * @param string $interval_slug The slug (e.g., 'day', 'week', 'month', 'quarter', 'yearly'). * @return string The translated interval label, or the slug itself if not found. * @since 0.0.1 */ private function get_interval_label( $interval_slug ) { $interval_labels = [ 'day' => __( 'day', 'suredonation' ), 'week' => __( 'week', 'suredonation' ), 'month' => __( 'month', 'suredonation' ), 'quarter' => __( 'quarter', 'suredonation' ), 'yearly' => __( 'year', 'suredonation' ), ]; return $interval_labels[ $interval_slug ] ?? $interval_slug; } /** * Render test mode notice for admin users. * Only shows if user has manage_options capability. * * @return string Test mode notice markup or empty string. * @since 0.0.1 */ private function get_test_mode_notice() { // Only show to users with manage_options capability. if ( ! current_user_can( 'manage_options' ) ) { return ''; } // Build dynamic link to payment settings. $settings_url = admin_url( 'admin.php?page=suredonation_settings&tab=payments' ); ob_start(); ?> get_field_classes( [ 'sd-payment-unavailable' ] ); ob_start(); ?>
label_markup ); ?>