PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.8.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.8.0
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 0.0.2 0.0.3 All 96 releases
sureforms / inc / fields / payment-markup.php
payment-markup.php
617 lines 18.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SureForms Payment Markup Class file.
4 *
5 * @package sureforms.
6 * @since 2.0.0
7 */
8
9 namespace SRFM\Inc\Fields;
10
11 use SRFM\Inc\Payments\Payment_Helper;
12 use SRFM\Inc\Payments\Stripe\Stripe_Helper;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit; // Exit if accessed directly.
16 }
17
18 /**
19 * SureForms Payment Markup Class.
20 *
21 * @since 2.0.0
22 */
23 class Payment_Markup extends Base {
24 /**
25 * Payment amount.
26 *
27 * @var float
28 * @since 2.0.0
29 */
30 protected $amount;
31
32 /**
33 * Payment currency.
34 *
35 * @var string
36 * @since 2.0.0
37 */
38 protected $currency;
39
40 /**
41 * Stripe publishable key.
42 *
43 * @var string
44 * @since 2.0.0
45 */
46 protected $stripe_publishable_key;
47
48 /**
49 * Whether Stripe is connected.
50 *
51 * @var bool
52 * @since 2.0.0
53 */
54 protected $stripe_connected;
55
56 /**
57 * Payment mode (live or test).
58 *
59 * @var string
60 * @since 2.0.0
61 */
62 protected $payment_mode;
63
64 /**
65 * Payment type.
66 *
67 * @var string
68 * @since 2.0.0
69 */
70 protected $payment_type;
71
72 /**
73 * Subscription plans.
74 *
75 * @var array
76 * @since 2.0.0
77 */
78 protected $subscription_plan;
79
80 /**
81 * Amount type.
82 *
83 * @var string
84 * @since 2.0.0
85 */
86 protected $amount_type;
87
88 /**
89 * Fixed amount.
90 *
91 * @var float
92 * @since 2.0.0
93 */
94 protected $fixed_amount;
95
96 /**
97 * Minimum amount.
98 *
99 * @var float
100 * @since 2.0.0
101 */
102 protected $minimum_amount;
103
104 /**
105 * Customer name field slug.
106 *
107 * @var string
108 * @since 2.0.0
109 */
110 protected $customer_name_field;
111
112 /**
113 * Customer email field slug.
114 *
115 * @var string
116 * @since 2.0.0
117 */
118 protected $customer_email_field;
119
120 /**
121 * Variable amount field slug.
122 *
123 * @var string
124 * @since 2.0.0
125 */
126 protected $variable_amount_field;
127
128 /**
129 * Payment methods enabled for this form.
130 *
131 * @var array
132 * @since 2.4.0
133 */
134 protected $payment_methods;
135
136 /**
137 * Payment description shown on receipts and in the payment dashboard.
138 *
139 * @var string
140 * @since 2.7.1
141 */
142 protected $payment_description;
143
144 /**
145 * Constructor for the Payment Markup class.
146 *
147 * @param array<mixed> $attributes Block attributes.
148 * @since 2.0.0
149 */
150 public function __construct( $attributes ) {
151 // Get payment settings from Stripe Helper.
152 $this->stripe_connected = Stripe_Helper::is_stripe_connected();
153 $this->payment_mode = Stripe_Helper::get_stripe_mode();
154
155 $this->slug = 'payment';
156 $this->set_properties( $attributes );
157 $this->set_input_label( 'Payment' );
158 $this->set_error_msg( $attributes, 'srfm_payment_block_required_text' );
159 $this->set_unique_slug();
160 $this->set_markup_properties();
161 $this->set_aria_described_by();
162
163 $this->set_field_name( $this->unique_slug );
164
165 // Set payment-specific properties.
166 $this->amount = $attributes['amount'] ?? 10;
167 $this->currency = $attributes['currency'] ?? 'USD';
168
169 // Use currency from settings if not specified in block.
170 if ( empty( $this->currency ) || 'USD' === $this->currency ) {
171 $this->currency = Stripe_Helper::get_currency();
172 }
173
174 // Get appropriate Stripe publishable key based on mode.
175 $this->stripe_publishable_key = Stripe_Helper::get_stripe_publishable_key();
176
177 $this->payment_type = $attributes['paymentType'] ?? 'one-time';
178 $this->subscription_plan = $attributes['subscriptionPlan'] ?? [];
179 $this->amount_type = $attributes['amountType'] ?? 'fixed';
180 $this->fixed_amount = $attributes['fixedAmount'] ?? 10;
181 $this->minimum_amount = $attributes['minimumAmount'] ?? 0;
182
183 // Set customer field mappings.
184 $this->customer_name_field = $attributes['customerNameField'] ?? '';
185 $this->customer_email_field = $attributes['customerEmailField'] ?? '';
186
187 // Set variable amount field mapping.
188 $this->variable_amount_field = $attributes['variableAmountField'] ?? '';
189
190 // Set payment methods from block attributes, default to 'stripe' for backward compatibility.
191 $this->payment_methods = $attributes['paymentMethods'] ?? [ 'stripe' ];
192
193 // Set custom payment description (empty means JS/gateway will use its own default).
194 $this->payment_description = $attributes['paymentDescription'] ?? '';
195
196 // BACKWARD COMPATIBILITY: Migrate customer fields from subscriptionPlan.
197 if ( empty( $this->customer_name_field ) && ! empty( $this->subscription_plan['customer_name'] ) ) {
198 $this->customer_name_field = $this->subscription_plan['customer_name'];
199 }
200
201 if ( empty( $this->customer_email_field ) && ! empty( $this->subscription_plan['customer_email'] ) ) {
202 $this->customer_email_field = $this->subscription_plan['customer_email'];
203 }
204 }
205
206 /**
207 * Render the payment field markup.
208 *
209 * @return string|bool
210 * @since 2.0.0
211 */
212 public function markup() {
213 // Get registered payment methods.
214 $registered_methods = $this->get_registered_payment_methods();
215
216 // Check if any payment methods are available.
217 if ( empty( $registered_methods ) ) {
218 return '';
219 }
220
221 // Validate payment field requirements.
222 $is_valid = $this->validate_payment_requirements();
223 if ( ! $is_valid ) {
224 return '';
225 }
226
227 $field_classes = $this->get_field_classes();
228
229 // Get first payment method as default.
230 $first_method_id = array_key_first( $registered_methods );
231
232 $data_input_attributes = [
233 'name' => $this->field_name,
234 'class' => 'srfm-payment-input',
235 'data-currency' => strtolower( $this->currency ),
236 'data-stripe-key' => $this->stripe_publishable_key,
237 'data-payment-mode' => $this->payment_mode,
238 'data-amount-type' => $this->amount_type,
239 'data-fixed-amount' => $this->fixed_amount,
240 'aria-describedby' => trim( $this->aria_described_by ),
241 'data-payment-type' => $this->payment_type,
242 'data-customer-name-field' => $this->customer_name_field,
243 'data-customer-email-field' => $this->customer_email_field,
244 'data-payment-methods' => wp_json_encode( array_keys( $registered_methods ) ),
245 'data-selected-method' => $first_method_id,
246 ];
247
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';
251 $data_input_attributes['data-subscription-billing-cycles'] = $this->subscription_plan['billingCycles'] ?? 0;
252 }
253
254 if ( ! empty( $this->payment_description ) ) {
255 $data_input_attributes['data-description'] = $this->payment_description;
256 }
257
258 if ( 'variable' === $this->amount_type ) {
259 $data_input_attributes['data-variable-amount-field'] = $this->variable_amount_field;
260 }
261
262 // If minimum amount is greater than 0, add it to the data input attributes.
263 if ( $this->minimum_amount > 0 ) {
264 $data_input_attributes['data-minimum-amount'] = $this->minimum_amount;
265 }
266
267 ob_start();
268 ?>
269 <div data-block-id="<?php echo esc_attr( $this->block_id ); ?>" class="<?php echo esc_attr( $field_classes ); ?>">
270 <?php echo wp_kses_post( $this->label_markup ); ?>
271 <?php echo wp_kses_post( $this->help_markup ); ?>
272 <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 );
282
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>
318 </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 } ?>
360 </div>
361 <?php } ?>
362
363 <?php
364 if ( 'test' === $this->payment_mode ) {
365 echo $this->get_test_mode_notice(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
366 }
367 ?>
368
369 <!-- Payment Methods Accordion -->
370 <?php echo $this->render_payment_methods_accordion( $registered_methods ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
371
372 <!-- Hidden fields for payment data -->
373 <input type="hidden"
374 <?php
375 foreach ( $data_input_attributes as $attr_key => $attr_value ) {
376 echo esc_attr( $attr_key ) . '="' . esc_attr( $attr_value ) . '" ';
377 }
378 ?>
379 />
380
381 <!-- Payment processing status -->
382 <div id="srfm-payment-status-<?php echo esc_attr( $this->block_id ); ?>" class="srfm-payment-status" style="display: none;">
383 <div class="srfm-payment-processing">
384 <span class="srfm-spinner"></span>
385 <?php esc_html_e( 'Processing payment...', 'sureforms' ); ?>
386 </div>
387 </div>
388 </div>
389 </div>
390 <?php
391 return ob_get_clean();
392 }
393
394 /**
395 * Get registered payment methods for display.
396 *
397 * @return array Array of payment method configurations.
398 * @since 2.4.0
399 */
400 private function get_registered_payment_methods() {
401 $methods = [];
402
403 // Get enabled payment methods from block attributes.
404 $enabled_methods = $this->payment_methods;
405
406 // Filter to get method configurations - start with Stripe as default.
407 $available_methods = apply_filters(
408 'srfm_payment_methods_registry',
409 [
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 );
420
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 ];
425 }
426 }
427
428 return $methods;
429 }
430
431 /**
432 * Render payment methods as accordion.
433 * Each payment method is an accordion item with header and collapsible content.
434 *
435 * @param array<mixed> $methods Array of payment methods.
436 * @return string Payment methods accordion markup.
437 * @since 2.4.0
438 */
439 private function render_payment_methods_accordion( $methods ) {
440 if ( empty( $methods ) || ! is_array( $methods ) ) {
441 return '';
442 }
443
444 $is_single_method = count( $methods ) === 1;
445 $is_first = true; // Track the first payment method.
446
447 ob_start();
448 ?>
449 <div class="srfm-payment-methods-accordion <?php echo $is_single_method ? 'srfm-single-payment-method' : ''; ?>">
450 <?php foreach ( $methods as $method ) { ?>
451 <div
452 class="srfm-accordion-item"
453 data-method="<?php echo esc_attr( $method['id'] ); ?>"
454 >
455 <div
456 class="srfm-accordion-header"
457 role="button"
458 tabindex="0"
459 aria-expanded="<?php echo $is_first ? 'true' : 'false'; ?>"
460 aria-controls="srfm-accordion-content-<?php echo esc_attr( $method['id'] ); ?>-<?php echo esc_attr( $this->block_id ); ?>"
461 >
462 <div class="srfm-payment-input-wrapper">
463 <input
464 type="radio"
465 name="payment-method-<?php echo esc_attr( $this->block_id ); ?>"
466 value="<?php echo esc_attr( $method['id'] ); ?>"
467 class="srfm-payment-method-radio"
468 data-method="<?php echo esc_attr( $method['id'] ); ?>"
469 <?php checked( $is_first ); ?>
470 aria-label="<?php echo esc_attr( $method['label'] ); ?>"
471 />
472 <span class="srfm-accordion-title srfm-block-label">
473 <?php echo esc_html( $method['label'] ); ?>
474 </span>
475 </div>
476 </div>
477 <div
478 id="srfm-accordion-content-<?php echo esc_attr( $method['id'] ); ?>-<?php echo esc_attr( $this->block_id ); ?>"
479 class="srfm-accordion-content"
480 role="region"
481 aria-labelledby="srfm-accordion-header-<?php echo esc_attr( $method['id'] ); ?>"
482 >
483 <div
484 class="srfm-payment-method-content"
485 data-method="<?php echo esc_attr( $method['id'] ); ?>"
486 >
487 <div
488 id="srfm-<?php echo esc_attr( $method['id'] ); ?>-<?php echo esc_attr( $this->block_id ); ?>"
489 class="<?php echo esc_attr( $method['container_class'] ); ?>"
490 >
491 <?php
492 // Output provider's placeholder content if available, otherwise show a generic hint for JS rendering.
493 if ( ! empty( $method['place_holder_content'] ) ) {
494 echo $method['place_holder_content']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
495 } else {
496 ?>
497 <!-- Provider JS will render content here -->
498 <?php
499 }
500 ?>
501 </div>
502 </div>
503 </div>
504 </div>
505 <?php $is_first = false; // Set to false after first iteration. ?>
506 <?php } ?>
507 </div>
508 <?php
509 $template = ob_get_clean();
510
511 return is_string( $template ) ? $template : '';
512 }
513
514 /**
515 * Validate payment field requirements.
516 *
517 * @return bool True if validation passes, false otherwise.
518 * @since 2.0.0
519 */
520 private function validate_payment_requirements() {
521 // Check customer email field requirement (highest priority).
522 if ( empty( $this->customer_email_field ) ) {
523 return false;
524 }
525
526 // Check subscription-specific requirements.
527 if ( 'subscription' === $this->payment_type ) {
528 if ( empty( $this->customer_name_field ) ) {
529 return false;
530 }
531 }
532
533 return true;
534 }
535
536 /**
537 * Format currency for display.
538 *
539 * @param float $amount Amount to format.
540 * @param string $currency Currency code.
541 * @return string
542 * @since 2.0.0
543 */
544 private function format_currency( $amount, $currency ) {
545 $symbol = Stripe_Helper::get_currency_symbol( $currency );
546 $position = Payment_Helper::get_currency_sign_position();
547
548 // Format based on currency.
549 if ( in_array( $currency, [ 'JPY', 'KRW' ], true ) ) {
550 // No decimal places for these currencies.
551 $formatted_amount = number_format( $amount, 0 );
552 } else {
553 $formatted_amount = number_format( $amount, 2 );
554 }
555
556 // Apply currency sign position.
557 switch ( $position ) {
558 case 'right':
559 return $formatted_amount . $symbol;
560 case 'left_space':
561 return $symbol . ' ' . $formatted_amount;
562 case 'right_space':
563 return $formatted_amount . ' ' . $symbol;
564 case 'left':
565 default:
566 return $symbol . $formatted_amount;
567 }
568 }
569
570 /**
571 * Get the human-readable label for a payment interval slug.
572 *
573 * @param string $interval_slug The slug (e.g., 'day', 'week', 'month', 'quarter', 'yearly').
574 * @return string The translated interval label, or the slug itself if not found.
575 * @since 2.0.0
576 */
577 private function get_interval_label( $interval_slug ) {
578 $interval_labels = [
579 'day' => __( 'day', 'sureforms' ),
580 'week' => __( 'week', 'sureforms' ),
581 'month' => __( 'month', 'sureforms' ),
582 'quarter' => __( 'quarter', 'sureforms' ),
583 'yearly' => __( 'year', 'sureforms' ),
584 ];
585
586 return $interval_labels[ $interval_slug ] ?? $interval_slug;
587 }
588
589 /**
590 * Render test mode notice for admin users.
591 * Only shows if user has manage_options capability.
592 *
593 * @return string|bool Test mode notice markup or empty string.
594 * @since 2.0.0
595 */
596 private function get_test_mode_notice() {
597 // Only show to users with manage_options capability.
598 if ( ! current_user_can( 'manage_options' ) ) {
599 return '';
600 }
601
602 // Build dynamic link to payment settings.
603 $settings_url = admin_url( 'admin.php?page=sureforms_form_settings&tab=payments-settings&subpage=general' );
604
605 ob_start();
606 ?>
607 <div class="srfm-test-mode-notice" style="background-color: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 12px; margin-bottom: 16px; color: #856404;">
608 <strong><?php esc_html_e( 'Test mode is enabled:', 'sureforms' ); ?></strong>
609 <a href="<?php echo esc_url( $settings_url ); ?>" style="color: #856404;" target="_blank" rel="noopener noreferrer">
610 <?php esc_html_e( 'Click here to enable live mode and accept payment', 'sureforms' ); ?>
611 </a>
612 </div>
613 <?php
614 return ob_get_clean();
615 }
616 }
617