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

739 lines 24.8 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 // Payment methods accordion ( $methods computed above ).
313 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.
314 ?>
315
316 <!-- Payment error display (hidden via CSS; shown by JS on error). -->
317 <div class="sd-payment-error"></div>
318 </div>
319 </div>
320 <?php
321 $output = ob_get_clean();
322 return false !== $output ? $output : '';
323 }
324
325 /**
326 * Get registered payment methods for display.
327 *
328 * @return array<mixed> Array of payment method configurations.
329 * @since 1.0.0
330 */
331 private function get_registered_payment_methods() {
332 $available_methods = [
333 'stripe' => [
334 'id' => 'stripe',
335 'label' => __( 'Credit / Debit Card', 'suredonation' ),
336 'enabled' => $this->stripe_connected && ! empty( $this->stripe_publishable_key ),
337 'container_class' => 'sd-payment-element',
338 'container_id' => 'sd-payment-element-' . $this->block_id,
339 ],
340 'offline' => [
341 'id' => 'offline',
342 'label' => __( 'Offline Donation', 'suredonation' ),
343 'enabled' => Offline_Helper::is_offline_enabled(),
344 'container_class' => 'sd-offline-instructions',
345 'content' => Offline_Helper::get_offline_instructions( $this->get_campaign_name() ),
346 ],
347 ];
348
349 $methods = [];
350 foreach ( $this->payment_methods as $method_id ) {
351 if ( isset( $available_methods[ $method_id ] ) && $available_methods[ $method_id ]['enabled'] ) {
352 $methods[ $method_id ] = $available_methods[ $method_id ];
353 }
354 }
355
356 /**
357 * Filter the registered payment methods and their display config.
358 *
359 * Allows extensions to add payment methods (e.g., PayPal) with their
360 * label, container class, and enabled state for frontend rendering.
361 *
362 * @param array<string, array<string,mixed>> $methods Registered methods keyed by ID.
363 * @param array<string> $payment_methods Methods selected in this block.
364 * @param string $block_id The block ID.
365 * @since 1.0.0
366 */
367 return apply_filters( 'suredonation_registered_payment_methods', $methods, $this->payment_methods, $this->block_id );
368 }
369
370 /**
371 * Get the campaign name for this form.
372 *
373 * @return string Campaign name or empty string.
374 * @since 1.0.0
375 */
376 private function get_campaign_name() {
377 if ( empty( $this->form_id ) ) {
378 return '';
379 }
380
381 $campaign_id = Donation_Form::get_form_campaign_id( absint( $this->form_id ) );
382
383 if ( empty( $campaign_id ) ) {
384 return '';
385 }
386
387 return get_the_title( $campaign_id );
388 }
389
390 /**
391 * Render payment methods as accordion.
392 * Each payment method is an accordion item with header and collapsible content.
393 *
394 * @param array<mixed> $methods Array of payment methods.
395 * @return string Payment methods accordion markup.
396 * @since 1.0.0
397 */
398 private function render_payment_methods_accordion( $methods ) {
399 if ( empty( $methods ) || ! is_array( $methods ) ) {
400 return '';
401 }
402
403 $is_single_method = count( $methods ) === 1;
404 $is_first = true;
405
406 ob_start();
407 ?>
408 <div class="sd-payment-methods-accordion <?php echo esc_attr( $is_single_method ? 'sd-single-payment-method' : '' ); ?>">
409 <?php foreach ( $methods as $method ) { ?>
410 <div
411 class="sd-accordion-item <?php echo esc_attr( $is_first ? 'sd-payment-active' : '' ); ?>"
412 data-method="<?php echo esc_attr( $method['id'] ); ?>"
413 >
414 <?php if ( $is_single_method ) { ?>
415 <?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"). ?>
416 <div class="sd-accordion-header">
417 <?php } else { ?>
418 <div
419 class="sd-accordion-header"
420 role="button"
421 tabindex="0"
422 aria-expanded="<?php echo esc_attr( $is_first ? 'true' : 'false' ); ?>"
423 aria-controls="sd-accordion-content-<?php echo esc_attr( $method['id'] ); ?>-<?php echo esc_attr( $this->block_id ); ?>"
424 >
425 <?php } ?>
426 <div class="sd-payment-input-wrapper">
427 <input
428 type="radio"
429 name="sd-gateway-choice-<?php echo esc_attr( $this->block_id ); ?>"
430 value="<?php echo esc_attr( $method['id'] ); ?>"
431 class="sd-payment-method-radio"
432 data-method="<?php echo esc_attr( $method['id'] ); ?>"
433 <?php checked( $is_first ); ?>
434 aria-label="<?php echo esc_attr( $method['label'] ); ?>"
435 />
436 <span class="sd-accordion-title sd-block-label">
437 <?php echo esc_html( $method['label'] ); ?>
438 </span>
439 </div>
440 </div>
441 <div
442 id="sd-accordion-content-<?php echo esc_attr( $method['id'] ); ?>-<?php echo esc_attr( $this->block_id ); ?>"
443 class="sd-accordion-content<?php echo Offline_Helper::is_blank_instructions( $method['content'] ?? '' ) ? ' sd-accordion-content-empty' : ''; ?>"
444 role="region"
445 >
446 <div
447 class="sd-payment-method-content"
448 data-method="<?php echo esc_attr( $method['id'] ); ?>"
449 >
450 <div
451 <?php if ( ! empty( $method['container_id'] ) ) { ?>
452 id="<?php echo esc_attr( $method['container_id'] ); ?>"
453 <?php } ?>
454 class="<?php echo esc_attr( $method['container_class'] ); ?>"
455 >
456 <?php
457 if ( ! empty( $method['content'] ) ) {
458 echo wp_kses_post( $method['content'] );
459 }
460 ?>
461 </div>
462 </div>
463 </div>
464 </div>
465 <?php $is_first = false; ?>
466 <?php } ?>
467 </div>
468 <?php
469 $template = ob_get_clean();
470
471 return is_string( $template ) ? $template : '';
472 }
473
474 /**
475 * Render amount display section.
476 *
477 * @return string Amount display HTML.
478 * @since 0.0.1
479 */
480 private function render_amount_display() {
481 ob_start();
482
483 if ( 'fixed' === $this->amount_type ) {
484 ?>
485 <!-- Fixed Payment Amount Display. -->
486 <div class="sd-payment-amount sd-label">
487 <span class="sd-payment-value">
488 <?php
489 if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) {
490 $interval = isset( $this->subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month';
491 $billing_cycles = isset( $this->subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0';
492 $interval_label = $this->get_interval_label( $interval );
493
494 // Build subscription text.
495 if ( 'ongoing' === $billing_cycles ) {
496 echo esc_html(
497 sprintf(
498 /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */
499 __( '%1$s per %2$s (until cancelled)', 'suredonation' ),
500 $this->format_currency( $this->fixed_amount, $this->currency ),
501 $interval_label
502 )
503 );
504 } elseif ( (int) $billing_cycles > 0 ) {
505 echo esc_html(
506 sprintf(
507 /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */
508 __( '%1$s per %2$s (%3$s payments)', 'suredonation' ),
509 $this->format_currency( $this->fixed_amount, $this->currency ),
510 $interval_label,
511 $billing_cycles
512 )
513 );
514 } else {
515 echo esc_html(
516 sprintf(
517 /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */
518 __( '%1$s per %2$s', 'suredonation' ),
519 $this->format_currency( $this->fixed_amount, $this->currency ),
520 $interval_label
521 )
522 );
523 }
524 } else {
525 echo esc_html( $this->format_currency( $this->fixed_amount, $this->currency ) );
526 }
527 ?>
528 </span>
529 </div>
530 <?php
531 } else {
532 ?>
533 <!-- Variable Payment Amount Display. -->
534 <div class="sd-variable-amount-display sd-label">
535 <div class="sd-payment-amount-wrapper">
536 <?php
537 // Generate message format for variable amounts.
538 $message_format = '{amount}';
539 if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) {
540 $interval = isset( $this->subscription_plan['interval'] ) ? Helper::get_string_value( $this->subscription_plan['interval'] ) : 'month';
541 $billing_cycles = isset( $this->subscription_plan['billingCycles'] ) ? Helper::get_string_value( $this->subscription_plan['billingCycles'] ) : '0';
542 $interval_label = $this->get_interval_label( $interval );
543
544 // Build message format based on billing cycles.
545 if ( 'ongoing' === $billing_cycles ) {
546 /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */
547 $message_format = sprintf( __( '{amount} per %s (until cancelled)', 'suredonation' ), $interval_label );
548 } elseif ( (int) $billing_cycles > 0 ) {
549 /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */
550 $message_format = sprintf( __( '{amount} per %1$s (%2$s payments)', 'suredonation' ), $interval_label, $billing_cycles );
551 } else {
552 /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */
553 $message_format = sprintf( __( '{amount} per %s', 'suredonation' ), $interval_label );
554 }
555 }
556 ?>
557 <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 ); ?>">
558 <?php esc_html_e( 'Complete the form to view the amount.', 'suredonation' ); ?>
559 </span>
560 </div>
561 <?php if ( $this->minimum_amount > 0 ) { ?>
562 <span class="sd-help">
563 <?php
564 echo esc_html(
565 sprintf(
566 /* translators: %s: Minimum amount with currency */
567 __( 'Minimum amount: %s', 'suredonation' ),
568 $this->format_currency( $this->minimum_amount, $this->currency )
569 )
570 );
571 ?>
572 </span>
573 <?php } ?>
574 </div>
575 <?php
576 }
577
578 return (string) ob_get_clean();
579 }
580
581 /**
582 * Validate payment field requirements.
583 *
584 * @return bool True if validation passes, false otherwise.
585 * @since 0.0.1
586 */
587 private function validate_payment_requirements() {
588 // Check customer email field requirement (highest priority).
589 if ( empty( $this->customer_email_field ) ) {
590 return false;
591 }
592
593 // Check subscription-specific requirements.
594 if ( 'subscription' === $this->payment_type ) {
595 if ( empty( $this->customer_name_field ) ) {
596 return false;
597 }
598
599 if ( empty( $this->subscription_plan ) || empty( $this->subscription_plan['name'] ) ) {
600 return false;
601 }
602 }
603
604 return true;
605 }
606
607 /**
608 * Format currency for display.
609 *
610 * @param float $amount Amount to format.
611 * @param string $currency Currency code.
612 * @return string
613 * @since 0.0.1
614 */
615 private function format_currency( $amount, $currency ) {
616 // Delegate to the single source of truth so decimal handling and the
617 // currency sign position stay consistent with every other surface.
618 return Payment_Helper::format_amount( $amount, $currency );
619 }
620
621 /**
622 * Get the human-readable label for a payment interval slug.
623 *
624 * @param string $interval_slug The slug (e.g., 'day', 'week', 'month', 'quarter', 'yearly').
625 * @return string The translated interval label, or the slug itself if not found.
626 * @since 0.0.1
627 */
628 private function get_interval_label( $interval_slug ) {
629 $interval_labels = [
630 'day' => __( 'day', 'suredonation' ),
631 'week' => __( 'week', 'suredonation' ),
632 'month' => __( 'month', 'suredonation' ),
633 'quarter' => __( 'quarter', 'suredonation' ),
634 'yearly' => __( 'year', 'suredonation' ),
635 ];
636
637 return $interval_labels[ $interval_slug ] ?? $interval_slug;
638 }
639
640 /**
641 * Render test mode notice for admin users.
642 * Only shows if user has manage_options capability.
643 *
644 * @return string Test mode notice markup or empty string.
645 * @since 0.0.1
646 */
647 private function get_test_mode_notice() {
648 // Only show to users with manage_options capability.
649 if ( ! current_user_can( 'manage_options' ) ) {
650 return '';
651 }
652
653 // Build dynamic link to payment settings (shared, hash-routed URL).
654 $settings_url = Payment_Helper::get_settings_url();
655
656 ob_start();
657 ?>
658 <div class="sd-test-mode-notice">
659 <strong><?php esc_html_e( 'Test mode is enabled:', 'suredonation' ); ?></strong>
660 <a href="<?php echo esc_url( $settings_url ); ?>" target="_blank" rel="noopener noreferrer">
661 <?php esc_html_e( 'Click here to enable live mode and accept payment', 'suredonation' ); ?>
662 </a>
663 </div>
664 <?php
665 $output = ob_get_clean();
666 return false !== $output ? $output : '';
667 }
668
669 /**
670 * Render the notice shown when no payment gateway is connected/available.
671 *
672 * Replaces the previous behavior of rendering nothing, which left donors with
673 * a silently broken form. The `data-payment-available="0"` marker lets the
674 * frontend script skip gateway init and hide the otherwise-inert submit
675 * button. See issue #219.
676 *
677 * @return string Notice markup.
678 * @since 1.1.1
679 */
680 private function render_gateway_unavailable_notice() {
681 $field_classes = $this->get_field_classes( [ 'sd-payment-unavailable' ] );
682
683 // The notice text is shown to everyone (admins and donors). Admins
684 // additionally get an actionable button; donors see the notice without
685 // it. Mirrors the capability + settings-URL pattern used by
686 // get_test_mode_notice().
687 $is_admin = current_user_can( 'manage_options' );
688 $configure_url = '';
689 $configure_text = '';
690
691 if ( $is_admin ) {
692 // Route the admin to the right place. If a gateway is already usable
693 // on the site, the block simply hasn't selected it — send them to
694 // this form's editor to fix the payment block. Otherwise no gateway
695 // is set up at all — send them to global payment settings.
696 $edit_link = $this->form_id > 0 ? get_edit_post_link( (int) $this->form_id ) : '';
697
698 if ( Payment_Helper::has_usable_gateway() && $edit_link ) {
699 $configure_url = $edit_link;
700 $configure_text = __( 'Edit this form’s payment settings', 'suredonation' );
701 } else {
702 // No gateway connected — send the admin straight to the gateway
703 // connect screen (Stripe) rather than the currency/mode page.
704 $configure_url = Payment_Helper::get_settings_url( 'stripe' );
705 $configure_text = __( 'Configure payment gateway', 'suredonation' );
706 }
707 }
708
709 ob_start();
710 ?>
711 <div
712 data-block-id="<?php echo esc_attr( $this->block_id ); ?>"
713 data-form-id="<?php echo esc_attr( $this->form_id ); ?>"
714 data-payment-available="0"
715 class="<?php echo esc_attr( $field_classes ); ?>"
716 >
717 <?php echo wp_kses_post( $this->label_markup ); ?>
718 <div class="sd-payment-field-wrapper">
719 <div class="sd-payment-notice" role="status">
720 <p><?php esc_html_e( 'Payment gateways are not configured. Please contact the site administrator.', 'suredonation' ); ?></p>
721 <?php if ( $is_admin && '' !== $configure_url ) { ?>
722 <a
723 class="sd-payment-notice__configure"
724 href="<?php echo esc_url( $configure_url ); ?>"
725 target="_blank"
726 rel="noopener noreferrer"
727 >
728 <?php echo esc_html( $configure_text ); ?>
729 </a>
730 <?php } ?>
731 </div>
732 </div>
733 </div>
734 <?php
735 $output = ob_get_clean();
736 return false !== $output ? $output : '';
737 }
738 }
739