PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.8
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.8
2.12.8 2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 All 98 releases
← All changes | inc/fields/payment-markup.php +413 -111 2.7.1 → 2.12.8 View file →
@@ -7,8 +7,9 @@
7 7 */
8 8
9 9 namespace SRFM\Inc\Fields;
10 10
11 +use SRFM\Inc\Helper;
11 12 use SRFM\Inc\Payments\Payment_Helper;
12 13 use SRFM\Inc\Payments\Stripe\Stripe_Helper;
13 14
14 15 if ( ! defined( 'ABSPATH' ) ) {
@@ -140,9 +141,61 @@
140 141 * @since 2.7.1
141 142 */
142 143 protected $payment_description;
143 144
145 + // BOTH MODE: start — admin-configurable dual-mode attributes.
144 146 /**
147 + * Original payment type as configured in the editor (one-time / subscription / both).
148 + * While rendering we may rewrite $this->payment_type to the resolved default-choice,
149 + * so we keep the original intent here for conditionals.
150 + *
151 + * @var string
152 + * @since 2.8.2
153 + */
154 + protected $original_payment_type;
155 +
156 + /**
157 + * Label shown next to the "one-time" radio choice in both mode.
158 + *
159 + * @var string
160 + * @since 2.8.2
161 + */
162 + protected $one_time_label;
163 +
164 + /**
165 + * Label shown next to the "subscription" radio choice in both mode.
166 + *
167 + * @var string
168 + * @since 2.8.2
169 + */
170 + protected $subscription_label;
171 +
172 + /**
173 + * Default radio selection in both mode ("one-time" or "subscription").
174 + *
175 + * @var string
176 + * @since 2.8.2
177 + */
178 + protected $default_payment_choice;
179 +
180 + /**
181 + * One-time amount configuration (used only in both mode).
182 + *
183 + * @var array<mixed>
184 + * @since 2.8.2
185 + */
186 + protected $one_time_config = [];
187 +
188 + /**
189 + * Subscription amount configuration (used only in both mode).
190 + *
191 + * @var array<mixed>
192 + * @since 2.8.2
193 + */
194 + protected $subscription_config = [];
195 + // BOTH MODE: end.
196 +
197 + /**
145 198 * Constructor for the Payment Markup class.
146 199 *
147 200 * @param array<mixed> $attributes Block attributes.
148 201 * @since 2.0.0
@@ -186,8 +239,46 @@
186 239
187 240 // Set variable amount field mapping.
188 241 $this->variable_amount_field = $attributes['variableAmountField'] ?? '';
189 242
243 + // BOTH MODE: start — capture original intent and dual-mode configuration.
244 + $this->original_payment_type = $this->payment_type;
245 +
246 + $this->one_time_label = $attributes['oneTimeLabel'] ?? __( 'One-Time Payment', 'sureforms' );
247 + $this->subscription_label = $attributes['subscriptionLabel'] ?? __( 'Subscription', 'sureforms' );
248 + $this->default_payment_choice = $attributes['defaultPaymentChoice'] ?? 'one-time';
249 + if ( ! in_array( $this->default_payment_choice, [ 'one-time', 'subscription' ], true ) ) {
250 + $this->default_payment_choice = 'one-time';
251 + }
252 +
253 + $this->one_time_config = [
254 + 'amount_type' => $attributes['oneTimeAmountType'] ?? 'fixed',
255 + 'fixed_amount' => isset( $attributes['oneTimeFixedAmount'] ) ? (float) $attributes['oneTimeFixedAmount'] : 10.0,
256 + 'minimum_amount' => isset( $attributes['oneTimeMinimumAmount'] ) ? (float) $attributes['oneTimeMinimumAmount'] : 0.0,
257 + 'variable_field' => $attributes['oneTimeVariableAmountField'] ?? '',
258 + ];
259 +
260 + $this->subscription_config = [
261 + 'amount_type' => $attributes['subscriptionAmountType'] ?? 'fixed',
262 + 'fixed_amount' => isset( $attributes['subscriptionFixedAmount'] ) ? (float) $attributes['subscriptionFixedAmount'] : 10.0,
263 + 'minimum_amount' => isset( $attributes['subscriptionMinimumAmount'] ) ? (float) $attributes['subscriptionMinimumAmount'] : 0.0,
264 + 'variable_field' => $attributes['subscriptionVariableAmountField'] ?? '',
265 + ];
266 +
267 + // When rendering the "both" block, the visible amount + Stripe mode is driven by
268 + // the default choice. Rewrite the scalar properties so the existing rendering
269 + // logic below continues to work with zero changes.
270 + if ( 'both' === $this->original_payment_type ) {
271 + $active_config = 'subscription' === $this->default_payment_choice ? $this->subscription_config : $this->one_time_config;
272 + $this->amount_type = $active_config['amount_type'];
273 + $this->fixed_amount = $active_config['fixed_amount'];
274 + $this->minimum_amount = $active_config['minimum_amount'];
275 + $this->variable_amount_field = $active_config['variable_field'];
276 + // $this->payment_type now represents the *active* rendering type, not the admin-set value.
277 + $this->payment_type = $this->default_payment_choice;
278 + }
279 + // BOTH MODE: end.
280 +
190 281 // Set payment methods from block attributes, default to 'stripe' for backward compatibility.
191 282 $this->payment_methods = $attributes['paymentMethods'] ?? [ 'stripe' ];
192 283
193 284 // Set custom payment description (empty means JS/gateway will use its own default).
@@ -244,11 +335,17 @@
244 335 'data-payment-methods' => wp_json_encode( array_keys( $registered_methods ) ),
245 336 'data-selected-method' => $first_method_id,
246 337 ];
247 338
248 - if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) {
249 - $data_input_attributes['data-subscription-plan-name'] = $this->subscription_plan['name'] ?? __( 'Subscription Plan', 'sureforms' );
250 - $data_input_attributes['data-subscription-interval'] = $this->subscription_plan['interval'] ?? 'month';
339 + // Subscription plan attributes are emitted when subscription is the active type
340 + // OR when "both" mode allows it as a possible end-user choice.
341 + $has_subscription_path = 'subscription' === $this->payment_type || 'both' === $this->original_payment_type;
342 + if ( $has_subscription_path && ! empty( $this->subscription_plan ) ) {
343 + // Defense-in-depth: coerce to scalar before output. A malformed REST
344 + // save could store these as arrays, which would otherwise emit notices
345 + // or empty strings through esc_attr() downstream.
346 + $data_input_attributes['data-subscription-plan-name'] = (string) ( $this->subscription_plan['name'] ?? __( 'Subscription Plan', 'sureforms' ) );
347 + $data_input_attributes['data-subscription-interval'] = (string) ( $this->subscription_plan['interval'] ?? 'month' );
251 348 $data_input_attributes['data-subscription-billing-cycles'] = $this->subscription_plan['billingCycles'] ?? 0;
252 349 }
253 350
254 351 if ( ! empty( $this->payment_description ) ) {
@@ -258,13 +355,47 @@
258 355 if ( 'variable' === $this->amount_type ) {
259 356 $data_input_attributes['data-variable-amount-field'] = $this->variable_amount_field;
260 357 }
261 358
359 + // BOTH MODE: ensure data-variable-amount-field is emitted at page load if
360 + // EITHER type uses variable amount. listenAmountChanges() runs once at init
361 + // and queries [data-variable-amount-field] — without this, the listener is
362 + // never wired when the default choice is fixed but the other type is variable.
363 + if ( 'both' === $this->original_payment_type && 'variable' !== $this->amount_type ) {
364 + $other_config = 'subscription' === $this->default_payment_choice
365 + ? $this->one_time_config
366 + : $this->subscription_config;
367 + if ( 'variable' === $other_config['amount_type'] && ! empty( $other_config['variable_field'] ) ) {
368 + $data_input_attributes['data-variable-amount-field'] = $other_config['variable_field'];
369 + }
370 + }
371 +
262 372 // If minimum amount is greater than 0, add it to the data input attributes.
263 373 if ( $this->minimum_amount > 0 ) {
264 374 $data_input_attributes['data-minimum-amount'] = $this->minimum_amount;
265 375 }
266 376
377 + // BOTH MODE: start — emit per-type configuration for the JS chooser to read.
378 + if ( 'both' === $this->original_payment_type ) {
379 + $data_input_attributes['data-original-payment-type'] = 'both';
380 + $data_input_attributes['data-default-payment-choice'] = $this->default_payment_choice;
381 +
382 + $data_input_attributes['data-one-time-amount-type'] = $this->one_time_config['amount_type'];
383 + $data_input_attributes['data-one-time-fixed-amount'] = $this->one_time_config['fixed_amount'];
384 + $data_input_attributes['data-one-time-minimum-amount'] = $this->one_time_config['minimum_amount'];
385 + if ( 'variable' === $this->one_time_config['amount_type'] ) {
386 + $data_input_attributes['data-one-time-variable-amount-field'] = $this->one_time_config['variable_field'];
387 + }
388 +
389 + $data_input_attributes['data-subscription-amount-type'] = $this->subscription_config['amount_type'];
390 + $data_input_attributes['data-subscription-fixed-amount'] = $this->subscription_config['fixed_amount'];
391 + $data_input_attributes['data-subscription-minimum-amount'] = $this->subscription_config['minimum_amount'];
392 + if ( 'variable' === $this->subscription_config['amount_type'] ) {
393 + $data_input_attributes['data-subscription-variable-amount-field'] = $this->subscription_config['variable_field'];
394 + }
395 + }
396 + // BOTH MODE: end.
397 +
267 398 ob_start();
268 399 ?>
269 400 <div data-block-id="<?php echo esc_attr( $this->block_id ); ?>" class="<?php echo esc_attr( $field_classes ); ?>">
270 401 <?php echo wp_kses_post( $this->label_markup ); ?>
@@ -269,97 +400,67 @@
269 400 <div data-block-id="<?php echo esc_attr( $this->block_id ); ?>" class="<?php echo esc_attr( $field_classes ); ?>">
270 401 <?php echo wp_kses_post( $this->label_markup ); ?>
271 402 <?php echo wp_kses_post( $this->help_markup ); ?>
272 403 <div class="srfm-payment-field-wrapper">
273 - <?php if ( 'fixed' === $this->amount_type ) { ?>
274 - <!-- Fixed Payment Amount Display. -->
275 - <div class="srfm-payment-amount srfm-block-label">
276 - <span class="srfm-payment-value">
277 - <?php
278 - if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) {
279 - $interval = $this->subscription_plan['interval'] ?? 'month';
280 - $billing_cycles = $this->subscription_plan['billingCycles'] ?? 0;
281 - $interval_label = $this->get_interval_label( $interval );
404 + <?php
405 + // BOTH MODE: start — render the payment-type chooser.
406 + if ( 'both' === $this->original_payment_type ) {
407 + echo $this->render_payment_type_chooser(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
408 + }
409 + // BOTH MODE: end.
282 410
283 - // Build subscription text.
284 - if ( 'ongoing' === $billing_cycles ) {
285 - echo esc_html(
286 - sprintf(
287 - /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */
288 - __( '%1$s per %2$s (until cancelled)', 'sureforms' ),
289 - $this->format_currency( $this->fixed_amount, $this->currency ),
290 - $interval_label
291 - )
292 - );
293 - } elseif ( $billing_cycles > 0 ) {
294 - echo esc_html(
295 - sprintf(
296 - /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */
297 - __( '%1$s per %2$s (%3$s payments)', 'sureforms' ),
298 - $this->format_currency( $this->fixed_amount, $this->currency ),
299 - $interval_label,
300 - $billing_cycles
301 - )
302 - );
303 - } else {
304 - echo esc_html(
305 - sprintf(
306 - /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */
307 - __( '%1$s per %2$s', 'sureforms' ),
308 - $this->format_currency( $this->fixed_amount, $this->currency ),
309 - $interval_label
310 - )
311 - );
312 - }
313 - } else {
314 - echo esc_html( $this->format_currency( $this->fixed_amount, $this->currency ) );
315 - }
316 - ?>
317 - </span>
411 + if ( 'both' === $this->original_payment_type ) {
412 + // BOTH MODE: render two amount displays, one per choice. JS toggles visibility.
413 + $is_one_time_default = 'one-time' === $this->default_payment_choice;
414 + ?>
415 + <div
416 + id="srfm-payment-amount-<?php echo esc_attr( $this->block_id ); ?>-one-time"
417 + class="srfm-payment-amount-block srfm-payment-amount-block-one-time"
418 + data-payment-type="one-time"
419 + aria-labelledby="srfm-payment-type-<?php echo esc_attr( $this->block_id ); ?>-one-time"
420 + <?php echo esc_attr( $is_one_time_default ? '' : 'hidden' ); ?>
421 + >
422 + <?php
423 + // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- render_amount_display handles its own escaping.
424 + echo $this->render_amount_display(
425 + 'one-time',
426 + Helper::get_string_value( $this->one_time_config['amount_type'] ?? 'fixed' ),
427 + is_numeric( $this->one_time_config['fixed_amount'] ?? null ) ? (float) $this->one_time_config['fixed_amount'] : 0.0,
428 + is_numeric( $this->one_time_config['minimum_amount'] ?? null ) ? (float) $this->one_time_config['minimum_amount'] : 0.0
429 + );
430 + // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
431 + ?>
318 432 </div>
319 - <?php } else { ?>
320 - <!-- Variable Payment Amount Display. -->
321 - <div class="srfm-variable-amount-display srfm-block-label">
322 - <div class="srfm-payment-amount-wrapper">
323 - <?php
324 - // Generate message format for variable amounts.
325 - $message_format = '{amount}';
326 - if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) {
327 - $interval = $this->subscription_plan['interval'] ?? 'month';
328 - $billing_cycles = $this->subscription_plan['billingCycles'] ?? 0;
329 - $interval_label = $this->get_interval_label( $interval );
330 -
331 - // Build message format based on billing cycles.
332 - if ( 'ongoing' === $billing_cycles ) {
333 - /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */
334 - $message_format = sprintf( __( '{amount} per %s (until cancelled)', 'sureforms' ), $interval_label );
335 - } elseif ( $billing_cycles > 0 ) {
336 - /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */
337 - $message_format = sprintf( __( '{amount} per %1$s (%2$s payments)', 'sureforms' ), $interval_label, $billing_cycles );
338 - } else {
339 - /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */
340 - $message_format = sprintf( __( '{amount} per %s', 'sureforms' ), $interval_label );
341 - }
342 - }
343 - ?>
344 - <span class="srfm-payment-value" data-currency="<?php echo esc_attr( strtolower( $this->currency ) ); ?>" data-currency-symbol="<?php echo esc_attr( Stripe_Helper::get_currency_symbol( $this->currency ) ); ?>" data-message-format="<?php echo esc_attr( $message_format ); ?>">
345 - </span>
346 - </div>
347 - <?php if ( $this->minimum_amount > 0 ) { ?>
348 - <span class="srfm-description">
349 - <?php
350 - echo esc_html(
351 - sprintf(
352 - /* translators: %s: Minimum amount with currency */
353 - __( 'Minimum amount: %s', 'sureforms' ),
354 - $this->format_currency( $this->minimum_amount, $this->currency )
355 - )
356 - );
357 - ?>
358 - </span>
359 - <?php } ?>
433 + <div
434 + id="srfm-payment-amount-<?php echo esc_attr( $this->block_id ); ?>-subscription"
435 + class="srfm-payment-amount-block srfm-payment-amount-block-subscription"
436 + data-payment-type="subscription"
437 + aria-labelledby="srfm-payment-type-<?php echo esc_attr( $this->block_id ); ?>-subscription"
438 + <?php echo esc_attr( $is_one_time_default ? 'hidden' : '' ); ?>
439 + >
440 + <?php
441 + // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- render_amount_display handles its own escaping.
442 + echo $this->render_amount_display(
443 + 'subscription',
444 + Helper::get_string_value( $this->subscription_config['amount_type'] ?? 'fixed' ),
445 + is_numeric( $this->subscription_config['fixed_amount'] ?? null ) ? (float) $this->subscription_config['fixed_amount'] : 0.0,
446 + is_numeric( $this->subscription_config['minimum_amount'] ?? null ) ? (float) $this->subscription_config['minimum_amount'] : 0.0
447 + );
448 + // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
449 + ?>
360 450 </div>
361 - <?php } ?>
451 + <?php
452 + } else {
453 + // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- render_amount_display handles its own escaping.
454 + echo $this->render_amount_display(
455 + $this->payment_type,
456 + $this->amount_type,
457 + $this->fixed_amount,
458 + $this->minimum_amount
459 + );
460 + // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
461 + }
462 + ?>
362 463
363 464 <?php
364 465 if ( 'test' === $this->payment_mode ) {
365 466 echo $this->get_test_mode_notice(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
@@ -397,39 +498,239 @@
397 498 * @return array Array of payment method configurations.
398 499 * @since 2.4.0
399 500 */
400 501 private function get_registered_payment_methods() {
401 - $methods = [];
502 + // Shared with the submission-side guard (Front_End::validate_payment_fields), so
503 + // the renderer and the guard can never disagree about whether this field is usable.
504 + return Payment_Helper::get_registered_payment_methods( [ 'paymentMethods' => $this->payment_methods ] );
505 + }
402 506
403 - // Get enabled payment methods from block attributes.
404 - $enabled_methods = $this->payment_methods;
507 + // BOTH MODE: start — helper renderers for the dual-mode chooser and amount displays.
405 508
406 - // Filter to get method configurations - start with Stripe as default.
407 - $available_methods = apply_filters(
408 - 'srfm_payment_methods_registry',
509 + /**
510 + * Render the one-time / subscription radio chooser shown only in "both" mode.
511 + *
512 + * @return string Chooser markup.
513 + * @since 2.8.2
514 + */
515 + private function render_payment_type_chooser() {
516 + $chooser_name = 'srfm-payment-type-choice-' . $this->block_id;
517 + $is_one_time = 'one-time' === $this->default_payment_choice;
518 +
519 + $options = [
409 520 [
410 - 'stripe' => [
411 - 'id' => 'stripe',
412 - 'label' => __( 'Stripe', 'sureforms' ),
413 - 'description' => __( 'Pay with credit or debit card', 'sureforms' ),
414 - 'icon' => 'credit-card',
415 - 'enabled' => $this->stripe_connected,
416 - 'container_class' => 'srfm-stripe-payment-element',
417 - ],
418 - ]
419 - );
521 + 'value' => 'one-time',
522 + 'label' => $this->one_time_label,
523 + 'checked' => $is_one_time,
524 + ],
525 + [
526 + 'value' => 'subscription',
527 + 'label' => $this->subscription_label,
528 + 'checked' => ! $is_one_time,
529 + ],
530 + ];
420 531
421 - // Filter enabled methods.
422 - foreach ( $enabled_methods as $method_id ) {
423 - if ( isset( $available_methods[ $method_id ] ) && $available_methods[ $method_id ]['enabled'] ) {
424 - $methods[ $method_id ] = $available_methods[ $method_id ];
532 + ob_start();
533 + ?>
534 + <div
535 + class="srfm-payment-type-chooser srfm-block-wrap"
536 + role="radiogroup"
537 + aria-label="<?php esc_attr_e( 'Choose payment type', 'sureforms' ); ?>"
538 + >
539 + <?php
540 + foreach ( $options as $opt ) {
541 + $radio_id = 'srfm-payment-type-' . $this->block_id . '-' . $opt['value'];
542 + $panel_id = 'srfm-payment-amount-' . $this->block_id . '-' . $opt['value'];
543 + // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- render_radio_pill handles its own escaping.
544 + echo $this->render_radio_pill(
545 + [
546 + 'wrapper_class' => 'srfm-payment-type-choice srfm-payment-type-choice--' . $opt['value'],
547 + 'radio_id' => $radio_id,
548 + 'name' => $chooser_name,
549 + 'value' => $opt['value'],
550 + 'label' => $opt['label'],
551 + 'checked' => $opt['checked'],
552 + 'radio_class' => 'srfm-payment-type-choice-radio',
553 + 'aria_controls' => $panel_id,
554 + 'data_attrs' => [ 'data-payment-type' => $opt['value'] ],
555 + ]
556 + );
557 + // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
425 558 }
559 + ?>
560 + </div>
561 + <?php
562 + $markup = ob_get_clean();
563 + return is_string( $markup ) ? $markup : '';
564 + }
565 +
566 + /**
567 + * Render a single radio "pill" using the same DOM shape as the multi-choice
568 + * block (`.srfm-block-content-wrap` + `.srfm-option-container` + circle SVGs).
569 + * This lets us inherit the existing visual language: bordered box, hover/focus
570 + * states, primary-tinted background when selected, animated check icon.
571 + *
572 + * @param array{wrapper_class:string,radio_id:string,name:string,value:string,label:string,checked:bool,radio_class:string,aria_controls?:string,data_attrs?:array<string,string>} $args Pill config.
573 + * @return string Pill markup.
574 + * @since 2.8.2
575 + */
576 + private function render_radio_pill( $args ) {
577 + $check_svg = Helper::fetch_svg( 'circle-checked', 'srfm-payment-icon', 'aria-hidden="true"' );
578 + $unchecked_svg = Helper::fetch_svg( 'circle-unchecked', 'srfm-payment-icon-unchecked', 'aria-hidden="true"' );
579 +
580 + $data_attrs_html = '';
581 + if ( ! empty( $args['data_attrs'] ) && is_array( $args['data_attrs'] ) ) {
582 + foreach ( $args['data_attrs'] as $key => $val ) {
583 + $data_attrs_html .= ' ' . esc_attr( $key ) . '="' . esc_attr( $val ) . '"';
584 + }
426 585 }
427 586
428 - return $methods;
587 + ob_start();
588 + ?>
589 + <label class="<?php echo esc_attr( $args['wrapper_class'] ); ?>">
590 + <input
591 + type="radio"
592 + id="<?php echo esc_attr( $args['radio_id'] ); ?>"
593 + name="<?php echo esc_attr( $args['name'] ); ?>"
594 + value="<?php echo esc_attr( $args['value'] ); ?>"
595 + class="<?php echo esc_attr( $args['radio_class'] ); ?>"
596 + <?php if ( ! empty( $args['aria_controls'] ) ) { ?>
597 + aria-controls="<?php echo esc_attr( $args['aria_controls'] ); ?>"
598 + <?php } ?>
599 + <?php echo $data_attrs_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
600 + <?php checked( ! empty( $args['checked'] ) ); ?>
601 + />
602 + <div class="srfm-block-content-wrap">
603 + <div class="srfm-option-container">
604 + <span class="srfm-payment-option-label"><?php echo esc_html( $args['label'] ); ?></span>
605 + </div>
606 + <div class="srfm-icon-container">
607 + <?php
608 + echo wp_kses( $check_svg, Helper::$allowed_tags_svg );
609 + echo wp_kses( $unchecked_svg, Helper::$allowed_tags_svg );
610 + ?>
611 + </div>
612 + </div>
613 + </label>
614 + <?php
615 + $markup = ob_get_clean();
616 + return is_string( $markup ) ? $markup : '';
429 617 }
430 618
431 619 /**
620 + * Render the amount display block (fixed or variable, one-time or subscription).
621 + * Extracted so the "both" mode can render two of these — one per choice.
622 + *
623 + * @param string $payment_type The payment type for this block ("one-time" or "subscription").
624 + * @param string $amount_type Either "fixed" or "variable".
625 + * @param float $fixed_amount The configured fixed amount.
626 + * @param float $minimum_amount The configured minimum amount.
627 + * @return string Amount display markup.
628 + * @since 2.8.2
629 + */
630 + private function render_amount_display( $payment_type, $amount_type, $fixed_amount, $minimum_amount ) {
631 + ob_start();
632 + if ( 'fixed' === $amount_type ) {
633 + ?>
634 + <!-- Fixed Payment Amount Display. -->
635 + <div class="srfm-payment-amount srfm-block-label">
636 + <span class="srfm-payment-value">
637 + <?php
638 + if ( 'subscription' === $payment_type && ! empty( $this->subscription_plan ) ) {
639 + $interval = $this->subscription_plan['interval'] ?? 'month';
640 + $billing_cycles = $this->subscription_plan['billingCycles'] ?? 0;
641 + $interval_label = $this->get_interval_label( $interval );
642 +
643 + if ( 'ongoing' === $billing_cycles ) {
644 + echo esc_html(
645 + sprintf(
646 + /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */
647 + __( '%1$s per %2$s (until cancelled)', 'sureforms' ),
648 + $this->format_currency( $fixed_amount, $this->currency ),
649 + $interval_label
650 + )
651 + );
652 + } elseif ( $billing_cycles > 0 ) {
653 + echo esc_html(
654 + sprintf(
655 + /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */
656 + __( '%1$s per %2$s (%3$s payments)', 'sureforms' ),
657 + $this->format_currency( $fixed_amount, $this->currency ),
658 + $interval_label,
659 + $billing_cycles
660 + )
661 + );
662 + } else {
663 + echo esc_html(
664 + sprintf(
665 + /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */
666 + __( '%1$s per %2$s', 'sureforms' ),
667 + $this->format_currency( $fixed_amount, $this->currency ),
668 + $interval_label
669 + )
670 + );
671 + }
672 + } else {
673 + echo esc_html( $this->format_currency( $fixed_amount, $this->currency ) );
674 + }
675 + ?>
676 + </span>
677 + </div>
678 + <?php
679 + } else {
680 + // Variable amount display.
681 + $message_format = '{amount}';
682 + if ( 'subscription' === $payment_type && ! empty( $this->subscription_plan ) ) {
683 + $interval = $this->subscription_plan['interval'] ?? 'month';
684 + $billing_cycles = $this->subscription_plan['billingCycles'] ?? 0;
685 + $interval_label = $this->get_interval_label( $interval );
686 +
687 + if ( 'ongoing' === $billing_cycles ) {
688 + /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */
689 + $message_format = sprintf( __( '{amount} per %s (until cancelled)', 'sureforms' ), $interval_label );
690 + } elseif ( $billing_cycles > 0 ) {
691 + /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */
692 + $message_format = sprintf( __( '{amount} per %1$s (%2$s payments)', 'sureforms' ), $interval_label, $billing_cycles );
693 + } else {
694 + /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */
695 + $message_format = sprintf( __( '{amount} per %s', 'sureforms' ), $interval_label );
696 + }
697 + }
698 + ?>
699 + <!-- Variable Payment Amount Display. -->
700 + <div class="srfm-variable-amount-display srfm-block-label">
701 + <div class="srfm-payment-amount-wrapper">
702 + <span
703 + class="srfm-payment-value"
704 + data-currency="<?php echo esc_attr( strtolower( $this->currency ) ); ?>"
705 + data-currency-symbol="<?php echo esc_attr( Stripe_Helper::get_currency_symbol( $this->currency ) ); ?>"
706 + data-message-format="<?php echo esc_attr( $message_format ); ?>"
707 + ></span>
708 + </div>
709 + <?php if ( $minimum_amount > 0 ) { ?>
710 + <span class="srfm-description">
711 + <?php
712 + echo esc_html(
713 + sprintf(
714 + /* translators: %s: Minimum amount with currency */
715 + __( 'Minimum amount: %s', 'sureforms' ),
716 + $this->format_currency( $minimum_amount, $this->currency )
717 + )
718 + );
719 + ?>
720 + </span>
721 + <?php } ?>
722 + </div>
723 + <?php
724 + }
725 +
726 + $markup = ob_get_clean();
727 + return is_string( $markup ) ? $markup : '';
728 + }
729 +
730 + // BOTH MODE: end.
731 +
732 + /**
432 733 * Render payment methods as accordion.
433 734 * Each payment method is an accordion item with header and collapsible content.
434 735 *
435 736 * @param array<mixed> $methods Array of payment methods.
@@ -523,9 +824,10 @@
523 824 return false;
524 825 }
525 826
526 827 // Check subscription-specific requirements.
527 - if ( 'subscription' === $this->payment_type ) {
828 + // BOTH MODE: subscription is a possible end-user choice, so name field is also required.
829 + if ( 'subscription' === $this->original_payment_type || 'both' === $this->original_payment_type ) {
528 830 if ( empty( $this->customer_name_field ) ) {
529 831 return false;
530 832 }
531 833 }