PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.2.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.2.0
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / fields / payment-markup.php

payment-markup.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.2.0, at inc/fields/payment-markup.php

706 lines 23.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SureDonation Payment Markup Class file.
4 *
5 * @package SureDonation
6 * @since 0.0.1
7 */
8
9 namespace SureDonation\Inc\Fields;
10
11 use SureDonation\Inc\Helper;
12 use SureDonation\Inc\Payments\Offline\Offline_Helper;
13 use SureDonation\Inc\Payments\Payment_Helper;
14 use SureDonation\Inc\Payments\Stripe\Stripe_Helper;
15 use SureDonation\Inc\Post_Types\Donation_Form;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit; // Exit if accessed directly.
19 }
20
21 /**
22 * SureDonation Payment Markup Class.
23 *
24 * @since 0.0.1
25 */
26 class Payment_Markup extends Base {
27 /**
28 * Payment amount.
29 *
30 * @var float
31 * @since 0.0.1
32 */
33 protected $amount;
34
35 /**
36 * Payment currency.
37 *
38 * @var string
39 * @since 0.0.1
40 */
41 protected $currency;
42
43 /**
44 * Stripe publishable key.
45 *
46 * @var string
47 * @since 0.0.1
48 */
49 protected $stripe_publishable_key;
50
51 /**
52 * Whether Stripe is connected.
53 *
54 * @var bool
55 * @since 0.0.1
56 */
57 protected $stripe_connected;
58
59 /**
60 * Payment mode (live or test).
61 *
62 * @var string
63 * @since 0.0.1
64 */
65 protected $payment_mode;
66
67 /**
68 * Payment type.
69 *
70 * @var string
71 * @since 0.0.1
72 */
73 protected $payment_type;
74
75 /**
76 * Subscription plans.
77 *
78 * @var array<string, mixed>
79 * @since 0.0.1
80 */
81 protected $subscription_plan;
82
83 /**
84 * Amount type.
85 *
86 * @var string
87 * @since 0.0.1
88 */
89 protected $amount_type;
90
91 /**
92 * Fixed amount.
93 *
94 * @var float
95 * @since 0.0.1
96 */
97 protected $fixed_amount;
98
99 /**
100 * Minimum amount.
101 *
102 * @var float
103 * @since 0.0.1
104 */
105 protected $minimum_amount;
106
107 /**
108 * Customer name field slug.
109 *
110 * @var string
111 * @since 0.0.1
112 */
113 protected $customer_name_field;
114
115 /**
116 * Customer email field slug.
117 *
118 * @var string
119 * @since 0.0.1
120 */
121 protected $customer_email_field;
122
123 /**
124 * Variable amount field slug.
125 *
126 * @var string
127 * @since 0.0.1
128 */
129 protected $variable_amount_field;
130
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 /**
140 * Aria-describedby attribute value.
141 *
142 * @var string
143 * @since 0.0.1
144 */
145 protected $aria_described_by = '';
146
147 /**
148 * Constructor for the Payment Markup class.
149 *
150 * @param array<mixed> $attributes Block attributes.
151 * @since 0.0.1
152 */
153 public function __construct( $attributes ) {
154 // Get payment settings from Stripe Helper.
155 $this->stripe_connected = Stripe_Helper::is_stripe_connected();
156 $this->payment_mode = Payment_Helper::get_payment_mode();
157
158 $this->slug = 'payment';
159 $this->set_properties( $attributes );
160 $this->set_unique_slug();
161 $this->set_markup_properties();
162
163 // Build aria-describedby attribute.
164 $this->aria_described_by = $this->get_aria_describedby();
165
166 // Set payment-specific properties.
167 $this->amount = $attributes['amount'] ?? 10;
168 $this->currency = $attributes['currency'] ?? 'USD';
169
170 // Use currency from settings if not specified in block.
171 if ( empty( $this->currency ) || 'USD' === $this->currency ) {
172 $this->currency = Payment_Helper::get_currency();
173 }
174
175 // Get appropriate Stripe publishable key based on mode.
176 $this->stripe_publishable_key = Stripe_Helper::get_stripe_publishable_key();
177
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;
181 $this->subscription_plan = $attributes['subscriptionPlan'] ?? [];
182 $this->amount_type = $attributes['amountType'] ?? 'fixed';
183 $this->fixed_amount = $attributes['fixedAmount'] ?? 10;
184 $this->minimum_amount = $attributes['minimumAmount'] ?? 0;
185
186 // Set customer field mappings.
187 $this->customer_name_field = $attributes['customerNameField'] ?? '';
188 $this->customer_email_field = $attributes['customerEmailField'] ?? '';
189
190 // Set variable amount field mapping.
191 $this->variable_amount_field = $attributes['variableAmountField'] ?? '';
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
224 // BACKWARD COMPATIBILITY: Migrate customer fields from subscriptionPlan.
225 if ( empty( $this->customer_name_field ) && ! empty( $this->subscription_plan['customer_name'] ) ) {
226 $this->customer_name_field = $this->subscription_plan['customer_name'];
227 }
228
229 if ( empty( $this->customer_email_field ) && ! empty( $this->subscription_plan['customer_email'] ) ) {
230 $this->customer_email_field = $this->subscription_plan['customer_email'];
231 }
232 }
233
234 /**
235 * Render the payment field markup.
236 *
237 * @return string
238 * @since 0.0.1
239 */
240 public function markup() {
241 $has_stripe = in_array( 'stripe', $this->payment_methods, true );
242
243 // Determine which gateways are actually connected/available for this form.
244 // get_registered_payment_methods() returns only enabled methods (Stripe when
245 // connected, Offline when enabled, plus any added by extensions such as
246 // PayPal), so it is the single gateway-agnostic source of truth.
247 $methods = $this->get_registered_payment_methods();
248
249 // No payment gateway is connected/available. Show a clear message instead of
250 // returning an empty string, which previously left donors with a silently
251 // broken form and no way to donate. See issue #219.
252 if ( empty( $methods ) ) {
253 return $this->render_gateway_unavailable_notice();
254 }
255
256 // Validate payment field requirements.
257 if ( ! $this->validate_payment_requirements() ) {
258 return '';
259 }
260
261 $field_classes = $this->get_field_classes();
262 $payment_methods_csv = implode( ',', $this->payment_methods );
263 $default_gateway = $this->payment_methods[0];
264
265 ob_start();
266 ?>
267 <div data-block-id="<?php echo esc_attr( $this->block_id ); ?>"
268 data-form-id="<?php echo esc_attr( $this->form_id ); ?>"
269 class="<?php echo esc_attr( $field_classes ); ?>"
270 data-payment-methods="<?php echo esc_attr( $payment_methods_csv ); ?>"
271 data-gateway="<?php echo esc_attr( $default_gateway ); ?>"
272 <?php if ( $has_stripe && ! empty( $this->stripe_publishable_key ) ) { ?>
273 data-stripe-key="<?php echo esc_attr( $this->stripe_publishable_key ); ?>"
274 <?php } ?>
275 data-currency="<?php echo esc_attr( strtolower( $this->currency ) ); ?>"
276 data-currency-symbol="<?php echo esc_attr( Payment_Helper::get_currency_symbol( $this->currency ) ); ?>"
277 data-payment-mode="<?php echo esc_attr( $this->payment_mode ); ?>"
278 data-amount-type="<?php echo esc_attr( $this->amount_type ); ?>"
279 data-fixed-amount="<?php echo esc_attr( (string) $this->fixed_amount ); ?>"
280 data-payment-type="<?php echo esc_attr( $this->payment_type ); ?>"
281 data-customer-name-field="<?php echo esc_attr( $this->customer_name_field ); ?>"
282 data-customer-email-field="<?php echo esc_attr( $this->customer_email_field ); ?>"
283 data-nonce="<?php echo esc_attr( wp_create_nonce( 'suredonation_donation_form' ) ); ?>"
284 <?php if ( 'variable' === $this->amount_type ) { ?>
285 data-variable-amount-field="<?php echo esc_attr( $this->variable_amount_field ); ?>"
286 <?php } ?>
287 <?php if ( $this->minimum_amount > 0 ) { ?>
288 data-minimum-amount="<?php echo esc_attr( (string) $this->minimum_amount ); ?>"
289 <?php } ?>
290 <?php if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) { ?>
291 data-subscription-plan-name="<?php echo esc_attr( isset( $this->subscription_plan['name'] ) ? Helper::get_string_value( $this->subscription_plan['name'] ) : __( 'Subscription Plan', 'suredonation' ) ); ?>"
292 data-subscription-interval="<?php echo esc_attr( isset( $this->subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month' ); ?>"
293 data-subscription-billing-cycles="<?php echo esc_attr( isset( $this->subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0' ); ?>"
294 <?php } ?>
295 >
296 <?php echo wp_kses_post( $this->label_markup ); ?>
297 <?php echo wp_kses_post( $this->help_markup ); ?>
298 <div class="sd-payment-field-wrapper">
299 <?php
300 // Amount display — uses wp_kses with data attributes allowed
301 // because wp_kses_post strips data-message-format, data-currency-symbol etc.
302 echo wp_kses( $this->render_amount_display(), Helper::get_allowed_form_html() );
303
304 // Test mode notice.
305 if ( 'test' === $this->payment_mode && $has_stripe ) {
306 echo wp_kses_post( $this->get_test_mode_notice() );
307 }
308
309 // Payment methods accordion ( $methods computed above ).
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.
311 ?>
312
313 <!-- Payment error display (hidden via CSS; shown by JS on error). -->
314 <div class="sd-payment-error"></div>
315 </div>
316 </div>
317 <?php
318 $output = ob_get_clean();
319 return false !== $output ? $output : '';
320 }
321
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 Donation', '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 <?php if ( $is_single_method ) { ?>
412 <?php // Single method: the header is a static label, not an accordion toggle, so omit the interactive button semantics (keyboard/screen-reader users shouldn't hit an inert "button"). ?>
413 <div class="sd-accordion-header">
414 <?php } else { ?>
415 <div
416 class="sd-accordion-header"
417 role="button"
418 tabindex="0"
419 aria-expanded="<?php echo esc_attr( $is_first ? 'true' : 'false' ); ?>"
420 aria-controls="sd-accordion-content-<?php echo esc_attr( $method['id'] ); ?>-<?php echo esc_attr( $this->block_id ); ?>"
421 >
422 <?php } ?>
423 <div class="sd-payment-input-wrapper">
424 <input
425 type="radio"
426 name="sd-gateway-choice-<?php echo esc_attr( $this->block_id ); ?>"
427 value="<?php echo esc_attr( $method['id'] ); ?>"
428 class="sd-payment-method-radio"
429 data-method="<?php echo esc_attr( $method['id'] ); ?>"
430 <?php checked( $is_first ); ?>
431 aria-label="<?php echo esc_attr( $method['label'] ); ?>"
432 />
433 <span class="sd-accordion-title sd-block-label">
434 <?php echo esc_html( $method['label'] ); ?>
435 </span>
436 </div>
437 </div>
438 <div
439 id="sd-accordion-content-<?php echo esc_attr( $method['id'] ); ?>-<?php echo esc_attr( $this->block_id ); ?>"
440 class="sd-accordion-content<?php echo Offline_Helper::is_blank_instructions( $method['content'] ?? '' ) ? ' sd-accordion-content-empty' : ''; ?>"
441 role="region"
442 >
443 <div
444 class="sd-payment-method-content"
445 data-method="<?php echo esc_attr( $method['id'] ); ?>"
446 >
447 <div
448 <?php if ( ! empty( $method['container_id'] ) ) { ?>
449 id="<?php echo esc_attr( $method['container_id'] ); ?>"
450 <?php } ?>
451 class="<?php echo esc_attr( $method['container_class'] ); ?>"
452 >
453 <?php
454 if ( ! empty( $method['content'] ) ) {
455 echo wp_kses_post( $method['content'] );
456 }
457 ?>
458 </div>
459 </div>
460 </div>
461 </div>
462 <?php $is_first = false; ?>
463 <?php } ?>
464 </div>
465 <?php
466 $template = ob_get_clean();
467
468 return is_string( $template ) ? $template : '';
469 }
470
471 /**
472 * Render amount display section.
473 *
474 * @return string Amount display HTML.
475 * @since 0.0.1
476 */
477 private function render_amount_display() {
478 ob_start();
479
480 if ( 'fixed' === $this->amount_type ) {
481 ?>
482 <!-- Fixed Payment Amount Display. -->
483 <div class="sd-payment-amount sd-label">
484 <span class="sd-payment-value">
485 <?php
486 if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) {
487 $interval = isset( $this->subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month';
488 $billing_cycles = isset( $this->subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0';
489 $interval_label = $this->get_interval_label( $interval );
490
491 // Build subscription text.
492 if ( 'ongoing' === $billing_cycles ) {
493 echo esc_html(
494 sprintf(
495 /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */
496 __( '%1$s per %2$s (until cancelled)', 'suredonation' ),
497 $this->format_currency( $this->fixed_amount, $this->currency ),
498 $interval_label
499 )
500 );
501 } elseif ( (int) $billing_cycles > 0 ) {
502 echo esc_html(
503 sprintf(
504 /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */
505 __( '%1$s per %2$s (%3$s payments)', 'suredonation' ),
506 $this->format_currency( $this->fixed_amount, $this->currency ),
507 $interval_label,
508 $billing_cycles
509 )
510 );
511 } else {
512 echo esc_html(
513 sprintf(
514 /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */
515 __( '%1$s per %2$s', 'suredonation' ),
516 $this->format_currency( $this->fixed_amount, $this->currency ),
517 $interval_label
518 )
519 );
520 }
521 } else {
522 echo esc_html( $this->format_currency( $this->fixed_amount, $this->currency ) );
523 }
524 ?>
525 </span>
526 </div>
527 <?php
528 } else {
529 ?>
530 <!-- Variable Payment Amount Display. -->
531 <div class="sd-variable-amount-display sd-label">
532 <div class="sd-payment-amount-wrapper">
533 <?php
534 // Generate message format for variable amounts.
535 $message_format = '{amount}';
536 if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) {
537 $interval = isset( $this->subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month';
538 $billing_cycles = isset( $this->subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0';
539 $interval_label = $this->get_interval_label( $interval );
540
541 // Build message format based on billing cycles.
542 if ( 'ongoing' === $billing_cycles ) {
543 /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */
544 $message_format = sprintf( __( '{amount} per %s (until cancelled)', 'suredonation' ), $interval_label );
545 } elseif ( (int) $billing_cycles > 0 ) {
546 /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */
547 $message_format = sprintf( __( '{amount} per %1$s (%2$s payments)', 'suredonation' ), $interval_label, $billing_cycles );
548 } else {
549 /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */
550 $message_format = sprintf( __( '{amount} per %s', 'suredonation' ), $interval_label );
551 }
552 }
553 ?>
554 <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 ); ?>">
555 <?php esc_html_e( 'Complete the form to view the amount.', 'suredonation' ); ?>
556 </span>
557 </div>
558 <?php if ( $this->minimum_amount > 0 ) { ?>
559 <span class="sd-help">
560 <?php
561 echo esc_html(
562 sprintf(
563 /* translators: %s: Minimum amount with currency */
564 __( 'Minimum amount: %s', 'suredonation' ),
565 $this->format_currency( $this->minimum_amount, $this->currency )
566 )
567 );
568 ?>
569 </span>
570 <?php } ?>
571 </div>
572 <?php
573 }
574
575 return (string) ob_get_clean();
576 }
577
578 /**
579 * Validate payment field requirements.
580 *
581 * @return bool True if validation passes, false otherwise.
582 * @since 0.0.1
583 */
584 private function validate_payment_requirements() {
585 // Check customer email field requirement (highest priority).
586 if ( empty( $this->customer_email_field ) ) {
587 return false;
588 }
589
590 // Check subscription-specific requirements.
591 if ( 'subscription' === $this->payment_type ) {
592 if ( empty( $this->customer_name_field ) ) {
593 return false;
594 }
595
596 if ( empty( $this->subscription_plan ) || empty( $this->subscription_plan['name'] ) ) {
597 return false;
598 }
599 }
600
601 return true;
602 }
603
604 /**
605 * Format currency for display.
606 *
607 * @param float $amount Amount to format.
608 * @param string $currency Currency code.
609 * @return string
610 * @since 0.0.1
611 */
612 private function format_currency( $amount, $currency ) {
613 $symbol = Payment_Helper::get_currency_symbol( $currency );
614
615 // Format based on currency.
616 if ( in_array( $currency, [ 'JPY', 'KRW' ], true ) ) {
617 // No decimal places for these currencies.
618 return $symbol . number_format( $amount, 0 );
619 }
620
621 return $symbol . number_format( $amount, 2 );
622 }
623
624 /**
625 * Get the human-readable label for a payment interval slug.
626 *
627 * @param string $interval_slug The slug (e.g., 'day', 'week', 'month', 'quarter', 'yearly').
628 * @return string The translated interval label, or the slug itself if not found.
629 * @since 0.0.1
630 */
631 private function get_interval_label( $interval_slug ) {
632 $interval_labels = [
633 'day' => __( 'day', 'suredonation' ),
634 'week' => __( 'week', 'suredonation' ),
635 'month' => __( 'month', 'suredonation' ),
636 'quarter' => __( 'quarter', 'suredonation' ),
637 'yearly' => __( 'year', 'suredonation' ),
638 ];
639
640 return $interval_labels[ $interval_slug ] ?? $interval_slug;
641 }
642
643 /**
644 * Render test mode notice for admin users.
645 * Only shows if user has manage_options capability.
646 *
647 * @return string Test mode notice markup or empty string.
648 * @since 0.0.1
649 */
650 private function get_test_mode_notice() {
651 // Only show to users with manage_options capability.
652 if ( ! current_user_can( 'manage_options' ) ) {
653 return '';
654 }
655
656 // Build dynamic link to payment settings.
657 $settings_url = admin_url( 'admin.php?page=suredonation_settings&tab=payments' );
658
659 ob_start();
660 ?>
661 <div class="sd-test-mode-notice">
662 <strong><?php esc_html_e( 'Test mode is enabled:', 'suredonation' ); ?></strong>
663 <a href="<?php echo esc_url( $settings_url ); ?>" target="_blank" rel="noopener noreferrer">
664 <?php esc_html_e( 'Click here to enable live mode and accept payment', 'suredonation' ); ?>
665 </a>
666 </div>
667 <?php
668 $output = ob_get_clean();
669 return false !== $output ? $output : '';
670 }
671
672 /**
673 * Render the notice shown when no payment gateway is connected/available.
674 *
675 * Replaces the previous behavior of rendering nothing, which left donors with
676 * a silently broken form. The `data-payment-available="0"` marker lets the
677 * frontend script skip gateway init and hide the otherwise-inert submit
678 * button. See issue #219.
679 *
680 * @return string Notice markup.
681 * @since 1.1.1
682 */
683 private function render_gateway_unavailable_notice() {
684 $field_classes = $this->get_field_classes( [ 'sd-payment-unavailable' ] );
685
686 ob_start();
687 ?>
688 <div
689 data-block-id="<?php echo esc_attr( $this->block_id ); ?>"
690 data-form-id="<?php echo esc_attr( $this->form_id ); ?>"
691 data-payment-available="0"
692 class="<?php echo esc_attr( $field_classes ); ?>"
693 >
694 <?php echo wp_kses_post( $this->label_markup ); ?>
695 <div class="sd-payment-field-wrapper">
696 <div class="sd-payment-notice" role="status">
697 <p><?php esc_html_e( 'Payment gateways are not configured. Please contact the site administrator.', 'suredonation' ); ?></p>
698 </div>
699 </div>
700 </div>
701 <?php
702 $output = ob_get_clean();
703 return false !== $output ? $output : '';
704 }
705 }
706