PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.4.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.4.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.4.0, at inc/fields/payment-markup.php

799 lines 26.7 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 the publishable key for this form's selected account (mode-aware).
176 $this->stripe_publishable_key = Stripe_Helper::get_stripe_publishable_key(
177 '',
178 Stripe_Helper::resolve_account_for_form( $this->form_id )
179 );
180
181 // Fall back to one-time if subscription is configured but pro is not active.
182 $configured_type = $attributes['paymentType'] ?? 'one-time';
183 $this->payment_type = 'subscription' === $configured_type && ! defined( 'SUREDONATION_PRO_VER' ) ? 'one-time' : $configured_type;
184 $this->subscription_plan = $attributes['subscriptionPlan'] ?? [];
185 $this->amount_type = $attributes['amountType'] ?? 'fixed';
186 $this->fixed_amount = $attributes['fixedAmount'] ?? 10;
187 $this->minimum_amount = $attributes['minimumAmount'] ?? 0;
188
189 // Set customer field mappings.
190 $this->customer_name_field = $attributes['customerNameField'] ?? '';
191 $this->customer_email_field = $attributes['customerEmailField'] ?? '';
192
193 // Set variable amount field mapping.
194 $this->variable_amount_field = $attributes['variableAmountField'] ?? '';
195
196 // Parse payment methods from block attributes (with backward compat fallback).
197 $block_payment_methods = $attributes['paymentMethods'] ?? null;
198 if ( ! is_array( $block_payment_methods ) || empty( $block_payment_methods ) ) {
199 $gateway = $attributes['gateway'] ?? 'stripe';
200 $block_payment_methods = [ is_string( $gateway ) ? $gateway : 'stripe' ];
201 }
202
203 // Filter to only globally-available gateways.
204 $available = [];
205 foreach ( $block_payment_methods as $method ) {
206 if ( 'stripe' === $method && $this->stripe_connected && ! empty( $this->stripe_publishable_key ) ) {
207 $available[] = 'stripe';
208 } elseif ( 'offline' === $method && Offline_Helper::is_offline_enabled() ) {
209 $available[] = 'offline';
210 }
211 }
212
213 /**
214 * Filter the list of available payment methods for this block.
215 *
216 * Allows extensions (e.g., PayPal) to add themselves when connected.
217 *
218 * @param array<string> $available Currently available method IDs.
219 * @param array<string> $block_payment_methods Methods selected in the block.
220 * @param array<string,mixed> $attributes Block attributes.
221 * @since 1.0.0
222 */
223 $available = apply_filters( 'suredonation_available_payment_methods', $available, $block_payment_methods, $attributes );
224
225 $this->payment_methods = ! empty( $available ) ? $available : $block_payment_methods;
226
227 // BACKWARD COMPATIBILITY: Migrate customer fields from subscriptionPlan.
228 if ( empty( $this->customer_name_field ) && ! empty( $this->subscription_plan['customer_name'] ) ) {
229 $this->customer_name_field = $this->subscription_plan['customer_name'];
230 }
231
232 if ( empty( $this->customer_email_field ) && ! empty( $this->subscription_plan['customer_email'] ) ) {
233 $this->customer_email_field = $this->subscription_plan['customer_email'];
234 }
235 }
236
237 /**
238 * Render the payment field markup.
239 *
240 * @return string
241 * @since 0.0.1
242 */
243 public function markup() {
244 $has_stripe = in_array( 'stripe', $this->payment_methods, true );
245
246 // Determine which gateways are actually connected/available for this form.
247 // get_registered_payment_methods() returns only enabled methods (Stripe when
248 // connected, Offline when enabled, plus any added by extensions such as
249 // PayPal), so it is the single gateway-agnostic source of truth.
250 $methods = $this->get_registered_payment_methods();
251
252 // No payment gateway is connected/available. Show a clear message instead of
253 // returning an empty string, which previously left donors with a silently
254 // broken form and no way to donate. See issue #219.
255 if ( empty( $methods ) ) {
256 return $this->render_gateway_unavailable_notice();
257 }
258
259 // Validate payment field requirements.
260 if ( ! $this->validate_payment_requirements() ) {
261 return '';
262 }
263
264 $field_classes = $this->get_field_classes();
265 $payment_methods_csv = implode( ',', $this->payment_methods );
266 $default_gateway = $this->payment_methods[0];
267
268 ob_start();
269 ?>
270 <div data-block-id="<?php echo esc_attr( $this->block_id ); ?>"
271 data-form-id="<?php echo esc_attr( $this->form_id ); ?>"
272 class="<?php echo esc_attr( $field_classes ); ?>"
273 data-payment-methods="<?php echo esc_attr( $payment_methods_csv ); ?>"
274 data-gateway="<?php echo esc_attr( $default_gateway ); ?>"
275 <?php if ( $has_stripe && ! empty( $this->stripe_publishable_key ) ) { ?>
276 data-stripe-key="<?php echo esc_attr( $this->stripe_publishable_key ); ?>"
277 <?php } ?>
278 data-currency="<?php echo esc_attr( strtolower( $this->currency ) ); ?>"
279 data-currency-symbol="<?php echo esc_attr( Payment_Helper::get_currency_symbol( $this->currency ) ); ?>"
280 data-payment-mode="<?php echo esc_attr( $this->payment_mode ); ?>"
281 data-amount-type="<?php echo esc_attr( $this->amount_type ); ?>"
282 data-fixed-amount="<?php echo esc_attr( (string) $this->fixed_amount ); ?>"
283 data-payment-type="<?php echo esc_attr( $this->payment_type ); ?>"
284 data-customer-name-field="<?php echo esc_attr( $this->customer_name_field ); ?>"
285 data-customer-email-field="<?php echo esc_attr( $this->customer_email_field ); ?>"
286 data-nonce="<?php echo esc_attr( wp_create_nonce( 'suredonation_donation_form' ) ); ?>"
287 <?php if ( 'variable' === $this->amount_type ) { ?>
288 data-variable-amount-field="<?php echo esc_attr( $this->variable_amount_field ); ?>"
289 <?php } ?>
290 <?php if ( $this->minimum_amount > 0 ) { ?>
291 data-minimum-amount="<?php echo esc_attr( (string) $this->minimum_amount ); ?>"
292 <?php } ?>
293 <?php if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) { ?>
294 data-subscription-plan-name="<?php echo esc_attr( isset( $this->subscription_plan['name'] ) ? Helper::get_string_value( $this->subscription_plan['name'] ) : __( 'Subscription Plan', 'suredonation' ) ); ?>"
295 data-subscription-interval="<?php echo esc_attr( isset( $this->subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month' ); ?>"
296 data-subscription-billing-cycles="<?php echo esc_attr( isset( $this->subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0' ); ?>"
297 <?php } ?>
298 >
299 <?php echo wp_kses_post( $this->label_markup ); ?>
300 <?php echo wp_kses_post( $this->help_markup ); ?>
301 <div class="sd-payment-field-wrapper">
302 <?php
303 // Amount display — uses wp_kses with data attributes allowed
304 // because wp_kses_post strips data-message-format, data-currency-symbol etc.
305 echo wp_kses( $this->render_amount_display(), Helper::get_allowed_form_html() );
306
307 // Test mode notice.
308 if ( 'test' === $this->payment_mode && $has_stripe ) {
309 echo wp_kses_post( $this->get_test_mode_notice() );
310 }
311
312 // Admin-only: the form renders (Offline is enabled) but no real
313 // gateway is connected.
314 echo wp_kses_post( $this->get_gateway_setup_notice() );
315
316 // Payment methods accordion ( $methods computed above ).
317 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.
318 ?>
319
320 <!-- Payment error display (hidden via CSS; shown by JS on error). -->
321 <div class="sd-payment-error"></div>
322 </div>
323 </div>
324 <?php
325 $output = ob_get_clean();
326 return false !== $output ? $output : '';
327 }
328
329 /**
330 * Get registered payment methods for display.
331 *
332 * @return array<mixed> Array of payment method configurations.
333 * @since 1.0.0
334 */
335 private function get_registered_payment_methods() {
336 $available_methods = [
337 'stripe' => [
338 'id' => 'stripe',
339 'label' => __( 'Credit / Debit Card', 'suredonation' ),
340 'enabled' => $this->stripe_connected && ! empty( $this->stripe_publishable_key ),
341 'container_class' => 'sd-payment-element',
342 'container_id' => 'sd-payment-element-' . $this->block_id,
343 ],
344 'offline' => [
345 'id' => 'offline',
346 'label' => __( 'Offline Donation', 'suredonation' ),
347 'enabled' => Offline_Helper::is_offline_enabled(),
348 'container_class' => 'sd-offline-instructions',
349 'content' => Offline_Helper::get_offline_instructions( $this->get_campaign_name() ),
350 ],
351 ];
352
353 $methods = [];
354 foreach ( $this->payment_methods as $method_id ) {
355 if ( isset( $available_methods[ $method_id ] ) && $available_methods[ $method_id ]['enabled'] ) {
356 $methods[ $method_id ] = $available_methods[ $method_id ];
357 }
358 }
359
360 /**
361 * Filter the registered payment methods and their display config.
362 *
363 * Allows extensions to add payment methods (e.g., PayPal) with their
364 * label, container class, and enabled state for frontend rendering.
365 *
366 * @param array<string, array<string,mixed>> $methods Registered methods keyed by ID.
367 * @param array<string> $payment_methods Methods selected in this block.
368 * @param string $block_id The block ID.
369 * @since 1.0.0
370 */
371 return apply_filters( 'suredonation_registered_payment_methods', $methods, $this->payment_methods, $this->block_id );
372 }
373
374 /**
375 * Get the campaign name for this form.
376 *
377 * @return string Campaign name or empty string.
378 * @since 1.0.0
379 */
380 private function get_campaign_name() {
381 if ( empty( $this->form_id ) ) {
382 return '';
383 }
384
385 $campaign_id = Donation_Form::get_form_campaign_id( absint( $this->form_id ) );
386
387 if ( empty( $campaign_id ) ) {
388 return '';
389 }
390
391 return get_the_title( $campaign_id );
392 }
393
394 /**
395 * Render payment methods as accordion.
396 * Each payment method is an accordion item with header and collapsible content.
397 *
398 * @param array<mixed> $methods Array of payment methods.
399 * @return string Payment methods accordion markup.
400 * @since 1.0.0
401 */
402 private function render_payment_methods_accordion( $methods ) {
403 if ( empty( $methods ) || ! is_array( $methods ) ) {
404 return '';
405 }
406
407 $is_single_method = count( $methods ) === 1;
408 $is_first = true;
409
410 ob_start();
411 ?>
412 <div class="sd-payment-methods-accordion <?php echo esc_attr( $is_single_method ? 'sd-single-payment-method' : '' ); ?>">
413 <?php foreach ( $methods as $method ) { ?>
414 <div
415 class="sd-accordion-item <?php echo esc_attr( $is_first ? 'sd-payment-active' : '' ); ?>"
416 data-method="<?php echo esc_attr( $method['id'] ); ?>"
417 >
418 <?php if ( $is_single_method ) { ?>
419 <?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"). ?>
420 <div class="sd-accordion-header">
421 <?php } else { ?>
422 <div
423 class="sd-accordion-header"
424 role="button"
425 tabindex="0"
426 aria-expanded="<?php echo esc_attr( $is_first ? 'true' : 'false' ); ?>"
427 aria-controls="sd-accordion-content-<?php echo esc_attr( $method['id'] ); ?>-<?php echo esc_attr( $this->block_id ); ?>"
428 >
429 <?php } ?>
430 <div class="sd-payment-input-wrapper">
431 <input
432 type="radio"
433 name="sd-gateway-choice-<?php echo esc_attr( $this->block_id ); ?>"
434 value="<?php echo esc_attr( $method['id'] ); ?>"
435 class="sd-payment-method-radio"
436 data-method="<?php echo esc_attr( $method['id'] ); ?>"
437 <?php checked( $is_first ); ?>
438 aria-label="<?php echo esc_attr( $method['label'] ); ?>"
439 />
440 <span class="sd-accordion-title sd-block-label">
441 <?php echo esc_html( $method['label'] ); ?>
442 </span>
443 </div>
444 </div>
445 <div
446 id="sd-accordion-content-<?php echo esc_attr( $method['id'] ); ?>-<?php echo esc_attr( $this->block_id ); ?>"
447 class="sd-accordion-content<?php echo Offline_Helper::is_blank_instructions( $method['content'] ?? '' ) ? ' sd-accordion-content-empty' : ''; ?>"
448 role="region"
449 >
450 <div
451 class="sd-payment-method-content"
452 data-method="<?php echo esc_attr( $method['id'] ); ?>"
453 >
454 <div
455 <?php if ( ! empty( $method['container_id'] ) ) { ?>
456 id="<?php echo esc_attr( $method['container_id'] ); ?>"
457 <?php } ?>
458 class="<?php echo esc_attr( $method['container_class'] ); ?>"
459 >
460 <?php
461 if ( ! empty( $method['content'] ) ) {
462 echo wp_kses_post( $method['content'] );
463 }
464 ?>
465 </div>
466 </div>
467 </div>
468 </div>
469 <?php $is_first = false; ?>
470 <?php } ?>
471 </div>
472 <?php
473 $template = ob_get_clean();
474
475 return is_string( $template ) ? $template : '';
476 }
477
478 /**
479 * Render amount display section.
480 *
481 * @return string Amount display HTML.
482 * @since 0.0.1
483 */
484 private function render_amount_display() {
485 ob_start();
486
487 if ( 'fixed' === $this->amount_type ) {
488 ?>
489 <!-- Fixed Payment Amount Display. -->
490 <div class="sd-payment-amount sd-label">
491 <span class="sd-payment-value">
492 <?php
493 if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) {
494 $interval = isset( $this->subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month';
495 $billing_cycles = isset( $this->subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0';
496 $interval_label = $this->get_interval_label( $interval );
497
498 // Build subscription text.
499 if ( 'ongoing' === $billing_cycles ) {
500 echo esc_html(
501 sprintf(
502 /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */
503 __( '%1$s per %2$s (until cancelled)', 'suredonation' ),
504 $this->format_currency( $this->fixed_amount, $this->currency ),
505 $interval_label
506 )
507 );
508 } elseif ( (int) $billing_cycles > 0 ) {
509 echo esc_html(
510 sprintf(
511 /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */
512 __( '%1$s per %2$s (%3$s payments)', 'suredonation' ),
513 $this->format_currency( $this->fixed_amount, $this->currency ),
514 $interval_label,
515 $billing_cycles
516 )
517 );
518 } else {
519 echo esc_html(
520 sprintf(
521 /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */
522 __( '%1$s per %2$s', 'suredonation' ),
523 $this->format_currency( $this->fixed_amount, $this->currency ),
524 $interval_label
525 )
526 );
527 }
528 } else {
529 echo esc_html( $this->format_currency( $this->fixed_amount, $this->currency ) );
530 }
531 ?>
532 </span>
533 </div>
534 <?php
535 } else {
536 ?>
537 <!-- Variable Payment Amount Display. -->
538 <div class="sd-variable-amount-display sd-label">
539 <div class="sd-payment-amount-wrapper">
540 <?php
541 // Generate message format for variable amounts.
542 $message_format = '{amount}';
543 if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) {
544 $interval = isset( $this->subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month';
545 $billing_cycles = isset( $this->subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0';
546 $interval_label = $this->get_interval_label( $interval );
547
548 // Build message format based on billing cycles.
549 if ( 'ongoing' === $billing_cycles ) {
550 /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */
551 $message_format = sprintf( __( '{amount} per %s (until cancelled)', 'suredonation' ), $interval_label );
552 } elseif ( (int) $billing_cycles > 0 ) {
553 /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */
554 $message_format = sprintf( __( '{amount} per %1$s (%2$s payments)', 'suredonation' ), $interval_label, $billing_cycles );
555 } else {
556 /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */
557 $message_format = sprintf( __( '{amount} per %s', 'suredonation' ), $interval_label );
558 }
559 }
560 ?>
561 <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 ); ?>">
562 <?php esc_html_e( 'Complete the form to view the amount.', 'suredonation' ); ?>
563 </span>
564 </div>
565 <?php if ( $this->minimum_amount > 0 ) { ?>
566 <span class="sd-help">
567 <?php
568 echo esc_html(
569 sprintf(
570 /* translators: %s: Minimum amount with currency */
571 __( 'Minimum amount: %s', 'suredonation' ),
572 $this->format_currency( $this->minimum_amount, $this->currency )
573 )
574 );
575 ?>
576 </span>
577 <?php } ?>
578 </div>
579 <?php
580 }
581
582 return (string) ob_get_clean();
583 }
584
585 /**
586 * Validate payment field requirements.
587 *
588 * @return bool True if validation passes, false otherwise.
589 * @since 0.0.1
590 */
591 private function validate_payment_requirements() {
592 // Check customer email field requirement (highest priority).
593 if ( empty( $this->customer_email_field ) ) {
594 return false;
595 }
596
597 // Check subscription-specific requirements.
598 if ( 'subscription' === $this->payment_type ) {
599 if ( empty( $this->customer_name_field ) ) {
600 return false;
601 }
602
603 if ( empty( $this->subscription_plan ) || empty( $this->subscription_plan['name'] ) ) {
604 return false;
605 }
606 }
607
608 return true;
609 }
610
611 /**
612 * Format currency for display.
613 *
614 * @param float $amount Amount to format.
615 * @param string $currency Currency code.
616 * @return string
617 * @since 0.0.1
618 */
619 private function format_currency( $amount, $currency ) {
620 // Delegate to the single source of truth so decimal handling and the
621 // currency sign position stay consistent with every other surface.
622 return Payment_Helper::format_amount( $amount, $currency );
623 }
624
625 /**
626 * Get the human-readable label for a payment interval slug.
627 *
628 * @param string $interval_slug The slug (e.g., 'day', 'week', 'month', 'quarter', 'yearly').
629 * @return string The translated interval label, or the slug itself if not found.
630 * @since 0.0.1
631 */
632 private function get_interval_label( $interval_slug ) {
633 $interval_labels = [
634 'day' => __( 'day', 'suredonation' ),
635 'week' => __( 'week', 'suredonation' ),
636 'month' => __( 'month', 'suredonation' ),
637 'quarter' => __( 'quarter', 'suredonation' ),
638 'yearly' => __( 'year', 'suredonation' ),
639 ];
640
641 return $interval_labels[ $interval_slug ] ?? $interval_slug;
642 }
643
644 /**
645 * Render test mode notice for admin users.
646 * Only shows if user has manage_options capability.
647 *
648 * @return string Test mode notice markup or empty string.
649 * @since 0.0.1
650 */
651 private function get_test_mode_notice() {
652 // Only show to users with manage_options capability.
653 if ( ! current_user_can( 'manage_options' ) ) {
654 return '';
655 }
656
657 // Build dynamic link to payment settings (shared, hash-routed URL).
658 $settings_url = Payment_Helper::get_settings_url();
659
660 ob_start();
661 ?>
662 <div class="sd-test-mode-notice">
663 <strong><?php esc_html_e( 'Test mode is enabled:', 'suredonation' ); ?></strong>
664 <a href="<?php echo esc_url( $settings_url ); ?>" target="_blank" rel="noopener noreferrer">
665 <?php esc_html_e( 'Click here to enable live mode and accept payment', 'suredonation' ); ?>
666 </a>
667 </div>
668 <?php
669 $output = ob_get_clean();
670 return false !== $output ? $output : '';
671 }
672
673 /**
674 * Render the notice shown when no payment gateway is connected/available.
675 *
676 * Replaces the previous behavior of rendering nothing, which left donors with
677 * a silently broken form. The `data-payment-available="0"` marker lets the
678 * frontend script skip gateway init and hide the otherwise-inert submit
679 * button. See issue #219.
680 *
681 * @return string Notice markup.
682 * @since 1.1.1
683 */
684 private function render_gateway_unavailable_notice() {
685 $field_classes = $this->get_field_classes( [ 'sd-payment-unavailable' ] );
686
687 // The notice text is shown to everyone (admins and donors). Admins
688 // additionally get an actionable button; donors see the notice without
689 // it. Mirrors the capability + settings-URL pattern used by
690 // get_test_mode_notice().
691 $is_admin = current_user_can( 'manage_options' );
692 $configure_url = '';
693 $configure_text = '';
694
695 if ( $is_admin ) {
696 // Route the admin to the right place. If a gateway is already usable
697 // on the site, the block simply hasn't selected it — send them to
698 // this form's editor to fix the payment block. Otherwise no gateway
699 // is set up at all — send them to global payment settings.
700 $edit_link = $this->form_id > 0 ? get_edit_post_link( (int) $this->form_id ) : '';
701
702 if ( Payment_Helper::has_usable_gateway() && $edit_link ) {
703 $configure_url = $edit_link;
704 $configure_text = __( 'Edit this form’s payment settings', 'suredonation' );
705 } else {
706 // No gateway connected — send the admin straight to the gateway
707 // connect screen (Stripe) rather than the currency/mode page.
708 $configure_url = Payment_Helper::get_settings_url( 'stripe' );
709 $configure_text = __( 'Configure payment gateway', 'suredonation' );
710 }
711 }
712
713 ob_start();
714 ?>
715 <div
716 data-block-id="<?php echo esc_attr( $this->block_id ); ?>"
717 data-form-id="<?php echo esc_attr( $this->form_id ); ?>"
718 data-payment-available="0"
719 class="<?php echo esc_attr( $field_classes ); ?>"
720 >
721 <?php echo wp_kses_post( $this->label_markup ); ?>
722 <div class="sd-payment-field-wrapper">
723 <div class="sd-payment-notice" role="status">
724 <p><?php esc_html_e( 'Payment gateways are not configured. Please contact the site administrator.', 'suredonation' ); ?></p>
725 <?php echo wp_kses_post( $is_admin ? $this->render_configure_link( $configure_url, $configure_text ) : '' ); ?>
726 </div>
727 </div>
728 </div>
729 <?php
730 $output = ob_get_clean();
731 return false !== $output ? $output : '';
732 }
733
734 /**
735 * Render the admin-only notice shown when the payment field renders but no
736 * real payment gateway is connected.
737 *
738 * Enabling Offline Donation makes the field render normally, so the
739 * donor-facing "gateways are not configured" notice never fires — an admin
740 * viewing the live form gets no signal that neither Stripe nor PayPal was
741 * ever connected. Offline is a manual method, not a gateway, so the prompt
742 * is still warranted. Donors see nothing: the form works for them.
743 *
744 * @return string Notice markup, or empty string when a gateway is connected
745 * or the viewer is not an administrator.
746 * @since 1.4.0
747 */
748 private function get_gateway_setup_notice() {
749 if ( ! current_user_can( 'manage_options' ) || Payment_Helper::is_any_gateway_connected() ) {
750 return '';
751 }
752
753 ob_start();
754 ?>
755 <div class="sd-payment-notice sd-payment-notice--admin" role="status">
756 <p><?php esc_html_e( 'No payment gateway is connected, so donors can only give using the offline method.', 'suredonation' ); ?></p>
757 <?php
758 echo wp_kses_post(
759 $this->render_configure_link(
760 Payment_Helper::get_settings_url( 'stripe' ),
761 __( 'Configure payment gateway', 'suredonation' )
762 )
763 );
764 ?>
765 </div>
766 <?php
767 $output = ob_get_clean();
768 return false !== $output ? $output : '';
769 }
770
771 /**
772 * Render the actionable settings link shared by the payment notices.
773 *
774 * @param string $url Target URL.
775 * @param string $text Link text.
776 * @return string Link markup, or empty string when there is nothing to link to.
777 * @since 1.4.0
778 */
779 private function render_configure_link( $url, $text ) {
780 if ( '' === $url || '' === $text ) {
781 return '';
782 }
783
784 ob_start();
785 ?>
786 <a
787 class="sd-payment-notice__configure"
788 href="<?php echo esc_url( $url ); ?>"
789 target="_blank"
790 rel="noopener noreferrer"
791 >
792 <?php echo esc_html( $text ); ?>
793 </a>
794 <?php
795 $output = ob_get_clean();
796 return false !== $output ? $output : '';
797 }
798 }
799