| @@ -8,10 +8,12 @@ | ||
| 8 | 8 | |
| 9 | 9 | namespace SureDonation\Inc\Fields; |
| 10 | 10 | |
| 11 | 11 | use SureDonation\Inc\Helper; |
| 12 | +use SureDonation\Inc\Payments\Offline\Offline_Helper; | |
| 12 | 13 | use SureDonation\Inc\Payments\Payment_Helper; |
| 13 | 14 | use SureDonation\Inc\Payments\Stripe\Stripe_Helper; |
| 15 | +use SureDonation\Inc\Post_Types\Donation_Form; | |
| 14 | 16 | |
| 15 | 17 | if ( ! defined( 'ABSPATH' ) ) { |
| 16 | 18 | exit; // Exit if accessed directly. |
| 17 | 19 | } |
| @@ -126,8 +128,16 @@ | ||
| 126 | 128 | */ |
| 127 | 129 | protected $variable_amount_field; |
| 128 | 130 | |
| 129 | 131 | /** |
| 132 | + * Payment methods enabled for this block. | |
| 133 | + * | |
| 134 | + * @var array<string> | |
| 135 | + * @since 1.0.0 | |
| 136 | + */ | |
| 137 | + protected $payment_methods = []; | |
| 138 | + | |
| 139 | + /** | |
| 130 | 140 | * Aria-describedby attribute value. |
| 131 | 141 | * |
| 132 | 142 | * @var string |
| 133 | 143 | * @since 0.0.1 |
| @@ -164,9 +174,11 @@ | ||
| 164 | 174 | |
| 165 | 175 | // Get appropriate Stripe publishable key based on mode. |
| 166 | 176 | $this->stripe_publishable_key = Stripe_Helper::get_stripe_publishable_key(); |
| 167 | 177 | |
| 168 | - $this->payment_type = $attributes['paymentType'] ?? 'one-time'; | |
| 178 | + // Fall back to one-time if subscription is configured but pro is not active. | |
| 179 | + $configured_type = $attributes['paymentType'] ?? 'one-time'; | |
| 180 | + $this->payment_type = 'subscription' === $configured_type && ! defined( 'SUREDONATION_PRO_VER' ) ? 'one-time' : $configured_type; | |
| 169 | 181 | $this->subscription_plan = $attributes['subscriptionPlan'] ?? []; |
| 170 | 182 | $this->amount_type = $attributes['amountType'] ?? 'fixed'; |
| 171 | 183 | $this->fixed_amount = $attributes['fixedAmount'] ?? 10; |
| 172 | 184 | $this->minimum_amount = $attributes['minimumAmount'] ?? 0; |
| @@ -177,8 +189,39 @@ | ||
| 177 | 189 | |
| 178 | 190 | // Set variable amount field mapping. |
| 179 | 191 | $this->variable_amount_field = $attributes['variableAmountField'] ?? ''; |
| 180 | 192 | |
| 193 | + // Parse payment methods from block attributes (with backward compat fallback). | |
| 194 | + $block_payment_methods = $attributes['paymentMethods'] ?? null; | |
| 195 | + if ( ! is_array( $block_payment_methods ) || empty( $block_payment_methods ) ) { | |
| 196 | + $gateway = $attributes['gateway'] ?? 'stripe'; | |
| 197 | + $block_payment_methods = [ is_string( $gateway ) ? $gateway : 'stripe' ]; | |
| 198 | + } | |
| 199 | + | |
| 200 | + // Filter to only globally-available gateways. | |
| 201 | + $available = []; | |
| 202 | + foreach ( $block_payment_methods as $method ) { | |
| 203 | + if ( 'stripe' === $method && $this->stripe_connected && ! empty( $this->stripe_publishable_key ) ) { | |
| 204 | + $available[] = 'stripe'; | |
| 205 | + } elseif ( 'offline' === $method && Offline_Helper::is_offline_enabled() ) { | |
| 206 | + $available[] = 'offline'; | |
| 207 | + } | |
| 208 | + } | |
| 209 | + | |
| 210 | + /** | |
| 211 | + * Filter the list of available payment methods for this block. | |
| 212 | + * | |
| 213 | + * Allows extensions (e.g., PayPal) to add themselves when connected. | |
| 214 | + * | |
| 215 | + * @param array<string> $available Currently available method IDs. | |
| 216 | + * @param array<string> $block_payment_methods Methods selected in the block. | |
| 217 | + * @param array<string,mixed> $attributes Block attributes. | |
| 218 | + * @since 1.0.0 | |
| 219 | + */ | |
| 220 | + $available = apply_filters( 'suredonation_available_payment_methods', $available, $block_payment_methods, $attributes ); | |
| 221 | + | |
| 222 | + $this->payment_methods = ! empty( $available ) ? $available : $block_payment_methods; | |
| 223 | + | |
| 181 | 224 | // BACKWARD COMPATIBILITY: Migrate customer fields from subscriptionPlan. |
| 182 | 225 | if ( empty( $this->customer_name_field ) && ! empty( $this->subscription_plan['customer_name'] ) ) { |
| 183 | 226 | $this->customer_name_field = $this->subscription_plan['customer_name']; |
| 184 | 227 | } |
| @@ -194,13 +237,21 @@ | ||
| 194 | 237 | * @return string |
| 195 | 238 | * @since 0.0.1 |
| 196 | 239 | */ |
| 197 | 240 | public function markup() { |
| 198 | - // Check if Stripe is connected. | |
| 199 | - if ( ! $this->stripe_connected || empty( $this->stripe_publishable_key ) ) { | |
| 241 | + $has_stripe = in_array( 'stripe', $this->payment_methods, true ); | |
| 242 | + $has_offline = in_array( 'offline', $this->payment_methods, true ); | |
| 243 | + | |
| 244 | + // If only Stripe is selected, it must be connected. | |
| 245 | + if ( $has_stripe && ! $has_offline && ( ! $this->stripe_connected || empty( $this->stripe_publishable_key ) ) ) { | |
| 200 | 246 | return ''; |
| 201 | 247 | } |
| 202 | 248 | |
| 249 | + // If no gateway is available at all, hide the block. | |
| 250 | + if ( empty( $this->payment_methods ) ) { | |
| 251 | + return ''; | |
| 252 | + } | |
| 253 | + | |
| 203 | 254 | // Validate payment field requirements. |
| 204 | 255 | $is_valid = $this->validate_payment_requirements(); |
| 205 | 256 | if ( ! $is_valid ) { |
| 206 | 257 | return ''; |
| @@ -205,9 +256,11 @@ | ||
| 205 | 256 | if ( ! $is_valid ) { |
| 206 | 257 | return ''; |
| 207 | 258 | } |
| 208 | 259 | |
| 209 | - $field_classes = $this->get_field_classes(); | |
| 260 | + $field_classes = $this->get_field_classes(); | |
| 261 | + $payment_methods_csv = implode( ',', $this->payment_methods ); | |
| 262 | + $default_gateway = $this->payment_methods[0]; | |
| 210 | 263 | |
| 211 | 264 | ob_start(); |
| 212 | 265 | ?> |
| 213 | 266 | <div data-block-id="<?php echo esc_attr( $this->block_id ); ?>" |
| @@ -212,11 +265,15 @@ | ||
| 212 | 265 | ?> |
| 213 | 266 | <div data-block-id="<?php echo esc_attr( $this->block_id ); ?>" |
| 214 | 267 | data-form-id="<?php echo esc_attr( $this->form_id ); ?>" |
| 215 | 268 | class="<?php echo esc_attr( $field_classes ); ?>" |
| 216 | - data-gateway="stripe" | |
| 217 | - data-stripe-key="<?php echo esc_attr( $this->stripe_publishable_key ); ?>" | |
| 269 | + data-payment-methods="<?php echo esc_attr( $payment_methods_csv ); ?>" | |
| 270 | + data-gateway="<?php echo esc_attr( $default_gateway ); ?>" | |
| 271 | + <?php if ( $has_stripe && ! empty( $this->stripe_publishable_key ) ) { ?> | |
| 272 | + data-stripe-key="<?php echo esc_attr( $this->stripe_publishable_key ); ?>" | |
| 273 | + <?php } ?> | |
| 218 | 274 | data-currency="<?php echo esc_attr( strtolower( $this->currency ) ); ?>" |
| 275 | + data-currency-symbol="<?php echo esc_attr( Payment_Helper::get_currency_symbol( $this->currency ) ); ?>" | |
| 219 | 276 | data-payment-mode="<?php echo esc_attr( $this->payment_mode ); ?>" |
| 220 | 277 | data-amount-type="<?php echo esc_attr( $this->amount_type ); ?>" |
| 221 | 278 | data-fixed-amount="<?php echo esc_attr( (string) $this->fixed_amount ); ?>" |
| 222 | 279 | data-payment-type="<?php echo esc_attr( $this->payment_type ); ?>" |
| @@ -238,22 +295,22 @@ | ||
| 238 | 295 | <?php echo wp_kses_post( $this->label_markup ); ?> |
| 239 | 296 | <?php echo wp_kses_post( $this->help_markup ); ?> |
| 240 | 297 | <div class="sd-payment-field-wrapper"> |
| 241 | 298 | <?php |
| 242 | - // Amount display. | |
| 243 | - echo wp_kses_post( $this->render_amount_display() ); | |
| 299 | + // Amount display — uses wp_kses with data attributes allowed | |
| 300 | + // because wp_kses_post strips data-message-format, data-currency-symbol etc. | |
| 301 | + echo wp_kses( $this->render_amount_display(), Helper::get_allowed_form_html() ); | |
| 244 | 302 | |
| 245 | 303 | // Test mode notice. |
| 246 | - if ( 'test' === $this->payment_mode ) { | |
| 304 | + if ( 'test' === $this->payment_mode && $has_stripe ) { | |
| 247 | 305 | echo wp_kses_post( $this->get_test_mode_notice() ); |
| 248 | 306 | } |
| 307 | + | |
| 308 | + // Payment methods accordion. | |
| 309 | + $methods = $this->get_registered_payment_methods(); | |
| 310 | + 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. | |
| 249 | 311 | ?> |
| 250 | 312 | |
| 251 | - <!-- Stripe Elements Container --> | |
| 252 | - <div id="sd-payment-element-<?php echo esc_attr( $this->block_id ); ?>" class="sd-payment-element"> | |
| 253 | - <!-- Stripe Elements will be inserted here --> | |
| 254 | - </div> | |
| 255 | - | |
| 256 | 313 | <!-- Payment error display --> |
| 257 | 314 | <div class="sd-payment-error" style="display: none;"></div> |
| 258 | 315 | </div> |
| 259 | 316 | </div> |
| @@ -262,8 +319,152 @@ | ||
| 262 | 319 | return false !== $output ? $output : ''; |
| 263 | 320 | } |
| 264 | 321 | |
| 265 | 322 | /** |
| 323 | + * Get registered payment methods for display. | |
| 324 | + * | |
| 325 | + * @return array<mixed> Array of payment method configurations. | |
| 326 | + * @since 1.0.0 | |
| 327 | + */ | |
| 328 | + private function get_registered_payment_methods() { | |
| 329 | + $available_methods = [ | |
| 330 | + 'stripe' => [ | |
| 331 | + 'id' => 'stripe', | |
| 332 | + 'label' => __( 'Credit / Debit Card', 'suredonation' ), | |
| 333 | + 'enabled' => $this->stripe_connected && ! empty( $this->stripe_publishable_key ), | |
| 334 | + 'container_class' => 'sd-payment-element', | |
| 335 | + 'container_id' => 'sd-payment-element-' . $this->block_id, | |
| 336 | + ], | |
| 337 | + 'offline' => [ | |
| 338 | + 'id' => 'offline', | |
| 339 | + 'label' => __( 'Offline Payment', 'suredonation' ), | |
| 340 | + 'enabled' => Offline_Helper::is_offline_enabled(), | |
| 341 | + 'container_class' => 'sd-offline-instructions', | |
| 342 | + 'content' => Offline_Helper::get_offline_instructions( $this->get_campaign_name() ), | |
| 343 | + ], | |
| 344 | + ]; | |
| 345 | + | |
| 346 | + $methods = []; | |
| 347 | + foreach ( $this->payment_methods as $method_id ) { | |
| 348 | + if ( isset( $available_methods[ $method_id ] ) && $available_methods[ $method_id ]['enabled'] ) { | |
| 349 | + $methods[ $method_id ] = $available_methods[ $method_id ]; | |
| 350 | + } | |
| 351 | + } | |
| 352 | + | |
| 353 | + /** | |
| 354 | + * Filter the registered payment methods and their display config. | |
| 355 | + * | |
| 356 | + * Allows extensions to add payment methods (e.g., PayPal) with their | |
| 357 | + * label, container class, and enabled state for frontend rendering. | |
| 358 | + * | |
| 359 | + * @param array<string, array<string,mixed>> $methods Registered methods keyed by ID. | |
| 360 | + * @param array<string> $payment_methods Methods selected in this block. | |
| 361 | + * @param string $block_id The block ID. | |
| 362 | + * @since 1.0.0 | |
| 363 | + */ | |
| 364 | + return apply_filters( 'suredonation_registered_payment_methods', $methods, $this->payment_methods, $this->block_id ); | |
| 365 | + } | |
| 366 | + | |
| 367 | + /** | |
| 368 | + * Get the campaign name for this form. | |
| 369 | + * | |
| 370 | + * @return string Campaign name or empty string. | |
| 371 | + * @since 1.0.0 | |
| 372 | + */ | |
| 373 | + private function get_campaign_name() { | |
| 374 | + if ( empty( $this->form_id ) ) { | |
| 375 | + return ''; | |
| 376 | + } | |
| 377 | + | |
| 378 | + $campaign_id = Donation_Form::get_form_campaign_id( absint( $this->form_id ) ); | |
| 379 | + | |
| 380 | + if ( empty( $campaign_id ) ) { | |
| 381 | + return ''; | |
| 382 | + } | |
| 383 | + | |
| 384 | + return get_the_title( $campaign_id ); | |
| 385 | + } | |
| 386 | + | |
| 387 | + /** | |
| 388 | + * Render payment methods as accordion. | |
| 389 | + * Each payment method is an accordion item with header and collapsible content. | |
| 390 | + * | |
| 391 | + * @param array<mixed> $methods Array of payment methods. | |
| 392 | + * @return string Payment methods accordion markup. | |
| 393 | + * @since 1.0.0 | |
| 394 | + */ | |
| 395 | + private function render_payment_methods_accordion( $methods ) { | |
| 396 | + if ( empty( $methods ) || ! is_array( $methods ) ) { | |
| 397 | + return ''; | |
| 398 | + } | |
| 399 | + | |
| 400 | + $is_single_method = count( $methods ) === 1; | |
| 401 | + $is_first = true; | |
| 402 | + | |
| 403 | + ob_start(); | |
| 404 | + ?> | |
| 405 | + <div class="sd-payment-methods-accordion <?php echo esc_attr( $is_single_method ? 'sd-single-payment-method' : '' ); ?>"> | |
| 406 | + <?php foreach ( $methods as $method ) { ?> | |
| 407 | + <div | |
| 408 | + class="sd-accordion-item <?php echo esc_attr( $is_first ? 'sd-payment-active' : '' ); ?>" | |
| 409 | + data-method="<?php echo esc_attr( $method['id'] ); ?>" | |
| 410 | + > | |
| 411 | + <div | |
| 412 | + class="sd-accordion-header" | |
| 413 | + role="button" | |
| 414 | + tabindex="0" | |
| 415 | + aria-expanded="<?php echo esc_attr( $is_first ? 'true' : 'false' ); ?>" | |
| 416 | + aria-controls="sd-accordion-content-<?php echo esc_attr( $method['id'] ); ?>-<?php echo esc_attr( $this->block_id ); ?>" | |
| 417 | + > | |
| 418 | + <div class="sd-payment-input-wrapper"> | |
| 419 | + <input | |
| 420 | + type="radio" | |
| 421 | + name="sd-gateway-choice-<?php echo esc_attr( $this->block_id ); ?>" | |
| 422 | + value="<?php echo esc_attr( $method['id'] ); ?>" | |
| 423 | + class="sd-payment-method-radio" | |
| 424 | + data-method="<?php echo esc_attr( $method['id'] ); ?>" | |
| 425 | + <?php checked( $is_first ); ?> | |
| 426 | + aria-label="<?php echo esc_attr( $method['label'] ); ?>" | |
| 427 | + /> | |
| 428 | + <span class="sd-accordion-title sd-block-label"> | |
| 429 | + <?php echo esc_html( $method['label'] ); ?> | |
| 430 | + </span> | |
| 431 | + </div> | |
| 432 | + </div> | |
| 433 | + <div | |
| 434 | + id="sd-accordion-content-<?php echo esc_attr( $method['id'] ); ?>-<?php echo esc_attr( $this->block_id ); ?>" | |
| 435 | + class="sd-accordion-content" | |
| 436 | + role="region" | |
| 437 | + > | |
| 438 | + <div | |
| 439 | + class="sd-payment-method-content" | |
| 440 | + data-method="<?php echo esc_attr( $method['id'] ); ?>" | |
| 441 | + > | |
| 442 | + <div | |
| 443 | + <?php if ( ! empty( $method['container_id'] ) ) { ?> | |
| 444 | + id="<?php echo esc_attr( $method['container_id'] ); ?>" | |
| 445 | + <?php } ?> | |
| 446 | + class="<?php echo esc_attr( $method['container_class'] ); ?>" | |
| 447 | + > | |
| 448 | + <?php | |
| 449 | + if ( ! empty( $method['content'] ) ) { | |
| 450 | + echo wp_kses_post( $method['content'] ); | |
| 451 | + } | |
| 452 | + ?> | |
| 453 | + </div> | |
| 454 | + </div> | |
| 455 | + </div> | |
| 456 | + </div> | |
| 457 | + <?php $is_first = false; ?> | |
| 458 | + <?php } ?> | |
| 459 | + </div> | |
| 460 | + <?php | |
| 461 | + $template = ob_get_clean(); | |
| 462 | + | |
| 463 | + return is_string( $template ) ? $template : ''; | |
| 464 | + } | |
| 465 | + | |
| 466 | + /** | |
| 266 | 467 | * Render amount display section. |
| 267 | 468 | * |
| 268 | 469 | * @return string Amount display HTML. |
| 269 | 470 | * @since 0.0.1 |
| @@ -345,8 +546,9 @@ | ||
| 345 | 546 | } |
| 346 | 547 | } |
| 347 | 548 | ?> |
| 348 | 549 | <span class="sd-payment-value" data-currency="<?php echo esc_attr( strtolower( $this->currency ) ); ?>" data-currency-symbol="<?php echo esc_attr( Payment_Helper::get_currency_symbol( $this->currency ) ); ?>" data-message-format="<?php echo esc_attr( $message_format ); ?>"> |
| 550 | + <?php esc_html_e( 'Complete the form to view the amount.', 'suredonation' ); ?> | |
| 349 | 551 | </span> |
| 350 | 552 | </div> |
| 351 | 553 | <?php if ( $this->minimum_amount > 0 ) { ?> |
| 352 | 554 | <span class="sd-help"> |
| @@ -450,11 +652,11 @@ | ||
| 450 | 652 | $settings_url = admin_url( 'admin.php?page=suredonation_settings&tab=payments' ); |
| 451 | 653 | |
| 452 | 654 | ob_start(); |
| 453 | 655 | ?> |
| 454 | - <div class="sd-test-mode-notice" style="background-color: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 12px; margin-bottom: 16px; color: #856404;"> | |
| 656 | + <div class="sd-test-mode-notice"> | |
| 455 | 657 | <strong><?php esc_html_e( 'Test mode is enabled:', 'suredonation' ); ?></strong> |
| 456 | - <a href="<?php echo esc_url( $settings_url ); ?>" style="color: #856404;" target="_blank" rel="noopener noreferrer"> | |
| 658 | + <a href="<?php echo esc_url( $settings_url ); ?>" target="_blank" rel="noopener noreferrer"> | |
| 457 | 659 | <?php esc_html_e( 'Click here to enable live mode and accept payment', 'suredonation' ); ?> |
| 458 | 660 | </a> |
| 459 | 661 | </div> |
| 460 | 662 | <?php |