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 +428 -111 2.7.0 → 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' ) ) {
@@ -133,8 +134,68 @@
133 134 */
134 135 protected $payment_methods;
135 136
136 137 /**
138 + * Payment description shown on receipts and in the payment dashboard.
139 + *
140 + * @var string
141 + * @since 2.7.1
142 + */
143 + protected $payment_description;
144 +
145 + // BOTH MODE: start — admin-configurable dual-mode attributes.
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 + /**
137 198 * Constructor for the Payment Markup class.
138 199 *
139 200 * @param array<mixed> $attributes Block attributes.
140 201 * @since 2.0.0
@@ -178,11 +239,52 @@
178 239
179 240 // Set variable amount field mapping.
180 241 $this->variable_amount_field = $attributes['variableAmountField'] ?? '';
181 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 +
182 281 // Set payment methods from block attributes, default to 'stripe' for backward compatibility.
183 282 $this->payment_methods = $attributes['paymentMethods'] ?? [ 'stripe' ];
184 283
284 + // Set custom payment description (empty means JS/gateway will use its own default).
285 + $this->payment_description = $attributes['paymentDescription'] ?? '';
286 +
185 287 // BACKWARD COMPATIBILITY: Migrate customer fields from subscriptionPlan.
186 288 if ( empty( $this->customer_name_field ) && ! empty( $this->subscription_plan['customer_name'] ) ) {
187 289 $this->customer_name_field = $this->subscription_plan['customer_name'];
188 290 }
@@ -233,23 +335,67 @@
233 335 'data-payment-methods' => wp_json_encode( array_keys( $registered_methods ) ),
234 336 'data-selected-method' => $first_method_id,
235 337 ];
236 338
237 - if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) {
238 - $data_input_attributes['data-subscription-plan-name'] = $this->subscription_plan['name'] ?? __( 'Subscription Plan', 'sureforms' );
239 - $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' );
240 348 $data_input_attributes['data-subscription-billing-cycles'] = $this->subscription_plan['billingCycles'] ?? 0;
241 349 }
242 350
351 + if ( ! empty( $this->payment_description ) ) {
352 + $data_input_attributes['data-description'] = $this->payment_description;
353 + }
354 +
243 355 if ( 'variable' === $this->amount_type ) {
244 356 $data_input_attributes['data-variable-amount-field'] = $this->variable_amount_field;
245 357 }
246 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 +
247 372 // If minimum amount is greater than 0, add it to the data input attributes.
248 373 if ( $this->minimum_amount > 0 ) {
249 374 $data_input_attributes['data-minimum-amount'] = $this->minimum_amount;
250 375 }
251 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 +
252 398 ob_start();
253 399 ?>
254 400 <div data-block-id="<?php echo esc_attr( $this->block_id ); ?>" class="<?php echo esc_attr( $field_classes ); ?>">
255 401 <?php echo wp_kses_post( $this->label_markup ); ?>
@@ -254,97 +400,67 @@
254 400 <div data-block-id="<?php echo esc_attr( $this->block_id ); ?>" class="<?php echo esc_attr( $field_classes ); ?>">
255 401 <?php echo wp_kses_post( $this->label_markup ); ?>
256 402 <?php echo wp_kses_post( $this->help_markup ); ?>
257 403 <div class="srfm-payment-field-wrapper">
258 - <?php if ( 'fixed' === $this->amount_type ) { ?>
259 - <!-- Fixed Payment Amount Display. -->
260 - <div class="srfm-payment-amount srfm-block-label">
261 - <span class="srfm-payment-value">
262 - <?php
263 - if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) {
264 - $interval = $this->subscription_plan['interval'] ?? 'month';
265 - $billing_cycles = $this->subscription_plan['billingCycles'] ?? 0;
266 - $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.
267 410
268 - // Build subscription text.
269 - if ( 'ongoing' === $billing_cycles ) {
270 - echo esc_html(
271 - sprintf(
272 - /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */
273 - __( '%1$s per %2$s (until cancelled)', 'sureforms' ),
274 - $this->format_currency( $this->fixed_amount, $this->currency ),
275 - $interval_label
276 - )
277 - );
278 - } elseif ( $billing_cycles > 0 ) {
279 - echo esc_html(
280 - sprintf(
281 - /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */
282 - __( '%1$s per %2$s (%3$s payments)', 'sureforms' ),
283 - $this->format_currency( $this->fixed_amount, $this->currency ),
284 - $interval_label,
285 - $billing_cycles
286 - )
287 - );
288 - } else {
289 - echo esc_html(
290 - sprintf(
291 - /* translators: 1: Amount with currency, 2: Interval (day/week/month/quarter/year) */
292 - __( '%1$s per %2$s', 'sureforms' ),
293 - $this->format_currency( $this->fixed_amount, $this->currency ),
294 - $interval_label
295 - )
296 - );
297 - }
298 - } else {
299 - echo esc_html( $this->format_currency( $this->fixed_amount, $this->currency ) );
300 - }
301 - ?>
302 - </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 + ?>
303 432 </div>
304 - <?php } else { ?>
305 - <!-- Variable Payment Amount Display. -->
306 - <div class="srfm-variable-amount-display srfm-block-label">
307 - <div class="srfm-payment-amount-wrapper">
308 - <?php
309 - // Generate message format for variable amounts.
310 - $message_format = '{amount}';
311 - if ( 'subscription' === $this->payment_type && ! empty( $this->subscription_plan ) ) {
312 - $interval = $this->subscription_plan['interval'] ?? 'month';
313 - $billing_cycles = $this->subscription_plan['billingCycles'] ?? 0;
314 - $interval_label = $this->get_interval_label( $interval );
315 -
316 - // Build message format based on billing cycles.
317 - if ( 'ongoing' === $billing_cycles ) {
318 - /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */
319 - $message_format = sprintf( __( '{amount} per %s (until cancelled)', 'sureforms' ), $interval_label );
320 - } elseif ( $billing_cycles > 0 ) {
321 - /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year), 3: Number of billing cycles */
322 - $message_format = sprintf( __( '{amount} per %1$s (%2$s payments)', 'sureforms' ), $interval_label, $billing_cycles );
323 - } else {
324 - /* translators: 1: Amount with currency placeholder, 2: Interval (day/week/month/quarter/year) */
325 - $message_format = sprintf( __( '{amount} per %s', 'sureforms' ), $interval_label );
326 - }
327 - }
328 - ?>
329 - <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 ); ?>">
330 - </span>
331 - </div>
332 - <?php if ( $this->minimum_amount > 0 ) { ?>
333 - <span class="srfm-description">
334 - <?php
335 - echo esc_html(
336 - sprintf(
337 - /* translators: %s: Minimum amount with currency */
338 - __( 'Minimum amount: %s', 'sureforms' ),
339 - $this->format_currency( $this->minimum_amount, $this->currency )
340 - )
341 - );
342 - ?>
343 - </span>
344 - <?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 + ?>
345 450 </div>
346 - <?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 + ?>
347 463
348 464 <?php
349 465 if ( 'test' === $this->payment_mode ) {
350 466 echo $this->get_test_mode_notice(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
@@ -382,39 +498,239 @@
382 498 * @return array Array of payment method configurations.
383 499 * @since 2.4.0
384 500 */
385 501 private function get_registered_payment_methods() {
386 - $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 + }
387 506
388 - // Get enabled payment methods from block attributes.
389 - $enabled_methods = $this->payment_methods;
507 + // BOTH MODE: start — helper renderers for the dual-mode chooser and amount displays.
390 508
391 - // Filter to get method configurations - start with Stripe as default.
392 - $available_methods = apply_filters(
393 - '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 = [
394 520 [
395 - 'stripe' => [
396 - 'id' => 'stripe',
397 - 'label' => __( 'Stripe', 'sureforms' ),
398 - 'description' => __( 'Pay with credit or debit card', 'sureforms' ),
399 - 'icon' => 'credit-card',
400 - 'enabled' => $this->stripe_connected,
401 - 'container_class' => 'srfm-stripe-payment-element',
402 - ],
403 - ]
404 - );
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 + ];
405 531
406 - // Filter enabled methods.
407 - foreach ( $enabled_methods as $method_id ) {
408 - if ( isset( $available_methods[ $method_id ] ) && $available_methods[ $method_id ]['enabled'] ) {
409 - $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
410 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 + }
411 585 }
412 586
413 - 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 : '';
414 617 }
415 618
416 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 + /**
417 733 * Render payment methods as accordion.
418 734 * Each payment method is an accordion item with header and collapsible content.
419 735 *
420 736 * @param array<mixed> $methods Array of payment methods.
@@ -508,9 +824,10 @@
508 824 return false;
509 825 }
510 826
511 827 // Check subscription-specific requirements.
512 - 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 ) {
513 830 if ( empty( $this->customer_name_field ) ) {
514 831 return false;
515 832 }
516 833 }